summaryrefslogtreecommitdiffstats
path: root/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Cluster.java
blob: 5478999e4fe43a1a6187e5f2c055daa69296219f (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
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.provision.applications;

import com.yahoo.config.provision.ClusterResources;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.vespa.hosted.provision.autoscale.Autoscaler;

import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

/**
 * The node repo's view of a cluster in an application deployment.
 *
 * This is immutable, and must be locked with the application lock on read-modify-write.
 *
 * @author bratseth
 */
public class Cluster {

    public static final int maxScalingEvents = 15;

    private final ClusterSpec.Id id;
    private final boolean exclusive;
    private final ClusterResources min, max;
    private final Optional<Suggestion> suggested;
    private final Optional<ClusterResources> target;

    /** The maxScalingEvents last scaling events of this, sorted by increasing time (newest last) */
    private final List<ScalingEvent> scalingEvents;
    private final AutoscalingStatus autoscalingStatus;

    public Cluster(ClusterSpec.Id id,
                   boolean exclusive,
                   ClusterResources minResources,
                   ClusterResources maxResources,
                   Optional<Suggestion> suggestedResources,
                   Optional<ClusterResources> targetResources,
                   List<ScalingEvent> scalingEvents,
                   AutoscalingStatus autoscalingStatus) {
        this.id = Objects.requireNonNull(id);
        this.exclusive = exclusive;
        this.min = Objects.requireNonNull(minResources);
        this.max = Objects.requireNonNull(maxResources);
        this.suggested = Objects.requireNonNull(suggestedResources);
        Objects.requireNonNull(targetResources);
        if (targetResources.isPresent() && ! targetResources.get().isWithin(minResources, maxResources))
            this.target = Optional.empty();
        else
            this.target = targetResources;
        this.scalingEvents = List.copyOf(scalingEvents);
        this.autoscalingStatus = autoscalingStatus;
    }

    public ClusterSpec.Id id() { return id; }

    /** Returns the configured minimal resources in this cluster */
    public ClusterResources minResources() { return min; }

    /** Returns the configured maximal resources in this cluster */
    public ClusterResources maxResources() { return max; }

    /** Returns whether the nodes allocated to this cluster must be on host exclusively dedicated to this application */
    public boolean exclusive() { return exclusive; }

    /**
     * Returns the computed resources (between min and max, inclusive) this cluster should
     * have allocated at the moment (whether or not it actually has it),
     * or empty if the system currently has no target.
     */
    public Optional<ClusterResources> targetResources() { return target; }

    /**
     * The suggested size of this cluster, which may or may not be within the min and max limits,
     * or empty if there is currently no recorded suggestion.
     */
    public Optional<Suggestion> suggestedResources() { return suggested; }

    /** Returns true if there is a current suggestion and we should actually make this suggestion to users. */
    public boolean shouldSuggestResources(ClusterResources currentResources) {
        if (suggested.isEmpty()) return false;
        if (suggested.get().resources().isWithin(min, max)) return false;
        if ( ! Autoscaler.worthRescaling(currentResources, suggested.get().resources())) return false;
        return true;
    }

    /** Returns the recent scaling events in this cluster */
    public List<ScalingEvent> scalingEvents() { return scalingEvents; }

    public Optional<ScalingEvent> lastScalingEvent() {
        if (scalingEvents.isEmpty()) return Optional.empty();
        return Optional.of(scalingEvents.get(scalingEvents.size() - 1));
    }

    /** The latest autoscaling status of this cluster, or unknown (never null) if none */
    public AutoscalingStatus autoscalingStatus() { return autoscalingStatus; }

    public Cluster withConfiguration(boolean exclusive, ClusterResources min, ClusterResources max) {
        return new Cluster(id, exclusive, min, max, suggested, target, scalingEvents, autoscalingStatus);
    }

    public Cluster withSuggested(Optional<Suggestion> suggested) {
        return new Cluster(id, exclusive, min, max, suggested, target, scalingEvents, autoscalingStatus);
    }

    public Cluster withTarget(Optional<ClusterResources> target) {
        return new Cluster(id, exclusive, min, max, suggested, target, scalingEvents, autoscalingStatus);
    }

    /** Add or update (based on "at" time) a scaling event */
    public Cluster with(ScalingEvent scalingEvent) {
        List<ScalingEvent> scalingEvents = new ArrayList<>(this.scalingEvents);

        int existingIndex = eventIndexAt(scalingEvent.at());
        if (existingIndex >= 0)
            scalingEvents.set(existingIndex, scalingEvent);
        else
            scalingEvents.add(scalingEvent);

        prune(scalingEvents);
        return new Cluster(id, exclusive, min, max, suggested, target, scalingEvents, autoscalingStatus);
    }

    public Cluster with(AutoscalingStatus autoscalingStatus) {
        if (autoscalingStatus.equals(this.autoscalingStatus)) return this;
        return new Cluster(id, exclusive, min, max, suggested, target, scalingEvents, autoscalingStatus);
    }

    @Override
    public int hashCode() { return id.hashCode(); }

    @Override
    public boolean equals(Object other) {
        if (other == this) return true;
        if ( ! (other instanceof Cluster)) return false;
        return ((Cluster)other).id().equals(this.id);
    }

    @Override
    public String toString() {
        return "cluster '" + id + "'";
    }

    private void prune(List<ScalingEvent> scalingEvents) {
        while (scalingEvents.size() > maxScalingEvents)
            scalingEvents.remove(0);
    }

    private int eventIndexAt(Instant at) {
        for (int i = 0; i < scalingEvents.size(); i++) {
            if (scalingEvents.get(i).at().equals(at))
                return i;
        }
        return -1;
    }

    public static class Suggestion {

        private final ClusterResources resources;
        private final Instant at;

        public Suggestion(ClusterResources resources, Instant at) {
            this.resources = resources;
            this.at = at;
        }

        /** Returns the suggested resources */
        public ClusterResources resources() { return resources; }

        /** Returns the instant this suggestion was made */
        public Instant at() { return at; }

        @Override
        public String toString() {
            return "suggestion made at " + at + ": " + resources;
        }

        @Override
        public int hashCode() {
            return Objects.hash(resources, at);
        }

        @Override
        public boolean equals(Object o) {
            if ( ! (o instanceof Suggestion)) return false;
            Suggestion other = (Suggestion)o;
            if ( ! this.at.equals(other.at)) return false;
            if ( ! this.resources.equals(other.resources)) return false;
            return true;
        }

    }

}