aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@vespa.ai>2023-07-09 19:14:43 +0200
committerJon Bratseth <bratseth@vespa.ai>2023-07-09 19:14:43 +0200
commitc6a0d441b4493f4cdc8a8d3e8cb221f070dfc305 (patch)
tree7ff24788826b5a5a166b4c5c2759f764165a17f6
parent3168abdd33e16054275719f1c33f3fd474413eac (diff)
Allocate all groups in one go
With many groups and dynamic allocation allocating group by group is too slow.
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/ClusterMembership.java4
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/ClusterSpec.java13
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/NodeAllocationException.java5
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/LockedNodeList.java2
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/HostCapacityMaintainer.java6
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Activator.java18
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupIndices.java163
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java19
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeAllocation.java89
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidate.java31
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodePrioritizer.java9
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java6
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java57
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java57
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/NodeListMicroBenchmarkTest.java1
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicAllocationTest.java1
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicProvisioningTest.java2
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/InPlaceResizeProvisionTest.java14
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidateTest.java24
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTest.java30
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/VirtualNodeProvisioningTest.java8
-rw-r--r--vespajlib/src/main/java/com/yahoo/yolean/Exceptions.java2
22 files changed, 346 insertions, 215 deletions
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterMembership.java b/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterMembership.java
index 9e8388b6442..36f77a179ca 100644
--- a/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterMembership.java
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterMembership.java
@@ -44,7 +44,7 @@ public class ClusterMembership {
this.cluster = ClusterSpec.specification(ClusterSpec.Type.valueOf(components[0]),
ClusterSpec.Id.from(components[1]))
- .group(ClusterSpec.Group.from(Integer.parseInt(components[2])))
+ .group(components[2].isEmpty() ? null : ClusterSpec.Group.from(Integer.parseInt(components[2])))
.vespaVersion(vespaVersion)
.exclusive(exclusive)
.combinedId(combinedId.map(ClusterSpec.Id::from))
@@ -67,7 +67,7 @@ public class ClusterMembership {
protected String toStringValue() {
return cluster.type().name() +
"/" + cluster.id().value() +
- (cluster.group().isPresent() ? "/" + cluster.group().get().index() : "") +
+ (cluster.group().isPresent() ? "/" + cluster.group().get().index() : "/") +
"/" + index +
( cluster.isExclusive() ? "/exclusive" : "") +
( retired ? "/retired" : "") +
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterSpec.java b/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterSpec.java
index ccc24e60edf..4a3045c9cdd 100644
--- a/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterSpec.java
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterSpec.java
@@ -102,19 +102,18 @@ public final class ClusterSpec {
/** Creates a ClusterSpec when requesting a cluster */
public static Builder request(Type type, Id id) {
- return new Builder(type, id, false);
+ return new Builder(type, id);
}
/** Creates a ClusterSpec for an existing cluster, group id and Vespa version needs to be set */
public static Builder specification(Type type, Id id) {
- return new Builder(type, id, true);
+ return new Builder(type, id);
}
public static class Builder {
private final Type type;
private final Id id;
- private final boolean specification;
private Optional<Group> groupId = Optional.empty();
private Optional<DockerImage> dockerImageRepo = Optional.empty();
@@ -124,19 +123,13 @@ public final class ClusterSpec {
private ZoneEndpoint zoneEndpoint = ZoneEndpoint.defaultEndpoint;
private boolean stateful;
- private Builder(Type type, Id id, boolean specification) {
+ private Builder(Type type, Id id) {
this.type = type;
this.id = id;
- this.specification = specification;
this.stateful = type.isContent(); // Default to true for content clusters
}
public ClusterSpec build() {
- if (specification) {
- if (groupId.isEmpty()) throw new IllegalArgumentException("groupId is required to be set when creating a ClusterSpec with specification()");
- if (vespaVersion == null) throw new IllegalArgumentException("vespaVersion is required to be set when creating a ClusterSpec with specification()");
- } else
- if (groupId.isPresent()) throw new IllegalArgumentException("groupId is not allowed to be set when creating a ClusterSpec with request()");
return new ClusterSpec(type, id, groupId, vespaVersion, exclusive, combinedId, dockerImageRepo, zoneEndpoint, stateful);
}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/NodeAllocationException.java b/config-provisioning/src/main/java/com/yahoo/config/provision/NodeAllocationException.java
index 507d95c1d7b..64d028db7b0 100644
--- a/config-provisioning/src/main/java/com/yahoo/config/provision/NodeAllocationException.java
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/NodeAllocationException.java
@@ -16,6 +16,11 @@ public class NodeAllocationException extends RuntimeException {
this.retryable = retryable;
}
+ public NodeAllocationException(String message, Throwable cause, boolean retryable) {
+ super(message, cause);
+ this.retryable = retryable;
+ }
+
public boolean retryable() {
return retryable;
}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/LockedNodeList.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/LockedNodeList.java
index 9bc18533ddf..e760e36f90b 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/LockedNodeList.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/LockedNodeList.java
@@ -24,7 +24,7 @@ public final class LockedNodeList extends NodeList {
this.lock = Objects.requireNonNull(lock, "lock must be non-null");
}
- /** Returns a new LockedNodeList with the for the same lock. */
+ /** Returns a new LockedNodeList with the same lock. */
public LockedNodeList childList(List<Node> nodes) {
return new LockedNodeList(nodes, lock);
}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/HostCapacityMaintainer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/HostCapacityMaintainer.java
index 2a0b4f02b20..331759127e4 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/HostCapacityMaintainer.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/HostCapacityMaintainer.java
@@ -268,11 +268,9 @@ public class HostCapacityMaintainer extends NodeRepositoryMaintainer {
// build() requires a version, even though it is not (should not be) used
.vespaVersion(Vtag.currentVersion)
.build();
- NodeSpec nodeSpec = NodeSpec.from(clusterCapacity.count(), nodeResources, false, true,
+ NodeSpec nodeSpec = NodeSpec.from(clusterCapacity.count(), 1, nodeResources, false, true,
nodeRepository().zone().cloud().account(), Duration.ZERO);
- int wantedGroups = 1;
-
- NodePrioritizer prioritizer = new NodePrioritizer(allNodes, applicationId, clusterSpec, nodeSpec, wantedGroups,
+ NodePrioritizer prioritizer = new NodePrioritizer(allNodes, applicationId, clusterSpec, nodeSpec,
true, nodeRepository().nameResolver(), nodeRepository().nodes(), nodeRepository().resourcesCalculator(),
nodeRepository().spareCount(), nodeSpec.cloudAccount().isExclave(nodeRepository().zone()));
List<NodeCandidate> nodeCandidates = prioritizer.collect(List.of());
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Activator.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Activator.java
index c25f33bc8c2..462f43ee7af 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Activator.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Activator.java
@@ -14,7 +14,6 @@ import com.yahoo.vespa.hosted.provision.NodeMutex;
import com.yahoo.vespa.hosted.provision.NodeRepository;
import com.yahoo.vespa.hosted.provision.applications.Application;
import com.yahoo.vespa.hosted.provision.applications.ScalingEvent;
-import com.yahoo.vespa.hosted.provision.autoscale.Autoscaling;
import com.yahoo.vespa.hosted.provision.node.Agent;
import com.yahoo.vespa.hosted.provision.node.Allocation;
@@ -71,8 +70,9 @@ class Activator {
NodeList allNodes = nodeRepository.nodes().list();
NodeList applicationNodes = allNodes.owner(application);
- NodeList reserved = updatePortsFrom(hosts, applicationNodes.state(Node.State.reserved)
- .matching(node -> hostnames.contains(node.hostname())));
+ NodeList reserved = applicationNodes.state(Node.State.reserved).matching(node -> hostnames.contains(node.hostname()));
+ reserved = updatePortsFrom(hosts, reserved);
+ reserved = updateGroupFrom(hosts, reserved);
nodeRepository.nodes().reserve(reserved.asList()); // Re-reserve nodes to avoid reservation expiry
NodeList oldActive = applicationNodes.state(Node.State.active); // All nodes active now
@@ -240,6 +240,18 @@ class Activator {
return NodeList.copyOf(updated);
}
+ /**
+ * Reserved nodes are stored before they are assigned a group.
+ */
+ private NodeList updateGroupFrom(Collection<HostSpec> hosts, NodeList nodes) {
+ List<Node> updated = new ArrayList<>();
+ for (Node node : nodes) {
+ var membership = getHost(node.hostname(), hosts).membership().get();
+ updated.add(node.with(node.allocation().get().with(membership)));
+ }
+ return NodeList.copyOf(updated);
+ }
+
private HostSpec getHost(String hostname, Collection<HostSpec> fromHosts) {
for (HostSpec host : fromHosts)
if (host.hostname().equals(hostname))
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupIndices.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupIndices.java
new file mode 100644
index 00000000000..44f371be293
--- /dev/null
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupIndices.java
@@ -0,0 +1,163 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.provision.provisioning;
+
+import com.yahoo.config.provision.ClusterSpec;
+import com.yahoo.config.provision.Flavor;
+import com.yahoo.config.provision.NodeResources;
+import com.yahoo.vespa.hosted.provision.Node;
+import com.yahoo.vespa.hosted.provision.NodeList;
+import com.yahoo.vespa.hosted.provision.node.Agent;
+
+import java.time.Clock;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Knows how to assign a group index to a number of nodes (some of which have an index already),
+ * such that the nodes are placed in the desired groups with minimal group movement.
+ *
+ * @author bratseth
+ */
+class GroupIndices {
+
+ private final NodeSpec requested;
+ private final NodeList allNodes;
+ private final Clock clock;
+
+ GroupIndices(NodeSpec requested, NodeList allNodes, Clock clock) {
+ if (requested.groups() > 1 && requested.count().isEmpty())
+ throw new IllegalArgumentException("Unlimited nodes cannot be grouped");
+ this.requested = requested;
+ this.allNodes = allNodes;
+ this.clock = clock;
+ }
+
+ Collection<NodeCandidate> assignTo(Collection<NodeCandidate> nodes) {
+ int[] countInGroup = countInEachGroup(nodes);
+ nodes = byUnretiringPriority(nodes).stream().map(node -> unretireNodeInExpandedGroup(node, countInGroup)).toList();
+ nodes = nodes.stream().map(node -> assignGroupToNewNode(node, countInGroup)).toList();
+ nodes = byUnretiringPriority(nodes).stream().map(node -> moveNodeInSurplusGroup(node, countInGroup)).toList();
+ nodes = byRetiringPriority(nodes).stream().map(node -> retireSurplusNodeInGroup(node, countInGroup)).toList();
+ nodes = nodes.stream().filter(node -> ! shouldRemove(node)).toList();
+ return nodes;
+ }
+
+ /** Prefer to retire nodes we want the least */
+ private List<NodeCandidate> byRetiringPriority(Collection<NodeCandidate> candidates) {
+ return candidates.stream().sorted(Comparator.reverseOrder()).toList();
+ }
+
+ /** Prefer to unretire nodes we don't want to retire, and otherwise those with lower index */
+ private List<NodeCandidate> byUnretiringPriority(Collection<NodeCandidate> candidates) {
+ return candidates.stream()
+ .sorted(Comparator.comparing(NodeCandidate::wantToRetire)
+ .thenComparing(n -> n.allocation().get().membership().index()))
+ .toList();
+ }
+
+ private int[] countInEachGroup(Collection<NodeCandidate> nodes) {
+ int[] countInGroup = new int[requested.groups()];
+ for (var node : nodes) {
+ if (node.allocation().get().membership().retired()) continue;
+ var currentGroup = node.allocation().get().membership().cluster().group();
+ if (currentGroup.isEmpty()) continue;
+ if (currentGroup.get().index() >= requested.groups()) continue;
+ countInGroup[currentGroup.get().index()]++;
+ }
+ return countInGroup;
+ }
+
+ /** Assign a group to new or to be reactivated nodes. */
+ private NodeCandidate assignGroupToNewNode(NodeCandidate node, int[] countInGroup) {
+ if (node.state() == Node.State.active && node.allocation().get().membership().retired()) return node;
+ if (node.state() == Node.State.active && node.allocation().get().membership().cluster().group().isPresent()) return node;
+ return inFirstGroupWithDeficiency(node, countInGroup);
+ }
+
+ private NodeCandidate moveNodeInSurplusGroup(NodeCandidate node, int[] countInGroup) {
+ var currentGroup = node.allocation().get().membership().cluster().group();
+ if (currentGroup.isEmpty()) return node; // Shouldn't happen
+ if (currentGroup.get().index() < requested.groups()) return node;
+ return inFirstGroupWithDeficiency(node, countInGroup);
+ }
+
+ private NodeCandidate retireSurplusNodeInGroup(NodeCandidate node, int[] countInGroup) {
+ if (node.allocation().get().membership().retired()) return node;
+ var currentGroup = node.allocation().get().membership().cluster().group();
+ if (currentGroup.isEmpty()) return node;
+ if (currentGroup.get().index() >= requested.groups()) return node;
+ if (requested.count().isEmpty()) return node; // Can't retire
+ if (countInGroup[currentGroup.get().index()] <= requested.count().get() / requested.groups()) return node;
+ countInGroup[currentGroup.get().index()]--;
+ return node.withNode(node.toNode().retire(Agent.application, clock.instant()));
+ }
+
+ /** Unretire nodes that are already in the correct group when the group is deficient. */
+ private NodeCandidate unretireNodeInExpandedGroup(NodeCandidate node, int[] countInGroup) {
+ if ( ! node.allocation().get().membership().retired()) return node;
+ var currentGroup = node.allocation().get().membership().cluster().group();
+ if (currentGroup.isEmpty()) return node;
+ if (currentGroup.get().index() >= requested.groups()) return node;
+ if (node.preferToRetire() || node.wantToRetire()) return node;
+ if (requested.count().isPresent() && countInGroup[currentGroup.get().index()] >= requested.count().get() / requested.groups()) return node;
+ node = unretire(node);
+ if (node.allocation().get().membership().retired()) return node;
+ countInGroup[currentGroup.get().index()]++;
+ return node;
+ }
+
+ private NodeCandidate inFirstGroupWithDeficiency(NodeCandidate node, int[] countInGroup) {
+ for (int group = 0; group < requested.groups(); group++) {
+ if (requested.count().isEmpty() || countInGroup[group] < requested.count().get() / requested.groups()) {
+ return inGroup(group, node, countInGroup);
+ }
+ }
+ return node;
+ }
+
+ private boolean shouldRemove(NodeCandidate node) {
+ var currentGroup = node.allocation().get().membership().cluster().group();
+ if (currentGroup.isEmpty()) return true; // new and not assigned an index: Not needed
+ return currentGroup.get().index() >= requested.groups();
+ }
+
+ private NodeCandidate inGroup(int group, NodeCandidate node, int[] countInGroup) {
+ node = unretire(node);
+ if (node.allocation().get().membership().retired()) return node;
+ var membership = node.allocation().get().membership();
+ var currentGroup = membership.cluster().group();
+ countInGroup[group]++;
+ if ( ! currentGroup.isEmpty() && currentGroup.get().index() < requested.groups())
+ countInGroup[membership.cluster().group().get().index()]--;
+ return node.withNode(node.toNode().with(node.allocation().get().with(membership.with(membership.cluster().with(Optional.of(ClusterSpec.Group.from(group)))))));
+ }
+
+ /** Attempt to unretire the given node if it is retired. */
+ private NodeCandidate unretire(NodeCandidate node) {
+ if (node.retiredNow()) return node;
+ if ( ! node.allocation().get().membership().retired()) return node;
+ if ( ! hasCompatibleResources(node) ) return node;
+ var parent = node.parentHostname().flatMap(hostname -> allNodes.node(hostname));
+ if (parent.isPresent() && (parent.get().status().wantToRetire() || parent.get().status().preferToRetire())) return node;
+ node = node.withNode();
+ if ( ! requested.isCompatible(node.resources()))
+ node = node.withNode(resize(node.toNode()));
+ return node.withNode(node.toNode().unretire());
+ }
+
+ private Node resize(Node node) {
+ NodeResources hostResources = allNodes.parentOf(node).get().flavor().resources();
+ return node.with(new Flavor(requested.resources().get()
+ .with(hostResources.diskSpeed())
+ .with(hostResources.storageType())
+ .with(hostResources.architecture())),
+ Agent.application, clock.instant());
+ }
+
+ private boolean hasCompatibleResources(NodeCandidate candidate) {
+ return requested.isCompatible(candidate.resources()) || candidate.isResizable;
+ }
+
+}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java
index 0c4838abe4d..3c8f1b070e7 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java
@@ -61,14 +61,14 @@ public class GroupPreparer {
// but it may not change the set of active nodes, as the active nodes must stay in sync with the
// active config model which is changed on activate
public PrepareResult prepare(ApplicationId application, ClusterSpec cluster, NodeSpec requestedNodes,
- List<Node> surplusActiveNodes, NodeIndices indices, int wantedGroups,
+ List<Node> surplusActiveNodes, NodeIndices indices,
LockedNodeList allNodes) {
log.log(Level.FINE, () -> "Preparing " + cluster.type().name() + " " + cluster.id() + " with requested resources " +
requestedNodes.resources().orElse(NodeResources.unspecified()));
// Try preparing in memory without global unallocated lock. Most of the time there should be no changes,
// and we can return nodes previously allocated.
NodeAllocation probeAllocation = prepareAllocation(application, cluster, requestedNodes, surplusActiveNodes,
- indices::probeNext, wantedGroups, allNodes);
+ indices::probeNext, allNodes);
if (probeAllocation.fulfilledAndNoChanges()) {
List<Node> acceptedNodes = probeAllocation.finalNodes();
surplusActiveNodes.removeAll(acceptedNodes);
@@ -77,7 +77,7 @@ public class GroupPreparer {
} else {
// There were some changes, so re-do the allocation with locks
indices.resetProbe();
- List<Node> prepared = prepareWithLocks(application, cluster, requestedNodes, surplusActiveNodes, indices, wantedGroups);
+ List<Node> prepared = prepareWithLocks(application, cluster, requestedNodes, surplusActiveNodes, indices);
return new PrepareResult(prepared, createUnlockedNodeList());
}
}
@@ -87,12 +87,12 @@ public class GroupPreparer {
/// Note that this will write to the node repo.
private List<Node> prepareWithLocks(ApplicationId application, ClusterSpec cluster, NodeSpec requestedNodes,
- List<Node> surplusActiveNodes, NodeIndices indices, int wantedGroups) {
+ List<Node> surplusActiveNodes, NodeIndices indices) {
try (Mutex lock = nodeRepository.applications().lock(application);
Mutex allocationLock = nodeRepository.nodes().lockUnallocated()) {
LockedNodeList allNodes = nodeRepository.nodes().list(allocationLock);
NodeAllocation allocation = prepareAllocation(application, cluster, requestedNodes, surplusActiveNodes,
- indices::next, wantedGroups, allNodes);
+ indices::next, allNodes);
NodeType hostType = allocation.nodeType().hostType();
if (canProvisionDynamically(hostType) && allocation.hostDeficit().isPresent()) {
HostSharing sharing = hostSharing(cluster, hostType);
@@ -134,15 +134,13 @@ public class GroupPreparer {
// Non-dynamically provisioned zone with a deficit because we just now retired some nodes.
// Try again, but without retiring
indices.resetProbe();
- List<Node> accepted = prepareWithLocks(application, cluster, cns.withoutRetiring(), surplusActiveNodes, indices, wantedGroups);
+ List<Node> accepted = prepareWithLocks(application, cluster, cns.withoutRetiring(), surplusActiveNodes, indices);
log.warning("Prepared " + application + " " + cluster.id() + " without retirement due to lack of capacity");
return accepted;
}
if (! allocation.fulfilled() && requestedNodes.canFail())
- throw new NodeAllocationException((cluster.group().isPresent() ? "Node allocation failure on " + cluster.group().get()
- : "") + allocation.allocationFailureDetails(),
- true);
+ throw new NodeAllocationException(allocation.allocationFailureDetails(), true);
// Carry out and return allocation
nodeRepository.nodes().reserve(allocation.reservableNodes());
@@ -154,7 +152,7 @@ public class GroupPreparer {
}
private NodeAllocation prepareAllocation(ApplicationId application, ClusterSpec cluster, NodeSpec requestedNodes,
- List<Node> surplusActiveNodes, Supplier<Integer> nextIndex, int wantedGroups,
+ List<Node> surplusActiveNodes, Supplier<Integer> nextIndex,
LockedNodeList allNodes) {
NodeAllocation allocation = new NodeAllocation(allNodes, application, cluster, requestedNodes, nextIndex, nodeRepository);
@@ -162,7 +160,6 @@ public class GroupPreparer {
application,
cluster,
requestedNodes,
- wantedGroups,
nodeRepository.zone().cloud().dynamicProvisioning(),
nodeRepository.nameResolver(),
nodeRepository.nodes(),
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeAllocation.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeAllocation.java
index a2e0e59e329..7715dce3a6a 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeAllocation.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeAllocation.java
@@ -19,7 +19,6 @@ import com.yahoo.vespa.hosted.provision.node.Allocation;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedHashMap;
@@ -120,7 +119,6 @@ class NodeAllocation {
ClusterMembership membership = allocation.membership();
if ( ! allocation.owner().equals(application)) continue; // wrong application
if ( ! membership.cluster().satisfies(cluster)) continue; // wrong cluster id/type
- if ((! candidate.isSurplus || saturated()) && ! membership.cluster().group().equals(cluster.group())) continue; // wrong group, and we can't or have no reason to change it
if ( candidate.state() == Node.State.active && allocation.removable()) continue; // don't accept; causes removal
if ( candidate.state() == Node.State.active && candidate.wantToFail()) continue; // don't accept; causes failing
if ( indexes.contains(membership.index())) continue; // duplicate index (just to be sure)
@@ -175,6 +173,7 @@ class NodeAllocation {
if (candidate.preferToRetire() && candidate.replaceableBy(candidates)) return Retirement.softRequest;
if (violatesExclusivity(candidate)) return Retirement.violatesExclusivity;
if (requiredHostFlavor.isPresent() && ! candidate.parent.map(node -> node.flavor().name()).equals(requiredHostFlavor)) return Retirement.violatesHostFlavor;
+ if (candidate.violatesSpares) return Retirement.violatesSpares;
return Retirement.none;
}
@@ -243,12 +242,10 @@ class NodeAllocation {
*/
private boolean acceptIncompatible(NodeCandidate candidate) {
if (candidate.state() != Node.State.active) return false;
- if (! candidate.allocation().get().membership().cluster().group().equals(cluster.group())) return false;
if (candidate.allocation().get().membership().retired()) return true; // don't second-guess if already retired
if ( ! requestedNodes.considerRetiring()) // the node is active and we are not allowed to remove gracefully, so keep
return true;
-
return cluster.isStateful() ||
(cluster.type() == ClusterSpec.Type.container && !hasCompatibleResources(candidate));
}
@@ -259,7 +256,6 @@ class NodeAllocation {
private Node acceptNode(NodeCandidate candidate, Retirement retirement, boolean resizeable) {
Node node = candidate.toNode();
-
if (node.allocation().isPresent()) // Record the currently requested resources
node = node.with(node.allocation().get().withRequestedResources(requestedNodes.resources().orElse(node.resources())));
@@ -268,10 +264,11 @@ class NodeAllocation {
// We want to allocate new nodes rather than unretiring with resize, so count without those
// for the purpose of deciding when to stop accepting nodes (saturation)
if (node.allocation().isEmpty()
- || ! ( requestedNodes.needsResize(node) &&
- (node.allocation().get().membership().retired() || ! requestedNodes.considerRetiring()))) {
+ || (canBeUsedInGroupWithDeficiency(node) &&
+ ! ( requestedNodes.needsResize(node) && (node.allocation().get().membership().retired() || ! requestedNodes.considerRetiring())))) {
acceptedAndCompatible++;
}
+
if (hasCompatibleResources(candidate))
acceptedAndCompatibleOrResizable++;
@@ -289,15 +286,28 @@ class NodeAllocation {
node = node.retire(nodeRepository.clock().instant());
}
if ( ! node.allocation().get().membership().cluster().equals(cluster)) {
- // group may be different
- node = setCluster(cluster, node);
+ // Cluster has the updated settings but do not set a group
+ node = setCluster(cluster.with(node.allocation().get().membership().cluster().group()), node);
}
- candidate = candidate.withNode(node);
+ candidate = candidate.withNode(node, retirement != Retirement.none && retirement != Retirement.alreadyRetired );
indexes.add(node.allocation().get().membership().index());
nodes.put(node.hostname(), candidate);
return node;
}
+ private boolean canBeUsedInGroupWithDeficiency(Node node) {
+ if (requestedNodes.count().isEmpty()) return true;
+ if (node.allocation().isEmpty()) return true;
+ var group = node.allocation().get().membership().cluster().group();
+ if (group.isEmpty()) return true;
+ long nodesInGroup = nodes.values().stream().filter(n -> groupOf(n).equals(group)).count();
+ return nodesInGroup < requestedNodes.count().get() / requestedNodes.groups();
+ }
+
+ private Optional<ClusterSpec.Group> groupOf(NodeCandidate candidate) {
+ return candidate.allocation().flatMap(a -> a.membership().cluster().group());
+ }
+
private Node resize(Node node) {
NodeResources hostResources = allNodes.parentOf(node).get().flavor().resources();
return node.with(new Flavor(requestedNodes.resources().get()
@@ -391,52 +401,19 @@ class NodeAllocation {
return requestedNodes.type();
}
- /**
- * Make the number of <i>non-retired</i> nodes in the list equal to the requested number
- * of nodes, and retire the rest of the list. Only retire currently active nodes.
- * Prefer to retire nodes of the wrong flavor.
- * Make as few changes to the retired set as possible.
- *
- * @return the final list of nodes
- */
List<Node> finalNodes() {
- int wantToRetireCount = (int) matching(NodeCandidate::wantToRetire).count();
- int currentRetiredCount = (int) matching(node -> node.allocation().get().membership().retired()).count();
- int deltaRetiredCount = requestedNodes.idealRetiredCount(nodes.size(), wantToRetireCount, currentRetiredCount);
-
- if (deltaRetiredCount > 0) { // retire until deltaRetiredCount is 0
- for (NodeCandidate candidate : byRetiringPriority(nodes.values())) {
- if ( ! candidate.allocation().get().membership().retired() && candidate.state() == Node.State.active) {
- candidate = candidate.withNode();
- candidate = candidate.withNode(candidate.toNode().retire(Agent.application, nodeRepository.clock().instant()));
- nodes.put(candidate.toNode().hostname(), candidate);
- if (--deltaRetiredCount == 0) break;
- }
- }
- }
- else if (deltaRetiredCount < 0) { // unretire until deltaRetiredCount is 0
- for (NodeCandidate candidate : byUnretiringPriority(nodes.values())) {
- if (candidate.allocation().get().membership().retired() && hasCompatibleResources(candidate) ) {
- candidate = candidate.withNode();
- if (candidate.isResizable)
- candidate = candidate.withNode(resize(candidate.toNode()));
- candidate = candidate.withNode(candidate.toNode().unretire());
- nodes.put(candidate.toNode().hostname(), candidate);
- if (++deltaRetiredCount == 0) break;
- }
- }
- }
-
+ // Set whether the node is exclusive
for (NodeCandidate candidate : nodes.values()) {
- // Set whether the node is exclusive
candidate = candidate.withNode();
Allocation allocation = candidate.allocation().get();
candidate = candidate.withNode(candidate.toNode().with(allocation.with(allocation.membership()
- .with(allocation.membership().cluster().exclusive(cluster.isExclusive())))));
+ .with(allocation.membership().cluster().exclusive(cluster.isExclusive())))));
nodes.put(candidate.toNode().hostname(), candidate);
}
- return nodes.values().stream().map(NodeCandidate::toNode).toList();
+ GroupIndices groupIndices = new GroupIndices(requestedNodes, allNodes, nodeRepository.clock());
+ Collection<NodeCandidate> finalNodes = groupIndices.assignTo(nodes.values());
+ return finalNodes.stream().map(NodeCandidate::toNode).toList();
}
List<Node> reservableNodes() {
@@ -461,19 +438,6 @@ class NodeAllocation {
return allNodes.nodeType(nodeType()).size();
}
- /** Prefer to retire nodes we want the least */
- private List<NodeCandidate> byRetiringPriority(Collection<NodeCandidate> candidates) {
- return candidates.stream().sorted(Comparator.reverseOrder()).toList();
- }
-
- /** Prefer to unretire nodes we don't want to retire, and otherwise those with lower index */
- private List<NodeCandidate> byUnretiringPriority(Collection<NodeCandidate> candidates) {
- return candidates.stream()
- .sorted(Comparator.comparing(NodeCandidate::wantToRetire)
- .thenComparing(n -> n.allocation().get().membership().index()))
- .toList();
- }
-
String allocationFailureDetails() {
List<String> reasons = new ArrayList<>();
if (rejectedDueToExclusivity > 0)
@@ -486,7 +450,7 @@ class NodeAllocation {
reasons.add("insufficient real resources on hosts");
if (reasons.isEmpty()) return "";
- return ": Not enough suitable nodes available due to " + String.join(", ", reasons);
+ return "Not enough suitable nodes available due to " + String.join(", ", reasons);
}
private static Integer parseIndex(String hostname) {
@@ -510,6 +474,7 @@ class NodeAllocation {
violatesExclusivity("node violates host exclusivity"),
violatesHostFlavor("node violates host flavor"),
violatesHostFlavorGeneration("node violates host flavor generation"),
+ violatesSpares("node is assigned to a host we want to use as a spare"),
none("");
private final String description;
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidate.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidate.java
index 8462e23fbfd..adc04c491e2 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidate.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidate.java
@@ -81,6 +81,9 @@ public abstract class NodeCandidate implements Nodelike, Comparable<NodeCandidat
public abstract boolean preferToRetire();
+ /** Returns true if we have decided to retire this node as part of this deployment */
+ public boolean retiredNow() { return false; }
+
public abstract boolean wantToFail();
public abstract Flavor flavor();
@@ -217,7 +220,12 @@ public abstract class NodeCandidate implements Nodelike, Comparable<NodeCandidat
/** Returns a copy of this with node set to given value */
NodeCandidate withNode(Node node) {
- return new ConcreteNodeCandidate(node, freeParentCapacity, parent, violatesSpares, exclusiveSwitch, isSurplus, isNew, isResizable);
+ return withNode(node, retiredNow());
+ }
+
+ /** Returns a copy of this with node set to given value */
+ NodeCandidate withNode(Node node, boolean retiredNow) {
+ return new ConcreteNodeCandidate(node, retiredNow, freeParentCapacity, parent, violatesSpares, exclusiveSwitch, isSurplus, isNew, isResizable);
}
/** Returns the switch priority, based on switch exclusivity, of this compared to other */
@@ -260,7 +268,7 @@ public abstract class NodeCandidate implements Nodelike, Comparable<NodeCandidat
boolean isSurplus,
boolean isNew,
boolean isResizeable) {
- return new ConcreteNodeCandidate(node, freeParentCapacity, Optional.of(parent), violatesSpares, true, isSurplus, isNew, isResizeable);
+ return new ConcreteNodeCandidate(node, false, freeParentCapacity, Optional.of(parent), violatesSpares, true, isSurplus, isNew, isResizeable);
}
public static NodeCandidate createNewChild(NodeResources resources,
@@ -274,26 +282,33 @@ public abstract class NodeCandidate implements Nodelike, Comparable<NodeCandidat
}
public static NodeCandidate createNewExclusiveChild(Node node, Node parent) {
- return new ConcreteNodeCandidate(node, node.resources(), Optional.of(parent), false, true, false, true, false);
+ return new ConcreteNodeCandidate(node, false, node.resources(), Optional.of(parent), false, true, false, true, false);
}
public static NodeCandidate createStandalone(Node node, boolean isSurplus, boolean isNew) {
- return new ConcreteNodeCandidate(node, node.resources(), Optional.empty(), false, true, isSurplus, isNew, false);
+ return new ConcreteNodeCandidate(node, false, node.resources(), Optional.empty(), false, true, isSurplus, isNew, false);
}
/** A candidate backed by a node */
static class ConcreteNodeCandidate extends NodeCandidate {
private final Node node;
+ private final boolean retiredNow;
- ConcreteNodeCandidate(Node node, NodeResources freeParentCapacity, Optional<Node> parent,
+ ConcreteNodeCandidate(Node node,
+ boolean retiredNow,
+ NodeResources freeParentCapacity, Optional<Node> parent,
boolean violatesSpares, boolean exclusiveSwitch,
boolean isSurplus, boolean isNew, boolean isResizeable) {
super(freeParentCapacity, parent, violatesSpares, exclusiveSwitch, isSurplus, isNew, isResizeable);
+ this.retiredNow = retiredNow;
this.node = Objects.requireNonNull(node, "Node cannot be null");
}
@Override
+ public boolean retiredNow() { return retiredNow; }
+
+ @Override
public NodeResources resources() { return node.resources(); }
@Override
@@ -322,7 +337,7 @@ public abstract class NodeCandidate implements Nodelike, Comparable<NodeCandidat
@Override
public NodeCandidate allocate(ApplicationId owner, ClusterMembership membership, NodeResources requestedResources, Instant at) {
- return new ConcreteNodeCandidate(node.allocate(owner, membership, requestedResources, at),
+ return new ConcreteNodeCandidate(node.allocate(owner, membership, requestedResources, at), retiredNow,
freeParentCapacity, parent, violatesSpares, exclusiveSwitch, isSurplus, isNew, isResizable);
}
@@ -332,7 +347,7 @@ public abstract class NodeCandidate implements Nodelike, Comparable<NodeCandidat
@Override
public NodeCandidate withExclusiveSwitch(boolean exclusiveSwitch) {
- return new ConcreteNodeCandidate(node, freeParentCapacity, parent, violatesSpares, exclusiveSwitch,
+ return new ConcreteNodeCandidate(node, retiredNow, freeParentCapacity, parent, violatesSpares, exclusiveSwitch,
isSurplus, isNew, isResizable);
}
@@ -439,7 +454,7 @@ public abstract class NodeCandidate implements Nodelike, Comparable<NodeCandidat
NodeType.tenant)
.cloudAccount(parent.get().cloudAccount())
.build();
- return new ConcreteNodeCandidate(node, freeParentCapacity, parent, violatesSpares, exclusiveSwitch, isSurplus, isNew, isResizable);
+ return new ConcreteNodeCandidate(node, false, freeParentCapacity, parent, violatesSpares, exclusiveSwitch, isSurplus, isNew, isResizable);
}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodePrioritizer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodePrioritizer.java
index 4f21c8dcd50..9f00e5fdbba 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodePrioritizer.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodePrioritizer.java
@@ -46,7 +46,7 @@ public class NodePrioritizer {
private final boolean enclave;
public NodePrioritizer(LockedNodeList allNodes, ApplicationId application, ClusterSpec clusterSpec, NodeSpec nodeSpec,
- int wantedGroups, boolean dynamicProvisioning, NameResolver nameResolver, Nodes nodes,
+ boolean dynamicProvisioning, NameResolver nameResolver, Nodes nodes,
HostResourcesCalculator hostResourcesCalculator, int spareCount, boolean enclave) {
this.allNodes = allNodes;
this.calculator = hostResourcesCalculator;
@@ -70,12 +70,9 @@ public class NodePrioritizer {
.stream())
.distinct()
.count();
- this.topologyChange = currentGroups != wantedGroups;
+ this.topologyChange = currentGroups != requestedNodes.groups();
- this.currentClusterSize = (int) nonRetiredNodesInCluster.state(Node.State.active).stream()
- .map(node -> node.allocation().flatMap(alloc -> alloc.membership().cluster().group()))
- .filter(clusterSpec.group()::equals)
- .count();
+ this.currentClusterSize = (int) nonRetiredNodesInCluster.state(Node.State.active).stream().count();
// In dynamically provisioned zones, we can always take spare hosts since we can provision new on-demand,
// NodeCandidate::compareTo will ensure that they will not be used until there is no room elsewhere.
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java
index ad91bdef478..c29c51ccbd5 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java
@@ -96,19 +96,17 @@ public class NodeRepositoryProvisioner implements Provisioner {
validate(actual, target, cluster, application);
logIfDownscaled(requested.minResources().nodes(), actual.minResources().nodes(), cluster, logger);
- groups = target.groups();
resources = getNodeResources(cluster, target.nodeResources(), application);
- nodeSpec = NodeSpec.from(target.nodes(), resources, cluster.isExclusive(), actual.canFail(),
+ nodeSpec = NodeSpec.from(target.nodes(), target.groups(), resources, cluster.isExclusive(), actual.canFail(),
requested.cloudAccount().orElse(nodeRepository.zone().cloud().account()),
requested.clusterInfo().hostTTL());
}
else {
- groups = 1; // type request with multiple groups is not supported
cluster = cluster.withExclusivity(true);
resources = getNodeResources(cluster, requested.minResources().nodeResources(), application);
nodeSpec = NodeSpec.from(requested.type(), nodeRepository.zone().cloud().account());
}
- return asSortedHosts(preparer.prepare(application, cluster, nodeSpec, groups),
+ return asSortedHosts(preparer.prepare(application, cluster, nodeSpec),
requireCompatibleResources(resources, cluster));
}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java
index f32928a9ec4..f4b2c4ceee0 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java
@@ -35,21 +35,20 @@ public interface NodeSpec {
return fulfilledDeficitCount(count) == 0;
}
+ /** Returns the total number of nodes this is requesting, or empty if not specified */
+ Optional<Integer> count();
+
+ int groups();
+
/** Returns whether this should throw an exception if the requested nodes are not fully available */
boolean canFail();
/** Returns whether we should retire nodes at all when fulfilling this spec */
boolean considerRetiring();
- /** Returns the ideal number of nodes that should be retired to fulfill this spec */
- int idealRetiredCount(int acceptedCount, int wantToRetireCount, int currentRetiredCount);
-
/** Returns number of additional nodes needed for this spec to be fulfilled given the current node count */
int fulfilledDeficitCount(int count);
- /** Returns a specification of a fraction of all the nodes of this. It is assumed the argument is a valid divisor. */
- NodeSpec fraction(int divisor);
-
/** Returns the resources requested by this or empty if none are explicitly requested */
Optional<NodeResources> resources();
@@ -77,9 +76,9 @@ public interface NodeSpec {
return false;
}
- static NodeSpec from(int nodeCount, NodeResources resources, boolean exclusive, boolean canFail,
+ static NodeSpec from(int nodeCount, int groupCount, NodeResources resources, boolean exclusive, boolean canFail,
CloudAccount cloudAccount, Duration hostTTL) {
- return new CountNodeSpec(nodeCount, resources, exclusive, canFail, canFail, cloudAccount, hostTTL);
+ return new CountNodeSpec(nodeCount, groupCount, resources, exclusive, canFail, canFail, cloudAccount, hostTTL);
}
static NodeSpec from(NodeType type, CloudAccount cloudAccount) {
@@ -90,6 +89,7 @@ public interface NodeSpec {
class CountNodeSpec implements NodeSpec {
private final int count;
+ private final int groups;
private final NodeResources requestedNodeResources;
private final boolean exclusive;
private final boolean canFail;
@@ -97,9 +97,10 @@ public interface NodeSpec {
private final CloudAccount cloudAccount;
private final Duration hostTTL;
- private CountNodeSpec(int count, NodeResources resources, boolean exclusive, boolean canFail,
+ private CountNodeSpec(int count, int groups, NodeResources resources, boolean exclusive, boolean canFail,
boolean considerRetiring, CloudAccount cloudAccount, Duration hostTTL) {
this.count = count;
+ this.groups = groups;
this.requestedNodeResources = Objects.requireNonNull(resources, "Resources must be specified");
this.exclusive = exclusive;
this.canFail = canFail;
@@ -112,6 +113,12 @@ public interface NodeSpec {
}
@Override
+ public Optional<Integer> count() { return Optional.of(count); }
+
+ @Override
+ public int groups() { return groups; }
+
+ @Override
public Optional<NodeResources> resources() {
return Optional.of(requestedNodeResources);
}
@@ -136,22 +143,12 @@ public interface NodeSpec {
}
@Override
- public int idealRetiredCount(int acceptedCount, int wantToRetireCount, int currentRetiredCount) {
- return acceptedCount - this.count - currentRetiredCount;
- }
-
- @Override
public int fulfilledDeficitCount(int count) {
return Math.max(this.count - count, 0);
}
- @Override
- public NodeSpec fraction(int divisor) {
- return new CountNodeSpec(count/divisor, requestedNodeResources, exclusive, canFail, considerRetiring, cloudAccount, hostTTL);
- }
-
public NodeSpec withoutRetiring() {
- return new CountNodeSpec(count, requestedNodeResources, exclusive, canFail, false, cloudAccount, hostTTL);
+ return new CountNodeSpec(count, groups, requestedNodeResources, exclusive, canFail, false, cloudAccount, hostTTL);
}
@Override
@@ -163,7 +160,6 @@ public interface NodeSpec {
public boolean canResize(NodeResources currentNodeResources, NodeResources currentSpareHostResources,
ClusterSpec.Type type, boolean hasTopologyChange, int currentClusterSize) {
if (exclusive) return false; // exclusive resources must match the host
-
// Never allow in-place resize when also changing topology or decreasing cluster size
if (hasTopologyChange || count < currentClusterSize) return false;
@@ -192,7 +188,10 @@ public interface NodeSpec {
public Duration hostTTL() { return hostTTL; }
@Override
- public String toString() { return "request for " + count + " nodes with " + requestedNodeResources; }
+ public String toString() {
+ return "request for " + count + " nodes" +
+ ( groups > 1 ? " (in " + groups + " groups)" : "") +
+ " with " + requestedNodeResources; }
}
@@ -211,6 +210,12 @@ public interface NodeSpec {
}
@Override
+ public Optional<Integer> count() { return Optional.empty(); }
+
+ @Override
+ public int groups() { return 1; }
+
+ @Override
public NodeType type() { return type; }
@Override
@@ -226,20 +231,12 @@ public interface NodeSpec {
public boolean considerRetiring() { return true; }
@Override
- public int idealRetiredCount(int acceptedCount, int wantToRetireCount, int currentRetiredCount) {
- return wantToRetireCount - currentRetiredCount;
- }
-
- @Override
public int fulfilledDeficitCount(int count) {
// If no wanted count is specified for this node type, then any count fulfills the deficit
return Math.max(0, WANTED_NODE_COUNT.getOrDefault(type, 0) - count);
}
@Override
- public NodeSpec fraction(int divisor) { return this; }
-
- @Override
public Optional<NodeResources> resources() {
return Optional.empty();
}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java
index 42b9e53dd8a..25efcabfe8e 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java
@@ -9,8 +9,8 @@ import com.yahoo.vespa.hosted.provision.LockedNodeList;
import com.yahoo.vespa.hosted.provision.Node;
import com.yahoo.vespa.hosted.provision.NodeList;
import com.yahoo.vespa.hosted.provision.NodeRepository;
-import com.yahoo.yolean.Exceptions;
+import java.time.Clock;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
@@ -33,17 +33,15 @@ class Preparer {
}
/** Prepare all required resources for the given application and cluster */
- public List<Node> prepare(ApplicationId application, ClusterSpec cluster, NodeSpec requestedNodes, int wantedGroups) {
+ public List<Node> prepare(ApplicationId application, ClusterSpec cluster, NodeSpec requestedNodes) {
try {
- var nodes = prepareNodes(application, cluster, requestedNodes, wantedGroups);
+ var nodes = prepareNodes(application, cluster, requestedNodes);
prepareLoadBalancer(application, cluster, requestedNodes);
return nodes;
}
catch (NodeAllocationException e) {
- e.printStackTrace();
throw new NodeAllocationException("Could not satisfy " + requestedNodes +
- ( wantedGroups > 1 ? " (in " + wantedGroups + " groups)" : "") +
- " in " + application + " " + cluster + ": " + Exceptions.toMessageString(e),
+ " in " + application + " " + cluster, e,
e.retryable());
}
}
@@ -56,34 +54,29 @@ class Preparer {
// Note: This operation may make persisted changes to the set of reserved and inactive nodes,
// but it may not change the set of active nodes, as the active nodes must stay in sync with the
// active config model which is changed on activate
- private List<Node> prepareNodes(ApplicationId application, ClusterSpec cluster, NodeSpec requestedNodes,
- int wantedGroups) {
+ private List<Node> prepareNodes(ApplicationId application, ClusterSpec cluster, NodeSpec requestedNodes) {
LockedNodeList allNodes = groupPreparer.createUnlockedNodeList();
- NodeList appNodes = allNodes.owner(application);
- List<Node> surplusNodes = findNodesInRemovableGroups(appNodes, cluster, wantedGroups);
+ NodeList clusterNodes = allNodes.owner(application);
+ List<Node> surplusNodes = findNodesInRemovableGroups(clusterNodes, requestedNodes.groups());
- List<Integer> usedIndices = appNodes.cluster(cluster.id()).mapToList(node -> node.allocation().get().membership().index());
+ List<Integer> usedIndices = clusterNodes.mapToList(node -> node.allocation().get().membership().index());
NodeIndices indices = new NodeIndices(usedIndices);
List<Node> acceptedNodes = new ArrayList<>();
- for (int groupIndex = 0; groupIndex < wantedGroups; groupIndex++) {
- ClusterSpec clusterGroup = cluster.with(Optional.of(ClusterSpec.Group.from(groupIndex)));
- GroupPreparer.PrepareResult result = groupPreparer.prepare(application, clusterGroup,
- requestedNodes.fraction(wantedGroups),
- surplusNodes, indices, wantedGroups,
- allNodes);
- allNodes = result.allNodes(); // Might have changed
- List<Node> accepted = result.prepared();
- if (requestedNodes.rejectNonActiveParent()) {
- NodeList activeHosts = allNodes.state(Node.State.active).parents().nodeType(requestedNodes.type().hostType());
- accepted = accepted.stream()
- .filter(node -> node.parentHostname().isEmpty() || activeHosts.parentOf(node).isPresent())
- .toList();
- }
-
- replace(acceptedNodes, accepted);
+ GroupPreparer.PrepareResult result = groupPreparer.prepare(application, cluster,
+ requestedNodes,
+ surplusNodes, indices,
+ allNodes);
+ List<Node> accepted = result.prepared();
+ if (requestedNodes.rejectNonActiveParent()) {
+ NodeList activeHosts = result.allNodes().state(Node.State.active).parents().nodeType(requestedNodes.type().hostType());
+ accepted = accepted.stream()
+ .filter(node -> node.parentHostname().isEmpty() || activeHosts.parentOf(node).isPresent())
+ .toList();
}
- moveToActiveGroup(surplusNodes, wantedGroups, cluster.group());
+
+ replace(acceptedNodes, accepted);
+ moveToActiveGroup(surplusNodes, requestedNodes.groups(), cluster.group());
acceptedNodes.removeAll(surplusNodes);
return acceptedNodes;
}
@@ -97,18 +90,16 @@ class Preparer {
* Returns a list of the nodes which are
* in groups with index number above or equal the group count
*/
- private List<Node> findNodesInRemovableGroups(NodeList appNodes, ClusterSpec requestedCluster, int wantedGroups) {
+ private List<Node> findNodesInRemovableGroups(NodeList clusterNodes, int wantedGroups) {
List<Node> surplusNodes = new ArrayList<>();
- for (Node node : appNodes.state(Node.State.active)) {
+ for (Node node : clusterNodes.state(Node.State.active)) {
ClusterSpec nodeCluster = node.allocation().get().membership().cluster();
- if ( ! nodeCluster.id().equals(requestedCluster.id())) continue;
- if ( ! nodeCluster.type().equals(requestedCluster.type())) continue;
if (nodeCluster.group().get().index() >= wantedGroups)
surplusNodes.add(node);
}
return surplusNodes;
}
-
+
/** Move nodes from unwanted groups to wanted groups to avoid lingering groups consisting of retired nodes */
private void moveToActiveGroup(List<Node> surplusNodes, int wantedGroups, Optional<ClusterSpec.Group> targetGroup) {
for (ListIterator<Node> i = surplusNodes.listIterator(); i.hasNext(); ) {
diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/NodeListMicroBenchmarkTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/NodeListMicroBenchmarkTest.java
index 85338bdb2b4..deaee02f992 100644
--- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/NodeListMicroBenchmarkTest.java
+++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/NodeListMicroBenchmarkTest.java
@@ -58,7 +58,6 @@ public class NodeListMicroBenchmarkTest {
nodeList.childrenOf(nodes.get(indexes.get(i)));
}
Duration duration = Duration.between(start, Instant.now());
- System.out.println("Calling NodeList.childrenOf took " + duration + " (" + duration.toNanos() / iterations / 1000 + " microseconds per invocation)");
}
private List<Node> createHosts() {
diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicAllocationTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicAllocationTest.java
index 478b201d71b..8e9417008b9 100644
--- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicAllocationTest.java
+++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicAllocationTest.java
@@ -94,7 +94,6 @@ public class DynamicAllocationTest {
hostsWithChildren.add(node.parentHostname().get());
}
assertEquals(4 - spareCount, hostsWithChildren.size());
-
}
/**
diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicProvisioningTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicProvisioningTest.java
index c99728b714b..5539bb0cb6e 100644
--- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicProvisioningTest.java
+++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicProvisioningTest.java
@@ -334,7 +334,7 @@ public class DynamicProvisioningTest {
.flagSource(flagSource)
.build();
- ApplicationId app = ProvisioningTester.applicationId();
+ ApplicationId app = ProvisioningTester.applicationId("a1");
ClusterSpec cluster = ClusterSpec.request(ClusterSpec.Type.content, new ClusterSpec.Id("cluster1")).vespaVersion("8").build();
Capacity capacity = Capacity.from(new ClusterResources(4, 2, new NodeResources(2, 4, 50, 0.1, DiskSpeed.any, StorageType.any, Architecture.any)));
diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/InPlaceResizeProvisionTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/InPlaceResizeProvisionTest.java
index 0bb6dc61d1b..54f0507831d 100644
--- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/InPlaceResizeProvisionTest.java
+++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/InPlaceResizeProvisionTest.java
@@ -57,7 +57,7 @@ public class InPlaceResizeProvisionTest {
private final ProvisioningTester tester = new ProvisioningTester.Builder()
.flagSource(flagSource)
.zone(new Zone(Environment.prod, RegionName.from("us-east"))).build();
- private final ApplicationId app = ProvisioningTester.applicationId();
+ private final ApplicationId app = ProvisioningTester.applicationId("a1");
@Test
public void single_group_same_cluster_size_resource_increase() {
@@ -167,8 +167,6 @@ public class InPlaceResizeProvisionTest {
assertEquals(0, listCluster(content1).retired().size());
}
-
- /** In this scenario there should be no resizing */
@Test
public void increase_size_decrease_resources() {
addParentHosts(14, largeResources.with(fast));
@@ -198,15 +196,15 @@ public class InPlaceResizeProvisionTest {
assertSizeAndResources(listCluster(content1).retired(), 4, resources);
assertSizeAndResources(listCluster(content1).not().retired(), 8, halvedResources);
- // ... same with setting a node to want to retire
- Node nodeToWantoToRetire = listCluster(content1).not().retired().asList().get(0);
- try (NodeMutex lock = tester.nodeRepository().nodes().lockAndGetRequired(nodeToWantoToRetire)) {
+ // Here we'll unretire and resize one of the previously retired nodes as there is no rule against it
+ Node nodeToWantToRetire = listCluster(content1).not().retired().asList().get(0);
+ try (NodeMutex lock = tester.nodeRepository().nodes().lockAndGetRequired(nodeToWantToRetire)) {
tester.nodeRepository().nodes().write(lock.node().withWantToRetire(true, Agent.system,
tester.clock().instant()), lock);
}
new PrepareHelper(tester, app).prepare(content1, 8, 1, halvedResources).activate();
- assertTrue(listCluster(content1).retired().stream().anyMatch(n -> n.equals(nodeToWantoToRetire)));
- assertEquals(5, listCluster(content1).retired().size());
+ assertTrue(listCluster(content1).retired().stream().anyMatch(n -> n.equals(nodeToWantToRetire)));
+ assertEquals(4, listCluster(content1).retired().size());
assertSizeAndResources(listCluster(content1).not().retired(), 8, halvedResources);
}
diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidateTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidateTest.java
index 32db213c445..c82b29c7d65 100644
--- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidateTest.java
+++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidateTest.java
@@ -24,17 +24,17 @@ public class NodeCandidateTest {
@Test
public void testOrdering() {
List<NodeCandidate> expected = List.of(
- new NodeCandidate.ConcreteNodeCandidate(node("01", Node.State.ready), new NodeResources(2, 2, 2, 2), Optional.empty(), false, true, true, false, false),
- new NodeCandidate.ConcreteNodeCandidate(node("02", Node.State.active), new NodeResources(2, 2, 2, 2), Optional.empty(), true, true, false, false, false),
- new NodeCandidate.ConcreteNodeCandidate(node("04", Node.State.reserved), new NodeResources(2, 2, 2, 2), Optional.empty(), true, true, false, false, false),
- new NodeCandidate.ConcreteNodeCandidate(node("03", Node.State.inactive), new NodeResources(2, 2, 2, 2), Optional.empty(), true, true, false, false, false),
- new NodeCandidate.ConcreteNodeCandidate(node("05", Node.State.ready), new NodeResources(2, 2, 2, 2), Optional.of(node("host1", Node.State.active)), true, true, false, true, false),
- new NodeCandidate.ConcreteNodeCandidate(node("06", Node.State.ready), new NodeResources(2, 2, 2, 2), Optional.of(node("host1", Node.State.ready)), true, true, false, true, false),
- new NodeCandidate.ConcreteNodeCandidate(node("07", Node.State.ready), new NodeResources(2, 2, 2, 2), Optional.of(node("host1", Node.State.provisioned)), true, true, false, true, false),
- new NodeCandidate.ConcreteNodeCandidate(node("08", Node.State.ready), new NodeResources(2, 2, 2, 2), Optional.of(node("host1", Node.State.failed)), true, true, false, true, false),
- new NodeCandidate.ConcreteNodeCandidate(node("09", Node.State.ready), new NodeResources(1, 1, 1, 1), Optional.empty(), true, true, false, true, false),
- new NodeCandidate.ConcreteNodeCandidate(node("10", Node.State.ready), new NodeResources(2, 2, 2, 2), Optional.empty(), true, true, false, true, false),
- new NodeCandidate.ConcreteNodeCandidate(node("11", Node.State.ready), new NodeResources(2, 2, 2, 2), Optional.empty(), true, true, false, true, false)
+ new NodeCandidate.ConcreteNodeCandidate(node("01", Node.State.ready), false, new NodeResources(2, 2, 2, 2), Optional.empty(), false, true, true, false, false),
+ new NodeCandidate.ConcreteNodeCandidate(node("02", Node.State.active), false, new NodeResources(2, 2, 2, 2), Optional.empty(), true, true, false, false, false),
+ new NodeCandidate.ConcreteNodeCandidate(node("04", Node.State.reserved), false, new NodeResources(2, 2, 2, 2), Optional.empty(), true, true, false, false, false),
+ new NodeCandidate.ConcreteNodeCandidate(node("03", Node.State.inactive), false, new NodeResources(2, 2, 2, 2), Optional.empty(), true, true, false, false, false),
+ new NodeCandidate.ConcreteNodeCandidate(node("05", Node.State.ready), false, new NodeResources(2, 2, 2, 2), Optional.of(node("host1", Node.State.active)), true, true, false, true, false),
+ new NodeCandidate.ConcreteNodeCandidate(node("06", Node.State.ready), false, new NodeResources(2, 2, 2, 2), Optional.of(node("host1", Node.State.ready)), true, true, false, true, false),
+ new NodeCandidate.ConcreteNodeCandidate(node("07", Node.State.ready), false, new NodeResources(2, 2, 2, 2), Optional.of(node("host1", Node.State.provisioned)), true, true, false, true, false),
+ new NodeCandidate.ConcreteNodeCandidate(node("08", Node.State.ready), false, new NodeResources(2, 2, 2, 2), Optional.of(node("host1", Node.State.failed)), true, true, false, true, false),
+ new NodeCandidate.ConcreteNodeCandidate(node("09", Node.State.ready), false, new NodeResources(1, 1, 1, 1), Optional.empty(), true, true, false, true, false),
+ new NodeCandidate.ConcreteNodeCandidate(node("10", Node.State.ready), false, new NodeResources(2, 2, 2, 2), Optional.empty(), true, true, false, true, false),
+ new NodeCandidate.ConcreteNodeCandidate(node("11", Node.State.ready), false, new NodeResources(2, 2, 2, 2), Optional.empty(), true, true, false, true, false)
);
assertOrder(expected);
}
@@ -148,7 +148,7 @@ public class NodeCandidateTest {
Node parent = Node.create(hostname + "parent", hostname, new Flavor(totalHostResources), Node.State.ready, NodeType.host)
.ipConfig(IP.Config.of(Set.of("::1"), Set.of("::2")))
.build();
- return new NodeCandidate.ConcreteNodeCandidate(node, totalHostResources.subtract(allocatedHostResources), Optional.of(parent),
+ return new NodeCandidate.ConcreteNodeCandidate(node, false, totalHostResources.subtract(allocatedHostResources), Optional.of(parent),
false, exclusiveSwitch, false, true, false);
}
diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTest.java
index a76b576e430..cb4644f179f 100644
--- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTest.java
+++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTest.java
@@ -241,7 +241,7 @@ public class ProvisioningTest {
public void application_deployment_variable_application_size() {
ProvisioningTester tester = new ProvisioningTester.Builder().zone(new Zone(Environment.prod, RegionName.from("us-east"))).build();
- ApplicationId application1 = ProvisioningTester.applicationId();
+ ApplicationId application1 = ProvisioningTester.applicationId("a1");
tester.makeReadyHosts(30, defaultResources);
tester.activateTenantHosts();
@@ -821,7 +821,7 @@ public class ProvisioningTest {
public void highest_node_indexes_are_retired_first() {
ProvisioningTester tester = new ProvisioningTester.Builder().zone(new Zone(Environment.prod, RegionName.from("us-east"))).build();
- ApplicationId application1 = ProvisioningTester.applicationId();
+ ApplicationId application1 = ProvisioningTester.applicationId("a1");
tester.makeReadyHosts(14, defaultResources).activateTenantHosts();
@@ -833,17 +833,19 @@ public class ProvisioningTest {
SystemState state2 = prepare(application1, 2, 2, 2, 2, defaultResources, tester);
tester.activate(application1, state2.allHosts);
- // content0
- assertFalse(state2.hostByMembership("content0", 0, 0).membership().get().retired());
- assertFalse(state2.hostByMembership("content0", 0, 1).membership().get().retired());
- assertTrue( state2.hostByMembership("content0", 0, 2).membership().get().retired());
- assertTrue( state2.hostByMembership("content0", 0, 3).membership().get().retired());
-
- // content1
- assertFalse(state2.hostByMembership("content1", 0, 0).membership().get().retired());
- assertFalse(state2.hostByMembership("content1", 0, 1).membership().get().retired());
- assertTrue( state2.hostByMembership("content1", 0, 2).membership().get().retired());
- assertTrue( state2.hostByMembership("content1", 0, 3).membership().get().retired());
+ List<Integer> unretiredInContent0Indices = state2.content0.stream().filter(h -> ! h.membership().get().retired()).map(h -> h.membership().get().index()).toList();
+ for (var host : state2.content0) {
+ if ( ! host.membership().get().retired()) continue;
+ for (int unretiredIndex : unretiredInContent0Indices)
+ assertTrue(host.membership().get().index() > unretiredIndex);
+ }
+
+ List<Integer> unretiredInContent1Indices = state2.content1.stream().filter(h -> ! h.membership().get().retired()).map(h -> h.membership().get().index()).toList();
+ for (var host : state2.content1) {
+ if ( ! host.membership().get().retired()) continue;
+ for (int unretiredIndex : unretiredInContent1Indices)
+ assertTrue(host.membership().get().index() > unretiredIndex);
+ }
}
@Test
@@ -857,7 +859,7 @@ public class ProvisioningTest {
tester.deploy(application, spec, Capacity.from(new ClusterResources(6, 1, defaultResources)));
- // Pick out a random application node and make it's parent larger, this will make it the spare host
+ // Pick out a random application node and make its parent larger, this will make it the spare host
NodeList nodes = tester.nodeRepository().nodes().list();
Node randomNode = nodes.owner(application).shuffle(new Random()).first().get();
tester.nodeRepository().nodes().write(nodes.parentOf(randomNode).get()
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 a6a988052e6..6ec189d98c3 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
@@ -23,6 +23,7 @@ import com.yahoo.config.provision.Zone;
import com.yahoo.transaction.NestedTransaction;
import com.yahoo.vespa.hosted.provision.Node;
import com.yahoo.vespa.hosted.provision.NodeList;
+import com.yahoo.yolean.Exceptions;
import org.junit.Test;
import java.util.HashSet;
@@ -442,9 +443,8 @@ public class VirtualNodeProvisioningTest {
"Could not satisfy request for 3 nodes with " +
"[vcpu: 2.0, memory: 4.0 Gb, disk: 100.0 Gb, bandwidth: 1.0 Gbps, architecture: any] " +
"in tenant2.app2 container cluster 'my-container' 6.39: " +
- "Node allocation failure on group 0: " +
"Not enough suitable nodes available due to host exclusivity constraints",
- e.getMessage());
+ Exceptions.toMessageString(e));
}
// Adding 3 nodes of another application for the same tenant works
@@ -469,8 +469,8 @@ public class VirtualNodeProvisioningTest {
assertEquals("Could not satisfy request for 2 nodes with " +
"[vcpu: 1.0, memory: 4.0 Gb, disk: 100.0 Gb, bandwidth: 1.0 Gbps, storage type: remote, architecture: any] " +
"in tenant.app1 content cluster 'my-content'" +
- " 6.42: Node allocation failure on group 0",
- e.getMessage());
+ " 6.42",
+ Exceptions.toMessageString(e));
}
}
diff --git a/vespajlib/src/main/java/com/yahoo/yolean/Exceptions.java b/vespajlib/src/main/java/com/yahoo/yolean/Exceptions.java
index 4f3f048eb0c..a50ea2b97c1 100644
--- a/vespajlib/src/main/java/com/yahoo/yolean/Exceptions.java
+++ b/vespajlib/src/main/java/com/yahoo/yolean/Exceptions.java
@@ -27,6 +27,8 @@ public class Exceptions {
for (; t != null; t = t.getCause()) {
message = getMessage(t);
if (message == null) continue;
+ message = message.trim();
+ if (message.isEmpty()) continue;
if (message.equals(lastMessage)) continue;
if (b.length() > 0) {
b.append(": ");