private Optional<Node> recycle(Node node, List<Node> children, NodeList allNodes) {
        Optional<String> reason = shouldPark(node, allNodes);
        if (reason.isPresent()) {
            List<String> unparkedChildren = children.stream()
                                                    .filter(child -> child.state() != Node.State.parked)
                                                    .map(Node::hostname)
                                                    .toList();
            if (unparkedChildren.isEmpty()) {
                // Only forcing de-provisioning of off premises nodes
                return Optional.of(nodeRepository.nodes().park(node.hostname(), nodeRepository.zone().cloud().dynamicProvisioning(), Agent.FailedExpirer,
                                                               "Parked by FailedExpirer due to " + reason.get()));
            } else {
                log.info(String.format("Expired failed node %s was not parked because of unparked children: %s",
                                       node.hostname(), String.join(", ", unparkedChildren)));
                return Optional.empty();
            }
        } else {
            List<String> childrenBlockingDirtying = children
                    .stream()
                    // Examples: a failed child node may have an index we want to preserve. A dirty child node has
                    // log we want to sync.  A parked child w/o wTD may have been parked by an operator for inspection.
                    .filter(child -> child.state() != Node.State.parked || !child.status().wantToDeprovision())
                    .map(Node::hostname)
                    .toList();

            if (childrenBlockingDirtying.isEmpty()) {
                return Optional.of(nodeRepository.nodes().deallocate(node, Agent.FailedExpirer, "Expired by FailedExpirer"));
            } else {
                log.info(String.format("Expired failed host %s was not dirtied because it has children: %s",
                                       node.hostname(), String.join(", ", childrenBlockingDirtying)));
                return Optional.empty();
            }

        }
    }
This moves expired failed nodes:

- To parked: If the node has known hardware failure, hosts are moved to parked only when all of their
             children are already in parked.
- To dirty: If the node is a host and has failed less than 5 times, or always if the node is a child.
- Otherwise the node will remain in failed.

Failed content nodes are given a long expiry time to enable us to manually moved them back to
active to recover data in cases where the node was failed accidentally.

Failed containers (Vespa, not Linux) are expired early as there's no data to potentially recover.

The purpose of the automatic recycling to dirty + fail count is that nodes which were moved
to failed due to some undetected hardware failure will end up being failed again.
When that has happened enough they will not be recycled, and need manual inspection to move on.

Nodes with detected hardware issues will not be recycled.

@author bratseth
@author mpolden
node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/FailedExpirer.java