aboutsummaryrefslogtreecommitdiffstats
path: root/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java
blob: 9fb87735b42edd7353ab3b86feb64ddcc119ef9b (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
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.versions;

import com.yahoo.component.Version;
import com.yahoo.config.provision.HostName;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.api.identifiers.ControllerVersion;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
import com.yahoo.vespa.hosted.controller.application.ApplicationList;
import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.SystemApplication;
import com.yahoo.vespa.hosted.controller.maintenance.SystemUpgrader;
import com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

/**
 * Information about the current platform versions in use.
 * The versions in use are the set of all versions running in current applications, versions
 * of config servers in all zones, and the version of this controller itself.
 * 
 * @author bratseth
 * @author mpolden
 */
public record VersionStatus(List<VespaVersion> versions, int currentMajor) {

    private static final Logger log = Logger.getLogger(VersionStatus.class.getName());
    
    /** Create a version status. DO NOT USE: Public for testing and serialization only */
    public VersionStatus(List<VespaVersion> versions, int currentMajor) {
        this.versions = List.copyOf(versions);
        this.currentMajor = currentMajor;
    }

    /** Returns the current version of controllers in this system */
    public Optional<VespaVersion> controllerVersion() {
        return versions().stream().filter(VespaVersion::isControllerVersion).findFirst();
    }
    
    /** 
     * Returns the current Vespa version of the system controlled by this, 
     * or empty if we have not currently determined what the system version is in this status.
     */
    public Optional<VespaVersion> systemVersion() {
        return versions().stream().filter(VespaVersion::isSystemVersion).findFirst();
    }

    /** Returns whether the system is currently upgrading */
    public boolean isUpgrading() {
        return systemVersion().map(VespaVersion::versionNumber).orElse(Version.emptyVersion)
                              .isBefore(controllerVersion().map(VespaVersion::versionNumber)
                                                           .orElse(Version.emptyVersion));
    }

    /** 
     * Lists all currently active Vespa versions, with deployment statistics, 
     * sorted from lowest to highest version number.
     * The returned list is immutable.
     * Calling this is free, but the returned status is slightly out of date.
     */
    public List<VespaVersion> versions() { return versions; }

    /** Lists all currently active Vespa versions, from lowest to highest number, which are not newer than the system version. */
    public List<VespaVersion> deployableVersions() {
        List<VespaVersion> deployable = new ArrayList<>();
        for (VespaVersion version : versions) {
            deployable.add(version);
            if (version.isSystemVersion())
                return deployable;
        }
        return List.of();
    }
    
    /** Returns the given version, or null if it is not present */
    public VespaVersion version(Version version) {
        return versions.stream().filter(v -> v.versionNumber().equals(version)).findFirst().orElse(null);
    }

    /** Returns whether given version is active in this system */
    public boolean isActive(Version version) {
        if (version(version) != null) return true;
        // Occasionally we may deploy unofficial versions of a given Vespa version, i.e. given the version 8.42.1,
        // an unofficial version 8.42.1.a may exist. Count such versions as active if their root version is active
        Version rootVersion = new Version(version.getMajor(), version.getMinor(), version.getMicro());
        return version(rootVersion) != null;
    }

    /** Create the empty version status */
    public static VersionStatus empty() { return new VersionStatus(List.of(), -1); }

    /** Create a full, updated version status. This is expensive and should be done infrequently */
    public static VersionStatus compute(Controller controller) {
        VersionStatus versionStatus = controller.readVersionStatus();
        int currentMajor = versionStatus.currentMajor();
        List<NodeVersion> systemApplicationVersions = findSystemApplicationVersions(controller, versionStatus);
        Map<ControllerVersion, List<HostName>> controllerVersions = findControllerVersions(controller);

        Map<Version, List<HostName>> infrastructureVersions = new HashMap<>();
        for (var kv : controllerVersions.entrySet()) {
            infrastructureVersions.computeIfAbsent(kv.getKey().version(), (k) -> new ArrayList<>())
                                  .addAll(kv.getValue());
        }
        for (var nodeVersion : systemApplicationVersions) {
            infrastructureVersions.computeIfAbsent(nodeVersion.currentVersion(), (k) -> new ArrayList<>())
                                  .add(nodeVersion.hostname());
        }

        // The system version is the oldest infrastructure version, if that version is newer than the current system
        // version
        Version newSystemVersion = infrastructureVersions.keySet().stream().min(Comparator.naturalOrder()).get();
        Version systemVersion = versionStatus.systemVersion()
                                             .map(VespaVersion::versionNumber)
                                             .orElse(newSystemVersion);
        if (newSystemVersion.isBefore(systemVersion)) {
            log.warning("Refusing to lower system version from " +
                        systemVersion.toFullString() +
                        " to " +
                        newSystemVersion.toFullString() +
                        ", nodes on " + newSystemVersion.toFullString() + ": " +
                        infrastructureVersions.get(newSystemVersion).stream()
                                              .map(HostName::value)
                                              .collect(Collectors.joining(", ")));
        } else {
            systemVersion = newSystemVersion;
        }

        Set<Version> allVersions = new HashSet<>(infrastructureVersions.keySet());
        for (Application application : controller.applications().asList())
            for (Instance instance : application.instances().values())
                for (Deployment deployment : instance.deployments().values())
                    allVersions.add(deployment.version());

        List<DeploymentStatistics> deploymentStatistics = DeploymentStatistics.compute(allVersions,
                                                                                       controller.jobController().deploymentStatuses(ApplicationList.from(controller.applications().asList())
                                                                                                                                                    .withProjectId()
                                                                                                                                                    .withJobs()));
        List<VespaVersion> versions = new ArrayList<>();
        List<Version> releasedVersions = controller.mavenRepository().metadata().versions(controller.clock().instant());

        for (DeploymentStatistics statistics : deploymentStatistics) {
            if (statistics.version().isEmpty()) continue;

            try {
                boolean isReleased = Collections.binarySearch(releasedVersions, statistics.version()) >= 0;
                List<NodeVersion> nodeVersions = systemApplicationVersions.stream()
                                                                          .filter(nodeVersion -> nodeVersion.currentVersion().equals(statistics.version()))
                                                                          .toList();
                VespaVersion vespaVersion = createVersion(statistics,
                                                          controllerVersions.keySet(),
                                                          systemVersion,
                                                          isReleased,
                                                          nodeVersions,
                                                          controller,
                                                          versionStatus);
                versions.add(vespaVersion);
                if (vespaVersion.confidence().equalOrHigherThan(Confidence.high))
                    currentMajor = Math.max(currentMajor, vespaVersion.versionNumber().getMajor());
            } catch (IllegalArgumentException e) {
                log.log(Level.WARNING, "Unable to create VespaVersion for version " +
                                       statistics.version().toFullString(), e);
            }
        }

        Collections.sort(versions);

        return new VersionStatus(versions, currentMajor);
    }

    private static List<NodeVersion> findSystemApplicationVersions(Controller controller, VersionStatus versionStatus) {
        List<NodeVersion> nodeVersions = new ArrayList<>();
        for (var zone : controller.zoneRegistry().zones().controllerUpgraded().zones()) {
            for (var application : SystemApplication.notController()) {
                var nodes = controller.serviceRegistry().configServer().nodeRepository()
                                      .list(zone.getId(), NodeFilter.all().applications(application.id())).stream()
                                      .filter(SystemUpgrader::eligibleForUpgrade)
                                      .toList();
                if (nodes.isEmpty()) continue;
                boolean configConverged = application.configConvergedIn(zone.getId(), controller, Optional.empty());
                if (!configConverged) {
                    log.log(Level.WARNING, "Config for " + application.id() + " in " + zone.getId() +
                                              " has not converged");
                }
                for (var node : nodes) {
                    // Only use current node version if config has converged
                    var version = configConverged ? node.currentVersion() : controller.systemVersion(versionStatus);
                    var nodeVersion = new NodeVersion(node.hostname(), zone.getId(), version, node.wantedVersion(),
                                                      node.suspendedSince());
                    nodeVersions.add(nodeVersion);
                }
            }
        }
        return nodeVersions;
    }

    private static Map<ControllerVersion, List<HostName>> findControllerVersions(Controller controller) {
        Map<ControllerVersion, List<HostName>> versions = new HashMap<>();
        if (controller.curator().cluster().isEmpty()) { // Use vtag if we do not have cluster
            versions.computeIfAbsent(ControllerVersion.CURRENT, (k) -> new ArrayList<>())
                    .add(controller.hostname());
        } else {
            for (String host : controller.curator().cluster()) {
                HostName hostname = HostName.of(host);
                versions.computeIfAbsent(controller.curator().readControllerVersion(hostname), (k) -> new ArrayList<>())
                        .add(hostname);
            }
        }
        return versions;
    }

    private static VespaVersion createVersion(DeploymentStatistics statistics,
                                              Set<ControllerVersion> controllerVersions,
                                              Version systemVersion,
                                              boolean isReleased,
                                              List<NodeVersion> nodeVersions,
                                              Controller controller,
                                              VersionStatus versionStatus) {
        ControllerVersion latestVersion = controllerVersions.stream().max(Comparator.naturalOrder()).get();
        boolean isSystemVersion = statistics.version().equals(systemVersion);
        boolean isControllerVersion = controllerVersions.size() == 1 &&
                                      statistics.version().equals(controllerVersions.iterator().next().version());
        VespaVersion.Confidence confidence = controller.curator().readConfidenceOverrides().get(statistics.version());
        boolean confidenceIsOverridden = confidence != null;
        VespaVersion existingVespaVersion = versionStatus.version(statistics.version());

        // Compute confidence
        if (!confidenceIsOverridden) {
            Confidence newConfidence = VespaVersion.confidenceFrom(statistics, controller, versionStatus);
            Confidence oldConfidence = Optional.ofNullable(versionStatus.version(statistics.version()))
                                               .map(VespaVersion::confidence)
                                               .orElse(newConfidence);
            // Always update confidence for system and controller
            // Also allow older versions to transition from normal to high confidence
            if (isSystemVersion || isControllerVersion || oldConfidence == Confidence.normal && newConfidence == Confidence.high) {
                confidence = newConfidence;
            } else {
                // Otherwise, this is an older version, so we preserve the existing confidence, if any
                confidence = oldConfidence;
            }
        }

        // Preserve existing commit details if we've previously computed status for this version
        var commitSha = latestVersion.commitSha();
        var commitDate = latestVersion.commitDate();
        if (existingVespaVersion != null) {
            commitSha = existingVespaVersion.releaseCommit();
            commitDate = existingVespaVersion.committedAt();

            // Keep existing confidence if we cannot raise it at this moment in time
            if (!confidenceIsOverridden &&
                !existingVespaVersion.confidence().canChangeTo(confidence,
                                                               controller.serviceRegistry().zoneRegistry().system(),
                                                               controller.clock().instant())) {
                confidence = existingVespaVersion.confidence();
            }
        }

        return new VespaVersion(statistics.version(),
                                commitSha,
                                commitDate,
                                isControllerVersion,
                                isSystemVersion,
                                isReleased,
                                nodeVersions,
                                confidence);
    }

    /** Whether no version on a newer major, with high confidence, can be deployed. */
    public boolean isOnCurrentMajor(Version version) {
        return version.getMajor() >= currentMajor;
    }

}