aboutsummaryrefslogtreecommitdiffstats
path: root/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java
blob: cb50f6dff2b3777919a8bac640c950bb59033ed4 (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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
// 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.CloudAccount;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.NodeResources;
import com.yahoo.config.provision.NodeType;
import com.yahoo.vespa.hosted.provision.Node;
import com.yahoo.vespa.hosted.provision.autoscale.ResourceChange;

import java.time.Duration;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
 * A specification of a set of nodes.
 * This reflects that nodes can be requested either by count and flavor or by type,
 * and encapsulates the differences in logic between these two cases.
 *
 * @author bratseth
 */
public interface NodeSpec {

    /** The node type this requests */
    NodeType type();

    /** Returns whether the given node resources is compatible with this spec */
    boolean isCompatible(NodeResources resources);

    /** Returns whether the given node count is sufficient to consider this spec fulfilled to the maximum amount */
    boolean saturatedBy(int count);

    /** Returns whether the given node count is sufficient to fulfill this spec */
    default boolean fulfilledBy(int count) {
        return fulfilledDeficitCount(count) == 0;
    }

    /** Returns the total number of nodes this is requesting, or empty if not specified */
    Optional<Integer> count();

    int groups();

    /** Returns the group size requested if count() is present. Throws RuntimeException otherwise. */
    default int groupSize() { return count().get() / 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 number of additional nodes needed for this spec to be fulfilled given the current node count */
    int fulfilledDeficitCount(int count);

    /** Returns the resources requested by this or empty if none are explicitly requested */
    Optional<NodeResources> resources();

    /** Returns whether the given node must be resized to match this spec */
    boolean needsResize(Node node);

    /** Returns true if there exist some circumstance where we may accept to have this node allocated */
    boolean acceptable(NodeCandidate node);

    /** Returns true if nodes with non-active parent hosts should be rejected */
    boolean rejectNonActiveParent();

    /** Returns the cloud account to use when fulfilling this spec */
    CloudAccount cloudAccount();

    /** Returns the host TTL to use for any hosts provisioned as a result of this fulfilling this spec. */
    default Duration hostTTL() { return Duration.ZERO; }

    /**
     * Returns true if a node with given current resources and current spare host resources can be resized
     * in-place to resources in this spec.
     */
    default boolean canResize(NodeResources currentNodeResources, NodeResources currentSpareHostResources,
                              ClusterSpec.Type type, boolean hasTopologyChange, int currentClusterSize) {
        return false;
    }

    static NodeSpec from(int nodeCount, int groupCount, NodeResources resources, boolean exclusive, boolean canFail,
                         CloudAccount cloudAccount, Duration hostTTL) {
        return new CountNodeSpec(nodeCount, groupCount, resources, exclusive, canFail, canFail, cloudAccount, hostTTL);
    }

    static NodeSpec from(NodeType type, CloudAccount cloudAccount) {
        return new TypeNodeSpec(type, cloudAccount);
    }

    /** A node spec specifying a node count and a flavor */
    class CountNodeSpec implements NodeSpec {

        private final int count;
        private final int groups;
        private final NodeResources requestedNodeResources;
        private final boolean exclusive;
        private final boolean canFail;
        private final boolean considerRetiring;
        private final CloudAccount cloudAccount;
        private final Duration hostTTL;

        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;
            this.considerRetiring = considerRetiring;
            this.cloudAccount = Objects.requireNonNull(cloudAccount);
            this.hostTTL = Objects.requireNonNull(hostTTL);

            if (!canFail && considerRetiring)
                throw new IllegalArgumentException("Cannot consider retiring nodes if we cannot fail");
        }

        @Override
        public Optional<Integer> count() { return Optional.of(count); }

        @Override
        public int groups() { return groups; }

        @Override
        public Optional<NodeResources> resources() {
            return Optional.of(requestedNodeResources);
        }

        @Override
        public NodeType type() { return NodeType.tenant; }

        @Override
        public boolean isCompatible(NodeResources resources) {
            return resources.equalsWhereSpecified(requestedNodeResources);
        }

        @Override
        public boolean saturatedBy(int count) { return fulfilledBy(count); } // min=max for count specs

        @Override
        public boolean canFail() { return canFail; }

        @Override
        public boolean considerRetiring() {
            return considerRetiring;
        }

        @Override
        public int fulfilledDeficitCount(int count) {
            return Math.max(this.count - count, 0);
        }

        public NodeSpec withoutRetiring() {
            return new CountNodeSpec(count, groups, requestedNodeResources, exclusive, canFail, false, cloudAccount, hostTTL);
        }

        @Override
        public boolean needsResize(Node node) {
            return ! node.resources().equalsWhereSpecified(requestedNodeResources);
        }

        @Override
        public boolean canResize(NodeResources currentNodeResources, NodeResources currentSpareHostResources,
                                 ClusterSpec.Type type, boolean hasTopologyChange, int currentClusterSize) {
            return ResourceChange.canInPlaceResize(currentClusterSize, currentNodeResources, count, requestedNodeResources,
                                                   type, exclusive, hasTopologyChange)
                   &&
                   currentSpareHostResources.add(currentNodeResources.justNumbers()).satisfies(requestedNodeResources);

        }

        @Override
        public boolean acceptable(NodeCandidate node) { return true; }

        @Override
        public boolean rejectNonActiveParent() {
            return false;
        }

        @Override
        public CloudAccount cloudAccount() {
            return cloudAccount;
        }

        @Override
        public Duration hostTTL() { return hostTTL; }

        @Override
        public String toString() {
            return "request for " + count + " nodes" +
                   ( groups > 1 ? " (in " + groups + " groups)" : "") +
                   " with " + requestedNodeResources; }

    }

    /** A node spec specifying a node type. */
    class TypeNodeSpec implements NodeSpec {

        private static final Map<NodeType, Integer> WANTED_NODE_COUNT = Map.of(NodeType.config, 3,
                                                                               NodeType.controller, 3);

        private final NodeType type;
        private final CloudAccount cloudAccount;

        private TypeNodeSpec(NodeType type, CloudAccount cloudAccount) {
            this.type = type;
            this.cloudAccount = cloudAccount;
        }

        @Override
        public Optional<Integer> count() { return Optional.empty(); }

        @Override
        public int groups() { return 1; }

        @Override
        public NodeType type() { return type; }

        @Override
        public boolean isCompatible(NodeResources resources) { return true; }

        @Override
        public boolean saturatedBy(int count) { return false; }

        @Override
        public boolean canFail() { return false; }

        @Override
        public boolean considerRetiring() { return true; }

        @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 Optional<NodeResources> resources() {
            return Optional.empty();
        }

        @Override
        public boolean needsResize(Node node) { return false; }

        @Override
        public boolean acceptable(NodeCandidate node) {
            // Since we consume all offered nodes we should not accept previously deactivated nodes
            return node.state() != Node.State.inactive;
        }

        @Override
        public boolean rejectNonActiveParent() {
            return true;
        }

        @Override
        public CloudAccount cloudAccount() {
            return cloudAccount;
        }

        @Override
        public String toString() { return "request for nodes of type '" + type + "'"; }

    }

}