aboutsummaryrefslogtreecommitdiffstats
path: root/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Run.java
blob: 4b38306f905202c116655265f39fe5ba7845780b (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
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.deployment;

import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;

import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.aborted;
import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.running;
import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.success;
import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.succeeded;
import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.unfinished;
import static java.util.Objects.requireNonNull;

/**
 * Immutable class containing status information for a deployment job run by a {@link JobController}.
 *
 * @author jonmv
 */
public class Run {

    private final RunId id;
    private final Map<Step, StepInfo> steps;
    private final Versions versions;
    private final boolean isRedeployment;
    private final Instant start;
    private final Optional<Instant> end;
    private final RunStatus status;
    private final long lastTestRecord;
    private final Instant lastVespaLogTimestamp;
    private final Optional<Instant> noNodesDownSince;
    private final Optional<ConvergenceSummary> convergenceSummary;
    private final Optional<X509Certificate> testerCertificate;
    private final boolean dryRun;

    // For deserialisation only -- do not use!
    public Run(RunId id, Map<Step, StepInfo> steps, Versions versions, boolean isRedeployment, Instant start, Optional<Instant> end,
               RunStatus status, long lastTestRecord, Instant lastVespaLogTimestamp, Optional<Instant> noNodesDownSince,
               Optional<ConvergenceSummary> convergenceSummary, Optional<X509Certificate> testerCertificate, boolean dryRun) {
        this.id = id;
        this.steps = Collections.unmodifiableMap(new EnumMap<>(steps));
        this.versions = versions;
        this.isRedeployment = isRedeployment;
        this.start = start;
        this.end = end;
        this.status = status;
        this.lastTestRecord = lastTestRecord;
        this.lastVespaLogTimestamp = lastVespaLogTimestamp;
        this.noNodesDownSince = noNodesDownSince;
        this.convergenceSummary = convergenceSummary;
        this.testerCertificate = testerCertificate;
        this.dryRun = dryRun;
    }

    public static Run initial(RunId id, Versions versions, boolean isRedeployment, Instant now, JobProfile profile) {
        EnumMap<Step, StepInfo> steps = new EnumMap<>(Step.class);
        profile.steps().forEach(step -> steps.put(step, StepInfo.initial(step)));
        return new Run(id, steps, requireNonNull(versions), isRedeployment, requireNonNull(now), Optional.empty(), running,
                       -1, Instant.EPOCH, Optional.empty(), Optional.empty(), Optional.empty(), profile == JobProfile.developmentDryRun);
    }

    /** Returns a new Run with the status of the given completed step set accordingly. */
    public Run with(RunStatus status, LockedStep step) {
        requireActive();
        StepInfo stepInfo = getRequiredStepInfo(step.get());
        if (stepInfo.status() != unfinished)
            throw new IllegalStateException("Step '" + step.get() + "' can't be set to '" + status + "'" +
                                     " -- it already completed with status '" + stepInfo.status() + "'!");

        EnumMap<Step, StepInfo> steps = new EnumMap<>(this.steps);
        steps.put(step.get(), stepInfo.with(Step.Status.of(status)));
        return new Run(id, steps, versions, isRedeployment, start, end, this.status == running ? status : this.status,
                       lastTestRecord, lastVespaLogTimestamp, noNodesDownSince, convergenceSummary, testerCertificate, dryRun);
    }

    /** Returns a new Run with a new start time*/
    public Run with(Instant startTime, LockedStep step) {
        requireActive();
        StepInfo stepInfo = getRequiredStepInfo(step.get());
        if (stepInfo.status() != unfinished)
            throw new IllegalStateException("Unable to set start timestamp of step " + step.get() +
                    ": it has already completed with status " + stepInfo.status() + "!");

        EnumMap<Step, StepInfo> steps = new EnumMap<>(this.steps);
        steps.put(step.get(), stepInfo.with(startTime));

        return new Run(id, steps, versions, isRedeployment, start, end, status, lastTestRecord, lastVespaLogTimestamp,
                       noNodesDownSince, convergenceSummary, testerCertificate, dryRun);
    }

    public Run finished(Instant now) {
        requireActive();
        return new Run(id, steps, versions, isRedeployment, start, Optional.of(now), status == running ? success : status,
                       lastTestRecord, lastVespaLogTimestamp, noNodesDownSince, convergenceSummary, Optional.empty(), dryRun);
    }

    public Run aborted() {
        requireActive();
        return new Run(id, steps, versions, isRedeployment, start, end, aborted, lastTestRecord, lastVespaLogTimestamp,
                       noNodesDownSince, convergenceSummary, testerCertificate, dryRun);
    }

    public Run with(long lastTestRecord) {
        requireActive();
        return new Run(id, steps, versions, isRedeployment, start, end, status, lastTestRecord, lastVespaLogTimestamp,
                       noNodesDownSince, convergenceSummary, testerCertificate, dryRun);
    }

    public Run with(Instant lastVespaLogTimestamp) {
        requireActive();
        return new Run(id, steps, versions, isRedeployment, start, end, status, lastTestRecord, lastVespaLogTimestamp,
                       noNodesDownSince, convergenceSummary, testerCertificate, dryRun);
    }

    public Run noNodesDownSince(Instant noNodesDownSince) {
        requireActive();
        return new Run(id, steps, versions, isRedeployment, start, end, status, lastTestRecord, lastVespaLogTimestamp,
                       Optional.ofNullable(noNodesDownSince), convergenceSummary, testerCertificate, dryRun);
    }

    public Run withSummary(ConvergenceSummary convergenceSummary) {
        requireActive();
        return new Run(id, steps, versions, isRedeployment, start, end, status, lastTestRecord, lastVespaLogTimestamp,
                       noNodesDownSince, Optional.ofNullable(convergenceSummary), testerCertificate, dryRun);
    }

    public Run with(X509Certificate testerCertificate) {
        requireActive();
        return new Run(id, steps, versions, isRedeployment, start, end, status, lastTestRecord, lastVespaLogTimestamp,
                       noNodesDownSince, convergenceSummary, Optional.of(testerCertificate), dryRun);
    }

    /** Returns the id of this run. */
    public RunId id() {
        return id;
    }

    /** Whether this run contains this step. */
    public boolean hasStep(Step step) {
        return steps.containsKey(step);
    }

    /** Returns info on step, or empty if the given step is not a part of this run. */
    public Optional<StepInfo> stepInfo(Step step) {
        return Optional.ofNullable(steps.get(step));
    }

    private StepInfo getRequiredStepInfo(Step step) {
        return stepInfo(step).orElseThrow(() -> new IllegalArgumentException("There is no such step " + step + " for run " + id));
    }

    /** Returns status of step, or empty if the given step is not a part of this run. */
    public Optional<Step.Status> stepStatus(Step step) {
        return stepInfo(step).map(StepInfo::status);
    }

    /** Returns an unmodifiable view of all step information in this run. */
    public Map<Step, StepInfo> steps() {
        return steps;
    }

    /** Returns an unmodifiable view of the status of all steps in this run. */
    public Map<Step, Step.Status> stepStatuses() {
        return Collections.unmodifiableMap(steps.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().status())));
    }

    public RunStatus status() {
        return status;
    }

    /** Returns the instant at which this run began. */
    public Instant start() {
        return start;
    }

    /** Returns the instant at which this run ended, if it has. */
    public Optional<Instant> end() {
        return end;
    }

    /** Returns whether the run has failed, and should switch to its run-always steps. */
    public boolean hasFailed() {
        return status != running && status != success;
    }

    /** Returns whether the run has ended, i.e., has become inactive, and can no longer be updated. */
    public boolean hasEnded() {
        return end.isPresent();
    }

    /** Returns the target, and possibly source, versions for this run. */
    public Versions versions() {
        return versions;
    }

    /** Returns the sequence id of the last test record received from the tester, for the test logs of this run. */
    public long lastTestLogEntry() {
        return lastTestRecord;
    }

    /** Returns the timestamp of the last Vespa log record fetched and stored for this run. */
    public Instant lastVespaLogTimestamp() {
        return lastVespaLogTimestamp;
    }

    /** Returns since when no nodes have been allowed to be down. */
    public Optional<Instant> noNodesDownSince() {
        return noNodesDownSince;
    }

    /** Returns a summary of convergence status during an application deployment — staging or upgrade. */
    public Optional<ConvergenceSummary> convergenceSummary() {
        return convergenceSummary;
    }

    /** Returns the tester certificate for this run, or empty. */
    public Optional<X509Certificate> testerCertificate() {
        return testerCertificate;
    }

    /** Whether this is a automatic redeployment. */
    public boolean isRedeployment() {
        return isRedeployment;
    }

    /** Whether this is a dry run deployment. */
    public boolean isDryRun() { return dryRun; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if ( ! (o instanceof Run)) return false;

        Run run = (Run) o;

        return id.equals(run.id);
    }

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

    @Override
    public String toString() {
        return "RunStatus{" +
               "id=" + id +
               ", versions=" + versions +
               ", start=" + start +
               ", end=" + end +
               ", status=" + status +
               '}';
    }

    /** Returns the list of steps to run for this job right now, depending on whether the job has failed. */
    public List<Step> readySteps() {
        return hasFailed() ? forcedSteps() : normalSteps();
    }

    /** Returns the list of unfinished steps whose prerequisites have all succeeded. */
    private List<Step> normalSteps() {
        return steps.entrySet().stream()
                    .filter(entry -> entry.getValue().status() == unfinished
                                     && entry.getKey().prerequisites().stream()
                                             .allMatch(step -> steps.get(step) == null
                                                               || steps.get(step).status() == succeeded))
                    .map(Map.Entry::getKey)
                    .collect(Collectors.toUnmodifiableList());
    }

    /** Returns the list of not-yet-run run-always steps whose run-always prerequisites have all run. */
    private List<Step> forcedSteps() {
        return steps.entrySet().stream()
                    .filter(entry -> entry.getValue().status() == unfinished
                                     && entry.getKey().alwaysRun()
                                     && entry.getKey().prerequisites().stream()
                                             .filter(Step::alwaysRun)
                                             .allMatch(step -> steps.get(step) == null
                                                               || steps.get(step).status() != unfinished))
                    .map(Map.Entry::getKey)
                    .collect(Collectors.toUnmodifiableList());
    }

    private void requireActive() {
        if (hasEnded())
            throw new IllegalStateException("This run ended at " + end.get() + " -- it can't be further modified!");
    }

}