aboutsummaryrefslogtreecommitdiffstats
path: root/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java
blob: 41a3784163da7540f9a28cc4367a22adba5e4600 (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
// Copyright 2019 Oath Inc. 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.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ListMultimap;
import com.yahoo.component.Version;
import com.yahoo.config.provision.HostName;
import java.util.logging.Level;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.application.ApplicationList;
import com.yahoo.vespa.hosted.controller.application.SystemApplication;
import com.yahoo.vespa.hosted.controller.maintenance.SystemUpgrader;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
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.
 * 
 * This is immutable.
 * 
 * @author bratseth
 * @author mpolden
 */
public class VersionStatus {

    private static final Logger log = Logger.getLogger(VersionStatus.class.getName());

    private final ImmutableList<VespaVersion> versions;
    
    /** Create a version status. DO NOT USE: Public for testing and serialization only */
    public VersionStatus(List<VespaVersion> versions) {
        this.versions = ImmutableList.copyOf(versions);
    }

    /** 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; }
    
    /** 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);
    }

    /** Create the empty version status */
    public static VersionStatus empty() { return new VersionStatus(ImmutableList.of()); }

    /** Create a full, updated version status. This is expensive and should be done infrequently */
    public static VersionStatus compute(Controller controller) {
        var systemApplicationVersions = findSystemApplicationVersions(controller);
        var controllerVersions = findControllerVersions(controller);

        var infrastructureVersions = ArrayListMultimap.<Version, HostName>create();
        for (var kv : controllerVersions.asMap().entrySet()) {
            infrastructureVersions.putAll(kv.getKey().version(), kv.getValue());
        }
        infrastructureVersions.putAll(systemApplicationVersions.asVersionMap());

        // 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 = controller.versionStatus().systemVersion()
                                          .map(VespaVersion::versionNumber)
                                          .orElse(newSystemVersion);
        if (newSystemVersion.isBefore(systemVersion)) {
            log.warning("Refusing to lower system version from " +
                        controller.systemVersion() +
                        " to " +
                        newSystemVersion +
                        ", nodes on " + newSystemVersion + ": " +
                        infrastructureVersions.get(newSystemVersion).stream()
                                              .map(HostName::value)
                                              .collect(Collectors.joining(", ")));
        } else {
            systemVersion = newSystemVersion;
        }


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

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

            try {
                boolean isReleased = Collections.binarySearch(releasedVersions, statistics.version()) >= 0;
                VespaVersion vespaVersion = createVersion(statistics,
                                                          controllerVersions.keySet(),
                                                          systemVersion,
                                                          isReleased,
                                                          systemApplicationVersions.matching(statistics.version()),
                                                          controller);
                versions.add(vespaVersion);
            } catch (IllegalArgumentException e) {
                log.log(Level.WARNING, "Unable to create VespaVersion for version " +
                                       statistics.version().toFullString(), e);
            }
        }

        Collections.sort(versions);

        return new VersionStatus(versions);
    }

    private static NodeVersions findSystemApplicationVersions(Controller controller) {
        var nodeVersions = new LinkedHashMap<HostName, NodeVersion>();
        for (var zone : controller.zoneRegistry().zones().controllerUpgraded().zones()) {
            for (var application : SystemApplication.all()) {
                var nodes = controller.serviceRegistry().configServer().nodeRepository()
                                      .list(zone.getId(), application.id()).stream()
                                      .filter(SystemUpgrader::eligibleForUpgrade)
                                      .collect(Collectors.toList());
                if (nodes.isEmpty()) continue;
                var configConverged = application.configConvergedIn(zone.getId(), controller, Optional.empty());
                if (!configConverged) {
                    log.log(LogLevel.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();
                    var nodeVersion = new NodeVersion(node.hostname(), zone.getId(), version, node.wantedVersion(),
                                                      node.suspendedSince());
                    nodeVersions.put(nodeVersion.hostname(), nodeVersion);
                }
            }
        }
        return NodeVersions.copyOf(nodeVersions);
    }

    private static ListMultimap<ControllerVersion, HostName> findControllerVersions(Controller controller) {
        ListMultimap<ControllerVersion, HostName> versions = ArrayListMultimap.create();
        if (controller.curator().cluster().isEmpty()) { // Use vtag if we do not have cluster
            versions.put(ControllerVersion.CURRENT, controller.hostname());
        } else {
            for (HostName hostname : controller.curator().cluster()) {
                versions.put(controller.curator().readControllerVersion(hostname), hostname);
            }
        }
        return versions;
    }

    private static VespaVersion createVersion(DeploymentStatistics statistics,
                                              Set<ControllerVersion> controllerVersions,
                                              Version systemVersion,
                                              boolean isReleased,
                                              NodeVersions nodeVersions,
                                              Controller controller) {
        var latestVersion = controllerVersions.stream().max(Comparator.naturalOrder()).get();
        var controllerVersion = controllerVersions.stream().min(Comparator.naturalOrder()).get();
        var isSystemVersion = statistics.version().equals(systemVersion);
        var isControllerVersion = statistics.version().equals(controllerVersion.version());
        var confidence = controller.curator().readConfidenceOverrides().get(statistics.version());
        var confidenceIsOverridden = confidence != null;
        var previousStatus = controller.versionStatus().version(statistics.version());

        // Compute confidence
        if (!confidenceIsOverridden) {
            // Always compute confidence for system and controller
            if (isSystemVersion || isControllerVersion) {
                confidence = VespaVersion.confidenceFrom(statistics, controller);
            } else {
                // This is an older version so we preserve the existing confidence, if any
                confidence = getOrUpdateConfidence(statistics, controller);
            }
        }

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

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

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

    /**
     * Calculate confidence from given deployment statistics.
     *
     * @return previously calculated confidence for this version. If none exists, a new confidence will be calculated.
     */
    private static VespaVersion.Confidence getOrUpdateConfidence(DeploymentStatistics statistics, Controller controller) {
        return controller.versionStatus().versions().stream()
                         .filter(v -> statistics.version().equals(v.versionNumber()))
                         .map(VespaVersion::confidence)
                         .findFirst()
                         .orElseGet(() -> VespaVersion.confidenceFrom(statistics, controller));
    }

}