summaryrefslogtreecommitdiffstats
path: root/node-repository
diff options
context:
space:
mode:
authortoby <smorgrav@yahoo-inc.com>2017-05-22 13:41:53 +0200
committertoby <smorgrav@yahoo-inc.com>2017-05-22 13:41:53 +0200
commit1a5a822bcbf20dc88325f7eb47b495099d7ff8cd (patch)
tree2f8892d9dcea57820d6eed351053c370af025eb7 /node-repository
parent6d9a1700f5b168285e1049d52742918f27ed8aeb (diff)
Replace NodeList with the refactored NodeAllocation class
Diffstat (limited to 'node-repository')
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java272
1 files changed, 18 insertions, 254 deletions
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 52e689441d1..1c6bf8881c6 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
@@ -3,26 +3,16 @@ package com.yahoo.vespa.hosted.provision.provisioning;
import com.google.common.collect.ComparisonChain;
import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ClusterMembership;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.OutOfCapacityException;
import com.yahoo.lang.MutableInteger;
import com.yahoo.transaction.Mutex;
import com.yahoo.vespa.hosted.provision.Node;
import com.yahoo.vespa.hosted.provision.NodeRepository;
-import com.yahoo.vespa.hosted.provision.node.Agent;
import java.time.Clock;
-import java.util.ArrayList;
-import java.util.Collection;
import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
/**
* Performs preparation of node activation changes for a single host group in an application.
@@ -59,46 +49,46 @@ class GroupPreparer {
public List<Node> prepare(ApplicationId application, ClusterSpec cluster, NodeSpec requestedNodes,
List<Node> surplusActiveNodes, MutableInteger highestIndex) {
try (Mutex lock = nodeRepository.lock(application)) {
- NodeList nodeList = new NodeList(application, cluster, requestedNodes, highestIndex);
+ NodeAllocation allocation = new NodeAllocation(application, cluster, requestedNodes, highestIndex, clock);
// Use active nodes
- nodeList.offer(nodeRepository.getNodes(application, Node.State.active), !canChangeGroup);
- if (nodeList.saturated()) return nodeList.finalNodes(surplusActiveNodes);
+ allocation.offer(nodeRepository.getNodes(application, Node.State.active), !canChangeGroup);
+ if (allocation.saturated()) return allocation.finalNodes(surplusActiveNodes);
// Use active nodes from other groups that will otherwise be retired
- List<Node> accepted = nodeList.offer(prioritizeNodes(surplusActiveNodes, requestedNodes), canChangeGroup);
+ List<Node> accepted = allocation.offer(prioritizeNodes(surplusActiveNodes, requestedNodes), canChangeGroup);
surplusActiveNodes.removeAll(accepted);
- if (nodeList.saturated()) return nodeList.finalNodes(surplusActiveNodes);
+ if (allocation.saturated()) return allocation.finalNodes(surplusActiveNodes);
// Use previously reserved nodes
- nodeList.offer(nodeRepository.getNodes(application, Node.State.reserved), !canChangeGroup);
- if (nodeList.saturated()) return nodeList.finalNodes(surplusActiveNodes);
+ allocation.offer(nodeRepository.getNodes(application, Node.State.reserved), !canChangeGroup);
+ if (allocation.saturated()) return allocation.finalNodes(surplusActiveNodes);
// Use inactive nodes
- accepted = nodeList.offer(prioritizeNodes(nodeRepository.getNodes(application, Node.State.inactive), requestedNodes), !canChangeGroup);
- nodeList.update(nodeRepository.reserve(accepted));
- if (nodeList.saturated()) return nodeList.finalNodes(surplusActiveNodes);
+ accepted = allocation.offer(prioritizeNodes(nodeRepository.getNodes(application, Node.State.inactive), requestedNodes), !canChangeGroup);
+ allocation.update(nodeRepository.reserve(accepted));
+ if (allocation.saturated()) return allocation.finalNodes(surplusActiveNodes);
// Use new, ready nodes. Lock ready pool to ensure that nodes are not grabbed by others.
try (Mutex readyLock = nodeRepository.lockUnallocated()) {
List<Node> readyNodes = nodeRepository.getNodes(requestedNodes.type(), Node.State.ready);
- accepted = nodeList.offer(prioritizeNodes(readyNodes, requestedNodes), !canChangeGroup);
- nodeList.update(nodeRepository.reserve(accepted));
+ accepted = allocation.offer(prioritizeNodes(readyNodes, requestedNodes), !canChangeGroup);
+ allocation.update(nodeRepository.reserve(accepted));
}
- if (nodeList.fullfilled())
- return nodeList.finalNodes(surplusActiveNodes);
+ if (allocation.fullfilled())
+ return allocation.finalNodes(surplusActiveNodes);
else
throw new OutOfCapacityException("Could not satisfy " + requestedNodes + " for " + cluster +
- outOfCapacityDetails(nodeList));
+ outOfCapacityDetails(allocation));
}
}
- private String outOfCapacityDetails(NodeList nodeList) {
- if (nodeList.wouldBeFulfilledWithClashingParentHost()) {
+ private String outOfCapacityDetails(NodeAllocation allocation) {
+ if (allocation.wouldBeFulfilledWithClashingParentHost()) {
return ": Not enough nodes available on separate physical hosts.";
}
- if (nodeList.wouldBeFulfilledWithRetiredNodes()) {
+ if (allocation.wouldBeFulfilledWithRetiredNodes()) {
return ": Not enough nodes available due to retirement.";
}
return ".";
@@ -128,230 +118,4 @@ class GroupPreparer {
}
return nodeList;
}
-
- /** Used to manage a list of nodes during the node reservation process */
- private class NodeList {
-
- /** The application this list is for */
- private final ApplicationId application;
-
- /** The cluster this list is for */
- private final ClusterSpec cluster;
-
- /** The requested nodes of this list */
- private final NodeSpec requestedNodes;
-
- /** The nodes this has accepted so far */
- private final Set<Node> nodes = new LinkedHashSet<>();
-
- /** The number of nodes in the accepted nodes which are of the requested flavor */
- private int acceptedOfRequestedFlavor = 0;
-
- /** The number of nodes rejected because of clashing parentHostname */
- private int rejectedWithClashingParentHost = 0;
-
- /** The number of nodes that just now was changed to retired */
- private int wasRetiredJustNow = 0;
-
- /** The node indexes to verify uniqueness of each members index */
- private Set<Integer> indexes = new HashSet<>();
-
- /** The next membership index to assign to a new node */
- private MutableInteger highestIndex;
-
- public NodeList(ApplicationId application, ClusterSpec cluster, NodeSpec requestedNodes, MutableInteger highestIndex) {
- this.application = application;
- this.cluster = cluster;
- this.requestedNodes = requestedNodes;
- this.highestIndex = highestIndex;
- }
-
- /**
- * Offer some nodes to this. The nodes may have an allocation to a different application or cluster,
- * an allocation to this cluster, or no current allocation (in which case one is assigned).
- * <p>
- * Note that if unallocated nodes are offered before allocated nodes, this will unnecessarily
- * reject allocated nodes due to index duplicates.
- *
- * @param offeredNodes the nodes which are potentially on offer. These may belong to a different application etc.
- * @param canChangeGroup whether it is ok to change the group the offered node is to belong to if necessary
- * @return the subset of offeredNodes which was accepted, with the correct allocation assigned
- */
- public List<Node> offer(List<Node> offeredNodes, boolean canChangeGroup) {
- List<Node> accepted = new ArrayList<>();
- for (Node offered : offeredNodes) {
- if (offered.allocation().isPresent()) {
- boolean wantToRetireNode = false;
- ClusterMembership membership = offered.allocation().get().membership();
- if ( ! offered.allocation().get().owner().equals(application)) continue; // wrong application
- if ( ! membership.cluster().equalsIgnoringGroupAndVespaVersion(cluster)) continue; // wrong cluster id/type
- if ((! canChangeGroup || saturated()) && ! membership.cluster().group().equals(cluster.group())) continue; // wrong group and we can't or have no reason to change it
- if ( offered.allocation().get().isRemovable()) continue; // don't accept; causes removal
- if ( indexes.contains(membership.index())) continue; // duplicate index (just to be sure)
-
- // conditions on which we want to retire nodes that were allocated previously
- if ( offeredNodeHasParentHostnameAlreadyAccepted(this.nodes, offered)) wantToRetireNode = true;
- if ( !hasCompatibleFlavor(offered)) wantToRetireNode = true;
- if ( offered.flavor().isRetired()) wantToRetireNode = true;
- if ( offered.status().wantToRetire()) wantToRetireNode = true;
-
- if ((!saturated() && hasCompatibleFlavor(offered)) || acceptToRetire(offered) )
- accepted.add(acceptNode(offered, wantToRetireNode));
- }
- else if (! saturated() && hasCompatibleFlavor(offered)) {
- if ( offeredNodeHasParentHostnameAlreadyAccepted(this.nodes, offered)) {
- ++rejectedWithClashingParentHost;
- continue;
- }
- if (offered.flavor().isRetired()) {
- continue;
- }
- if (offered.status().wantToRetire()) {
- continue;
- }
- Node alloc = offered.allocate(application, ClusterMembership.from(cluster, highestIndex.add(1)), clock.instant());
- accepted.add(acceptNode(alloc, false));
- }
- }
-
- return accepted;
- }
-
- private boolean offeredNodeHasParentHostnameAlreadyAccepted(Collection<Node> accepted, Node offered) {
- for (Node acceptedNode : accepted) {
- if (acceptedNode.parentHostname().isPresent() && offered.parentHostname().isPresent() &&
- acceptedNode.parentHostname().get().equals(offered.parentHostname().get())) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Returns whether this node should be accepted into the cluster even if it is not currently desired
- * (already enough nodes, or wrong flavor).
- * Such nodes will be marked retired during finalization of the list of accepted nodes.
- * The conditions for this are
- * <ul>
- * <li>This is a content node. These must always be retired before being removed to allow the cluster to
- * migrate away data.
- * <li>This is a container node and it is not desired due to having the wrong flavor. In this case this
- * will (normally) obtain for all the current nodes in the cluster and so retiring before removing must
- * be used to avoid removing all the current nodes at once, before the newly allocated replacements are
- * initialized. (In the other case, where a container node is not desired because we have enough nodes we
- * do want to remove it immediately to get immediate feedback on how the size reduction works out.)
- * </ul>
- */
- private boolean acceptToRetire(Node node) {
- if (node.state() != Node.State.active) return false;
- if (! node.allocation().get().membership().cluster().group().equals(cluster.group())) return false;
-
- return (cluster.type() == ClusterSpec.Type.content) ||
- (cluster.type() == ClusterSpec.Type.container && ! hasCompatibleFlavor(node));
- }
-
- private boolean hasCompatibleFlavor(Node node) {
- return requestedNodes.isCompatible(node.flavor());
- }
-
- /** Updates the state of some existing nodes in this list by replacing them by id with the given instances. */
- public void update(List<Node> updatedNodes) {
- nodes.removeAll(updatedNodes);
- nodes.addAll(updatedNodes);
- }
-
- private Node acceptNode(Node node, boolean wantToRetire) {
- if (! wantToRetire) {
- if ( ! node.state().equals(Node.State.active)) {
- // reactivated node - make sure its not retired
- node = node.unretire();
- }
- acceptedOfRequestedFlavor++;
- } else {
- ++wasRetiredJustNow;
- // Retire nodes which are of an unwanted flavor, retired flavor or have an overlapping parent host
- node = node.retire(clock.instant());
- }
- if ( ! node.allocation().get().membership().cluster().equals(cluster)) {
- // group may be different
- node = setCluster(cluster, node);
- }
- indexes.add(node.allocation().get().membership().index());
- highestIndex.set(Math.max(highestIndex.get(), node.allocation().get().membership().index()));
- nodes.add(node);
- return node;
- }
-
- private Node setCluster(ClusterSpec cluster, Node node) {
- ClusterMembership membership = node.allocation().get().membership().changeCluster(cluster);
- return node.with(node.allocation().get().with(membership));
- }
-
- /** Returns true if no more nodes are needed in this list */
- public boolean saturated() {
- return requestedNodes.saturatedBy(acceptedOfRequestedFlavor);
- }
-
- /** Returns true if the content of this list is sufficient to meet the request */
- public boolean fullfilled() {
- return requestedNodes.fulfilledBy(acceptedOfRequestedFlavor);
- }
-
- public boolean wouldBeFulfilledWithRetiredNodes() {
- return requestedNodes.fulfilledBy(acceptedOfRequestedFlavor + wasRetiredJustNow);
- }
-
- public boolean wouldBeFulfilledWithClashingParentHost() {
- return requestedNodes.fulfilledBy(acceptedOfRequestedFlavor + rejectedWithClashingParentHost);
- }
-
- /**
- * 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.
- *
- * @param surplusNodes this will add nodes not any longer needed by this group to this list
- * @return the final list of nodes
- */
- public List<Node> finalNodes(List<Node> surplusNodes) {
- long currentRetired = nodes.stream().filter(node -> node.allocation().get().membership().retired()).count();
- long surplus = requestedNodes.surplusGiven(nodes.size()) - currentRetired;
-
- List<Node> changedNodes = new ArrayList<>();
- if (surplus > 0) { // retire until surplus is 0, prefer to retire higher indexes to minimize redistribution
- for (Node node : byDecreasingIndex(nodes)) {
- if ( ! node.allocation().get().membership().retired() && node.state().equals(Node.State.active)) {
- changedNodes.add(node.retire(Agent.application, clock.instant()));
- surplusNodes.add(node); // offer this node to other groups
- if (--surplus == 0) break;
- }
- }
- }
- else if (surplus < 0) { // unretire until surplus is 0
- for (Node node : byIncreasingIndex(nodes)) {
- if ( node.allocation().get().membership().retired() && hasCompatibleFlavor(node)) {
- changedNodes.add(node.unretire());
- if (++surplus == 0) break;
- }
- }
- }
- update(changedNodes);
- return new ArrayList<>(nodes);
- }
-
- private List<Node> byDecreasingIndex(Set<Node> nodes) {
- return nodes.stream().sorted(nodeIndexComparator().reversed()).collect(Collectors.toList());
- }
-
- private List<Node> byIncreasingIndex(Set<Node> nodes) {
- return nodes.stream().sorted(nodeIndexComparator()).collect(Collectors.toList());
- }
-
- private Comparator<Node> nodeIndexComparator() {
- return Comparator.comparing((Node n) -> n.allocation().get().membership().index());
- }
-
- }
-
}