All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.yahoo.vespa.hosted.provision.provisioning.GroupAssigner Maven / Gradle / Ivy

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;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy