aboutsummaryrefslogtreecommitdiffstats
path: root/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RevisionHistory.java
blob: a8dd1c442fe2dfb3fffe3a8c31bc5596b57f5cc1 (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
package com.yahoo.vespa.hosted.controller.deployment;

import ai.vespa.validation.Validation;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;

import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.TreeMap;
import java.util.function.Predicate;

import static ai.vespa.validation.Validation.require;
import static java.util.Collections.emptyNavigableMap;
import static java.util.function.Predicate.not;

/**
 * History of application revisions for an {@link com.yahoo.vespa.hosted.controller.Application}.
 *
 * @author jonmv
 */
public class RevisionHistory {

    private static final Comparator<JobId> comparator = Comparator.comparing(JobId::application).thenComparing(JobId::type);

    private final NavigableMap<RevisionId, ApplicationVersion> production;
    private final NavigableMap<JobId, NavigableMap<RevisionId, ApplicationVersion>> development;

    private RevisionHistory(NavigableMap<RevisionId, ApplicationVersion> production,
                           NavigableMap<JobId, NavigableMap<RevisionId, ApplicationVersion>> development) {
        this.production = production;
        this.development = development;
    }

    public static RevisionHistory empty() {
        return ofRevisions(List.of(), Map.of());
    }

    public static RevisionHistory ofRevisions(Collection<ApplicationVersion> productionRevisions,
                                              Map<JobId, ? extends Collection<ApplicationVersion>> developmentRevisions) {
        NavigableMap<RevisionId, ApplicationVersion> production = new TreeMap<>();
        for (ApplicationVersion revision : productionRevisions)
            production.put(revision.id(), revision);

        NavigableMap<JobId, NavigableMap<RevisionId, ApplicationVersion>> development = new TreeMap<>(comparator);
        developmentRevisions.forEach((job, jobRevisions) -> {
            NavigableMap<RevisionId, ApplicationVersion> revisions = development.computeIfAbsent(job, __ -> new TreeMap<>());
            for (ApplicationVersion revision : jobRevisions)
                revisions.put(revision.id(), revision);
        });

        return new RevisionHistory(production, development);
    }

    /** Returns a copy of this where any production revisions without packages, and older than the given one, are removed. */
    public RevisionHistory withoutOlderThan(RevisionId id) {
        if (production.headMap(id).isEmpty()) return this;
        NavigableMap<RevisionId, ApplicationVersion> production = new TreeMap<>(this.production);
        production.headMap(id).values().removeIf(not(ApplicationVersion::hasPackage));
        return new RevisionHistory(production, development);
    }

    /** Returns a copy of this without any development revisions older than the given. */
    public RevisionHistory withoutOlderThan(RevisionId id, JobId job) {
        if ( ! development.containsKey(job) || development.get(job).headMap(id).isEmpty()) return this;
        NavigableMap<JobId, NavigableMap<RevisionId, ApplicationVersion>> development = new TreeMap<>(this.development);
        development.compute(job, (__, revisions) -> revisions.tailMap(id, true));
        return new RevisionHistory(production, development);
    }

    /** Returns a copy of this with the revision added or updated. */
    public RevisionHistory with(ApplicationVersion revision) {
        if (revision.id().isProduction()) {
            if ( ! production.isEmpty() && revision.bundleHash().flatMap(hash -> production.lastEntry().getValue().bundleHash().map(hash::equals)).orElse(false))
                revision = revision.skipped();

            NavigableMap<RevisionId, ApplicationVersion> production = new TreeMap<>(this.production);
            production.put(revision.id(), revision);
            return new RevisionHistory(production, development);
        }
        else {
            NavigableMap<JobId, NavigableMap<RevisionId, ApplicationVersion>> development = new TreeMap<>(this.development);
            NavigableMap<RevisionId, ApplicationVersion> revisions = development.compute(revision.id().job(), (__, old) -> new TreeMap<>(old != null ? old : emptyNavigableMap()));
            if ( ! revisions.isEmpty()) revisions.compute(revisions.lastKey(), (__, last) -> last.withoutPackage());
            revisions.put(revision.id(), revision);
            return new RevisionHistory(production, development);
        }
    }

    // Fallback for when an application version isn't known for the given key.
    private static ApplicationVersion revisionOf(RevisionId id) {
        return new ApplicationVersion(id, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), false, false, Optional.empty(), Optional.empty(), 0);
    }

    /** Returns the production {@link ApplicationVersion} with this revision ID. */
    public ApplicationVersion get(RevisionId id) {
        return id.isProduction() ? production.getOrDefault(id, revisionOf(id))
                                 : development.getOrDefault(id.job(), emptyNavigableMap())
                                              .getOrDefault(id, revisionOf(id));
    }

    /** Returns the last submitted production build. */
    public Optional<ApplicationVersion> last() {
        return Optional.ofNullable(production.lastEntry()).map(Map.Entry::getValue);
    }

    /** Returns all known production revisions we still have the package for, from oldest to newest. */
    public List<ApplicationVersion> withPackage() {
        return production.values().stream()
                         .filter(ApplicationVersion::hasPackage)
                         .toList();
    }

    /** Returns the currently deployable revisions of the application. */
    public Deque<ApplicationVersion> deployable(boolean ascending) {
        Deque<ApplicationVersion> versions = new ArrayDeque<>();
        for (ApplicationVersion version : withPackage()) {
            if (version.isDeployable()) {
                if (ascending) versions.addLast(version);
                else versions.addFirst(version);
            }
        }
        return versions;
    }

    /** All known production revisions, in ascending order. */
    public List<ApplicationVersion> production() {
        return List.copyOf(production.values());
    }

    /* All known development revisions, in ascending order, per job. */
    public NavigableMap<JobId, List<ApplicationVersion>> development() {
        NavigableMap<JobId, List<ApplicationVersion>> copy = new TreeMap<>(comparator);
        development.forEach((job, revisions) -> copy.put(job, List.copyOf(revisions.values())));
        return Collections.unmodifiableNavigableMap(copy);
    }

}