aboutsummaryrefslogtreecommitdiffstats
path: root/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupAssigner.java
blob: e314542a40cf028d976992ebd5f1332e7647c5ad (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
// 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<NodeCandidate> assignTo(Collection<NodeCandidate> 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<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> 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;
    }

}