com.yahoo.vespa.hosted.provision.provisioning.GroupAssigner Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of node-repository Show documentation
Show all versions of node-repository Show documentation
Keeps track of node assignment in a multi-application setup.
The newest version!
// Copyright Vespa.ai. 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 GroupAssigner {
private final NodeSpec requested;
private final NodeList allNodes;
private final Clock clock;
GroupAssigner(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 assignTo(Collection candidates) {
int[] countInGroup = countInEachGroup(candidates);
candidates = byUnretiringPriority(candidates).stream().map(node -> unretireNodeInExpandedGroup(node, countInGroup)).toList();
candidates = candidates.stream().map(node -> assignGroupToNewNode(node, countInGroup)).toList();
candidates = byUnretiringPriority(candidates).stream().map(node -> moveNodeInSurplusGroup(node, countInGroup)).toList();
candidates = byRetiringPriority(candidates).stream().map(node -> retireSurplusNodeInGroup(node, countInGroup)).toList();
candidates = candidates.stream().filter(node -> ! shouldRemove(node)).toList();
return candidates;
}
/** Prefer to retire nodes we want the least */
private List byRetiringPriority(Collection 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 byUnretiringPriority(Collection candidates) {
return candidates.stream()
.sorted(Comparator.comparing(NodeCandidate::wantToRetire)
.thenComparing(n -> n.allocation().get().membership().index()))
.toList();
}
private int[] countInEachGroup(Collection candidates) {
int[] countInGroup = new int[requested.groups()];
for (var candidate : candidates) {
if (candidate.allocation().get().membership().retired()) continue;
var currentGroup = candidate.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 candidate, int[] countInGroup) {
if (candidate.state() == Node.State.active && candidate.allocation().get().membership().retired()) return candidate;
if (candidate.state() == Node.State.active && candidate.allocation().get().membership().cluster().group().isPresent()) return candidate;
return inFirstGroupWithDeficiency(candidate, countInGroup);
}
private NodeCandidate moveNodeInSurplusGroup(NodeCandidate candidate, int[] countInGroup) {
var currentGroup = candidate.allocation().get().membership().cluster().group();
if (currentGroup.isEmpty()) return candidate;
if (currentGroup.get().index() < requested.groups()) return candidate;
return inFirstGroupWithDeficiency(candidate, countInGroup);
}
private NodeCandidate retireSurplusNodeInGroup(NodeCandidate candidate, int[] countInGroup) {
if (candidate.allocation().get().membership().retired()) return candidate;
var currentGroup = candidate.allocation().get().membership().cluster().group();
if (currentGroup.isEmpty()) return candidate;
if (currentGroup.get().index() >= requested.groups()) return candidate;
if (requested.count().isEmpty()) return candidate; // Can't retire
if (countInGroup[currentGroup.get().index()] <= requested.groupSize()) return candidate;
countInGroup[currentGroup.get().index()]--;
return candidate.withNode(candidate.toNode().retire(Agent.application, clock.instant()));
}
/** Unretire nodes that are already in the correct group when the group is deficient. */
private NodeCandidate unretireNodeInExpandedGroup(NodeCandidate candidate, int[] countInGroup) {
if ( ! candidate.allocation().get().membership().retired()) return candidate;
var currentGroup = candidate.allocation().get().membership().cluster().group();
if (currentGroup.isEmpty()) return candidate;
if (currentGroup.get().index() >= requested.groups()) return candidate;
if (candidate.preferToRetire() || candidate.wantToRetire()) return candidate;
if (requested.count().isPresent() && countInGroup[currentGroup.get().index()] >= requested.groupSize()) return candidate;
candidate = unretire(candidate);
if (candidate.allocation().get().membership().retired()) return candidate;
countInGroup[currentGroup.get().index()]++;
return candidate;
}
private NodeCandidate inFirstGroupWithDeficiency(NodeCandidate candidate, int[] countInGroup) {
for (int group = 0; group < requested.groups(); group++) {
if (requested.count().isEmpty() || countInGroup[group] < requested.groupSize()) {
return inGroup(group, candidate, countInGroup);
}
}
return candidate;
}
private boolean shouldRemove(NodeCandidate candidate) {
var currentGroup = candidate.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 candidate, int[] countInGroup) {
candidate = unretire(candidate);
if (candidate.allocation().get().membership().retired()) return candidate;
var membership = candidate.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 candidate.withNode(candidate.toNode().with(candidate.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 candidate) {
if (candidate.retiredNow()) return candidate;
if ( ! candidate.allocation().get().membership().retired()) return candidate;
if ( ! hasCompatibleResources(candidate) ) return candidate;
var parent = candidate.parentHostname().flatMap(hostname -> allNodes.node(hostname));
if (parent.isPresent() && (parent.get().status().wantToRetire() || parent.get().status().preferToRetire())) return candidate;
candidate = candidate.withNode();
if ( ! requested.isCompatible(candidate.resources()))
candidate = candidate.withNode(resize(candidate.toNode()));
return candidate.withNode(candidate.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;
}
}