@Override
    protected double maintain() {
        List<Node> provisionedSnapshot;
        try {
            NodeList nodes;
            // Host and child nodes are written in separate transactions, but both are written while holding the
            // unallocated lock. Hold the unallocated lock while reading nodes to ensure we get all the children
            // of newly provisioned hosts.
            try (Mutex ignored = nodeRepository().nodes().lockUnallocated()) {
                nodes = nodeRepository().nodes().list();
            }
            provisionedSnapshot = provision(nodes);
        } catch (NodeAllocationException | IllegalStateException e) {
            log.log(Level.WARNING, "Failed to allocate preprovisioned capacity and/or find excess hosts: " + e.getMessage());
            return 0;  // avoid removing excess hosts
        } catch (RuntimeException e) {
            log.log(Level.WARNING, "Failed to allocate preprovisioned capacity and/or find excess hosts", e);
            return 0;  // avoid removing excess hosts
        }

        return markForRemoval(provisionedSnapshot);
    }
@Test
    public void deprovision_node_when_no_allocation_and_past_ttl() {
        tester = new DynamicProvisioningTester();
        ManualClock clock = (ManualClock) tester.nodeRepository.clock();
        tester.hostProvisioner.with(Behaviour.failProvisioning);
        tester.provisioningTester.makeReadyHosts(2, new NodeResources(1, 1, 1, 1)).activateTenantHosts();
        List<Node> hosts = tester.nodeRepository.nodes().list(Node.State.active).asList();
        Node host1 = hosts.get(0);
        Node host2 = hosts.get(1);
        tester.nodeRepository.nodes().write(host1.withHostTTL(Duration.ofDays(1)), () -> { });
        tester.nodeRepository.nodes().write(host2.withHostTTL(Duration.ofHours(1)), () -> { });
        Node host11 = tester.addNode("host1-1", Optional.of(host1.hostname()), NodeType.tenant, State.active, DynamicProvisioningTester.tenantApp);

        // Host is not marked for deprovisioning by maintainer, because child is present
        tester.maintain();
        assertFalse(node(host1.hostname()).get().status().wantToDeprovision());
        assertEquals(Optional.empty(), node(host1.hostname()).get().hostEmptyAt());

        // Child is set to deprovision, but turns active
        tester.nodeRepository.nodes().park(host11.hostname(), true, Agent.system, "not good");
        tester.nodeRepository.nodes().reactivate(host11.hostname(), Agent.operator, "all good");
        assertTrue(node(host11.hostname()).get().status().wantToDeprovision());
        assertEquals(State.active, node(host11.hostname()).get().state());
        tester.maintain();
        assertFalse(node(host1.hostname()).get().status().wantToDeprovision());
        assertEquals(Optional.empty(), node(host1.hostname()).get().hostEmptyAt());

        // Child is parked, to make the host effectively empty
        tester.nodeRepository.nodes().park(host11.hostname(), true, Agent.system, "not good");
        tester.maintain();
        assertFalse(node(host1.hostname()).get().status().wantToDeprovision());
        assertEquals(Optional.of(clock.instant().truncatedTo(ChronoUnit.MILLIS)),
                     node(host1.hostname()).get().hostEmptyAt());

        // Some time passes, but not enough for host1 to be deprovisioned
        clock.advance(Duration.ofDays(1).minusSeconds(1));
        tester.maintain();
        assertFalse(node(host1.hostname()).get().status().wantToDeprovision());
        assertEquals(Optional.of(clock.instant().minus(Duration.ofDays(1).minusSeconds(1)).truncatedTo(ChronoUnit.MILLIS)),
                     node(host1.hostname()).get().hostEmptyAt());
        assertTrue(node(host2.hostname()).get().status().wantToDeprovision());
        assertTrue(node(host2.hostname()).get().status().wantToRetire());
        assertEquals(State.active, node(host2.hostname()).get().state());
        assertEquals(Optional.of(clock.instant().minus(Duration.ofDays(1).minusSeconds(1)).truncatedTo(ChronoUnit.MILLIS)),
                     node(host2.hostname()).get().hostEmptyAt());

        // Some more time passes, but child is reactivated on host1, rendering the host non-empty again
        clock.advance(Duration.ofDays(1));
        tester.nodeRepository.nodes().reactivate(host11.hostname(), Agent.operator, "all good");
        tester.maintain();
        assertFalse(node(host1.hostname()).get().status().wantToDeprovision());
        assertEquals(Optional.empty(), node(host1.hostname()).get().hostEmptyAt());

        // Child is removed, and host is marked as empty
        tester.nodeRepository.database().writeTo(State.deprovisioned, host11, Agent.operator, Optional.empty());
        tester.nodeRepository.nodes().forget(node(host11.hostname()).get());
        assertEquals(Optional.empty(), node(host11.hostname()));
        tester.maintain();
        assertFalse(node(host1.hostname()).get().status().wantToDeprovision());
        assertEquals(Optional.of(clock.instant().truncatedTo(ChronoUnit.MILLIS)),
                     node(host1.hostname()).get().hostEmptyAt());

        // Enough time passes for the host to be deprovisioned
        clock.advance(Duration.ofDays(1));
        tester.maintain();
        assertTrue(node(host1.hostname()).get().status().wantToDeprovision());
        assertTrue(node(host1.hostname()).get().status().wantToRetire());
        assertEquals(State.active, node(host1.hostname()).get().state());
        assertEquals(Optional.of(clock.instant().minus(Duration.ofDays(1)).truncatedTo(ChronoUnit.MILLIS)),
                     node(host1.hostname()).get().hostEmptyAt());

        // Let tenant host app redeploy, retiring the obsolete host.
        tester.provisioningTester.activateTenantHosts();
        clock.advance(Duration.ofHours(1));
        new RetiredExpirer(tester.nodeRepository,
                           new MockDeployer(tester.nodeRepository),
                           new NullMetric(),
                           Duration.ofHours(1),
                           Duration.ofHours(1)).maintain();

        tester.provisioningTester.activateTenantHosts();
        // Hosts move themselves to parked (via ready) once they've synced up their logs to archive and are then deprovisioned
        tester.nodeRepository.nodes().list(State.dirty).forEach(node ->
                tester.nodeRepository.nodes().markNodeAvailableForNewAllocation(node.hostname(), Agent.nodeAdmin, "Readied by host-admin"));
        tester.deprovisioner.maintain();
        assertEquals(List.of(), tester.nodeRepository.nodes().list().not().state(State.deprovisioned).asList());
    }