summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2022-04-11 09:03:41 +0200
committerGitHub <noreply@github.com>2022-04-11 09:03:41 +0200
commite5df1ee99b8468bca986af0ad1ced87773f21154 (patch)
tree11c318db38258697922af140122c92c2d9047cbc
parentdc810e70e30432c743fc617d0ed32619b7ea7dab (diff)
parent3b1f64c52e4e456fbee8f874f8823505cc631c98 (diff)
Merge pull request #22076 from vespa-engine/jonmv/deployment-risk
Jonmv/deployment risk
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/ApplicationVersion.java59
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/RevisionId.java59
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java46
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java38
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java26
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java59
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Change.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java113
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java19
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java105
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RevisionHistory.java145
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Versions.java8
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java71
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java19
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java9
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java11
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java43
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ApplicationStoreMock.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java6
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java43
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/root.json12
26 files changed, 561 insertions, 349 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/ApplicationVersion.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/ApplicationVersion.java
index ae9c5285be6..b2f817a2393 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/ApplicationVersion.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/ApplicationVersion.java
@@ -17,14 +17,10 @@ import java.util.OptionalLong;
*/
public class ApplicationVersion implements Comparable<ApplicationVersion> {
- /**
- * Used in cases where application version cannot be determined, such as manual deployments (e.g. in dev
- * environment)
- */
- public static final ApplicationVersion unknown = new ApplicationVersion(Optional.empty(), OptionalLong.empty(),
+ /** Should not be used, but may still exist in serialized data :S */
+ public static final ApplicationVersion unknown = new ApplicationVersion(Optional.empty(), OptionalLong.empty(), Optional.empty(),
Optional.empty(), Optional.empty(), Optional.empty(),
- Optional.empty(), Optional.empty(), true,
- Optional.empty());
+ Optional.empty(), true, Optional.empty(), false, true);
// This never changes and is only used to create a valid semantic version number, as required by application bundles
private static final String majorVersion = "1.0";
@@ -38,11 +34,14 @@ public class ApplicationVersion implements Comparable<ApplicationVersion> {
private final Optional<String> commit;
private final boolean deployedDirectly;
private final Optional<String> bundleHash;
+ private final boolean hasPackage;
+ private final boolean shouldSkip;
/** Public for serialisation only. */
public ApplicationVersion(Optional<SourceRevision> source, OptionalLong buildNumber, Optional<String> authorEmail,
Optional<Version> compileVersion, Optional<Instant> buildTime, Optional<String> sourceUrl,
- Optional<String> commit, boolean deployedDirectly, Optional<String> bundleHash) {
+ Optional<String> commit, boolean deployedDirectly, Optional<String> bundleHash,
+ boolean hasPackage, boolean shouldSkip) {
if (buildNumber.isEmpty() && ( source.isPresent() || authorEmail.isPresent() || compileVersion.isPresent()
|| buildTime.isPresent() || sourceUrl.isPresent() || commit.isPresent()))
throw new IllegalArgumentException("Build number must be present if any other attribute is");
@@ -68,13 +67,20 @@ public class ApplicationVersion implements Comparable<ApplicationVersion> {
this.commit = Objects.requireNonNull(commit, "commit cannot be null");
this.deployedDirectly = deployedDirectly;
this.bundleHash = bundleHash;
+ this.hasPackage = hasPackage;
+ this.shouldSkip = shouldSkip;
+ }
+
+ public RevisionId id() {
+ return isDeployedDirectly() ? RevisionId.forDevelopment(buildNumber().orElse(0))
+ : RevisionId.forProduction(buildNumber().orElseThrow());
}
/** Create an application package version from a completed build, without an author email */
public static ApplicationVersion from(SourceRevision source, long buildNumber) {
return new ApplicationVersion(Optional.of(source), OptionalLong.of(buildNumber), Optional.empty(),
Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), false,
- Optional.empty());
+ Optional.empty(), true, false);
}
/** Creates a version from a completed build, an author email, and build meta data. */
@@ -82,7 +88,7 @@ public class ApplicationVersion implements Comparable<ApplicationVersion> {
Version compileVersion, Instant buildTime) {
return new ApplicationVersion(Optional.of(source), OptionalLong.of(buildNumber), Optional.of(authorEmail),
Optional.of(compileVersion), Optional.of(buildTime), Optional.empty(), Optional.empty(), false,
- Optional.empty());
+ Optional.empty(), true, false);
}
/** Creates a version from a completed build, an author email, and build meta data. */
@@ -90,11 +96,13 @@ public class ApplicationVersion implements Comparable<ApplicationVersion> {
Optional<Version> compileVersion, Optional<Instant> buildTime,
Optional<String> sourceUrl, Optional<String> commit, boolean deployedDirectly,
Optional<String> bundleHash) {
- return new ApplicationVersion(source, OptionalLong.of(buildNumber), authorEmail, compileVersion, buildTime, sourceUrl, commit, deployedDirectly, bundleHash);
+ return new ApplicationVersion(source, OptionalLong.of(buildNumber), authorEmail, compileVersion,
+ buildTime, sourceUrl, commit, deployedDirectly, bundleHash, true, false);
}
/** Returns a unique identifier for this version or "unknown" if version is not known */
- public String id() {
+ // TODO jonmv: kill
+ public String stringId() {
if (isUnknown()) return "unknown";
return source.map(SourceRevision::commit).map(ApplicationVersion::abbreviateCommit)
@@ -151,6 +159,31 @@ public class ApplicationVersion implements Comparable<ApplicationVersion> {
return deployedDirectly;
}
+ /** Returns a copy of this without a package stored. */
+ public ApplicationVersion withoutPackage() {
+ return new ApplicationVersion(source, buildNumber, authorEmail, compileVersion, buildTime, sourceUrl, commit, deployedDirectly, bundleHash, false, shouldSkip);
+ }
+
+ /** Whether we still have the package for this revision. */
+ public boolean hasPackage() {
+ return hasPackage;
+ }
+
+ /** Returns a copy of this which will not be rolled out to production. */
+ public ApplicationVersion skipped() {
+ return new ApplicationVersion(source, buildNumber, authorEmail, compileVersion, buildTime, sourceUrl, commit, deployedDirectly, bundleHash, hasPackage, true);
+ }
+
+ /** Whether we still have the package for this revision. */
+ public boolean shouldSkip() {
+ return shouldSkip;
+ }
+
+ /** Whether this revision should be deployed. */
+ public boolean isDeployable() {
+ return hasPackage && ! shouldSkip;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -168,7 +201,7 @@ public class ApplicationVersion implements Comparable<ApplicationVersion> {
@Override
public String toString() {
- return "Application package version: " + id()
+ return "Application package version: " + stringId()
+ source.map(s -> ", " + s.toString()).orElse("")
+ authorEmail.map(e -> ", by " + e).orElse("")
+ compileVersion.map(v -> ", built against " + v).orElse("")
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/RevisionId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/RevisionId.java
new file mode 100644
index 00000000000..1e9f412d4da
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/RevisionId.java
@@ -0,0 +1,59 @@
+package com.yahoo.vespa.hosted.controller.api.integration.deployment;
+
+import java.util.Objects;
+
+import static ai.vespa.validation.Validation.requireAtLeast;
+
+/**
+ * ID of a revision of an application package. This is the build number, and whether it was submitted for production deployment.
+ *
+ * @author jonmv
+ */
+public class RevisionId implements Comparable<RevisionId> {
+
+ private final long number;
+ private final boolean production;
+
+ private RevisionId(long number, boolean production) {
+ this.number = number;
+ this.production = production;
+ }
+
+ public static RevisionId forProduction(long number) {
+ return new RevisionId(requireAtLeast(number, "build number", 1L), true);
+ }
+
+ public static RevisionId forDevelopment(long number) {
+ return new RevisionId(requireAtLeast(number, "build number", 0L), false);
+ }
+
+ public long number() { return number; }
+
+ public boolean isProduction() { return production; }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ RevisionId that = (RevisionId) o;
+ return number == that.number && production == that.production;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(number, production);
+ }
+
+ /** Unknown, manual builds sort first, then known manual builds, then production builds, by increasing build number */
+ @Override
+ public int compareTo(RevisionId o) {
+ return production != o.production ? Boolean.compare(production, o.production)
+ : Long.compare(number, o.number);
+ }
+
+ @Override
+ public String toString() {
+ return (production ? "prod" : "dev") + " build " + number;
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java
index 30f416747e0..ccdf4390d61 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java
@@ -14,16 +14,15 @@ import com.yahoo.vespa.hosted.controller.application.ApplicationActivity;
import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.QuotaUsage;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
+import com.yahoo.vespa.hosted.controller.deployment.RevisionHistory;
import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import java.security.PublicKey;
import java.time.Instant;
-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.Objects;
@@ -31,9 +30,7 @@ import java.util.Optional;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.Set;
-import java.util.SortedSet;
import java.util.TreeMap;
-import java.util.TreeSet;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -50,8 +47,7 @@ public class Application {
private final Instant createdAt;
private final DeploymentSpec deploymentSpec;
private final ValidationOverrides validationOverrides;
- private final Optional<ApplicationVersion> latestVersion;
- private final SortedSet<ApplicationVersion> versions;
+ private final RevisionHistory revisions;
private final OptionalLong projectId;
private final Optional<IssueId> deploymentIssueId;
private final Optional<IssueId> ownershipIssueId;
@@ -63,16 +59,16 @@ public class Application {
/** Creates an empty application. */
public Application(TenantAndApplicationId id, Instant now) {
- this(id, now, DeploymentSpec.empty, ValidationOverrides.empty,
- Optional.empty(), Optional.empty(), Optional.empty(), OptionalInt.empty(),
- new ApplicationMetrics(0, 0), Set.of(), OptionalLong.empty(), Optional.empty(), new TreeSet<>(), List.of());
+ this(id, now, DeploymentSpec.empty, ValidationOverrides.empty, Optional.empty(),
+ Optional.empty(), Optional.empty(), OptionalInt.empty(), new ApplicationMetrics(0, 0),
+ Set.of(), OptionalLong.empty(), RevisionHistory.empty(), List.of());
}
// DO NOT USE! For serialization purposes, only.
public Application(TenantAndApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides,
Optional<IssueId> deploymentIssueId, Optional<IssueId> ownershipIssueId, Optional<User> owner,
OptionalInt majorVersion, ApplicationMetrics metrics, Set<PublicKey> deployKeys, OptionalLong projectId,
- Optional<ApplicationVersion> latestVersion, SortedSet<ApplicationVersion> versions, Collection<Instance> instances) {
+ RevisionHistory revisions, Collection<Instance> instances) {
this.id = Objects.requireNonNull(id, "id cannot be null");
this.createdAt = Objects.requireNonNull(createdAt, "instant of creation cannot be null");
this.deploymentSpec = Objects.requireNonNull(deploymentSpec, "deploymentSpec cannot be null");
@@ -84,13 +80,12 @@ public class Application {
this.metrics = Objects.requireNonNull(metrics, "metrics cannot be null");
this.deployKeys = Objects.requireNonNull(deployKeys, "deployKeys cannot be null");
this.projectId = Objects.requireNonNull(projectId, "projectId cannot be null");
- this.latestVersion = requireNotUnknown(latestVersion);
- this.versions = versions;
+ this.revisions = revisions;
this.instances = instances.stream().collect(
Collectors.collectingAndThen(Collectors.toMap(Instance::name,
Function.identity(),
(i1, i2) -> {
- throw new IllegalArgumentException("Duplicate key " + i1);
+ throw new IllegalArgumentException("Duplicate instance " + i1.id());
},
TreeMap::new),
Collections::unmodifiableMap)
@@ -110,29 +105,8 @@ public class Application {
/** Returns the project id of this application, if it has any. */
public OptionalLong projectId() { return projectId; }
- /** Returns the last submitted version of this application. */
- public Optional<ApplicationVersion> latestVersion() {
- return versions.isEmpty() ? Optional.empty() : Optional.of(versions.last());
- }
-
- /** Returns the currently deployed versions of the application, ordered from oldest to newest. */
- public SortedSet<ApplicationVersion> versions() {
- return versions;
- }
-
- /** Returns the currently deployed versions of the application */
- public Collection<ApplicationVersion> deployableVersions(boolean ascending) {
- Deque<ApplicationVersion> versions = new ArrayDeque<>();
- String previousHash = "";
- for (ApplicationVersion version : versions()) {
- if (version.bundleHash().isEmpty() || ! previousHash.equals(version.bundleHash().get())) {
- if (ascending) versions.addLast(version);
- else versions.addFirst(version);
- }
- previousHash = version.bundleHash().orElse("");
- }
- return versions;
- }
+ /** Returns the known revisions for this application. */
+ public RevisionHistory revisions() { return revisions; }
/**
* Returns the last deployed validation overrides of this application,
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java
index 48f909df6b6..30648265ab9 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java
@@ -41,6 +41,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationS
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ArtifactRepository;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterId;
import com.yahoo.vespa.hosted.controller.api.integration.noderepository.RestartFilter;
import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
@@ -58,6 +59,7 @@ import com.yahoo.vespa.hosted.controller.certificate.EndpointCertificates;
import com.yahoo.vespa.hosted.controller.concurrent.Once;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger;
import com.yahoo.vespa.hosted.controller.deployment.JobStatus;
+import com.yahoo.vespa.hosted.controller.deployment.RevisionHistory;
import com.yahoo.vespa.hosted.controller.deployment.Run;
import com.yahoo.vespa.hosted.controller.deployment.RunStatus;
import com.yahoo.vespa.hosted.controller.notification.Notification;
@@ -82,6 +84,7 @@ import java.util.Collection;
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.Objects;
@@ -160,12 +163,31 @@ public class ApplicationController {
for (InstanceName instance : application.get().deploymentSpec().instanceNames())
if ( ! application.get().instances().containsKey(instance))
application = withNewInstance(application, id.instance(instance));
+ // TODO jonmv: remove when data is migrated
+ // Each controller will know only about the revisions which we have packages for when they upgrade.
+ // The last controller will populate any missing, historic data after it upgrades.
+ // When all controllers are upgraded, we can start using the data, and remove this.
+ Set<ApplicationVersion> production = new HashSet<>();
+ Map<JobId, Set<ApplicationVersion>> development = new HashMap<>();
+ for (InstanceName instance : application.get().instances().keySet()) {
+ for (JobType type : JobType.allIn(controller.system())) {
+ for (Run run : controller.jobController().runs(id.instance(instance), type).values()) {
+ ApplicationVersion revision = run.versions().targetApplication();
+ if ( ! revision.isDeployedDirectly()) production.add(revision);
+ else development.computeIfAbsent(run.id().job(), __ -> new HashSet<>()).add(revision);
+ }
+ }
+ }
+ application = application.withRevisions(revisions -> {
+ production.addAll(revisions.production()); // These are already properly set, and we want ot keep their hasPackage status.
+ return RevisionHistory.ofRevisions(production, development); // All the added data is just written for now. We'll use it later.
+ });
store(application);
});
count++;
}
log.log(Level.INFO, Text.format("Wrote %d applications in %s", count,
- Duration.between(start, clock.instant())));
+ Duration.between(start, clock.instant())));
});
}
@@ -175,7 +197,6 @@ public class ApplicationController {
}
/** Returns the instance with the given id, or null if it is not present */
- // TODO jonmv: remove or inline
public Optional<Instance> getInstance(ApplicationId id) {
return getApplication(TenantAndApplicationId.from(id)).flatMap(application -> application.get(id.instance()));
}
@@ -545,19 +566,6 @@ public class ApplicationController {
controller.notificationsDb().removeNotifications(notification.source());
}
- ApplicationVersion oldestDeployedVersion = application.get().oldestDeployedApplication()
- .orElse(ApplicationVersion.unknown);
-
- List<ApplicationVersion> olderVersions = application.get().versions().stream()
- .filter(version -> version.compareTo(oldestDeployedVersion) < 0)
- .sorted()
- .collect(Collectors.toList());
-
- // Remove any version not deployed anywhere - but keep one
- for (ApplicationVersion version : olderVersions) {
- application = application.withoutVersion(version);
- }
-
store(application);
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java
index e5ab13d2127..9ca23ca5d15 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java
@@ -45,17 +45,15 @@ public class Instance {
private final RotationStatus rotationStatus;
private final Map<JobType, Instant> jobPauses;
private final Change change;
- private final Optional<ApplicationVersion> latestDeployed;
/** Creates an empty instance */
public Instance(ApplicationId id) {
- this(id, Set.of(), Map.of(), List.of(), RotationStatus.EMPTY, Change.empty(), Optional.empty());
+ this(id, Set.of(), Map.of(), List.of(), RotationStatus.EMPTY, Change.empty());
}
/** Creates an empty instance*/
public Instance(ApplicationId id, Collection<Deployment> deployments, Map<JobType, Instant> jobPauses,
- List<AssignedRotation> rotations, RotationStatus rotationStatus, Change change,
- Optional<ApplicationVersion> latestDeployed) {
+ List<AssignedRotation> rotations, RotationStatus rotationStatus, Change change) {
this.id = Objects.requireNonNull(id, "id cannot be null");
this.deployments = Objects.requireNonNull(deployments, "deployments cannot be null").stream()
.collect(Collectors.toUnmodifiableMap(Deployment::zone, Function.identity()));
@@ -63,7 +61,6 @@ public class Instance {
this.rotations = List.copyOf(Objects.requireNonNull(rotations, "rotations cannot be null"));
this.rotationStatus = Objects.requireNonNull(rotationStatus, "rotationStatus cannot be null");
this.change = Objects.requireNonNull(change, "change cannot be null");
- this.latestDeployed = Objects.requireNonNull(latestDeployed, "latestDeployed cannot be null");
}
public Instance withNewDeployment(ZoneId zone, ApplicationVersion applicationVersion, Version version,
@@ -90,7 +87,7 @@ public class Instance {
else
jobPauses.remove(jobType);
- return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change, latestDeployed);
+ return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change);
}
public Instance recordActivityAt(Instant instant, ZoneId zone) {
@@ -121,19 +118,15 @@ public class Instance {
}
public Instance with(List<AssignedRotation> assignedRotations) {
- return new Instance(id, deployments.values(), jobPauses, assignedRotations, rotationStatus, change, latestDeployed);
+ return new Instance(id, deployments.values(), jobPauses, assignedRotations, rotationStatus, change);
}
public Instance with(RotationStatus rotationStatus) {
- return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change, latestDeployed);
+ return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change);
}
public Instance withChange(Change change) {
- return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change, latestDeployed);
- }
-
- public Instance withLatestDeployed(ApplicationVersion latestDeployed) {
- return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change, Optional.of(latestDeployed));
+ return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change);
}
private Instance with(Deployment deployment) {
@@ -143,7 +136,7 @@ public class Instance {
}
private Instance with(Map<ZoneId, Deployment> deployments) {
- return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change, latestDeployed);
+ return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change);
}
public ApplicationId id() { return id; }
@@ -188,11 +181,6 @@ public class Instance {
return change;
}
- /** Returns the application version that last rolled out to this instance. */
- public Optional<ApplicationVersion> latestDeployed() {
- return latestDeployed;
- }
-
/** Returns the total quota usage for this instance, excluding temporary deployments **/
public QuotaUsage quotaUsage() {
return deployments.values().stream()
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java
index 06ff381e4dc..551327fd2e5 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java
@@ -5,10 +5,10 @@ import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.application.api.ValidationOverrides;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.vespa.curator.Lock;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
+import com.yahoo.vespa.hosted.controller.deployment.RevisionHistory;
import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics;
import java.security.PublicKey;
@@ -21,8 +21,6 @@ import java.util.Optional;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
import java.util.function.UnaryOperator;
/**
@@ -44,8 +42,7 @@ public class LockedApplication {
private final ApplicationMetrics metrics;
private final Set<PublicKey> deployKeys;
private final OptionalLong projectId;
- private final Optional<ApplicationVersion> latestVersion;
- private final SortedSet<ApplicationVersion> versions;
+ private final RevisionHistory revisions;
private final Map<InstanceName, Instance> instances;
/**
@@ -59,15 +56,14 @@ public class LockedApplication {
application.deploymentSpec(), application.validationOverrides(),
application.deploymentIssueId(), application.ownershipIssueId(),
application.owner(), application.majorVersion(), application.metrics(), application.deployKeys(),
- application.projectId(), application.latestVersion(), application.versions(), application.instances());
+ application.projectId(), application.instances(), application.revisions());
}
private LockedApplication(Lock lock, TenantAndApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec,
ValidationOverrides validationOverrides,
Optional<IssueId> deploymentIssueId, Optional<IssueId> ownershipIssueId, Optional<User> owner,
OptionalInt majorVersion, ApplicationMetrics metrics, Set<PublicKey> deployKeys,
- OptionalLong projectId, Optional<ApplicationVersion> latestVersion, SortedSet<ApplicationVersion> versions,
- Map<InstanceName, Instance> instances) {
+ OptionalLong projectId, Map<InstanceName, Instance> instances, RevisionHistory revisions) {
this.lock = lock;
this.id = id;
this.createdAt = createdAt;
@@ -80,8 +76,7 @@ public class LockedApplication {
this.metrics = metrics;
this.deployKeys = deployKeys;
this.projectId = projectId;
- this.latestVersion = latestVersion;
- this.versions = versions;
+ this.revisions = revisions;
this.instances = Map.copyOf(instances);
}
@@ -89,7 +84,7 @@ public class LockedApplication {
public Application get() {
return new Application(id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
- projectId, latestVersion, versions, instances.values());
+ projectId, revisions, instances.values());
}
LockedApplication withNewInstance(InstanceName instance) {
@@ -97,7 +92,7 @@ public class LockedApplication {
instances.put(instance, new Instance(id.instance(instance)));
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
- projectId, latestVersion, versions, instances);
+ projectId, instances, revisions);
}
public LockedApplication with(InstanceName instance, UnaryOperator<Instance> modification) {
@@ -105,7 +100,7 @@ public class LockedApplication {
instances.put(instance, modification.apply(instances.get(instance)));
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
- projectId, latestVersion, versions, instances);
+ projectId, instances, revisions);
}
public LockedApplication without(InstanceName instance) {
@@ -113,51 +108,43 @@ public class LockedApplication {
instances.remove(instance);
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
- projectId, latestVersion, versions, instances);
- }
-
- public LockedApplication withNewSubmission(ApplicationVersion latestVersion) {
- SortedSet<ApplicationVersion> applicationVersions = new TreeSet<>(versions);
- applicationVersions.add(latestVersion);
- return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
- projectId, Optional.of(latestVersion), applicationVersions, instances);
+ projectId, instances, revisions);
}
public LockedApplication withProjectId(OptionalLong projectId) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
- projectId, latestVersion, versions, instances);
+ projectId, instances, revisions);
}
public LockedApplication withDeploymentIssueId(IssueId issueId) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
Optional.ofNullable(issueId), ownershipIssueId, owner, majorVersion, metrics, deployKeys,
- projectId, latestVersion, versions, instances);
+ projectId, instances, revisions);
}
public LockedApplication with(DeploymentSpec deploymentSpec) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
- projectId, latestVersion, versions, instances);
+ projectId, instances, revisions);
}
public LockedApplication with(ValidationOverrides validationOverrides) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
- projectId, latestVersion, versions, instances);
+ projectId, instances, revisions);
}
public LockedApplication withOwnershipIssueId(IssueId issueId) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, Optional.of(issueId), owner, majorVersion, metrics, deployKeys,
- projectId, latestVersion, versions, instances);
+ projectId, instances, revisions);
}
public LockedApplication withOwner(User owner) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, Optional.of(owner), majorVersion, metrics, deployKeys,
- projectId, latestVersion, versions, instances);
+ projectId, instances, revisions);
}
/** Set a major version for this, or set to null to remove any major version override */
@@ -165,13 +152,13 @@ public class LockedApplication {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, owner,
majorVersion == null ? OptionalInt.empty() : OptionalInt.of(majorVersion),
- metrics, deployKeys, projectId, latestVersion, versions, instances);
+ metrics, deployKeys, projectId, instances, revisions);
}
public LockedApplication with(ApplicationMetrics metrics) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
- projectId, latestVersion, versions, instances);
+ projectId, instances, revisions);
}
public LockedApplication withDeployKey(PublicKey pemDeployKey) {
@@ -179,7 +166,7 @@ public class LockedApplication {
keys.add(pemDeployKey);
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, keys,
- projectId, latestVersion, versions, instances);
+ projectId, instances, revisions);
}
public LockedApplication withoutDeployKey(PublicKey pemDeployKey) {
@@ -187,15 +174,13 @@ public class LockedApplication {
keys.remove(pemDeployKey);
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, keys,
- projectId, latestVersion, versions, instances);
+ projectId, instances, revisions);
}
- public LockedApplication withoutVersion(ApplicationVersion version) {
- SortedSet<ApplicationVersion> applicationVersions = new TreeSet<>(versions);
- applicationVersions.remove(version);
+ public LockedApplication withRevisions(UnaryOperator<RevisionHistory> change) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
- projectId, latestVersion, applicationVersions, instances);
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics,
+ deployKeys, projectId, instances, change.apply(revisions));
}
@Override
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Change.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Change.java
index 244fb952b3f..b5a4a2a71d7 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Change.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Change.java
@@ -127,7 +127,7 @@ public final class Change {
changes.add("pin to " + platform.map(Version::toString).orElse("current platform"));
else
platform.ifPresent(version -> changes.add("upgrade to " + version.toString()));
- application.ifPresent(version -> changes.add("application change to " + version.id()));
+ application.ifPresent(version -> changes.add("application change to " + version.stringId()));
changes.setEmptyValue("no change");
return changes.toString();
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java
index a042215616c..eb1e4e54fd7 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java
@@ -10,6 +10,7 @@ import com.yahoo.config.application.api.DeploymentSpec.DeclaredTest;
import com.yahoo.config.application.api.DeploymentSpec.DeclaredZone;
import com.yahoo.config.application.api.DeploymentSpec.UpgradeRollout;
import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.zone.ZoneId;
@@ -26,6 +27,7 @@ import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
@@ -49,6 +51,7 @@ import static java.util.Comparator.naturalOrder;
import static java.util.Objects.requireNonNull;
import static java.util.function.BinaryOperator.maxBy;
import static java.util.stream.Collectors.collectingAndThen;
+import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toUnmodifiableList;
@@ -59,27 +62,6 @@ import static java.util.stream.Collectors.toUnmodifiableList;
*/
public class DeploymentStatus {
- public static List<JobId> jobsFor(Application application, SystemName system) {
- if (DeploymentSpec.empty.equals(application.deploymentSpec()))
- return List.of();
-
- return application.deploymentSpec().instances().stream()
- .flatMap(spec -> Stream.concat(Stream.of(systemTest, stagingTest),
- flatten(spec).filter(step -> step.concerns(prod))
- .map(step -> {
- if (step instanceof DeclaredZone)
- return JobType.from(system, prod, ((DeclaredZone) step).region().get());
- return JobType.testFrom(system, ((DeclaredTest) step).region());
- })
- .flatMap(Optional::stream))
- .map(type -> new JobId(application.id().instance(spec.name()), type)))
- .collect(toUnmodifiableList());
- }
-
- private static Stream<DeploymentSpec.Step> flatten(DeploymentSpec.Step step) {
- return step instanceof DeploymentSpec.Steps ? step.steps().stream().flatMap(DeploymentStatus::flatten) : Stream.of(step);
- }
-
private static <T> List<T> union(List<T> first, List<T> second) {
return Stream.concat(first.stream(), second.stream()).distinct().collect(toUnmodifiableList());
}
@@ -93,17 +75,18 @@ public class DeploymentStatus {
private final Map<JobId, StepStatus> jobSteps;
private final List<StepStatus> allSteps;
- public DeploymentStatus(Application application, Map<JobId, JobStatus> allJobs, SystemName system,
+ public DeploymentStatus(Application application, Function<JobId, JobStatus> allJobs, SystemName system,
Version systemVersion, Function<InstanceName, VersionCompatibility> versionCompatibility, Instant now) {
this.application = requireNonNull(application);
- this.allJobs = JobList.from(allJobs.values());
this.system = requireNonNull(system);
this.systemVersion = requireNonNull(systemVersion);
this.versionCompatibility = versionCompatibility;
this.now = requireNonNull(now);
List<StepStatus> allSteps = new ArrayList<>();
- this.jobSteps = jobDependencies(application.deploymentSpec(), allSteps);
+ Map<JobId, JobStatus> jobs = new HashMap<>();
+ this.jobSteps = jobDependencies(application.deploymentSpec(), allSteps, job -> jobs.computeIfAbsent(job, allJobs));
this.allSteps = Collections.unmodifiableList(allSteps);
+ this.allJobs = JobList.from(jobSteps.keySet().stream().map(allJobs).collect(toList()));
}
/** The application this deployment status concerns. */
@@ -270,7 +253,7 @@ public class DeploymentStatus {
StepStatus status = instanceSteps().get(instance);
if (status == null) return Change.empty();
boolean ascending = next == application.deploymentSpec().requireInstance(instance).revisionTarget();
- for (ApplicationVersion version : application.deployableVersions(ascending)) {
+ for (ApplicationVersion version : application.revisions().deployable(ascending)) {
if (status.dependenciesCompletedAt(Change.of(version), Optional.empty()).map(now::isBefore).orElse(true)) continue;
Change change = Change.of(version);
if (application.productionDeployments().getOrDefault(instance, List.of()).stream()
@@ -486,31 +469,44 @@ public class DeploymentStatus {
private JobId firstDeclaredOrElseImplicitTest(JobType testJob) {
return application.deploymentSpec().instanceNames().stream()
.map(name -> new JobId(application.id().instance(name), testJob))
+ .filter(jobSteps::containsKey)
.min(comparing(id -> ! jobSteps.get(id).isDeclared())).orElseThrow();
}
/** JobId of any declared test of the given type, for the given instance. */
private Optional<JobId> declaredTest(ApplicationId instanceId, JobType testJob) {
JobId jobId = new JobId(instanceId, testJob);
- return jobSteps.get(jobId).isDeclared() ? Optional.of(jobId) : Optional.empty();
+ return jobSteps.containsKey(jobId) && jobSteps.get(jobId).isDeclared() ? Optional.of(jobId) : Optional.empty();
}
/** A DAG of the dependencies between the primitive steps in the spec, with iteration order equal to declaration order. */
- private Map<JobId, StepStatus> jobDependencies(DeploymentSpec spec, List<StepStatus> allSteps) {
+ private Map<JobId, StepStatus> jobDependencies(DeploymentSpec spec, List<StepStatus> allSteps, Function<JobId, JobStatus> jobs) {
if (DeploymentSpec.empty.equals(spec))
return Map.of();
Map<JobId, StepStatus> dependencies = new LinkedHashMap<>();
List<StepStatus> previous = List.of();
for (DeploymentSpec.Step step : spec.steps())
- previous = fillStep(dependencies, allSteps, step, previous, null);
+ previous = fillStep(dependencies, allSteps, step, previous, null, jobs,
+ instanceWithImplicitTest(test, spec),
+ instanceWithImplicitTest(staging, spec));
return Collections.unmodifiableMap(dependencies);
}
+ private static InstanceName instanceWithImplicitTest(Environment environment, DeploymentSpec spec) {
+ InstanceName first = null;
+ for (DeploymentInstanceSpec step : spec.instances()) {
+ if (step.concerns(environment)) return null;
+ first = first != null ? first : step.name();
+ }
+ return first;
+ }
+
/** Adds the primitive steps contained in the given step, which depend on the given previous primitives, to the dependency graph. */
- private List<StepStatus> fillStep(Map<JobId, StepStatus> dependencies, List<StepStatus> allSteps,
- DeploymentSpec.Step step, List<StepStatus> previous, InstanceName instance) {
+ private List<StepStatus> fillStep(Map<JobId, StepStatus> dependencies, List<StepStatus> allSteps, DeploymentSpec.Step step,
+ List<StepStatus> previous, InstanceName instance, Function<JobId, JobStatus> jobs,
+ InstanceName implicitSystemTest, InstanceName implicitStagingTest) {
if (step.steps().isEmpty() && ! (step instanceof DeploymentInstanceSpec)) {
if (instance == null)
return previous; // Ignore test and staging outside all instances.
@@ -522,31 +518,31 @@ public class DeploymentStatus {
}
JobType jobType;
+ JobId jobId;
StepStatus stepStatus;
if (step.concerns(test) || step.concerns(staging)) {
jobType = JobType.from(system, ((DeclaredZone) step).environment(), null)
.orElseThrow(() -> new IllegalStateException(application + " specifies " + step + ", but this has no job in " + system));
- stepStatus = JobStepStatus.ofTestDeployment((DeclaredZone) step, List.of(), this, instance, jobType, true);
+ jobId = new JobId(application.id().instance(instance), jobType);
+ stepStatus = JobStepStatus.ofTestDeployment((DeclaredZone) step, List.of(), this, jobs.apply(jobId), true);
previous = new ArrayList<>(previous);
previous.add(stepStatus);
}
else if (step.isTest()) {
jobType = JobType.testFrom(system, ((DeclaredTest) step).region())
.orElseThrow(() -> new IllegalStateException(application + " specifies " + step + ", but this has no job in " + system));
- JobType preType = JobType.from(system, prod, ((DeclaredTest) step).region())
- .orElseThrow(() -> new IllegalStateException(application + " specifies " + step + ", but this has no job in " + system));
- stepStatus = JobStepStatus.ofProductionTest((DeclaredTest) step, previous, this, instance, jobType, preType);
+ jobId = new JobId(application.id().instance(instance), jobType);
+ stepStatus = JobStepStatus.ofProductionTest((DeclaredTest) step, previous, this, jobs.apply(jobId));
previous = List.of(stepStatus);
}
else if (step.concerns(prod)) {
jobType = JobType.from(system, ((DeclaredZone) step).environment(), ((DeclaredZone) step).region().get())
.orElseThrow(() -> new IllegalStateException(application + " specifies " + step + ", but this has no job in " + system));
- stepStatus = JobStepStatus.ofProductionDeployment((DeclaredZone) step, previous, this, instance, jobType);
+ jobId = new JobId(application.id().instance(instance), jobType);
+ stepStatus = JobStepStatus.ofProductionDeployment((DeclaredZone) step, previous, this, jobs.apply(jobId));
previous = List.of(stepStatus);
}
else return previous; // Empty container steps end up here, and are simply ignored.
- JobId jobId = new JobId(application.id().instance(instance), jobType);
- allSteps.removeIf(existing -> existing.job().equals(Optional.of(jobId))); // Replace implicit tests with explicit ones.
allSteps.add(stepStatus);
dependencies.put(jobId, stepStatus);
return previous;
@@ -558,27 +554,32 @@ public class DeploymentStatus {
instance = spec.name();
allSteps.add(instanceStatus);
previous = List.of(instanceStatus);
- for (JobType test : List.of(systemTest, stagingTest)) {
- JobId job = new JobId(application.id().instance(instance), test);
- if ( ! dependencies.containsKey(job)) {
- var testStatus = JobStepStatus.ofTestDeployment(new DeclaredZone(test.environment()), List.of(),
- this, job.application().instance(), test, false);
- dependencies.put(job, testStatus);
- allSteps.add(testStatus);
- }
+ if (instance.equals(implicitSystemTest)) {
+ JobId job = new JobId(application.id().instance(instance), systemTest);
+ JobStepStatus testStatus = JobStepStatus.ofTestDeployment(new DeclaredZone(test), List.of(),
+ this, jobs.apply(job), false);
+ dependencies.put(job, testStatus);
+ allSteps.add(testStatus);
+ }
+ if (instance.equals(implicitStagingTest)) {
+ JobId job = new JobId(application.id().instance(instance), stagingTest);
+ JobStepStatus testStatus = JobStepStatus.ofTestDeployment(new DeclaredZone(staging), List.of(),
+ this, jobs.apply(job), false);
+ dependencies.put(job, testStatus);
+ allSteps.add(testStatus);
}
}
if (step.isOrdered()) {
for (DeploymentSpec.Step nested : step.steps())
- previous = fillStep(dependencies, allSteps, nested, previous, instance);
+ previous = fillStep(dependencies, allSteps, nested, previous, instance, jobs, implicitSystemTest, implicitStagingTest);
return previous;
}
List<StepStatus> parallel = new ArrayList<>();
for (DeploymentSpec.Step nested : step.steps())
- parallel.addAll(fillStep(dependencies, allSteps, nested, previous, instance));
+ parallel.addAll(fillStep(dependencies, allSteps, nested, previous, instance, jobs, implicitSystemTest, implicitStagingTest));
return List.copyOf(parallel);
}
@@ -787,10 +788,9 @@ public class DeploymentStatus {
}
private static JobStepStatus ofProductionDeployment(DeclaredZone step, List<StepStatus> dependencies,
- DeploymentStatus status, InstanceName instance, JobType jobType) {
+ DeploymentStatus status, JobStatus job) {
ZoneId zone = ZoneId.from(step.environment(), step.region().get());
- JobStatus job = status.instanceJobs(instance).get(jobType);
- Optional<Deployment> existingDeployment = Optional.ofNullable(status.application().require(instance)
+ Optional<Deployment> existingDeployment = Optional.ofNullable(status.application().require(job.id().application().instance())
.deployments().get(zone));
return new JobStepStatus(StepType.deployment, step, dependencies, job, status) {
@@ -816,7 +816,7 @@ public class DeploymentStatus {
&& dependent.equals(job())) // Job should (re-)run in this case, but other dependents need not wait.
return Optional.empty();
- Change fullChange = status.application().require(instance).change();
+ Change fullChange = status.application().require(job.id().application().instance()).change();
if (existingDeployment.map(deployment -> ! (change.upgrades(deployment.version()) || change.upgrades(deployment.applicationVersion()))
&& (fullChange.downgrades(deployment.version()) || fullChange.downgrades(deployment.applicationVersion())))
.orElse(false))
@@ -836,11 +836,8 @@ public class DeploymentStatus {
}
private static JobStepStatus ofProductionTest(DeclaredTest step, List<StepStatus> dependencies,
- DeploymentStatus status, InstanceName instance,
- JobType testType, JobType prodType) {
- JobStatus job = status.instanceJobs(instance).get(testType);
- JobId prodId = new JobId(status.application().id().instance(instance), prodType);
-
+ DeploymentStatus status, JobStatus job) {
+ JobId prodId = new JobId(job.id().application(), JobType.from(status.system, job.id().type().zone(status.system)).get());
return new JobStepStatus(StepType.test, step, dependencies, job, status) {
@Override
Optional<Instant> readyAt(Change change, Optional<JobId> dependent) {
@@ -863,9 +860,7 @@ public class DeploymentStatus {
}
private static JobStepStatus ofTestDeployment(DeclaredZone step, List<StepStatus> dependencies,
- DeploymentStatus status, InstanceName instance,
- JobType jobType, boolean declared) {
- JobStatus job = status.instanceJobs(instance).get(jobType);
+ DeploymentStatus status, JobStatus job, boolean declared) {
return new JobStepStatus(StepType.test, step, dependencies, job, status) {
@Override
Optional<Instant> completedAt(Change change, Optional<JobId> dependent) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java
index 7ef8a4b4ae6..ae180ec1192 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java
@@ -76,18 +76,6 @@ public class DeploymentTrigger {
}
public void notifyOfSubmission(TenantAndApplicationId id, ApplicationVersion version, long projectId) {
- if (applications().getApplication(id).isEmpty()) {
- log.log(Level.WARNING, "Ignoring submission from project '" + projectId +
- "': Unknown application '" + id + "'");
- return;
- }
-
- applications().lockApplicationOrThrow(id, application -> {
- application = application.withProjectId(OptionalLong.of(projectId));
- application = application.withNewSubmission(version);
- applications().store(application);
- });
- triggerNewRevision(id);
}
/**
@@ -464,11 +452,8 @@ public class DeploymentTrigger {
Change remaining = change;
if (status.hasCompleted(instance.name(), change.withoutApplication()))
remaining = remaining.withoutPlatform();
- if (status.hasCompleted(instance.name(), change.withoutPlatform())) {
+ if (status.hasCompleted(instance.name(), change.withoutPlatform()))
remaining = remaining.withoutApplication();
- if (change.application().isPresent())
- instance = instance.withLatestDeployed(change.application().get());
- }
return instance.withChange(remaining);
}
@@ -510,7 +495,7 @@ public class DeploymentTrigger {
public String toString() {
return jobType + " for " + instanceId +
" on (" + versions.targetPlatform() + versions.sourcePlatform().map(version -> " <-- " + version).orElse("") +
- ", " + versions.targetApplication().id() + versions.sourceApplication().map(version -> " <-- " + version.id()).orElse("") +
+ ", " + versions.targetApplication().stringId() + versions.sourceApplication().map(version -> " <-- " + version.stringId()).orElse("") +
"), ready since " + availableSince;
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java
index ae61c98cffb..0df4c0015bd 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java
@@ -185,14 +185,14 @@ public class InternalStepRunner implements StepRunner {
logger.log("Deploying platform version " +
versions.sourcePlatform().orElse(versions.targetPlatform()) +
" and application version " +
- versions.sourceApplication().orElse(versions.targetApplication()).id() + " ...");
+ versions.sourceApplication().orElse(versions.targetApplication()).stringId() + " ...");
return deployReal(id, true, logger);
}
private Optional<RunStatus> deployReal(RunId id, DualLogger logger) {
Versions versions = controller.jobController().run(id).get().versions();
logger.log("Deploying platform version " + versions.targetPlatform() +
- " and application version " + versions.targetApplication().id() + " ...");
+ " and application version " + versions.targetApplication().stringId() + " ...");
return deployReal(id, false, logger);
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java
index b9f09cc31ea..1806cd04389 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java
@@ -8,8 +8,10 @@ import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.Application;
+import com.yahoo.vespa.hosted.controller.ApplicationController;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.Instance;
+import com.yahoo.vespa.hosted.controller.LockedApplication;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
import com.yahoo.vespa.hosted.controller.api.integration.LogEntry;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
@@ -37,11 +39,11 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
-import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Optional;
+import java.util.OptionalLong;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
@@ -50,6 +52,7 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.UnaryOperator;
import java.util.logging.Level;
+import java.util.logging.Logger;
import java.util.stream.Stream;
import static com.yahoo.collections.Iterables.reversed;
@@ -63,8 +66,10 @@ import static com.yahoo.vespa.hosted.controller.deployment.Step.endStagingSetup;
import static com.yahoo.vespa.hosted.controller.deployment.Step.endTests;
import static com.yahoo.vespa.hosted.controller.deployment.Step.report;
import static java.time.temporal.ChronoUnit.SECONDS;
+import static java.util.Comparator.naturalOrder;
import static java.util.function.Predicate.not;
import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toUnmodifiableList;
@@ -86,6 +91,8 @@ public class JobController {
public static final Duration maxHistoryAge = Duration.ofDays(60);
+ private static final Logger log = Logger.getLogger(JobController.class.getName());
+
private final int historyLength;
private final Controller controller;
private final CuratorDb curator;
@@ -337,11 +344,7 @@ public class JobController {
private DeploymentStatus deploymentStatus(Application application, Version systemVersion) {
return new DeploymentStatus(application,
- DeploymentStatus.jobsFor(application, controller.system()).stream()
- .collect(toMap(job -> job,
- job -> jobStatus(job),
- (j1, j2) -> { throw new IllegalArgumentException("Duplicate key " + j1.id()); },
- LinkedHashMap::new)),
+ this::jobStatus,
controller.system(),
systemVersion,
instance -> controller.applications().versionCompatibility(application.id().instance(instance)),
@@ -417,20 +420,8 @@ public class JobController {
});
logs.flush(id);
metric.jobFinished(run.id().job(), finishedRun.status());
+ pruneRevisions(unlockedRun);
- DeploymentId deploymentId = new DeploymentId(unlockedRun.id().application(), unlockedRun.id().job().type().zone(controller.system()));
- (unlockedRun.versions().targetApplication().isDeployedDirectly() ?
- Stream.of(unlockedRun.id().type()) :
- JobType.allIn(controller.system()).stream().filter(jobType -> !jobType.environment().isManuallyDeployed()))
- .flatMap(jobType -> controller.jobController().runs(unlockedRun.id().application(), jobType).values().stream())
- .mapToLong(r -> r.versions().targetApplication().buildNumber().orElse(Integer.MAX_VALUE))
- .min()
- .ifPresent(oldestBuild -> {
- if (unlockedRun.versions().targetApplication().isDeployedDirectly())
- controller.applications().applicationStore().pruneDevDiffs(deploymentId, oldestBuild);
- else
- controller.applications().applicationStore().pruneDiffs(deploymentId.applicationId().tenant(), deploymentId.applicationId().application(), oldestBuild);
- });
return finishedRun;
});
}
@@ -454,10 +445,11 @@ public class JobController {
public ApplicationVersion submit(TenantAndApplicationId id, Optional<SourceRevision> revision, Optional<String> authorEmail,
Optional<String> sourceUrl, long projectId, ApplicationPackage applicationPackage,
byte[] testPackageBytes) {
+ ApplicationController applications = controller.applications();
AtomicReference<ApplicationVersion> version = new AtomicReference<>();
- controller.applications().lockApplicationOrThrow(id, application -> {
- Optional<ApplicationVersion> previousVersion = application.get().latestVersion();
- Optional<ApplicationPackage> previousPackage = previousVersion.flatMap(previous -> controller.applications().applicationStore().find(id.tenant(), id.application(), previous.buildNumber().getAsLong()))
+ applications.lockApplicationOrThrow(id, application -> {
+ Optional<ApplicationVersion> previousVersion = application.get().revisions().last();
+ Optional<ApplicationPackage> previousPackage = previousVersion.flatMap(previous -> applications.applicationStore().find(id.tenant(), id.application(), previous.buildNumber().getAsLong()))
.map(ApplicationPackage::new);
long previousBuild = previousVersion.map(latestVersion -> latestVersion.buildNumber().getAsLong()).orElse(0L);
String packageHash = applicationPackage.bundleHash() + ApplicationPackage.calculateHash(testPackageBytes);
@@ -471,28 +463,68 @@ public class JobController {
byte[] diff = previousPackage.map(previous -> ApplicationPackageDiff.diff(previous, applicationPackage))
.orElseGet(() -> ApplicationPackageDiff.diffAgainstEmpty(applicationPackage));
- controller.applications().applicationStore().put(id.tenant(),
+ applications.applicationStore().put(id.tenant(),
id.application(),
version.get(),
applicationPackage.zippedContent(),
diff);
- controller.applications().applicationStore().putTester(id.tenant(),
+ applications.applicationStore().putTester(id.tenant(),
id.application(),
version.get(),
testPackageBytes);
- controller.applications().applicationStore().putMeta(id.tenant(),
+ applications.applicationStore().putMeta(id.tenant(),
id.application(),
controller.clock().instant(),
applicationPackage.metaDataZip());
- prunePackages(id);
- controller.applications().storeWithUpdatedConfig(application, applicationPackage);
+ application = application.withProjectId(OptionalLong.of(projectId));
+ application = application.withRevisions(revisions -> revisions.with(version.get()));
+ application = withPrunedPackages(application);
- controller.applications().deploymentTrigger().notifyOfSubmission(id, version.get(), projectId);
+ applications.storeWithUpdatedConfig(application, applicationPackage);
+ applications.deploymentTrigger().triggerNewRevision(id);
});
return version.get();
}
+ private LockedApplication withPrunedPackages(LockedApplication application){
+ TenantAndApplicationId id = application.get().id();
+ Optional<ApplicationVersion> oldestDeployed = application.get().oldestDeployedApplication();
+ if (oldestDeployed.isPresent()) {
+ controller.applications().applicationStore().prune(id.tenant(), id.application(), oldestDeployed.get());
+ controller.applications().applicationStore().pruneTesters(id.tenant(), id.application(), oldestDeployed.get());
+
+ for (ApplicationVersion version : application.get().revisions().withPackage())
+ if (version.compareTo(oldestDeployed.get()) < 0)
+ application = application.withRevisions(revisions -> revisions.with(version.withoutPackage()));
+ }
+ return application;
+ }
+
+ /** Forget revisions no longer present in any relevant job history. */
+ private void pruneRevisions(Run run) {
+ TenantAndApplicationId applicationId = TenantAndApplicationId.from(run.id().application());
+ boolean isProduction = run.versions().targetApplication().id().isProduction();
+ (isProduction ? deploymentStatus(controller.applications().requireApplication(applicationId)).jobs().asList().stream()
+ : Stream.of(jobStatus(run.id().job())))
+ .flatMap(jobs -> jobs.runs().values().stream())
+ .map(r -> r.versions().targetApplication().id())
+ .filter(id -> id.isProduction() == isProduction)
+ .min(naturalOrder())
+ .ifPresent(oldestRevision -> {
+ controller.applications().lockApplicationOrThrow(applicationId, application -> {
+ if (isProduction) {
+ controller.applications().applicationStore().pruneDiffs(run.id().application().tenant(), run.id().application().application(), oldestRevision.number());
+ controller.applications().store(application.withRevisions(revisions -> revisions.withoutOlderThan(oldestRevision)));
+ }
+ else {
+ controller.applications().applicationStore().pruneDevDiffs(new DeploymentId(run.id().application(), run.id().job().type().zone(controller.system())), oldestRevision.number());
+ controller.applications().store(application.withRevisions(revisions -> revisions.withoutOlderThan(oldestRevision, run.id().job())));
+ }
+ });
+ });
+ }
+
/** Orders a run of the given type, or throws an IllegalStateException if that job type is already running. */
public void start(ApplicationId id, JobType type, Versions versions, boolean isRedeployment, Optional<String> reason) {
start(id, type, versions, isRedeployment, JobProfile.of(type), reason);
@@ -553,6 +585,7 @@ public class JobController {
false,
dryRun ? JobProfile.developmentDryRun : JobProfile.development,
Optional.empty());
+ controller.applications().store(application.withRevisions(revisions -> revisions.with(version, new JobId(id, type))));
});
locked(id, type, __ -> {
@@ -634,14 +667,14 @@ public class JobController {
// It's probably already deleted, so if we fail, that's OK.
}
curator.deleteRunData(id, type);
- logs.delete(id);
}
});
+ logs.delete(id);
+ curator.deleteRunData(id);
}
catch (Exception e) {
- return; // Don't remove the data if we couldn't clean up all resources.
+ log.log(WARNING, "failed cleaning up after deleted application", e);
}
- curator.deleteRunData(id);
});
}
@@ -649,16 +682,6 @@ public class JobController {
controller.serviceRegistry().configServer().deactivate(new DeploymentId(id.id(), type.zone(controller.system())));
}
- private void prunePackages(TenantAndApplicationId id) {
- controller.applications().lockApplicationIfPresent(id, application -> {
- application.get().oldestDeployedApplication()
- .ifPresent(oldestDeployed -> {
- controller.applications().applicationStore().prune(id.tenant(), id.application(), oldestDeployed);
- controller.applications().applicationStore().pruneTesters(id.tenant(), id.application(), oldestDeployed);
- });
- });
- }
-
/** Locks all runs and modifies the list of historic runs for the given application and job type. */
private void locked(ApplicationId id, JobType type, Consumer<SortedMap<RunId, Run>> modifications) {
try (Lock __ = curator.lock(id, type)) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RevisionHistory.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RevisionHistory.java
new file mode 100644
index 00000000000..a03a6e2aa4b
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RevisionHistory.java
@@ -0,0 +1,145 @@
+package com.yahoo.vespa.hosted.controller.deployment;
+
+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 static java.util.stream.Collectors.toList;
+
+/**
+ * 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 without any production revisions older than the given. */
+ public RevisionHistory withoutOlderThan(RevisionId id) {
+ if (production.headMap(id).isEmpty()) return this;
+ return new RevisionHistory(production.tailMap(id, true), 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 production revision added or updated */
+ public RevisionHistory with(ApplicationVersion revision) {
+ NavigableMap<RevisionId, ApplicationVersion> production = new TreeMap<>(this.production);
+ production.put(revision.id(), revision);
+ return new RevisionHistory(production, development);
+ }
+
+ /** Returns a copy of this with the new development revision added, and the previous version without a package. */
+ public RevisionHistory with(ApplicationVersion revision, JobId job) {
+ NavigableMap<JobId, NavigableMap<RevisionId, ApplicationVersion>> development = new TreeMap<>(this.development);
+ NavigableMap<RevisionId, ApplicationVersion> revisions = development.computeIfAbsent(job, __ -> new TreeMap<>());
+ 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, boolean production) {
+ return new ApplicationVersion(Optional.empty(), OptionalLong.of(id.number()), Optional.empty(),
+ Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(),
+ ! production, Optional.empty(), false, false);
+ }
+
+ /** Returns the production {@link ApplicationVersion} with this revision ID. */
+ public ApplicationVersion get(RevisionId id) {
+ return production.getOrDefault(id, revisionOf(id, true));
+ }
+
+ /** Returns the development {@link ApplicationVersion} for the give job, with this revision ID. */
+ public ApplicationVersion get(RevisionId id, JobId job) {
+ return development.getOrDefault(job, Collections.emptyNavigableMap())
+ .getOrDefault(id, revisionOf(id, false));
+ }
+
+ /** 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)
+ .collect(toList());
+ }
+
+ /** Returns the currently deployable revisions of the application. */
+ public Deque<ApplicationVersion> deployable(boolean ascending) {
+ Deque<ApplicationVersion> versions = new ArrayDeque<>();
+ String previousHash = "";
+ for (ApplicationVersion version : withPackage()) {
+ if (version.isDeployable() && (version.bundleHash().isEmpty() || ! previousHash.equals(version.bundleHash().get()))) {
+ if (ascending) versions.addLast(version);
+ else versions.addFirst(version);
+ }
+ previousHash = version.bundleHash().orElse("");
+ }
+ 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);
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Versions.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Versions.java
index d0b8e773cae..5299c59b774 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Versions.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Versions.java
@@ -104,8 +104,8 @@ public class Versions {
.map(source -> source + " -> ").orElse(""),
targetPlatform,
sourceApplication.filter(source -> !source.equals(targetApplication))
- .map(source -> source.id() + " -> ").orElse(""),
- targetApplication.id());
+ .map(source -> source.stringId() + " -> ").orElse(""),
+ targetApplication.stringId());
}
/** Create versions using given change and application */
@@ -144,8 +144,8 @@ public class Versions {
private static ApplicationVersion defaultApplicationVersion(Application application) {
return application.oldestDeployedApplication()
- .or(application::latestVersion)
- .orElse(ApplicationVersion.unknown);
+ .or(() -> application.revisions().last())
+ .orElseThrow(() -> new IllegalStateException("no known prod revisions, but asked for one, for " + application));
}
private static <T extends Comparable<T>> Optional<T> max(Optional<T> o1, Optional<T> o2) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java
index 0e880bb627c..02e1818932e 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java
@@ -1,7 +1,6 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.maintenance;
-import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.ApplicationController;
import com.yahoo.vespa.hosted.controller.Controller;
@@ -103,7 +102,7 @@ public class ApplicationOwnershipConfirmer extends ControllerMaintainer {
}
}
return new ApplicationSummary(app.id().defaultInstance(), app.activity().lastQueried(), app.activity().lastWritten(),
- app.latestVersion().flatMap(version -> version.buildTime()), metrics);
+ app.revisions().last().flatMap(version -> version.buildTime()), metrics);
}
/** Escalate ownership issues which have not been closed before a defined amount of time has passed. */
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java
index 6aa43d4db47..74bbf906653 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java
@@ -167,7 +167,7 @@ public class MetricsReporter extends ControllerMaintainer {
});
for (Application application : applications.asList())
- application.latestVersion()
+ application.revisions().last()
.flatMap(ApplicationVersion::buildTime)
.ifPresent(buildTime -> metric.set(DEPLOYMENT_BUILD_AGE_SECONDS,
controller().clock().instant().getEpochSecond() - buildTime.getEpochSecond(),
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java
index dee3c822465..3863733ad63 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java
@@ -18,6 +18,7 @@ import com.yahoo.slime.SlimeUtils;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Instance;
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.JobType;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision;
import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
@@ -30,6 +31,7 @@ import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
import com.yahoo.vespa.hosted.controller.application.EndpointId;
import com.yahoo.vespa.hosted.controller.application.QuotaUsage;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
+import com.yahoo.vespa.hosted.controller.deployment.RevisionHistory;
import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics;
import com.yahoo.vespa.hosted.controller.routing.rotation.RotationId;
import com.yahoo.vespa.hosted.controller.routing.rotation.RotationState;
@@ -49,8 +51,6 @@ import java.util.Optional;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
/**
* Serializes {@link Application}s to/from slime.
@@ -76,8 +76,9 @@ public class ApplicationSerializer {
private static final String instancesField = "instances";
private static final String deployingField = "deployingField";
private static final String projectIdField = "projectId";
- private static final String latestVersionField = "latestVersion";
private static final String versionsField = "versions";
+ private static final String prodVersionsField = "prodVersions";
+ private static final String devVersionsField = "devVersions";
private static final String pinnedField = "pinned";
private static final String deploymentIssueField = "deploymentIssueId";
private static final String ownershipIssueIdField = "ownershipIssueId";
@@ -97,7 +98,6 @@ public class ApplicationSerializer {
private static final String deploymentJobsField = "deploymentJobs"; // TODO jonmv: clean up serialisation format
private static final String assignedRotationsField = "assignedRotations";
private static final String assignedRotationEndpointField = "endpointId";
- private static final String latestDeployedField = "latestDeployed";
// Deployment fields
private static final String zoneField = "zone";
@@ -112,6 +112,8 @@ public class ApplicationSerializer {
private static final String commitField = "commitField";
private static final String authorEmailField = "authorEmailField";
private static final String deployedDirectlyField = "deployedDirectly";
+ private static final String hasPackageField = "hasPackage";
+ private static final String shouldSkipField = "shouldSkip";
private static final String compileVersionField = "compileVersion";
private static final String buildTimeField = "buildTime";
private static final String sourceUrlField = "sourceUrl";
@@ -167,8 +169,8 @@ public class ApplicationSerializer {
root.setDouble(queryQualityField, application.metrics().queryServiceQuality());
root.setDouble(writeQualityField, application.metrics().writeServiceQuality());
deployKeysToSlime(application.deployKeys(), root.setArray(pemDeployKeysField));
- application.latestVersion().ifPresent(version -> toSlime(version, root.setObject(latestVersionField)));
- versionsToSlime(application, root.setArray(versionsField));
+ revisionsToSlime(application.revisions().withPackage(), root.setArray(versionsField));
+ revisionsToSlime(application.revisions(), root.setArray(prodVersionsField), root.setArray(devVersionsField));
instancesToSlime(application, root.setArray(instancesField));
return slime;
}
@@ -182,7 +184,6 @@ public class ApplicationSerializer {
assignedRotationsToSlime(instance.rotations(), instanceObject);
toSlime(instance.rotationStatus(), instanceObject.setArray(rotationStatusField));
toSlime(instance.change(), instanceObject, deployingField);
- instance.latestDeployed().ifPresent(version -> toSlime(version, instanceObject.setObject(latestDeployedField)));
}
}
@@ -228,8 +229,18 @@ public class ApplicationSerializer {
object.setString(regionField, zone.region().value());
}
- private void versionsToSlime(Application application, Cursor object) {
- application.versions().forEach(version -> toSlime(version, object.addObject()));
+ private void revisionsToSlime(RevisionHistory revisions, Cursor revisionsArray, Cursor devRevisionsArray) {
+ revisionsToSlime(revisions.production(), revisionsArray);
+ revisions.development().forEach((job, devRevisions) -> {
+ Cursor devRevisionsObject = devRevisionsArray.addObject();
+ devRevisionsObject.setString(instanceNameField, job.application().instance().value());
+ devRevisionsObject.setString(jobTypeField, job.type().jobName());
+ revisionsToSlime(devRevisions, devRevisionsObject.setArray(versionsField));
+ });
+ }
+
+ private void revisionsToSlime(Iterable<ApplicationVersion> revisions, Cursor revisionsArray) {
+ revisions.forEach(version -> toSlime(version, revisionsArray.addObject()));
}
private void toSlime(ApplicationVersion applicationVersion, Cursor object) {
@@ -241,6 +252,8 @@ public class ApplicationSerializer {
applicationVersion.sourceUrl().ifPresent(url -> object.setString(sourceUrlField, url));
applicationVersion.commit().ifPresent(commit -> object.setString(commitField, commit));
object.setBool(deployedDirectlyField, applicationVersion.isDeployedDirectly());
+ object.setBool(hasPackageField, applicationVersion.hasPackage());
+ object.setBool(shouldSkipField, applicationVersion.shouldSkip());
applicationVersion.bundleHash().ifPresent(bundleHash -> object.setString(bundleHashField, bundleHash));
}
@@ -321,23 +334,34 @@ public class ApplicationSerializer {
Set<PublicKey> deployKeys = deployKeysFromSlime(root.field(pemDeployKeysField));
List<Instance> instances = instancesFromSlime(id, root.field(instancesField));
OptionalLong projectId = SlimeUtils.optionalLong(root.field(projectIdField));
- Optional<ApplicationVersion> latestVersion = latestVersionFromSlime(root.field(latestVersionField));
- SortedSet<ApplicationVersion> versions = versionsFromSlime(root.field(versionsField));
+ RevisionHistory revisions = revisionsFromSlime(root.field(versionsField), root.field(prodVersionsField), root.field(devVersionsField), id);
return new Application(id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics,
- deployKeys, projectId, latestVersion, versions, instances);
+ deployKeys, projectId, revisions, instances);
+ }
+
+ // TODO jonmv: read only from prodVersionsArray, once data is migrated.
+ private RevisionHistory revisionsFromSlime(Inspector versionsArray, Inspector prodVersionsArray, Inspector devVersionsArray, TenantAndApplicationId id) {
+ // Once last controller updates storage after upgrade, these should be in sync.
+ List<ApplicationVersion> revisions = prodVersionsArray.valid() ? revisionsFromSlime(prodVersionsArray)
+ : revisionsFromSlime(versionsArray);
+ Map<JobId, List<ApplicationVersion>> devRevisions = new HashMap<>();
+ devVersionsArray.traverse((ArrayTraverser) (__, devRevisionsObject) ->
+ devRevisions.put(jobIdFromSlime(id, devRevisionsObject), revisionsFromSlime(devRevisionsObject.field(versionsField))));
+
+ return RevisionHistory.ofRevisions(revisions, devRevisions);
}
- private Optional<ApplicationVersion> latestVersionFromSlime(Inspector latestVersionObject) {
- return Optional.of(applicationVersionFromSlime(latestVersionObject))
- .filter(version -> ! version.isUnknown());
+ private JobId jobIdFromSlime(TenantAndApplicationId base, Inspector idObject) {
+ return new JobId(base.instance(idObject.field(instanceNameField).asString()),
+ JobType.fromJobName(idObject.field(jobTypeField).asString()));
}
- private SortedSet<ApplicationVersion> versionsFromSlime(Inspector versionsObject) {
- SortedSet<ApplicationVersion> versions = new TreeSet<>();
- versionsObject.traverse((ArrayTraverser) (name, object) -> versions.add(applicationVersionFromSlime(object)));
- return versions;
+ private List<ApplicationVersion> revisionsFromSlime(Inspector versionsArray) {
+ List<ApplicationVersion> revisions = new ArrayList<>();
+ versionsArray.traverse((ArrayTraverser) (__, revisionObject) -> revisions.add(applicationVersionFromSlime(revisionObject)));
+ return revisions;
}
private List<Instance> instancesFromSlime(TenantAndApplicationId id, Inspector field) {
@@ -349,14 +373,12 @@ public class ApplicationSerializer {
List<AssignedRotation> assignedRotations = assignedRotationsFromSlime(object);
RotationStatus rotationStatus = rotationStatusFromSlime(object);
Change change = changeFromSlime(object.field(deployingField));
- Optional<ApplicationVersion> latestDeployed = latestVersionFromSlime(object.field(latestDeployedField));
instances.add(new Instance(id.instance(instanceName),
deployments,
jobPauses,
assignedRotations,
rotationStatus,
- change,
- latestDeployed));
+ change));
});
return instances;
}
@@ -445,9 +467,12 @@ public class ApplicationSerializer {
Optional<String> sourceUrl = SlimeUtils.optionalString(object.field(sourceUrlField));
Optional<String> commit = SlimeUtils.optionalString(object.field(commitField));
boolean deployedDirectly = object.field(deployedDirectlyField).asBool();
+ boolean hasPackage = ! object.field(hasPackageField).valid() || object.field(hasPackageField).asBool(); // TODO jonmv: remove default
+ boolean shouldSkip = object.field(shouldSkipField).asBool();
Optional<String> bundleHash = SlimeUtils.optionalString(object.field(bundleHashField));
- return new ApplicationVersion(sourceRevision, applicationBuildNumber, authorEmail, compileVersion, buildTime, sourceUrl, commit, deployedDirectly, bundleHash);
+ return new ApplicationVersion(sourceRevision, applicationBuildNumber, authorEmail, compileVersion, buildTime,
+ sourceUrl, commit, deployedDirectly, bundleHash, hasPackage, shouldSkip);
}
private Optional<SourceRevision> sourceRevisionFromSlime(Inspector object) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java
index c3d81b8dcd5..4d5fa0d278c 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java
@@ -187,8 +187,8 @@ class RunSerializer {
boolean deployedDirectly = versionObject.field(deployedDirectlyField).asBool();
Optional<String> bundleHash = SlimeUtils.optionalString(versionObject.field(bundleHashField));
- return new ApplicationVersion(source, OptionalLong.of(buildNumber), authorEmail,
- compileVersion, buildTime, sourceUrl, commit, deployedDirectly, bundleHash);
+ return new ApplicationVersion(source, OptionalLong.of(buildNumber), authorEmail, compileVersion,
+ buildTime, sourceUrl, commit, deployedDirectly, bundleHash, false, false);
}
// Don't change this — introduce a separate array instead.
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
index dcc916cc5a3..d965038e507 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
@@ -147,7 +147,6 @@ import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Scanner;
-import java.util.SortedSet;
import java.util.StringJoiner;
import java.util.function.Function;
import java.util.logging.Level;
@@ -817,7 +816,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
private HttpResponse applicationPackage(String tenantName, String applicationName, HttpRequest request) {
var tenantAndApplication = TenantAndApplicationId.from(tenantName, applicationName);
- SortedSet<ApplicationVersion> versions = controller.applications().requireApplication(tenantAndApplication).versions();
+ List<ApplicationVersion> versions = controller.applications().requireApplication(tenantAndApplication).revisions().withPackage();
if (versions.isEmpty())
throw new NotExistsException("No application package has been submitted for '" + tenantAndApplication + "'");
@@ -833,7 +832,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
.filter(ver -> ver.buildNumber().orElse(-1) == build)
.findFirst()
.orElseThrow(() -> new NotExistsException("No application package found for '" + tenantAndApplication + "' with build number " + build)))
- .orElseGet(versions::last);
+ .orElseGet(() -> versions.get(versions.size() - 1));
boolean tests = request.getBooleanProperty("tests");
byte[] applicationPackage = tests ?
@@ -1317,7 +1316,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
request.getUri()).toString());
DeploymentStatus status = controller.jobController().deploymentStatus(application);
- application.latestVersion().ifPresent(version -> JobControllerApiHandlerHelper.toSlime(object.setObject("latestVersion"), version));
+ application.revisions().last().ifPresent(version -> JobControllerApiHandlerHelper.toSlime(object.setObject("latestVersion"), version));
application.projectId().ifPresent(id -> object.setLong("projectId", id));
@@ -1440,7 +1439,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
"/instance/" + instance.id().instance().value() + "/job/",
request.getUri()).toString());
- application.latestVersion().ifPresent(version -> {
+ application.revisions().last().ifPresent(version -> {
version.sourceUrl().ifPresent(url -> object.setString("sourceUrl", url));
version.commit().ifPresent(commit -> object.setString("commit", commit));
});
@@ -1622,7 +1621,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
response.setString("nodes", withPathAndQuery("/zone/v2/" + deploymentId.zoneId().environment() + "/" + deploymentId.zoneId().region() + "/nodes/v2/node/", "recursive=true&application=" + deploymentId.applicationId().tenant() + "." + deploymentId.applicationId().application() + "." + deploymentId.applicationId().instance(), request.getUri()).toString());
response.setString("yamasUrl", monitoringSystemUri(deploymentId).toString());
response.setString("version", deployment.version().toFullString());
- response.setString("revision", deployment.applicationVersion().id());
+ response.setString("revision", deployment.applicationVersion().stringId());
Instant lastDeploymentStart = lastDeploymentStart(deploymentId.applicationId(), deployment);
response.setLong("deployTimeEpochMs", lastDeploymentStart.toEpochMilli());
controller.zoneRegistry().getDeploymentTimeToLive(deploymentId.zoneId())
@@ -1769,7 +1768,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
Cursor root = slime.setObject();
if ( ! instance.change().isEmpty()) {
instance.change().platform().ifPresent(version -> root.setString("platform", version.toString()));
- instance.change().application().ifPresent(applicationVersion -> root.setString("application", applicationVersion.id()));
+ instance.change().application().ifPresent(applicationVersion -> root.setString("application", applicationVersion.stringId()));
root.setBool("pinned", instance.change().isPinned());
}
return new SlimeJsonResponse(slime);
@@ -1914,7 +1913,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
StringBuilder response = new StringBuilder();
controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> {
- ApplicationVersion version = build == -1 ? application.get().latestVersion().get()
+ ApplicationVersion version = build == -1 ? application.get().revisions().last().get()
: getApplicationVersion(application.get(), build);
Change change = Change.of(version);
controller.applications().deploymentTrigger().forceChange(id, change);
@@ -1924,7 +1923,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
}
private ApplicationVersion getApplicationVersion(Application application, Long build) {
- return application.versions().stream()
+ return application.revisions().withPackage().stream()
.filter(version -> version.buildNumber().stream().anyMatch(build::equals))
.findFirst()
.filter(version -> controller.applications().applicationStore().hasBuild(application.id().tenant(),
@@ -2486,7 +2485,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
.map(Run::start)
.max(Comparator.naturalOrder()));
Optional<Instant> lastSubmission = applications.stream()
- .flatMap(app -> app.latestVersion().flatMap(ApplicationVersion::buildTime).stream())
+ .flatMap(app -> app.revisions().last().flatMap(ApplicationVersion::buildTime).stream())
.max(Comparator.naturalOrder());
object.setLong("createdAtMillis", tenant.createdAt().toEpochMilli());
if (tenant.type() == Tenant.Type.deleted)
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java
index 2375b5cf049..d3dc42eb352 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java
@@ -43,7 +43,6 @@ import java.time.format.TextStyle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
-import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -276,8 +275,8 @@ class JobControllerApiHandlerHelper {
stepStatus.coolingDownUntil(change).ifPresent(until -> stepObject.setLong("coolingDownUntil", until.toEpochMilli()));
stepStatus.blockedUntil(Change.of(controller.systemVersion(versionStatus))) // Dummy version — just anything with a platform.
.ifPresent(until -> stepObject.setLong("platformBlockedUntil", until.toEpochMilli()));
- application.latestVersion().map(Change::of).flatMap(stepStatus::blockedUntil) // Dummy version — just anything with an application.
- .ifPresent(until -> stepObject.setLong("applicationBlockedUntil", until.toEpochMilli()));
+ application.revisions().last().map(Change::of).flatMap(stepStatus::blockedUntil) // Dummy version — just anything with an application.
+ .ifPresent(until -> stepObject.setLong("applicationBlockedUntil", until.toEpochMilli()));
if (stepStatus.type() == DeploymentStatus.StepType.delay)
stepStatus.completedAt(change).ifPresent(completed -> stepObject.setLong("completedAt", completed.toEpochMilli()));
@@ -315,7 +314,7 @@ class JobControllerApiHandlerHelper {
change.platform().ifPresent(version -> availableArray.addObject().setString("platform", version.toFullString()));
toSlime(latestPlatformObject.setArray("blockers"), blockers.stream().filter(ChangeBlocker::blocksVersions));
}
- List<ApplicationVersion> availableApplications = new ArrayList<>(application.deployableVersions(false));
+ List<ApplicationVersion> availableApplications = new ArrayList<>(application.revisions().deployable(false));
if ( ! availableApplications.isEmpty()) {
var latestApplication = availableApplications.get(0);
Cursor latestApplicationObject = latestVersionsObject.setObject("application");
@@ -375,7 +374,7 @@ class JobControllerApiHandlerHelper {
}
Cursor buildsArray = responseObject.setArray("builds");
- application.versions().stream().sorted(reverseOrder()).forEach(version -> toSlime(buildsArray.addObject(), version));
+ application.revisions().withPackage().stream().sorted(reverseOrder()).forEach(version -> toSlime(buildsArray.addObject(), version));
return new SlimeJsonResponse(slime);
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java
index 974df695d07..56408b099c6 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java
@@ -14,6 +14,7 @@ import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.api.integration.billing.Quota;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.noderepository.ApplicationData;
+import com.yahoo.vespa.hosted.controller.deployment.RevisionHistory;
import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics;
import org.junit.Test;
@@ -61,11 +62,11 @@ public class DeploymentQuotaCalculatorTest {
@Test
public void quota_is_divided_among_prod_and_manual_instances() {
- var existing_dev_deployment = new Application(TenantAndApplicationId.from(ApplicationId.defaultId()), Instant.EPOCH, DeploymentSpec.empty, ValidationOverrides.empty,
- Optional.empty(), Optional.empty(), Optional.empty(), OptionalInt.empty(), new ApplicationMetrics(1, 1), Set.of(), OptionalLong.empty(),
- Optional.empty(), new TreeSet<>(), List.of(new Instance(ApplicationId.defaultId()).withNewDeployment(
- ZoneId.from(Environment.dev, RegionName.defaultName()), ApplicationVersion.unknown, Version.emptyVersion, Instant.EPOCH, Map.of(),
- QuotaUsage.create(0.53d))));
+ var existing_dev_deployment = new Application(TenantAndApplicationId.from(ApplicationId.defaultId()), Instant.EPOCH, DeploymentSpec.empty, ValidationOverrides.empty, Optional.empty(),
+ Optional.empty(), Optional.empty(), OptionalInt.empty(), new ApplicationMetrics(1, 1), Set.of(), OptionalLong.empty(), RevisionHistory.empty(),
+ List.of(new Instance(ApplicationId.defaultId()).withNewDeployment(
+ ZoneId.from(Environment.dev, RegionName.defaultName()), ApplicationVersion.unknown, Version.emptyVersion, Instant.EPOCH, Map.of(),
+ QuotaUsage.create(0.53d))));
Quota calculated = DeploymentQuotaCalculator.calculate(Quota.unlimited().withBudget(2), List.of(existing_dev_deployment), ApplicationId.defaultId(), ZoneId.defaultId(),
DeploymentSpec.fromXml(
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java
index 896f5355129..812a22d60a2 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java
@@ -169,7 +169,7 @@ public class DeploymentContext {
/** Completely deploy the current change */
public DeploymentContext deploy() {
Application application = application();
- assertTrue("Application package submitted", application.latestVersion().isPresent());
+ assertTrue("Application package submitted", application.revisions().last().isPresent());
assertFalse("Submission is not already deployed", application.instances().values().stream()
.anyMatch(instance -> instance.deployments().values().stream()
.anyMatch(deployment -> deployment.applicationVersion().equals(lastSubmission))));
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java
index f9de291ff0d..c67fcd05628 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java
@@ -7,11 +7,13 @@ import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.zone.RoutingMethod;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.flags.PermanentFlags;
+import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.application.Change;
+import com.yahoo.vespa.hosted.controller.application.Deployment;
+import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
import org.junit.Assert;
import org.junit.Test;
@@ -26,6 +28,7 @@ import java.util.Optional;
import java.util.OptionalLong;
import java.util.stream.Collectors;
+import static ai.vespa.validation.Validation.require;
import static com.yahoo.config.provision.SystemName.main;
import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.productionApNortheast1;
import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.productionApNortheast2;
@@ -608,15 +611,22 @@ public class DeploymentTriggerTest {
assertEquals(appVersion1, app.deployment(ZoneId.from("prod.us-central-1")).applicationVersion());
}
+ ApplicationVersion latestDeployed(Instance instance) {
+ return instance.productionDeployments().values().stream()
+ .map(Deployment::applicationVersion)
+ .reduce((o, n) -> require(o.equals(n), n, "all versions should be equal, but got " + o + " and " + n))
+ .orElseThrow(() -> new AssertionError("no versions deployed"));
+ }
+
@Test
public void downgradingApplicationVersionWorks() {
var app = tester.newDeploymentContext().submit().deploy();
ApplicationVersion appVersion0 = app.lastSubmission().get();
- assertEquals(Optional.of(appVersion0), app.instance().latestDeployed());
+ assertEquals(appVersion0, latestDeployed(app.instance()));
app.submit().deploy();
ApplicationVersion appVersion1 = app.lastSubmission().get();
- assertEquals(Optional.of(appVersion1), app.instance().latestDeployed());
+ assertEquals(appVersion1, latestDeployed(app.instance()));
// Downgrading application version.
tester.deploymentTrigger().forceChange(app.instanceId(), Change.of(appVersion0));
@@ -627,27 +637,26 @@ public class DeploymentTriggerTest {
.runJob(productionUsWest1);
assertEquals(Change.empty(), app.instance().change());
assertEquals(appVersion0, app.instance().deployments().get(productionUsEast3.zone(tester.controller().system())).applicationVersion());
- assertEquals(Optional.of(appVersion0), app.instance().latestDeployed());
+ assertEquals(appVersion0, latestDeployed(app.instance()));
}
@Test
public void settingANoOpChangeIsANoOp() {
var app = tester.newDeploymentContext().submit();
- assertEquals(Optional.empty(), app.instance().latestDeployed());
app.deploy();
ApplicationVersion appVersion0 = app.lastSubmission().get();
- assertEquals(Optional.of(appVersion0), app.instance().latestDeployed());
+ assertEquals(appVersion0, latestDeployed(app.instance()));
app.submit().deploy();
ApplicationVersion appVersion1 = app.lastSubmission().get();
- assertEquals(Optional.of(appVersion1), app.instance().latestDeployed());
+ assertEquals(appVersion1, latestDeployed(app.instance()));
// Triggering a roll-out of an already deployed application is a no-op.
assertEquals(Change.empty(), app.instance().change());
tester.deploymentTrigger().forceChange(app.instanceId(), Change.of(appVersion1));
assertEquals(Change.empty(), app.instance().change());
- assertEquals(Optional.of(appVersion1), app.instance().latestDeployed());
+ assertEquals(appVersion1, latestDeployed(app.instance()));
}
@Test
@@ -1021,8 +1030,8 @@ public class DeploymentTriggerTest {
// Since the post-deployment delay of i1 is incomplete, i3 doesn't yet get the change.
tester.outstandingChangeDeployer().run();
- assertEquals(v0, i1.instance().latestDeployed());
- assertEquals(v0, i2.instance().latestDeployed());
+ assertEquals(v0, Optional.of(latestDeployed(i1.instance())));
+ assertEquals(v0, Optional.of(latestDeployed(i2.instance())));
assertEquals(Optional.empty(), i1.instance().change().application());
assertEquals(Optional.empty(), i2.instance().change().application());
assertEquals(Optional.empty(), i3.instance().change().application());
@@ -1044,8 +1053,8 @@ public class DeploymentTriggerTest {
i4.runJob(systemTest).runJob(stagingTest);
i1.runJob(productionUsEast3); // v1
i2.runJob(productionUsEast3); // v1
- assertEquals(v1, i1.instance().latestDeployed());
- assertEquals(v1, i2.instance().latestDeployed());
+ assertEquals(v1, Optional.of(latestDeployed(i1.instance())));
+ assertEquals(v1, Optional.of(latestDeployed(i2.instance())));
assertEquals(Optional.empty(), i1.instance().change().application());
assertEquals(Optional.empty(), i2.instance().change().application());
assertEquals(v0, i3.instance().change().application());
@@ -1067,9 +1076,9 @@ public class DeploymentTriggerTest {
i3.runJob(testUsEast3);
assertEquals(Optional.empty(), i3.instance().change().application());
tester.outstandingChangeDeployer().run();
- assertEquals(v2, i1.instance().latestDeployed());
- assertEquals(v1, i2.instance().latestDeployed());
- assertEquals(v0, i3.instance().latestDeployed());
+ assertEquals(v2, Optional.of(latestDeployed(i1.instance())));
+ assertEquals(v1, Optional.of(latestDeployed(i2.instance())));
+ assertEquals(v0, Optional.of(latestDeployed(i3.instance())));
assertEquals(Optional.empty(), i1.instance().change().application());
assertEquals(v2, i2.instance().change().application());
assertEquals(v1, i3.instance().change().application());
@@ -1371,7 +1380,7 @@ public class DeploymentTriggerTest {
// Upgrade instance 1; upgrade rolls out first, with revision following.
// The new platform won't roll out to the conservative instance until the normal one is upgraded.
app1.submit(applicationPackage);
- assertEquals(Change.of(version).with(app1.application().latestVersion().get()), app1.instance().change());
+ assertEquals(Change.of(version).with(app1.application().revisions().last().get()), app1.instance().change());
// Upgrade platform.
app2.runJob(systemTest);
app1.runJob(stagingTest)
@@ -1434,7 +1443,7 @@ public class DeploymentTriggerTest {
tester.triggerJobs();
assertEquals(1, tester.jobs().active().size());
assertEquals(Change.empty(), app1.instance().change());
- assertEquals(Change.of(version).with(app1.application().latestVersion().get()), app2.instance().change());
+ assertEquals(Change.of(version).with(app1.application().revisions().last().get()), app2.instance().change());
app2.runJob(productionEuWest1)
.runJob(testEuWest1);
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ApplicationStoreMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ApplicationStoreMock.java
index a1e7dd4efe3..bedae15824f 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ApplicationStoreMock.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ApplicationStoreMock.java
@@ -53,7 +53,7 @@ public class ApplicationStoreMock implements ApplicationStore {
byte[] bytes = store.get(appId(tenantAndApplicationId.tenant(), tenantAndApplicationId.application())).get(applicationVersion);
if (bytes == null)
throw new IllegalArgumentException("No application package found for " + tenantAndApplicationId +
- " with version " + applicationVersion.id());
+ " with version " + applicationVersion.stringId());
return bytes;
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java
index 78341682f75..7b9a7fdf43b 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java
@@ -792,12 +792,12 @@ public class UpgraderTest {
// New application change
app.submit(applicationPackage("default"));
- String applicationVersion = app.lastSubmission().get().id();
+ String applicationVersion = app.lastSubmission().get().stringId();
// Application change recorded together with ongoing upgrade
assertTrue("Change contains both upgrade and application change",
app.instance().change().platform().get().equals(version) &&
- app.instance().change().application().get().id().equals(applicationVersion));
+ app.instance().change().application().get().stringId().equals(applicationVersion));
// Deployment completes
app.runJob(systemTest).runJob(stagingTest)
@@ -807,7 +807,7 @@ public class UpgraderTest {
for (Deployment deployment : app.instance().deployments().values()) {
assertEquals(version, deployment.version());
- assertEquals(applicationVersion, deployment.applicationVersion().id());
+ assertEquals(applicationVersion, deployment.applicationVersion().stringId());
}
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java
index e509a199c82..8b1751cbb8d 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java
@@ -11,6 +11,7 @@ import com.yahoo.slime.SlimeUtils;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Instance;
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.JobType;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision;
import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
@@ -22,6 +23,7 @@ import com.yahoo.vespa.hosted.controller.application.DeploymentActivity;
import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
import com.yahoo.vespa.hosted.controller.application.QuotaUsage;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
+import com.yahoo.vespa.hosted.controller.deployment.RevisionHistory;
import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics;
import com.yahoo.vespa.hosted.controller.routing.rotation.RotationId;
import com.yahoo.vespa.hosted.controller.routing.rotation.RotationState;
@@ -42,8 +44,6 @@ import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
import static org.junit.Assert.assertEquals;
@@ -92,13 +92,14 @@ public class ApplicationSerializerTest {
Optional.empty(),
Optional.of("best commit"),
true,
- Optional.of("hash1"));
+ Optional.of("hash1"),
+ true,
+ false);
assertEquals("https://github/org/repo/tree/commit1", applicationVersion1.sourceUrl().get());
ApplicationVersion applicationVersion2 = ApplicationVersion
.from(new SourceRevision("repo1", "branch1", "commit1"), 32, "a@b",
Version.fromString("6.3.1"), Instant.ofEpochMilli(496));
- SortedSet<ApplicationVersion> versions = new TreeSet<>(Set.of(applicationVersion2));
Instant activityAt = Instant.parse("2018-06-01T10:15:30.00Z");
deployments.add(new Deployment(zone1, applicationVersion1, Version.fromString("1.2.3"), Instant.ofEpochMilli(3),
DeploymentMetrics.none, DeploymentActivity.none, QuotaUsage.none, OptionalDouble.empty()));
@@ -119,20 +120,20 @@ public class ApplicationSerializerTest {
ApplicationId id1 = ApplicationId.from("t1", "a1", "i1");
ApplicationId id3 = ApplicationId.from("t1", "a1", "i3");
+ RevisionHistory revisions = RevisionHistory.ofRevisions(List.of(applicationVersion2),
+ Map.of(new JobId(id3, JobType.devUsEast1), List.of(applicationVersion1)));
List<Instance> instances = List.of(new Instance(id1,
deployments,
Map.of(JobType.systemTest, Instant.ofEpochMilli(333)),
List.of(AssignedRotation.fromStrings("foo", "default", "my-rotation", Set.of("us-west-1"))),
rotationStatus,
- Change.of(new Version("6.1")),
- Optional.of(applicationVersion2)),
+ Change.of(new Version("6.1"))),
new Instance(id3,
List.of(),
Map.of(),
List.of(),
RotationStatus.EMPTY,
- Change.of(Version.fromString("6.7")).withPin(),
- Optional.empty()));
+ Change.of(Version.fromString("6.7")).withPin()));
Application original = new Application(TenantAndApplicationId.from(id1),
Instant.now().truncatedTo(ChronoUnit.MILLIS),
@@ -145,23 +146,22 @@ public class ApplicationSerializerTest {
new ApplicationMetrics(0.5, 0.9),
Set.of(publicKey, otherPublicKey),
projectId,
- Optional.of(applicationVersion1),
- versions,
- instances);
+ revisions, instances
+ );
Application serialized = APPLICATION_SERIALIZER.fromSlime(SlimeUtils.toJsonBytes(APPLICATION_SERIALIZER.toSlime(original)));
assertEquals(original.id(), serialized.id());
assertEquals(original.createdAt(), serialized.createdAt());
- assertEquals(original.latestVersion(), serialized.latestVersion());
- assertEquals(original.latestVersion().get().authorEmail(), serialized.latestVersion().get().authorEmail());
- assertEquals(original.latestVersion().get().buildTime(), serialized.latestVersion().get().buildTime());
- assertEquals(original.latestVersion().get().sourceUrl(), serialized.latestVersion().get().sourceUrl());
- assertEquals(original.latestVersion().get().commit(), serialized.latestVersion().get().commit());
- assertEquals(original.latestVersion().get().bundleHash(), serialized.latestVersion().get().bundleHash());
- assertEquals(original.versions(), serialized.versions());
- assertEquals(original.versions(), serialized.versions());
-
+ assertEquals(original.revisions().last(), serialized.revisions().last());
+ assertEquals(original.revisions().last().get().authorEmail(), serialized.revisions().last().get().authorEmail());
+ assertEquals(original.revisions().last().get().buildTime(), serialized.revisions().last().get().buildTime());
+ assertEquals(original.revisions().last().get().sourceUrl(), serialized.revisions().last().get().sourceUrl());
+ assertEquals(original.revisions().last().get().commit(), serialized.revisions().last().get().commit());
+ assertEquals(original.revisions().last().get().bundleHash(), serialized.revisions().last().get().bundleHash());
+ assertEquals(original.revisions().withPackage(), serialized.revisions().withPackage());
+ assertEquals(original.revisions().production(), serialized.revisions().production());
+ assertEquals(original.revisions().development(), serialized.revisions().development());
assertEquals(original.deploymentSpec().xmlForm(), serialized.deploymentSpec().xmlForm());
assertEquals(original.validationOverrides().xmlForm(), serialized.validationOverrides().xmlForm());
@@ -193,9 +193,6 @@ public class ApplicationSerializerTest {
assertEquals(original.require(id1.instance()).change(), serialized.require(id1.instance()).change());
assertEquals(original.require(id3.instance()).change(), serialized.require(id3.instance()).change());
- assertEquals(original.require(id1.instance()).latestDeployed(), serialized.require(id1.instance()).latestDeployed());
- assertEquals(original.require(id3.instance()).latestDeployed(), serialized.require(id3.instance()).latestDeployed());
-
// Test metrics
assertEquals(original.metrics().queryServiceQuality(), serialized.metrics().queryServiceQuality(), Double.MIN_VALUE);
assertEquals(original.metrics().writeServiceQuality(), serialized.metrics().writeServiceQuality(), Double.MIN_VALUE);
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/root.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/root.json
index 211aa57d8ed..a707b9c7bc0 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/root.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/root.json
@@ -79,12 +79,6 @@
"upgradePolicy": "default",
"jobs": [
{
- "name": "system-test"
- },
- {
- "name": "staging-test"
- },
- {
"name": "production-us-west-1"
}
],
@@ -330,12 +324,6 @@
"upgradePolicy": "default",
"jobs": [
{
- "name": "system-test"
- },
- {
- "name": "staging-test"
- },
- {
"name": "production-us-west-1",
"pending": "platform"
}