diff options
author | Martin Polden <mpolden@mpolden.no> | 2022-04-11 09:03:41 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-04-11 09:03:41 +0200 |
commit | e5df1ee99b8468bca986af0ad1ced87773f21154 (patch) | |
tree | 11c318db38258697922af140122c92c2d9047cbc | |
parent | dc810e70e30432c743fc617d0ed32619b7ea7dab (diff) | |
parent | 3b1f64c52e4e456fbee8f874f8823505cc631c98 (diff) |
Merge pull request #22076 from vespa-engine/jonmv/deployment-risk
Jonmv/deployment risk
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" } |