summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2023-03-10 11:43:23 +0100
committerMartin Polden <mpolden@mpolden.no>2023-03-10 11:52:38 +0100
commit0f1333ac8b35fd91dae612bdbcf89a943c62792c (patch)
tree3ab3bb24ef9f06b463d21246d8f2aacdadc8e6c0
parentf6702b43e47bfdc925b3d9187766cdd7d4f82bf9 (diff)
Take a separate lock for maintenance deployments
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Applications.java5
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ApplicationMaintainer.java20
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/AutoscalingMaintainer.java18
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ExpeditedChangeApplicationMaintainer.java12
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/MaintenanceDeployment.java28
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/PeriodicApplicationMaintainer.java3
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/RetiredExpirer.java78
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/SpareCapacityMaintainer.java37
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/Nodes.java11
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDb.java5
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/HostCapacityMaintainerTest.java4
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/os/OsVersionsTest.java4
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/InfraDeployerImplTest.java4
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/VirtualNodeProvisioningTest.java2
14 files changed, 128 insertions, 103 deletions
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Applications.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Applications.java
index 9cfed5d046c..8daac029c7b 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Applications.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Applications.java
@@ -73,4 +73,9 @@ public class Applications {
return db.lock(application, timeout);
}
+ /** Create a lock which provides exclusive rights to perform a maintenance deployment */
+ public Mutex lockMaintenance(ApplicationId application) {
+ return db.lockMaintenance(application);
+ }
+
}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ApplicationMaintainer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ApplicationMaintainer.java
index 8ddf31289f9..50eee9e33b3 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ApplicationMaintainer.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ApplicationMaintainer.java
@@ -46,6 +46,8 @@ public abstract class ApplicationMaintainer extends NodeRepositoryMaintainer {
return 1.0;
}
+ protected final Deployer deployer() { return deployer; }
+
/** Returns the number of deployments that are pending execution */
public int pendingDeployments() {
return pendingDeployments.size();
@@ -54,6 +56,9 @@ public abstract class ApplicationMaintainer extends NodeRepositoryMaintainer {
/** Returns whether given application should be deployed at this moment in time */
protected abstract boolean canDeployNow(ApplicationId application);
+ /** Returns the applications that should be maintained by this now. */
+ protected abstract Map<ApplicationId, String> applicationsNeedingMaintenance();
+
/**
* Redeploy this application.
*
@@ -62,19 +67,14 @@ public abstract class ApplicationMaintainer extends NodeRepositoryMaintainer {
*/
protected void deploy(ApplicationId application, String reason) {
if (pendingDeployments.addIfAbsent(application)) { // Avoid queuing multiple deployments for same application
- deploymentExecutor.execute(() -> deployWithLock(application, reason));
+ deploymentExecutor.execute(() -> deployNow(application, reason));
}
}
- protected Deployer deployer() { return deployer; }
-
- /** Returns the applications that should be maintained by this now. */
- protected abstract Map<ApplicationId, String> applicationsNeedingMaintenance();
-
/**
* Redeploy this application. A lock will be taken for the duration of the deployment activation
*/
- protected final void deployWithLock(ApplicationId application, String reason) {
+ protected final void deployNow(ApplicationId application, String reason) {
try (MaintenanceDeployment deployment = new MaintenanceDeployment(application, deployer, metric, nodeRepository())) {
if ( ! deployment.isValid()) return; // this will be done at another config server
if ( ! canDeployNow(application)) return; // redeployment is no longer needed
@@ -95,7 +95,7 @@ public abstract class ApplicationMaintainer extends NodeRepositoryMaintainer {
@Override
public void shutdown() {
super.shutdown();
- this.deploymentExecutor.shutdownNow();
+ deploymentExecutor.shutdownNow();
}
@Override
@@ -103,7 +103,9 @@ public abstract class ApplicationMaintainer extends NodeRepositoryMaintainer {
super.awaitShutdown();
try {
// Give deployments in progress some time to complete
- this.deploymentExecutor.awaitTermination(1, TimeUnit.MINUTES);
+ if (!deploymentExecutor.awaitTermination(1, TimeUnit.MINUTES)) {
+ log.log(Level.WARNING, "Failed to shut down deployment executor within deadline");
+ }
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/AutoscalingMaintainer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/AutoscalingMaintainer.java
index 69c03dbf6dc..44944af15d7 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/AutoscalingMaintainer.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/AutoscalingMaintainer.java
@@ -6,7 +6,6 @@ import com.yahoo.config.provision.ApplicationLockException;
import com.yahoo.config.provision.ClusterResources;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.Deployer;
-import com.yahoo.config.provision.Environment;
import com.yahoo.jdisc.Metric;
import com.yahoo.vespa.hosted.provision.Node;
import com.yahoo.vespa.hosted.provision.NodeList;
@@ -19,6 +18,7 @@ import com.yahoo.vespa.hosted.provision.autoscale.Autoscaler;
import com.yahoo.vespa.hosted.provision.autoscale.Autoscaling;
import com.yahoo.vespa.hosted.provision.autoscale.NodeMetricSnapshot;
import com.yahoo.vespa.hosted.provision.node.History;
+
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
@@ -68,6 +68,7 @@ public class AutoscalingMaintainer extends NodeRepositoryMaintainer {
* @return true if an autoscaling decision was made or nothing should be done, false if there was an error
*/
private boolean autoscale(ApplicationId applicationId, ClusterSpec.Id clusterId) {
+ boolean redeploy = false;
try (var lock = nodeRepository().applications().lock(applicationId)) {
Optional<Application> application = nodeRepository().applications().get(applicationId);
if (application.isEmpty()) return true;
@@ -92,13 +93,9 @@ public class AutoscalingMaintainer extends NodeRepositoryMaintainer {
// Attempt to perform the autoscaling immediately, and log it regardless
if (autoscaling != null && autoscaling.resources().isPresent() && !current.equals(autoscaling.resources().get())) {
- try (MaintenanceDeployment deployment = new MaintenanceDeployment(applicationId, deployer, metric, nodeRepository())) {
- if (deployment.isValid())
- deployment.activate();
- logAutoscaling(current, autoscaling.resources().get(), applicationId, clusterNodes.not().retired());
- }
+ redeploy = true;
+ logAutoscaling(current, autoscaling.resources().get(), applicationId, clusterNodes.not().retired());
}
- return true;
}
catch (ApplicationLockException e) {
return false;
@@ -106,6 +103,13 @@ public class AutoscalingMaintainer extends NodeRepositoryMaintainer {
catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Illegal arguments for " + applicationId + " cluster " + clusterId, e);
}
+ if (redeploy) {
+ try (MaintenanceDeployment deployment = new MaintenanceDeployment(applicationId, deployer, metric, nodeRepository())) {
+ if (deployment.isValid())
+ deployment.activate();
+ }
+ }
+ return true;
}
private Applications applications() {
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ExpeditedChangeApplicationMaintainer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ExpeditedChangeApplicationMaintainer.java
index 2f3cea8ca64..ffa125230a8 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ExpeditedChangeApplicationMaintainer.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ExpeditedChangeApplicationMaintainer.java
@@ -63,7 +63,7 @@ public class ExpeditedChangeApplicationMaintainer extends ApplicationMaintainer
*/
@Override
protected void deploy(ApplicationId application, String reason) {
- deployWithLock(application, reason);
+ deployNow(application, reason);
}
/** Returns the reason for doing an expedited deploy. */
@@ -73,11 +73,11 @@ public class ExpeditedChangeApplicationMaintainer extends ApplicationMaintainer
List<String> reasons = nodes.stream()
.flatMap(node -> node.history()
- .events()
- .stream()
- .filter(event -> expediteChangeBy(event.agent()))
- .filter(event -> lastDeployTime.get().isBefore(event.at()))
- .map(event -> event.type() + (event.agent() == Agent.system ? "" : " by " + event.agent())))
+ .events()
+ .stream()
+ .filter(event -> expediteChangeBy(event.agent()))
+ .filter(event -> lastDeployTime.get().isBefore(event.at()))
+ .map(event -> event.type() + (event.agent() == Agent.system ? "" : " by " + event.agent())))
.sorted()
.distinct()
.toList();
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/MaintenanceDeployment.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/MaintenanceDeployment.java
index bcc571355e3..189238a1c11 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/MaintenanceDeployment.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/MaintenanceDeployment.java
@@ -1,8 +1,8 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.provision.maintenance;
+import com.yahoo.concurrent.UncheckedTimeoutException;
import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ApplicationLockException;
import com.yahoo.config.provision.Deployer;
import com.yahoo.config.provision.Deployment;
import com.yahoo.config.provision.TransientException;
@@ -16,7 +16,6 @@ import com.yahoo.vespa.hosted.provision.node.Agent;
import com.yahoo.yolean.Exceptions;
import java.io.Closeable;
-import java.time.Duration;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -63,18 +62,12 @@ class MaintenanceDeployment implements Closeable {
return deployment.isPresent();
}
- /**
- * Returns the application lock held by this, or empty if it is not held.
- *
- * @throws IllegalStateException id this is called when closed
- */
- public Optional<Mutex> applicationLock() {
- if (closed) throw new IllegalStateException(this + " is closed");
- return lock;
- }
-
+ /** Prepare this deployment. Returns whether prepare was successful */
public boolean prepare() {
- return doStep(() -> { deployment.get().prepare(); return 0L; }).isPresent();
+ return doStep(() -> {
+ deployment.get().prepare();
+ return 0L;
+ }).isPresent();
}
/**
@@ -104,13 +97,10 @@ class MaintenanceDeployment implements Closeable {
}
private Optional<Mutex> tryLock(ApplicationId application, NodeRepository nodeRepository) {
- Duration timeout = Duration.ofSeconds(3);
try {
- // Use a short lock to avoid interfering with change deployments
- return Optional.of(nodeRepository.applications().lock(application, timeout));
- }
- catch (ApplicationLockException e) {
- log.log(Level.INFO, () -> "Could not lock " + application + " for maintenance deployment within " + timeout);
+ return Optional.of(nodeRepository.applications().lockMaintenance(application));
+ } catch (UncheckedTimeoutException e) {
+ log.log(Level.INFO, () -> "Could not lock " + application + " for maintenance deployment within timeout");
return Optional.empty();
}
}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/PeriodicApplicationMaintainer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/PeriodicApplicationMaintainer.java
index 036c46479d1..10a828c887a 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/PeriodicApplicationMaintainer.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/PeriodicApplicationMaintainer.java
@@ -47,13 +47,12 @@ public class PeriodicApplicationMaintainer extends ApplicationMaintainer {
.orElse(false);
}
- // Returns the applications that need to be redeployed by this config server at this point in time.
@Override
protected Map<ApplicationId, String> applicationsNeedingMaintenance() {
if (deployer().bootstrapping()) return Map.of();
// Collect all deployment times before sorting as deployments may happen while we build the set, breaking
- // the comparable contract. Stale times are fine as the time is rechecked in ApplicationMaintainer#deployWithLock
+ // the comparable contract. Stale times are fine as the time is rechecked in ApplicationMaintainer#deployNow
Map<ApplicationId, Instant> deploymentTimes = nodesNeedingMaintenance().stream()
.map(node -> node.allocation().get().owner())
.distinct()
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/RetiredExpirer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/RetiredExpirer.java
index 87af8c05b14..1ae9b00d794 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/RetiredExpirer.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/RetiredExpirer.java
@@ -14,9 +14,10 @@ import com.yahoo.vespa.orchestrator.OrchestrationException;
import com.yahoo.yolean.Exceptions;
import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
-import java.util.OptionalLong;
/**
* Maintenance job which deactivates retired nodes, if given permission by orchestrator, or
@@ -47,40 +48,55 @@ public class RetiredExpirer extends NodeRepositoryMaintainer {
protected double maintain() {
int attempts = 0;
int successes = 0;
+ List<ApplicationId> applicationsWithRetiredNodes = nodeRepository().nodes().list(Node.State.active)
+ .retired()
+ .stream()
+ .map(node -> node.allocation().get().owner())
+ .distinct()
+ .toList();
+ for (var application : applicationsWithRetiredNodes) {
+ attempts++;
+ if (removeRetiredNodes(application)) {
+ successes++;
+ }
+ }
+ return attempts == 0 ? 1.0 : ((double)successes / attempts);
+ }
- NodeList activeNodes = nodeRepository().nodes().list(Node.State.active);
- Map<ApplicationId, NodeList> retiredNodesByApplication = activeNodes.retired().groupingBy(node -> node.allocation().get().owner());
- for (Map.Entry<ApplicationId, NodeList> entry : retiredNodesByApplication.entrySet()) {
- ApplicationId application = entry.getKey();
- NodeList retiredNodes = entry.getValue();
- Map<Removal, NodeList> nodesByRemovalReason = retiredNodes.groupingBy(node -> removalOf(node, activeNodes));
- if (nodesByRemovalReason.isEmpty()) continue;
-
- for (var kv : nodesByRemovalReason.entrySet()) {
- Removal removal = kv.getKey();
- if (removal.equals(Removal.none())) continue;
-
- NodeList nodes = kv.getValue();
- attempts++;
- try (MaintenanceDeployment deployment = new MaintenanceDeployment(application, deployer, metric, nodeRepository())) {
- if (!deployment.isValid()) {
- log.info("Skipping invalid deployment for " + application);
- continue;
- }
-
- nodeRepository().nodes().setRemovable(application, nodes.asList(), removal.isReusable());
- Optional<Long> session = deployment.activate();
- String nodeList = String.join(", ", nodes.mapToList(Node::hostname));
- if (session.isEmpty()) {
- log.info("Failed to redeploy " + application);
- continue;
- }
- log.info("Redeployed " + application + " at session " + session.get() + " to deactivate retired nodes: " + nodeList);
- successes++;
+ /** Mark retired nodes as removable and redeploy application */
+ private boolean removeRetiredNodes(ApplicationId application) {
+ try (MaintenanceDeployment deployment = new MaintenanceDeployment(application, deployer, metric, nodeRepository())) {
+ if (!deployment.isValid()) {
+ log.info("Skipping invalid deployment for " + application);
+ return false;
+ }
+ boolean redeploy = false;
+ List<String> nodesToDeactivate = new ArrayList<>();
+ try (var lock = nodeRepository().applications().lock(application)) {
+ NodeList activeNodes = nodeRepository().nodes().list(Node.State.active);
+ Map<Removal, NodeList> nodesByRemovalReason = activeNodes.owner(application)
+ .retired()
+ .groupingBy(node -> removalOf(node, activeNodes));
+ for (var kv : nodesByRemovalReason.entrySet()) {
+ Removal reason = kv.getKey();
+ if (reason.equals(Removal.none())) continue;
+ redeploy = true;
+ nodesToDeactivate.addAll(kv.getValue().hostnames());
+ nodeRepository().nodes().setRemovable(kv.getValue(), reason.isReusable());
}
}
+ if (!redeploy) {
+ return true;
+ }
+ Optional<Long> session = deployment.activate();
+ String nodeList = String.join(", ", nodesToDeactivate);
+ if (session.isEmpty()) {
+ log.info("Failed to redeploy " + application);
+ return false;
+ }
+ log.info("Redeployed " + application + " at session " + session.get() + " to deactivate retired nodes: " + nodeList);
+ return true;
}
- return attempts == 0 ? 1.0 : ((double)successes / attempts);
}
/**
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/SpareCapacityMaintainer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/SpareCapacityMaintainer.java
index 5ce88346178..dcdcbf09175 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/SpareCapacityMaintainer.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/SpareCapacityMaintainer.java
@@ -8,6 +8,7 @@ import com.yahoo.config.provision.NodeResources;
import com.yahoo.jdisc.Metric;
import com.yahoo.vespa.hosted.provision.Node;
import com.yahoo.vespa.hosted.provision.NodeList;
+import com.yahoo.vespa.hosted.provision.NodeMutex;
import com.yahoo.vespa.hosted.provision.NodeRepository;
import com.yahoo.vespa.hosted.provision.maintenance.MaintenanceDeployment.Move;
import com.yahoo.vespa.hosted.provision.node.Agent;
@@ -49,9 +50,7 @@ public class SpareCapacityMaintainer extends NodeRepositoryMaintainer {
NodeRepository nodeRepository,
Metric metric,
Duration interval) {
- this(deployer, nodeRepository, metric, interval,
- 10_000 // Should take less than a few minutes
- );
+ this(deployer, nodeRepository, metric, interval, 10_000 /* Should take less than a few minutes */);
}
public SpareCapacityMaintainer(Deployer deployer,
@@ -160,22 +159,32 @@ public class SpareCapacityMaintainer extends NodeRepositoryMaintainer {
.filter(node -> node.state() == Node.State.active)
.min(this::retireOvercomittedComparator);
if (nodeToRetire.isEmpty()) return;
+ retire(nodeToRetire.get());
+ }
- ApplicationId application = nodeToRetire.get().allocation().get().owner();
- try (MaintenanceDeployment deployment = new MaintenanceDeployment(application, deployer, metric, nodeRepository())) {
- if ( ! deployment.isValid()) return;
-
- Optional<Node> nodeWithWantToRetire = nodeRepository().nodes().node(nodeToRetire.get().hostname())
- .map(node -> node.withWantToRetire(true, Agent.SpareCapacityMaintainer, nodeRepository().clock().instant()));
- if (nodeWithWantToRetire.isEmpty()) return;
-
- nodeRepository().nodes().write(nodeWithWantToRetire.get(), deployment.applicationLock().get());
- log.log(Level.INFO, String.format("Redeploying %s to move %s from overcommitted host",
- application, nodeToRetire.get().hostname()));
+ /** Mark node for retirement and redeploy its application */
+ private void retire(Node node) {
+ ApplicationId owner = node.allocation().get().owner();
+ try (MaintenanceDeployment deployment = new MaintenanceDeployment(owner, deployer, metric, nodeRepository())) {
+ if (!deployment.isValid()) return;
+ if (!markWantToRetire(node.hostname())) return;
+ log.log(Level.INFO, String.format("Redeploying %s to move %s from over-committed host",
+ owner, node.hostname()));
deployment.activate();
}
}
+ private boolean markWantToRetire(String hostname) {
+ Optional<NodeMutex> optionalNodeMutex = nodeRepository().nodes().lockAndGet(hostname);
+ if (optionalNodeMutex.isEmpty()) return false;
+ try (var nodeMutex = optionalNodeMutex.get()) {
+ Node retiredNode = nodeMutex.node().withWantToRetire(true, Agent.SpareCapacityMaintainer,
+ nodeRepository().clock().instant());
+ nodeRepository().nodes().write(retiredNode, nodeMutex);
+ return true;
+ }
+ }
+
private static class CapacitySolver {
private final HostCapacity hostCapacity;
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/Nodes.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/Nodes.java
index bb3d288e555..10f0c8aa554 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/Nodes.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/Nodes.java
@@ -203,17 +203,12 @@ public class Nodes {
/**
* Sets a list of nodes to have their allocation removable (active to inactive) in the node repository.
*
- * @param application the application the nodes belong to
* @param nodes the nodes to make removable. These nodes MUST be in the active state
* @param reusable move the node directly to {@link Node.State#dirty} after removal
*/
- public void setRemovable(ApplicationId application, List<Node> nodes, boolean reusable) {
- try (Mutex lock = applications.lock(application)) {
- List<Node> removableNodes = nodes.stream()
- .map(node -> node.with(node.allocation().get().removable(true, reusable)))
- .toList();
- write(removableNodes, lock);
- }
+ public void setRemovable(NodeList nodes, boolean reusable) {
+ performOn(nodes, (node, mutex) -> write(node.with(node.allocation().get().removable(true, reusable)),
+ mutex));
}
/**
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDb.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDb.java
index c1ab8489f40..cec413cf4e3 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDb.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDb.java
@@ -391,6 +391,11 @@ public class CuratorDb {
return db.lock(lockPath.append("archiveUris"), defaultLockTimeout);
}
+ public Lock lockMaintenance(ApplicationId application) {
+ return db.lock(lockPath.append("maintenanceDeployment").append(application.serializedForm()),
+ Duration.ofSeconds(3));
+ }
+
// Load balancers -----------------------------------------------------------
public List<LoadBalancerId> readLoadBalancerIds() {
diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/HostCapacityMaintainerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/HostCapacityMaintainerTest.java
index 880a69b61e5..e075995c89e 100644
--- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/HostCapacityMaintainerTest.java
+++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/HostCapacityMaintainerTest.java
@@ -402,8 +402,8 @@ public class HostCapacityMaintainerTest {
// Config server becomes removable (done by RetiredExpirer in a real system) and redeployment moves it
// to parked
int removedIndex = nodeToRemove.get().allocation().get().membership().index();
- tester.nodeRepository().nodes().setRemovable(configSrvApp, List.of(nodeToRemove.get()), true);
- tester.nodeRepository().nodes().setRemovable(hostApp, List.of(hostToRemove.get()), true);
+ tester.nodeRepository().nodes().setRemovable(NodeList.of(nodeToRemove.get()), true);
+ tester.nodeRepository().nodes().setRemovable(NodeList.of(hostToRemove.get()), true);
tester.prepareAndActivateInfraApplication(configSrvApp, hostType.childNodeType());
tester.prepareAndActivateInfraApplication(hostApp, hostType);
tester.nodeRepository().nodes().markNodeAvailableForNewAllocation(nodeToRemove.get().hostname(), Agent.operator, "Readied by host-admin");
diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/os/OsVersionsTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/os/OsVersionsTest.java
index b5735cfae84..98c17eb4d5e 100644
--- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/os/OsVersionsTest.java
+++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/os/OsVersionsTest.java
@@ -515,9 +515,9 @@ public class OsVersionsTest {
private void replaceNodes(ApplicationId application) {
// Deploy to retire nodes
deployApplication(application);
- List<Node> retired = tester.nodeRepository().nodes().list().owner(application).retired().asList();
+ NodeList retired = tester.nodeRepository().nodes().list().owner(application).retired();
assertFalse("At least one node is retired", retired.isEmpty());
- tester.nodeRepository().nodes().setRemovable(application, retired, false);
+ tester.nodeRepository().nodes().setRemovable(retired, false);
// Redeploy to deactivate removable nodes and allocate new ones
deployApplication(application);
diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/InfraDeployerImplTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/InfraDeployerImplTest.java
index 9a38cbbba44..9cd5adef5f4 100644
--- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/InfraDeployerImplTest.java
+++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/InfraDeployerImplTest.java
@@ -10,8 +10,8 @@ import com.yahoo.config.provision.HostSpec;
import com.yahoo.config.provision.NodeType;
import com.yahoo.config.provision.Provisioner;
import com.yahoo.config.provision.Zone;
-import com.yahoo.vespa.flags.InMemoryFlagSource;
import com.yahoo.vespa.hosted.provision.Node;
+import com.yahoo.vespa.hosted.provision.NodeList;
import com.yahoo.vespa.hosted.provision.NodeRepository;
import com.yahoo.vespa.hosted.provision.NodeRepositoryTester;
import com.yahoo.vespa.hosted.provision.maintenance.InfrastructureVersions;
@@ -122,7 +122,7 @@ public class InfraDeployerImplTest {
addNode(5, Node.State.dirty, Optional.empty());
addNode(6, Node.State.ready, Optional.empty());
Node node7 = addNode(7, Node.State.active, Optional.of(target));
- nodeRepository.nodes().setRemovable(application.getApplicationId(), List.of(node7), false);
+ nodeRepository.nodes().setRemovable(NodeList.of(node7), false);
infraDeployer.getDeployment(application.getApplicationId()).orElseThrow().activate();
diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/VirtualNodeProvisioningTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/VirtualNodeProvisioningTest.java
index c1c4630f431..fb773f19b8a 100644
--- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/VirtualNodeProvisioningTest.java
+++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/VirtualNodeProvisioningTest.java
@@ -636,7 +636,7 @@ public class VirtualNodeProvisioningTest {
tester.activate(app1, cluster1, Capacity.from(new ClusterResources(2, 1, r)));
// Deactivate any retired nodes - usually done by the RetiredExpirer
- tester.nodeRepository().nodes().setRemovable(app1, tester.getNodes(app1).retired().asList(), false);
+ tester.nodeRepository().nodes().setRemovable(tester.getNodes(app1).retired(), false);
tester.activate(app1, cluster1, Capacity.from(new ClusterResources(2, 1, r)));
if (expectedReuse) {