From db755855b43890daa7e50bac9d7b01abda614867 Mon Sep 17 00:00:00 2001 From: jonmv Date: Sat, 9 Apr 2022 23:39:37 +0200 Subject: ** Add RevisionId as key for ApplicationVersion, and gather them all in RevisionHistory --- .../integration/deployment/ApplicationVersion.java | 54 ++++++-- .../api/integration/deployment/RevisionId.java | 59 +++++++++ .../yahoo/vespa/hosted/controller/Application.java | 39 +++--- .../hosted/controller/ApplicationController.java | 25 +++- .../vespa/hosted/controller/LockedApplication.java | 57 +++----- .../controller/deployment/JobController.java | 6 +- .../controller/deployment/RevisionHistory.java | 147 +++++++++++++++++++++ .../hosted/controller/deployment/Versions.java | 2 +- .../persistence/ApplicationSerializer.java | 60 +++++++-- .../controller/persistence/RunSerializer.java | 4 +- .../restapi/application/ApplicationApiHandler.java | 4 +- .../application/DeploymentQuotaCalculatorTest.java | 11 +- .../persistence/ApplicationSerializerTest.java | 21 +-- 13 files changed, 384 insertions(+), 105 deletions(-) create mode 100644 controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/RevisionId.java create mode 100644 controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RevisionHistory.java 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 bb610ef80cd..4938b76bf48 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 { - /** - * 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 :< */ + 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 { private final Optional commit; private final boolean deployedDirectly; private final Optional bundleHash; + private final boolean hasPackage; + private final boolean shouldSkip; /** Public for serialisation only. */ public ApplicationVersion(Optional source, OptionalLong buildNumber, Optional authorEmail, Optional compileVersion, Optional buildTime, Optional sourceUrl, - Optional commit, boolean deployedDirectly, Optional bundleHash) { + Optional commit, boolean deployedDirectly, Optional 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 { 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 { 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,7 +96,8 @@ public class ApplicationVersion implements Comparable { Optional compileVersion, Optional buildTime, Optional sourceUrl, Optional commit, boolean deployedDirectly, Optional 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 */ @@ -152,6 +159,31 @@ public class ApplicationVersion implements Comparable { 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; 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..b8b24fd9c77 --- /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 { + + 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; } + + private 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 67afa813bcd..b49739db012 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,7 +47,7 @@ public class Application { private final Instant createdAt; private final DeploymentSpec deploymentSpec; private final ValidationOverrides validationOverrides; - private final SortedSet versions; + private final RevisionHistory revisions; private final OptionalLong projectId; private final Optional deploymentIssueId; private final Optional ownershipIssueId; @@ -62,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(), 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 deploymentIssueId, Optional ownershipIssueId, Optional owner, OptionalInt majorVersion, ApplicationMetrics metrics, Set deployKeys, OptionalLong projectId, - SortedSet versions, Collection instances) { + RevisionHistory revisions, Collection 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"); @@ -83,7 +80,7 @@ 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.versions = versions; + this.revisions = revisions; this.instances = instances.stream().collect( Collectors.collectingAndThen(Collectors.toMap(Instance::name, Function.identity(), @@ -108,28 +105,22 @@ public class Application { /** Returns the project id of this application, if it has any. */ public OptionalLong projectId() { return projectId; } + /** Returns the known revisions for this application. */ + public RevisionHistory revisions() { return revisions; } + /** Returns the last submitted version of this application. */ public Optional latestVersion() { - return versions.isEmpty() ? Optional.empty() : Optional.of(versions.last()); + return revisions().last(); } /** Returns the currently deployed versions of the application, ordered from oldest to newest. */ - public SortedSet versions() { - return versions; + public List versions() { + return revisions().withPackage(); } - /** Returns the currently deployed versions of the application */ + /** Returns the currently deployable versions of the application */ public Collection deployableVersions(boolean ascending) { - Deque 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; + return revisions().deployable(ascending); } /** 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 d66c57b7d7e..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 production = new HashSet<>(); + Map> 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 getInstance(ApplicationId id) { return getApplication(TenantAndApplicationId.from(id)).flatMap(application -> application.get(id.instance())); } 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 fb271a586dd..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,7 +42,7 @@ public class LockedApplication { private final ApplicationMetrics metrics; private final Set deployKeys; private final OptionalLong projectId; - private final SortedSet versions; + private final RevisionHistory revisions; private final Map instances; /** @@ -58,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.versions(), application.instances()); + application.projectId(), application.instances(), application.revisions()); } private LockedApplication(Lock lock, TenantAndApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides, Optional deploymentIssueId, Optional ownershipIssueId, Optional owner, OptionalInt majorVersion, ApplicationMetrics metrics, Set deployKeys, - OptionalLong projectId, SortedSet versions, - Map instances) { + OptionalLong projectId, Map instances, RevisionHistory revisions) { this.lock = lock; this.id = id; this.createdAt = createdAt; @@ -79,7 +76,7 @@ public class LockedApplication { this.metrics = metrics; this.deployKeys = deployKeys; this.projectId = projectId; - this.versions = versions; + this.revisions = revisions; this.instances = Map.copyOf(instances); } @@ -87,7 +84,7 @@ public class LockedApplication { public Application get() { return new Application(id, createdAt, deploymentSpec, validationOverrides, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, - projectId, versions, instances.values()); + projectId, revisions, instances.values()); } LockedApplication withNewInstance(InstanceName instance) { @@ -95,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, versions, instances); + projectId, instances, revisions); } public LockedApplication with(InstanceName instance, UnaryOperator modification) { @@ -103,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, versions, instances); + projectId, instances, revisions); } public LockedApplication without(InstanceName instance) { @@ -111,51 +108,43 @@ public class LockedApplication { instances.remove(instance); return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, - projectId, versions, instances); - } - - public LockedApplication withNewSubmission(ApplicationVersion latestVersion) { - SortedSet applicationVersions = new TreeSet<>(versions); - applicationVersions.add(latestVersion); - return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, - deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, - projectId, 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, 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, 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, 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, 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, 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, versions, instances); + projectId, instances, revisions); } /** Set a major version for this, or set to null to remove any major version override */ @@ -163,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, 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, versions, instances); + projectId, instances, revisions); } public LockedApplication withDeployKey(PublicKey pemDeployKey) { @@ -177,7 +166,7 @@ public class LockedApplication { keys.add(pemDeployKey); return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, keys, - projectId, versions, instances); + projectId, instances, revisions); } public LockedApplication withoutDeployKey(PublicKey pemDeployKey) { @@ -185,15 +174,13 @@ public class LockedApplication { keys.remove(pemDeployKey); return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, keys, - projectId, versions, instances); + projectId, instances, revisions); } - public LockedApplication withoutVersion(ApplicationVersion version) { - SortedSet applicationVersions = new TreeSet<>(versions); - applicationVersions.remove(version); + public LockedApplication withRevisions(UnaryOperator change) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, - deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, - projectId, 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/deployment/JobController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java index b1a9f3abdd4..7ccf9b5757a 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 @@ -422,6 +422,7 @@ public class JobController { logs.flush(id); metric.jobFinished(run.id().job(), finishedRun.status()); + // TODO: update RevisionHistory, which should track all known revisions. controller.jobController().runs(id.job()).values().stream() .mapToLong(r -> r.versions().targetApplication().buildNumber().orElse(Integer.MAX_VALUE)) .min() @@ -487,7 +488,7 @@ public class JobController { applicationPackage.metaDataZip()); application = application.withProjectId(OptionalLong.of(projectId)); - application = application.withNewSubmission(version.get()); + application = application.withRevisions(revisions -> revisions.with(version.get())); application = withPrunedRevisions(application); applications.storeWithUpdatedConfig(application, applicationPackage); @@ -505,7 +506,7 @@ public class JobController { for (ApplicationVersion version : application.get().versions()) if (version.compareTo(oldestDeployed.get()) < 0) - application = application.withoutVersion(version); + application = application.withRevisions(revisions -> revisions.with(version.withoutPackage())); } return application; } @@ -570,6 +571,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, __ -> { 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..488d86df98b --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RevisionHistory.java @@ -0,0 +1,147 @@ +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 comparator = Comparator.comparing(JobId::application).thenComparing(JobId::type); + + private final NavigableMap production; + private final NavigableMap> development; + + private RevisionHistory(NavigableMap production, + NavigableMap> development) { + this.production = production; + this.development = development; + } + + public static RevisionHistory empty() { + return ofRevisions(List.of(), Map.of()); + } + + public static RevisionHistory ofRevisions(Collection productionRevisions, + Map> developmentRevisions) { + NavigableMap production = new TreeMap<>(); + for (ApplicationVersion revision : productionRevisions) + production.put(revision.id(), revision); + + NavigableMap> development = new TreeMap<>(comparator); + developmentRevisions.forEach((job, jobRevisions) -> { + NavigableMap 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 with given production revision forgotten. */ + public RevisionHistory without(RevisionId id) { + if ( ! production.containsKey(id)) return this; + TreeMap production = new TreeMap<>(this.production); + production.remove(id); + return new RevisionHistory(production, development); + } + + /** Returns a copy of this with the given development revision forgotten. */ + public RevisionHistory without(RevisionId id, JobId job) { + if ( ! development.containsKey(job) || ! development.get(job).containsKey(id)) return this; + NavigableMap> development = new TreeMap<>(this.development); + development.get(job).remove(id); + return new RevisionHistory(production, development); + } + + /** Returns a copy of this with the production revision added or updated */ + public RevisionHistory with(ApplicationVersion revision) { + NavigableMap 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> development = new TreeMap<>(this.development); + NavigableMap 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 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 withPackage() { + return production.values().stream() + .filter(ApplicationVersion::hasPackage) + .collect(toList()); + } + + /** Returns the currently deployable revisions of the application. */ + public Deque deployable(boolean ascending) { + Deque 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 production() { + return List.copyOf(production.values()); + } + + /* All known development revisions, in ascending order, per job. */ + public NavigableMap> development() { + NavigableMap> 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 379cee22386..a762dd8d861 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 @@ -145,7 +145,7 @@ public class Versions { private static ApplicationVersion defaultApplicationVersion(Application application) { return application.oldestDeployedApplication() .or(application::latestVersion) - .orElse(ApplicationVersion.unknown); + .orElseThrow(() -> new IllegalStateException("no known prod revisions, but asked for one, for " + application)); } private static > Optional max(Optional o1, Optional o2) { 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 1b5467b7eee..e036d38b875 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. @@ -77,6 +77,8 @@ public class ApplicationSerializer { private static final String deployingField = "deployingField"; private static final String projectIdField = "projectId"; 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"; @@ -110,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"; @@ -165,7 +169,8 @@ public class ApplicationSerializer { root.setDouble(queryQualityField, application.metrics().queryServiceQuality()); root.setDouble(writeQualityField, application.metrics().writeServiceQuality()); deployKeysToSlime(application.deployKeys(), root.setArray(pemDeployKeysField)); - 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; } @@ -224,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 revisions, Cursor revisionsArray) { + revisions.forEach(version -> toSlime(version, revisionsArray.addObject())); } private void toSlime(ApplicationVersion applicationVersion, Cursor object) { @@ -237,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)); } @@ -317,17 +334,33 @@ public class ApplicationSerializer { Set deployKeys = deployKeysFromSlime(root.field(pemDeployKeysField)); List instances = instancesFromSlime(id, root.field(instancesField)); OptionalLong projectId = SlimeUtils.optionalLong(root.field(projectIdField)); - SortedSet 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, 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) { + List revisions = prodVersionsArray.valid() ? revisionsFromSlime(prodVersionsArray) + : revisionsFromSlime(versionsArray); + Map> devRevisions = new HashMap<>(); + devVersionsArray.traverse((ArrayTraverser) (__, devRevisionsObject) -> + devRevisions.put(jobIdFromSlime(id, devRevisionsObject), revisionsFromSlime(devRevisionsObject.field(versionsField)))); + + return RevisionHistory.ofRevisions(revisions, devRevisions); + } + + private JobId jobIdFromSlime(TenantAndApplicationId base, Inspector idObject) { + return new JobId(base.instance(idObject.field(instanceNameField).asString()), + JobType.fromJobName(idObject.field(jobTypeField).asString())); } - private SortedSet versionsFromSlime(Inspector versionsObject) { - SortedSet versions = new TreeSet<>(); - versionsObject.traverse((ArrayTraverser) (name, object) -> versions.add(applicationVersionFromSlime(object))); - return versions; + private List revisionsFromSlime(Inspector versionsArray) { + List revisions = new ArrayList<>(); + versionsArray.traverse((ArrayTraverser) (__, revisionObject) -> revisions.add(applicationVersionFromSlime(revisionObject))); + return revisions; } private List instancesFromSlime(TenantAndApplicationId id, Inspector field) { @@ -433,9 +466,12 @@ public class ApplicationSerializer { Optional sourceUrl = SlimeUtils.optionalString(object.field(sourceUrlField)); Optional commit = SlimeUtils.optionalString(object.field(commitField)); boolean deployedDirectly = object.field(deployedDirectlyField).asBool(); + boolean hasPackage = object.field(hasPackageField).asBool(); + boolean shouldSkip = object.field(shouldSkipField).asBool(); Optional 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 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 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 eb2334086f5..e78f77c74ee 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 @@ -817,7 +817,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { private HttpResponse applicationPackage(String tenantName, String applicationName, HttpRequest request) { var tenantAndApplication = TenantAndApplicationId.from(tenantName, applicationName); - SortedSet versions = controller.applications().requireApplication(tenantAndApplication).versions(); + List versions = controller.applications().requireApplication(tenantAndApplication).versions(); if (versions.isEmpty()) throw new NotExistsException("No application package has been submitted for '" + tenantAndApplication + "'"); @@ -833,7 +833,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 ? 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 7561592be9b..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(), - 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/persistence/ApplicationSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java index de716a65551..4c7aa1a72f0 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 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,6 +120,8 @@ 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 instances = List.of(new Instance(id1, deployments, Map.of(JobType.systemTest, Instant.ofEpochMilli(333)), @@ -143,8 +146,8 @@ public class ApplicationSerializerTest { new ApplicationMetrics(0.5, 0.9), Set.of(publicKey, otherPublicKey), projectId, - versions, - instances); + revisions, instances + ); Application serialized = APPLICATION_SERIALIZER.fromSlime(SlimeUtils.toJsonBytes(APPLICATION_SERIALIZER.toSlime(original))); @@ -156,9 +159,9 @@ public class ApplicationSerializerTest { 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().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()); -- cgit v1.2.3