diff options
author | Valerij Fredriksen <freva@users.noreply.github.com> | 2021-02-18 19:21:45 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-02-18 19:21:45 +0100 |
commit | dbde8cfb86537f35fab9418dd78c7baf060fe597 (patch) | |
tree | 989b602c5f726b8c28c5b3b56e0e4e1f0acfafa0 | |
parent | 466a53ad422c819b81792f3ad682edfa65dc06b5 (diff) | |
parent | 9f87d959285f1d4b435df1c47c15c29c356980b8 (diff) |
Merge pull request #16577 from vespa-engine/bratseth/traffic-fraction
Bratseth/traffic fraction
30 files changed, 551 insertions, 41 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Application.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Application.java index 1499d5c1b79..722c9fc35d7 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Application.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Application.java @@ -15,8 +15,8 @@ import java.util.stream.Collectors; */ public class Application { - private ApplicationId id; - private Map<ClusterSpec.Id, Cluster> clusters; + private final ApplicationId id; + private final Map<ClusterSpec.Id, Cluster> clusters; public Application(ApplicationId id, Collection<Cluster> clusters) { this.id = id; diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/NodeRepository.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/NodeRepository.java index be3189eb1cf..25d8e7a261f 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/NodeRepository.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/NodeRepository.java @@ -74,6 +74,9 @@ public interface NodeRepository { Application getApplication(ZoneId zone, ApplicationId application); + void patchApplication(ZoneId zone, ApplicationId application, + double currentReadShare, double maxReadShare); + /** Upgrade all nodes of given type to a new version */ void upgrade(ZoneId zone, NodeType type, Version version); diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ApplicationPatch.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ApplicationPatch.java new file mode 100644 index 00000000000..aa2ed206dda --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ApplicationPatch.java @@ -0,0 +1,34 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.api.integration.noderepository; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Patchable data under Application + * + * @author bratseth + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ApplicationPatch { + + @JsonProperty + private final Double currentReadShare; + + @JsonProperty + private final Double maxReadShare; + + @JsonCreator + public ApplicationPatch(@JsonProperty("currentReadShare") Double currentReadShare, + @JsonProperty("maxReadShare") Double maxReadShare) { + this.currentReadShare = currentReadShare; + this.maxReadShare = maxReadShare; + } + + public Double getCurrentReadShare() { return currentReadShare; } + public Double getMaxReadShare() { return maxReadShare; } + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ProvisionResource.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ProvisionResource.java index 97720adbcc6..337b193a332 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ProvisionResource.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ProvisionResource.java @@ -61,6 +61,11 @@ public interface ProvisionResource { @Path("/application/{application}") ApplicationData getApplication(@PathParam("application") String applicationId); + @POST + @Path("/application/{application}") + String patchApplication(@PathParam("application") String applicationId, ApplicationPatch applicationPatch, + @HeaderParam("X-HTTP-Method-Override") String patchOverride); + @PUT @Path("/state/{state}/{hostname}") String setState(@PathParam("state") NodeState state, @PathParam("hostname") String hostname); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java index bc0295abca3..f7ab4d30088 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java @@ -64,6 +64,7 @@ public class ControllerMaintenance extends AbstractComponent { maintainers.add(new HostSwitchUpdater(controller, intervals.hostSwitchUpdater)); maintainers.add(new ReindexingTriggerer(controller, intervals.reindexingTriggerer)); maintainers.add(new EndpointCertificateMaintainer(controller, intervals.endpointCertificateMaintainer)); + maintainers.add(new TrafficShareUpdater(controller, intervals.trafficFractionUpdater)); } public Upgrader upgrader() { return upgrader; } @@ -113,6 +114,7 @@ public class ControllerMaintenance extends AbstractComponent { private final Duration hostSwitchUpdater; private final Duration reindexingTriggerer; private final Duration endpointCertificateMaintainer; + private final Duration trafficFractionUpdater; public Intervals(SystemName system) { this.system = Objects.requireNonNull(system); @@ -139,6 +141,7 @@ public class ControllerMaintenance extends AbstractComponent { this.hostSwitchUpdater = duration(12, HOURS); this.reindexingTriggerer = duration(1, HOURS); this.endpointCertificateMaintainer = duration(12, HOURS); + this.trafficFractionUpdater = duration(5, MINUTES); } private Duration duration(long amount, TemporalUnit unit) { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/TrafficShareUpdater.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/TrafficShareUpdater.java new file mode 100644 index 00000000000..7c95125c6c3 --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/TrafficShareUpdater.java @@ -0,0 +1,66 @@ +// Copyright Verizon Media. 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.SystemName; +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.api.integration.configserver.NodeRepository; +import com.yahoo.vespa.hosted.controller.application.Deployment; + +import java.time.Duration; + +/** + * This computes, for every application deployment + * - the current fraction of the application's global traffic it receives + * - the max fraction it can possibly receive, assuming traffic is evenly distributed over regions + * and max one region is down at any time. (We can let deployment.xml override these assumptions later). + * + * These two numbers are sent to a config server of each region where it is ultimately + * consumed by autoscaling. + * + * It depends on the traffic metrics collected by DeploymentMetricsMaintainer. + * + * @author bratseth + */ +public class TrafficShareUpdater extends ControllerMaintainer { + + private final ApplicationController applications; + private final NodeRepository nodeRepository; + + public TrafficShareUpdater(Controller controller, Duration duration) { + super(controller, duration, DeploymentMetricsMaintainer.class.getSimpleName(), SystemName.all()); + this.applications = controller.applications(); + this.nodeRepository = controller.serviceRegistry().configServer().nodeRepository(); + } + + @Override + protected boolean maintain() { + for (var application : applications.asList()) { + for (var instance : application.instances().values()) { + for (var deployment : instance.deployments().values()) { + if ( ! deployment.zone().environment().isProduction()) continue; + updateTrafficFraction(instance, deployment); + } + } + } + return true; + } + + private void updateTrafficFraction(Instance instance, Deployment deployment) { + double qpsInZone = deployment.metrics().queriesPerSecond(); + double totalQps = instance.deployments().values().stream() + .filter(i -> i.zone().environment().isProduction()) + .mapToDouble(i -> i.metrics().queriesPerSecond()).sum(); + long prodRegions = instance.deployments().values().stream() + .filter(i -> i.zone().environment().isProduction()) + .count(); + double currentReadShare = totalQps == 0 ? 0 : qpsInZone / totalQps; + double maxReadShare = prodRegions < 2 ? 1.0 : 1.0 / ( prodRegions - 1.0); + if (currentReadShare > maxReadShare) // This can happen because the assumption of equal traffic + maxReadShare = currentReadShare; // distribution can be incorrect + + nodeRepository.patchApplication(deployment.zone(), instance.id(), currentReadShare, maxReadShare); + } + +} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java index c0244b9ea17..f432a1f41ce 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java @@ -326,6 +326,10 @@ public final class ControllerTester { } } + public Application createApplication(ApplicationId id) { + return createApplication(id.tenant().value(), id.application().value(), id.instance().value()); + } + public Application createApplication(String tenant, String applicationName, String instanceName) { Application application = createApplication(tenant, applicationName); controller().applications().createInstance(application.id().instance(instanceName)); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/NodeRepositoryMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/NodeRepositoryMock.java index ca478905893..96240f2b6c7 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/NodeRepositoryMock.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/NodeRepositoryMock.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.hosted.controller.integration; import com.fasterxml.jackson.databind.JsonNode; +import com.yahoo.collections.Pair; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.HostName; @@ -40,6 +41,7 @@ public class NodeRepositoryMock implements NodeRepository { private final Map<ZoneId, Map<ApplicationId, Application>> applications = new HashMap<>(); private final Map<ZoneId, TargetVersions> targetVersions = new HashMap<>(); private final Map<Integer, Duration> osUpgradeBudgets = new HashMap<>(); + private final Map<DeploymentId, Pair<Double, Double>> trafficFractions = new HashMap<>(); private boolean allowPatching = false; @@ -55,6 +57,10 @@ public class NodeRepositoryMock implements NodeRepository { applications.get(zone).put(application.id(), application); } + public Pair<Double, Double> getTrafficFraction(ApplicationId application, ZoneId zone) { + return trafficFractions.get(new DeploymentId(application, zone)); + } + /** Add or update given node in zone */ public void putNodes(ZoneId zone, Node node) { putNodes(zone, Collections.singletonList(node)); @@ -180,6 +186,12 @@ public class NodeRepositoryMock implements NodeRepository { } @Override + public void patchApplication(ZoneId zone, ApplicationId application, + double currentReadShare, double maxReadShare) { + trafficFractions.put(new DeploymentId(application, zone), new Pair<>(currentReadShare, maxReadShare)); + } + + @Override public void upgrade(ZoneId zone, NodeType type, Version version) { this.targetVersions.compute(zone, (ignored, targetVersions) -> { if (targetVersions == null) { diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/TrafficShareUpdaterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/TrafficShareUpdaterTest.java new file mode 100644 index 00000000000..2674e155b98 --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/TrafficShareUpdaterTest.java @@ -0,0 +1,95 @@ +// Copyright Verizon Media. 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.component.Version; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.zone.ZoneId; +import com.yahoo.vespa.hosted.controller.api.application.v4.model.ClusterMetrics; +import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; +import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; +import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester; +import com.yahoo.vespa.hosted.controller.integration.NodeRepositoryMock; +import org.junit.Test; + +import java.time.Duration; + +import static org.junit.Assert.assertEquals; + +/** + * Tests the traffic fraction updater. This also tests its dependency on DeploymentMetricsMaintainer. + * + * @author bratseth + */ +public class TrafficShareUpdaterTest { + + @Test + public void testTrafficUpdater() { + DeploymentTester tester = new DeploymentTester(); + var application = tester.newDeploymentContext(); + var deploymentMetricsMaintainer = new DeploymentMetricsMaintainer(tester.controller(), Duration.ofDays(1)); + var updater = new TrafficShareUpdater(tester.controller(), Duration.ofDays(1)); + ZoneId prod1 = ZoneId.from("prod", "ap-northeast-1"); + ZoneId prod2 = ZoneId.from("prod", "us-east-3"); + ZoneId prod3 = ZoneId.from("prod", "us-west-1"); + application.runJob(JobType.productionApNortheast1, new ApplicationPackage(new byte[0]), Version.fromString("7.1")); + + // Single zone + setQpsMetric(50.0, application.application().id().defaultInstance(), prod1, tester); + deploymentMetricsMaintainer.maintain(); + updater.maintain(); + assertTrafficFraction(1.0, 1.0, application.instanceId(), prod1, tester); + + // Two zones + application.runJob(JobType.productionUsEast3, new ApplicationPackage(new byte[0]), Version.fromString("7.1")); + // - one cold + setQpsMetric(50.0, application.application().id().defaultInstance(), prod1, tester); + setQpsMetric(0.0, application.application().id().defaultInstance(), prod2, tester); + deploymentMetricsMaintainer.maintain(); + updater.maintain(); + assertTrafficFraction(1.0, 1.0, application.instanceId(), prod1, tester); + assertTrafficFraction(0.0, 1.0, application.instanceId(), prod2, tester); + // - both hot + setQpsMetric(53.0, application.application().id().defaultInstance(), prod1, tester); + setQpsMetric(47.0, application.application().id().defaultInstance(), prod2, tester); + deploymentMetricsMaintainer.maintain(); + updater.maintain(); + assertTrafficFraction(0.53, 1.0, application.instanceId(), prod1, tester); + assertTrafficFraction(0.47, 1.0, application.instanceId(), prod2, tester); + + // Three zones + application.runJob(JobType.productionUsWest1, new ApplicationPackage(new byte[0]), Version.fromString("7.1")); + // - one cold + setQpsMetric(53.0, application.application().id().defaultInstance(), prod1, tester); + setQpsMetric(47.0, application.application().id().defaultInstance(), prod2, tester); + setQpsMetric(0.0, application.application().id().defaultInstance(), prod3, tester); + deploymentMetricsMaintainer.maintain(); + updater.maintain(); + assertTrafficFraction(0.53, 0.53, application.instanceId(), prod1, tester); + assertTrafficFraction(0.47, 0.50, application.instanceId(), prod2, tester); + assertTrafficFraction(0.00, 0.50, application.instanceId(), prod3, tester); + // - all hot + setQpsMetric( 50.0, application.application().id().defaultInstance(), prod1, tester); + setQpsMetric(25.0, application.application().id().defaultInstance(), prod2, tester); + setQpsMetric(25.0, application.application().id().defaultInstance(), prod3, tester); + deploymentMetricsMaintainer.maintain(); + updater.maintain(); + assertTrafficFraction(0.50, 0.5, application.instanceId(), prod1, tester); + assertTrafficFraction(0.25, 0.5, application.instanceId(), prod2, tester); + assertTrafficFraction(0.25, 0.5, application.instanceId(), prod3, tester); + } + + private void setQpsMetric(double qps, ApplicationId application, ZoneId zone, DeploymentTester tester) { + var clusterMetrics = new ClusterMetrics("default", "container"); + clusterMetrics = clusterMetrics.addMetric(ClusterMetrics.QUERIES_PER_SECOND, qps); + tester.controllerTester().serviceRegistry().configServerMock().setMetrics(new DeploymentId(application, zone), clusterMetrics); + } + + private void assertTrafficFraction(double currentReadShare, double maxReadShare, + ApplicationId application, ZoneId zone, DeploymentTester tester) { + NodeRepositoryMock mock = (NodeRepositoryMock)tester.controller().serviceRegistry().configServer().nodeRepository(); + assertEquals(currentReadShare, mock.getTrafficFraction(application, zone).getFirst(), 0.00001); + assertEquals(maxReadShare, mock.getTrafficFraction(application, zone).getSecond(), 0.00001); + } + +} diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Application.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Application.java index 847b825a7a4..5eb01b4fe72 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Application.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Application.java @@ -21,23 +21,28 @@ import java.util.stream.Collectors; public class Application { private final ApplicationId id; + private final Status status; private final Map<ClusterSpec.Id, Cluster> clusters; - public Application(ApplicationId id) { - this(id, Map.of()); + /** Do not use */ + public Application(ApplicationId id, Status status, Collection<Cluster> clusters) { + this(id, status, clusters.stream().collect(Collectors.toMap(c -> c.id(), c -> c))); } - public Application(ApplicationId id, Collection<Cluster> clusters) { - this(id, clusters.stream().collect(Collectors.toMap(c -> c.id(), c -> c))); - } - - private Application(ApplicationId id, Map<ClusterSpec.Id, Cluster> clusters) { + private Application(ApplicationId id, Status status, Map<ClusterSpec.Id, Cluster> clusters) { this.id = id; this.clusters = clusters; + this.status = status; } public ApplicationId id() { return id; } + public Status status() { return status; } + + public Application with(Status status) { + return new Application(id, status, clusters); + } + public Map<ClusterSpec.Id, Cluster> clusters() { return clusters; } public Optional<Cluster> cluster(ClusterSpec.Id id) { @@ -47,7 +52,7 @@ public class Application { public Application with(Cluster cluster) { Map<ClusterSpec.Id, Cluster> clusters = new HashMap<>(this.clusters); clusters.put(cluster.id(), cluster); - return new Application(id, clusters); + return new Application(id, status, clusters); } /** @@ -80,4 +85,8 @@ public class Application { return "application '" + id + "'"; } + public static Application empty(ApplicationId id) { + return new Application(id, Status.initial(), Map.of()); + } + } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Applications.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Applications.java index 9db28652d0e..ccd5af1cb64 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Applications.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Applications.java @@ -40,6 +40,11 @@ public class Applications { return db.readApplication(id); } + /** Returns the application with the given id, or throws IllegalArgumentException if it does not exist */ + public Application require(ApplicationId id) { + return db.readApplication(id).orElseThrow(() -> new IllegalArgumentException("No application '" + id + "' was found")); + } + // TODO: Require ProvisionLock instead of Mutex public void put(Application application, Mutex applicationLock) { NestedTransaction transaction = new NestedTransaction(); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Status.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Status.java new file mode 100644 index 00000000000..ace05d85bbd --- /dev/null +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Status.java @@ -0,0 +1,67 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.provision.applications; + +import java.util.Objects; + +/** + * An application's status + * + * @author bratseth + */ +public class Status { + + private final double currentReadShare; + private final double maxReadShare; + + /** Do not use */ + public Status(double currentReadShare, double maxReadShare) { + this.currentReadShare = currentReadShare; + this.maxReadShare = maxReadShare; + } + + public Status withCurrentReadShare(double currentReadShare) { + return new Status(currentReadShare, maxReadShare); + } + + /** + * Returns the current fraction of the global traffic to this application that is received by the + * deployment in this zone. + */ + public double currentReadShare() { return currentReadShare; } + + public Status withMaxReadShare(double maxReadShare) { + return new Status(currentReadShare, maxReadShare); + } + + /** + * Returns an estimate of the max fraction of the global traffic to this application that may possibly + * be received by the deployment in this zone. + */ + public double maxReadShare() { return maxReadShare; } + + public static Status initial() { return new Status(0, 0); } + + @Override + public int hashCode() { + return Objects.hash(currentReadShare, maxReadShare); + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + if ( ! (o instanceof Status)) return false; + Status other = (Status)o; + if ( other.currentReadShare != this.currentReadShare) return false; + if ( other.maxReadShare != this.maxReadShare) return false; + return true; + } + + @Override + public String toString() { + return "application status: [" + + "currentReadShare: " + currentReadShare + ", " + + "maxReadShare: " + maxReadShare + + "]"; + } + +} diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Autoscaler.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Autoscaler.java index 9c87b13c244..2d192fae11f 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Autoscaler.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Autoscaler.java @@ -7,6 +7,7 @@ import com.yahoo.config.provision.NodeResources; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeList; import com.yahoo.vespa.hosted.provision.NodeRepository; +import com.yahoo.vespa.hosted.provision.applications.Application; import com.yahoo.vespa.hosted.provision.applications.Cluster; import com.yahoo.vespa.hosted.provision.applications.ScalingEvent; @@ -44,8 +45,8 @@ public class Autoscaler { * @param clusterNodes the list of all the active nodes in a cluster * @return scaling advice for this cluster */ - public Advice suggest(Cluster cluster, NodeList clusterNodes) { - return autoscale(cluster, clusterNodes, Limits.empty()); + public Advice suggest(Application application, Cluster cluster, NodeList clusterNodes) { + return autoscale(application, cluster, clusterNodes, Limits.empty()); } /** @@ -54,12 +55,12 @@ public class Autoscaler { * @param clusterNodes the list of all the active nodes in a cluster * @return scaling advice for this cluster */ - public Advice autoscale(Cluster cluster, NodeList clusterNodes) { + public Advice autoscale(Application application, Cluster cluster, NodeList clusterNodes) { if (cluster.minResources().equals(cluster.maxResources())) return Advice.none("Autoscaling is not enabled"); - return autoscale(cluster, clusterNodes, Limits.of(cluster)); + return autoscale(application, cluster, clusterNodes, Limits.of(cluster)); } - private Advice autoscale(Cluster cluster, NodeList clusterNodes, Limits limits) { + private Advice autoscale(Application application, Cluster cluster, NodeList clusterNodes, Limits limits) { if ( ! stable(clusterNodes, nodeRepository)) return Advice.none("Cluster change in progress"); @@ -87,7 +88,7 @@ public class Autoscaler { double memoryLoad = clusterTimeseries.averageLoad(Resource.memory); double diskLoad = clusterTimeseries.averageLoad(Resource.disk); - var target = ResourceTarget.idealLoad(cpuLoad, memoryLoad, diskLoad, currentAllocation); + var target = ResourceTarget.idealLoad(cpuLoad, memoryLoad, diskLoad, currentAllocation, application); Optional<AllocatableClusterResources> bestAllocation = allocationOptimizer.findBestAllocation(target, currentAllocation, limits); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Resource.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Resource.java index ddfb4c48e84..8353f56df91 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Resource.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Resource.java @@ -12,7 +12,7 @@ public enum Resource { /** Cpu utilization ratio */ cpu { - public double idealAverageLoad() { return 0.2; } + public double idealAverageLoad() { return 0.4; } double valueFrom(NodeResources resources) { return resources.vcpu(); } }, diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/ResourceTarget.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/ResourceTarget.java index b00323818d5..a2fbeb3b710 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/ResourceTarget.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/ResourceTarget.java @@ -1,6 +1,8 @@ // Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.provision.autoscale; +import com.yahoo.vespa.hosted.provision.applications.Application; + /** * A resource target to hit for the allocation optimizer. * The target is measured in cpu, memory and disk per node in the allocation given by current. @@ -46,8 +48,8 @@ public class ResourceTarget { /** Create a target of achieving ideal load given a current load */ public static ResourceTarget idealLoad(double currentCpuLoad, double currentMemoryLoad, double currentDiskLoad, - AllocatableClusterResources current) { - return new ResourceTarget(nodeUsage(Resource.cpu, currentCpuLoad, current) / Resource.cpu.idealAverageLoad(), + AllocatableClusterResources current, Application application) { + return new ResourceTarget(nodeUsage(Resource.cpu, currentCpuLoad, current) / idealCpuLoad(application), nodeUsage(Resource.memory, currentMemoryLoad, current) / Resource.memory.idealAverageLoad(), nodeUsage(Resource.disk, currentDiskLoad, current) / Resource.disk.idealAverageLoad(), true); @@ -61,4 +63,17 @@ public class ResourceTarget { false); } + /** Ideal cpu load must take the application traffic fraction into account */ + private static double idealCpuLoad(Application application) { + double trafficFactor; + if (application.status().maxReadShare() == 0) // No traffic fraction data + trafficFactor = 0.5; // assume we currently get half of the global share of traffic + else + trafficFactor = application.status().currentReadShare() / application.status().maxReadShare(); + + if (trafficFactor < 0.5) // The expectation that we have almost no load with almost no queries is incorrect due + trafficFactor = 0.5; // to write traffic; once that is separated we can lower this threshold (but not to 0) + return trafficFactor * Resource.cpu.idealAverageLoad(); + } + } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/AutoscalingMaintainer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/AutoscalingMaintainer.java index 171a42f3cf1..bcfdaefb305 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/AutoscalingMaintainer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/AutoscalingMaintainer.java @@ -72,11 +72,11 @@ public class AutoscalingMaintainer extends NodeRepositoryMaintainer { ClusterSpec.Id clusterId, NodeList clusterNodes, MaintenanceDeployment deployment) { - Application application = nodeRepository().applications().get(applicationId).orElse(new Application(applicationId)); + Application application = nodeRepository().applications().get(applicationId).orElse(Application.empty(applicationId)); if (application.cluster(clusterId).isEmpty()) return; Cluster cluster = application.cluster(clusterId).get(); cluster = updateCompletion(cluster, clusterNodes); - var advice = autoscaler.autoscale(cluster, clusterNodes); + var advice = autoscaler.autoscale(application, cluster, clusterNodes); cluster = cluster.withAutoscalingStatus(advice.reason()); if (advice.isPresent() && !cluster.targetResources().equals(advice.target())) { // autoscale diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ScalingSuggestionsMaintainer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ScalingSuggestionsMaintainer.java index 4f7ab498599..310df6e5875 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ScalingSuggestionsMaintainer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ScalingSuggestionsMaintainer.java @@ -63,10 +63,10 @@ public class ScalingSuggestionsMaintainer extends NodeRepositoryMaintainer { private boolean suggest(ApplicationId applicationId, ClusterSpec.Id clusterId, NodeList clusterNodes) { - Application application = applications().get(applicationId).orElse(new Application(applicationId)); + Application application = applications().get(applicationId).orElse(Application.empty(applicationId)); Optional<Cluster> cluster = application.cluster(clusterId); if (cluster.isEmpty()) return true; - var suggestion = autoscaler.suggest(cluster.get(), clusterNodes); + var suggestion = autoscaler.suggest(application, cluster.get(), clusterNodes); if (suggestion.isEmpty()) return false; // Wait only a short time for the lock to avoid interfering with change deployments try (Mutex lock = nodeRepository().nodes().lock(applicationId, Duration.ofSeconds(1))) { diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/Nodes.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/Nodes.java index 898e477b498..d637236e1b8 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/Nodes.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/Nodes.java @@ -637,6 +637,7 @@ public class Nodes { } /** Create a lock which provides exclusive rights to making changes to the given application */ + // TODO: Move to Applications public Mutex lock(ApplicationId application) { return db.lock(application); } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/ApplicationSerializer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/ApplicationSerializer.java index dd1c9028afe..c8b928779b9 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/ApplicationSerializer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/ApplicationSerializer.java @@ -12,6 +12,7 @@ import com.yahoo.slime.SlimeUtils; import com.yahoo.vespa.hosted.provision.applications.Application; import com.yahoo.vespa.hosted.provision.applications.Cluster; import com.yahoo.vespa.hosted.provision.applications.ScalingEvent; +import com.yahoo.vespa.hosted.provision.applications.Status; import java.io.IOException; import java.io.UncheckedIOException; @@ -37,6 +38,11 @@ public class ApplicationSerializer { // - CHANGING THE FORMAT OF A FIELD: Don't do it bro. private static final String idKey = "id"; + + private static final String statusKey = "status"; + private static final String currentReadShareKey = "currentReadShare"; + private static final String maxReadShareKey = "maxReadShare"; + private static final String clustersKey = "clusters"; private static final String exclusiveKey = "exclusive"; private static final String minResourcesKey = "min"; @@ -73,12 +79,26 @@ public class ApplicationSerializer { private static void toSlime(Application application, Cursor object) { object.setString(idKey, application.id().serializedForm()); + toSlime(application.status(), object.setObject(statusKey)); clustersToSlime(application.clusters().values(), object.setObject(clustersKey)); } private static Application applicationFromSlime(Inspector applicationObject) { ApplicationId id = ApplicationId.fromSerializedForm(applicationObject.field(idKey).asString()); - return new Application(id, clustersFromSlime(applicationObject.field(clustersKey))); + return new Application(id, + statusFromSlime(applicationObject.field(statusKey)), + clustersFromSlime(applicationObject.field(clustersKey))); + } + + private static void toSlime(Status status, Cursor statusObject) { + statusObject.setDouble(currentReadShareKey, status.currentReadShare()); + statusObject.setDouble(maxReadShareKey, status.maxReadShare()); + } + + private static Status statusFromSlime(Inspector statusObject) { + if ( ! statusObject.valid()) return Status.initial(); // TODO: Remove this line after March 2021 + return new Status(statusObject.field(currentReadShareKey).asDouble(), + statusObject.field(maxReadShareKey).asDouble()); } private static void clustersToSlime(Collection<Cluster> clusters, Cursor clustersObject) { diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java index 79e1005eb47..22242e526f9 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java @@ -138,7 +138,7 @@ public class NodeRepositoryProvisioner implements Provisioner { */ private ClusterResources decideTargetResources(ApplicationId applicationId, ClusterSpec clusterSpec, Capacity requested) { try (Mutex lock = nodeRepository.nodes().lock(applicationId)) { - Application application = nodeRepository.applications().get(applicationId).orElse(new Application(applicationId)); + Application application = nodeRepository.applications().get(applicationId).orElse(Application.empty(applicationId)); application = application.withCluster(clusterSpec.id(), clusterSpec.isExclusive(), requested.minResources(), requested.maxResources()); nodeRepository.applications().put(application, lock); return application.clusters().get(clusterSpec.id()).targetResources() diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/ApplicationPatcher.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/ApplicationPatcher.java new file mode 100644 index 00000000000..771b570a4fd --- /dev/null +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/ApplicationPatcher.java @@ -0,0 +1,79 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.provision.restapi; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.io.IOUtils; +import com.yahoo.slime.Inspector; +import com.yahoo.slime.SlimeUtils; +import com.yahoo.slime.Type; +import com.yahoo.transaction.Mutex; +import com.yahoo.vespa.hosted.provision.NodeRepository; +import com.yahoo.vespa.hosted.provision.applications.Application; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; + +/** + * A class which can take a partial JSON node/v2 application JSON structure and apply it to an application object. + * This is a one-time use object. + * + * @author bratseth + */ +public class ApplicationPatcher implements AutoCloseable { + + private final Inspector inspector; + + private final Mutex lock; + private Application application; + + public ApplicationPatcher(InputStream json, ApplicationId applicationId, NodeRepository nodeRepository) { + try { + this.inspector = SlimeUtils.jsonToSlime(IOUtils.readBytes(json, 1000 * 1000)).get(); + } catch (IOException e) { + throw new UncheckedIOException("Error reading request body", e); + } + lock = nodeRepository.nodes().lock(applicationId); + this.application = nodeRepository.applications().require(applicationId); + } + + /** Applies the json to the application and returns it. */ + public Application apply() { + inspector.traverse((String name, Inspector value) -> { + try { + application = applyField(application, name, value, inspector); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Could not set field '" + name + "'", e); + } + }); + return application; + } + + /** Returns the application in its current state (patch applied or not) */ + public Application application() { return application; } + + public Mutex lock() { return lock; } + + @Override + public void close() { + lock.close(); + } + + private Application applyField(Application application, String name, Inspector value, Inspector root) { + switch (name) { + case "currentReadShare" : + return application.with(application.status().withCurrentReadShare(asDouble(value))); + case "maxReadShare" : + return application.with(application.status().withMaxReadShare(asDouble(value))); + default : + throw new IllegalArgumentException("Could not apply field '" + name + "' on an application: No such modifiable field"); + } + } + + private Double asDouble(Inspector field) { + if (field.type() != Type.DOUBLE) + throw new IllegalArgumentException("Expected a DOUBLE value, got a " + field.type()); + return field.asDouble(); + } + +} diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java index 102018763ed..e1980714f9a 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java @@ -161,8 +161,9 @@ public class NodesV2ApiHandler extends LoggingRequestHandler { } private HttpResponse handlePATCH(HttpRequest request) { - String path = request.getUri().getPath(); - if (path.startsWith("/nodes/v2/node/")) { + Path path = new Path(request.getUri()); + String pathS = request.getUri().getPath(); + if (pathS.startsWith("/nodes/v2/node/")) { try (NodePatcher patcher = new NodePatcher(nodeFlavors, request.getData(), nodeFromRequest(request), nodeRepository)) { var patchedNodes = patcher.apply(); nodeRepository.nodes().write(patchedNodes, patcher.nodeMutexOfHost()); @@ -170,7 +171,15 @@ public class NodesV2ApiHandler extends LoggingRequestHandler { return new MessageResponse("Updated " + patcher.nodeMutexOfHost().node().hostname()); } } - else if (path.startsWith("/nodes/v2/upgrade/")) { + else if (path.matches("/nodes/v2/application/{applicationId}")) { + try (ApplicationPatcher patcher = new ApplicationPatcher(request.getData(), + ApplicationId.fromFullString(path.get("applicationId")), + nodeRepository)) { + nodeRepository.applications().put(patcher.apply(), patcher.lock()); + return new MessageResponse("Updated " + patcher.application()); + } + } + else if (pathS.startsWith("/nodes/v2/upgrade/")) { return setTargetVersions(request); } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/applications/ApplicationsTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/applications/ApplicationsTest.java index cc988b2ec1e..1d11b46acf5 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/applications/ApplicationsTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/applications/ApplicationsTest.java @@ -27,7 +27,7 @@ public class ApplicationsTest { assertTrue(applications.get(app1).isEmpty()); assertEquals(List.of(), applications.ids()); - applications.put(new Application(app1), () -> {}); + applications.put(Application.empty(app1), () -> {}); assertEquals(app1, applications.get(app1).get().id()); assertEquals(List.of(app1), applications.ids()); NestedTransaction t = new NestedTransaction(); @@ -36,10 +36,10 @@ public class ApplicationsTest { assertTrue(applications.get(app1).isEmpty()); assertEquals(List.of(), applications.ids()); - applications.put(new Application(app1), () -> {}); - applications.put(new Application(app2), () -> {}); + applications.put(Application.empty(app1), () -> {}); + applications.put(Application.empty(app2), () -> {}); t = new NestedTransaction(); - applications.put(new Application(app3), new ApplicationTransaction(provisionLock(app1), t)); + applications.put(Application.empty(app3), new ApplicationTransaction(provisionLock(app1), t)); assertEquals(List.of(app1, app2), applications.ids()); t.commit(); assertEquals(List.of(app1, app2, app3), applications.ids()); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/AutoscalingIntegrationTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/AutoscalingIntegrationTest.java index 3a74c3a3cf6..87b8ccdc348 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/AutoscalingIntegrationTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/AutoscalingIntegrationTest.java @@ -54,12 +54,12 @@ public class AutoscalingIntegrationTest { ClusterResources min = new ClusterResources(2, 1, nodes); ClusterResources max = new ClusterResources(2, 1, nodes); - Application application = tester.nodeRepository().applications().get(application1).orElse(new Application(application1)) + Application application = tester.nodeRepository().applications().get(application1).orElse(Application.empty(application1)) .withCluster(cluster1.id(), false, min, max); try (Mutex lock = tester.nodeRepository().nodes().lock(application1)) { tester.nodeRepository().applications().put(application, lock); } - var scaledResources = autoscaler.suggest(application.clusters().get(cluster1.id()), + var scaledResources = autoscaler.suggest(application, application.clusters().get(cluster1.id()), tester.nodeRepository().nodes().list().owner(application1)); assertTrue(scaledResources.isPresent()); } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/AutoscalingTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/AutoscalingTest.java index dbab02302f8..4b9dc2e6417 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/AutoscalingTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/AutoscalingTest.java @@ -14,6 +14,7 @@ import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.Zone; import com.yahoo.vespa.hosted.provision.NodeRepository; import com.yahoo.vespa.hosted.provision.Nodelike; +import com.yahoo.vespa.hosted.provision.applications.Application; import com.yahoo.vespa.hosted.provision.provisioning.HostResourcesCalculator; import org.junit.Test; @@ -279,7 +280,7 @@ public class AutoscalingTest { } @Test - public void test_autoscalinggroupsize_by_cpu() { + public void test_autoscaling_groupsize_by_cpu() { NodeResources resources = new NodeResources(3, 100, 100, 1); ClusterResources min = new ClusterResources( 3, 1, new NodeResources(1, 1, 1, 1)); ClusterResources max = new ClusterResources(21, 7, new NodeResources(100, 1000, 1000, 1)); @@ -429,6 +430,36 @@ public class AutoscalingTest { tester.autoscale(application1, cluster1.id(), min, max).target()); } + @Test + public void test_autoscaling_considers_read_share() { + NodeResources resources = new NodeResources(3, 100, 100, 1); + ClusterResources min = new ClusterResources( 1, 1, resources); + ClusterResources max = new ClusterResources(10, 1, resources); + AutoscalingTester tester = new AutoscalingTester(resources.withVcpu(resources.vcpu() * 2)); + + ApplicationId application1 = tester.applicationId("application1"); + ClusterSpec cluster1 = tester.clusterSpec(ClusterSpec.Type.container, "cluster1"); + + tester.deploy(application1, cluster1, 5, 1, resources); + tester.addCpuMeasurements(0.25f, 1f, 120, application1); + + // (no read share stored) + tester.assertResources("Advice to scale up since we set aside for bcp by default", + 7, 1, 3, 100, 100, + tester.autoscale(application1, cluster1.id(), min, max).target()); + + tester.storeReadShare(0.25, 0.5, application1); + tester.assertResources("Half of global share is the same as the default assumption used above", + 7, 1, 3, 100, 100, + tester.autoscale(application1, cluster1.id(), min, max).target()); + + tester.storeReadShare(0.5, 0.5, application1); + tester.assertResources("Advice to scale down since we don't need room for bcp", + 4, 1, 3, 100, 100, + tester.autoscale(application1, cluster1.id(), min, max).target()); + + } + /** * This calculator subtracts the memory tax when forecasting overhead, but not when actually * returning information about nodes. This is allowed because the forecast is a *worst case*. diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/AutoscalingTester.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/AutoscalingTester.java index eb490079c98..3f3655dcab6 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/AutoscalingTester.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/AutoscalingTester.java @@ -162,7 +162,7 @@ class AutoscalingTester { for (int i = 0; i < count; i++) { clock().advance(Duration.ofMinutes(1)); for (Node node : nodes) { - float cpu = (float) Resource.cpu.idealAverageLoad() * otherResourcesLoad * oneExtraNodeFactor; + float cpu = (float) 0.2 * otherResourcesLoad * oneExtraNodeFactor; float memory = value * oneExtraNodeFactor; float disk = (float) Resource.disk.idealAverageLoad() * otherResourcesLoad * oneExtraNodeFactor; db.add(List.of(new Pair<>(node.hostname(), new MetricSnapshot(clock().instant(), @@ -197,25 +197,32 @@ class AutoscalingTester { } } + public void storeReadShare(double currentReadShare, double maxReadShare, ApplicationId applicationId) { + Application application = nodeRepository().applications().require(applicationId); + application = application.with(application.status().withCurrentReadShare(currentReadShare) + .withMaxReadShare(maxReadShare)); + nodeRepository().applications().put(application, nodeRepository().nodes().lock(applicationId)); + } + public Autoscaler.Advice autoscale(ApplicationId applicationId, ClusterSpec.Id clusterId, ClusterResources min, ClusterResources max) { - Application application = nodeRepository().applications().get(applicationId).orElse(new Application(applicationId)) + Application application = nodeRepository().applications().get(applicationId).orElse(Application.empty(applicationId)) .withCluster(clusterId, false, min, max); try (Mutex lock = nodeRepository().nodes().lock(applicationId)) { nodeRepository().applications().put(application, lock); } - return autoscaler.autoscale(application.clusters().get(clusterId), + return autoscaler.autoscale(application, application.clusters().get(clusterId), nodeRepository().nodes().list(Node.State.active).owner(applicationId)); } public Autoscaler.Advice suggest(ApplicationId applicationId, ClusterSpec.Id clusterId, ClusterResources min, ClusterResources max) { - Application application = nodeRepository().applications().get(applicationId).orElse(new Application(applicationId)) + Application application = nodeRepository().applications().get(applicationId).orElse(Application.empty(applicationId)) .withCluster(clusterId, false, min, max); try (Mutex lock = nodeRepository().nodes().lock(applicationId)) { nodeRepository().applications().put(application, lock); } - return autoscaler.suggest(application.clusters().get(clusterId), + return autoscaler.suggest(application, application.clusters().get(clusterId), nodeRepository().nodes().list(Node.State.active).owner(applicationId)); } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/ScalingSuggestionsMaintainerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/ScalingSuggestionsMaintainerTest.java index 6581008268d..af745608679 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/ScalingSuggestionsMaintainerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/ScalingSuggestionsMaintainerTest.java @@ -99,7 +99,7 @@ public class ScalingSuggestionsMaintainerTest { var suggested = tester.nodeRepository().applications().get(app1).get().cluster(cluster1.id()).get().suggestedResources().get().resources(); tester.deploy(app1, cluster1, Capacity.from(suggested, suggested, false, true)); tester.clock().advance(Duration.ofDays(2)); - addMeasurements((float)Resource.cpu.idealAverageLoad(), + addMeasurements(0.2f, (float)Resource.memory.idealAverageLoad(), (float)Resource.disk.idealAverageLoad(), 0, 500, app1, tester.nodeRepository(), metricsDb); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/ApplicationSerializerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/ApplicationSerializerTest.java index 6881733324e..9cac6430d6e 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/ApplicationSerializerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/ApplicationSerializerTest.java @@ -8,6 +8,7 @@ import com.yahoo.config.provision.NodeResources; import com.yahoo.vespa.hosted.provision.applications.Application; import com.yahoo.vespa.hosted.provision.applications.Cluster; import com.yahoo.vespa.hosted.provision.applications.ScalingEvent; +import com.yahoo.vespa.hosted.provision.applications.Status; import org.junit.Test; import java.time.Instant; @@ -51,12 +52,15 @@ public class ApplicationSerializerTest { Optional.of(Instant.ofEpochMilli(67890L)))), "Autoscaling status")); Application original = new Application(ApplicationId.from("myTenant", "myApplication", "myInstance"), + Status.initial().withCurrentReadShare(0.3).withMaxReadShare(0.5), clusters); Application serialized = ApplicationSerializer.fromJson(ApplicationSerializer.toJson(original)); assertNotSame(original, serialized); assertEquals(original, serialized); assertEquals(original.id(), serialized.id()); + assertNotSame(original.status(), serialized.status()); + assertEquals(original.status(), serialized.status()); assertEquals(original.clusters(), serialized.clusters()); for (Cluster originalCluster : original.clusters().values()) { Cluster serializedCluster = serialized.clusters().get(originalCluster.id()); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/ApplicationPatcherTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/ApplicationPatcherTest.java new file mode 100644 index 00000000000..85469e74c0f --- /dev/null +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/ApplicationPatcherTest.java @@ -0,0 +1,34 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.provision.restapi; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.hosted.provision.NodeRepository; +import com.yahoo.vespa.hosted.provision.NodeRepositoryTester; +import com.yahoo.vespa.hosted.provision.applications.Application; +import org.junit.Test; + +import java.io.ByteArrayInputStream; + +import static org.junit.Assert.assertEquals; + +/** + * @author bratseth + */ +public class ApplicationPatcherTest { + + @Test + public void testPatching() { + NodeRepositoryTester tester = new NodeRepositoryTester(); + Application application = Application.empty(ApplicationId.from("t1", "a1", "i1")); + tester.nodeRepository().applications().put(application, tester.nodeRepository().nodes().lock(application.id())); + String patch = "{ \"currentReadShare\" :0.4, \"maxReadShare\": 1.0 }"; + ApplicationPatcher patcher = new ApplicationPatcher(new ByteArrayInputStream(patch.getBytes()), + application.id(), + tester.nodeRepository()); + Application patched = patcher.apply(); + assertEquals(0.4, patcher.application().status().currentReadShare(), 0.0000001); + assertEquals(1.0, patcher.application().status().maxReadShare(), 0.0000001); + patcher.close(); + } + +} diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiTest.java index d727d473b84..dce2d6f90c6 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiTest.java @@ -257,6 +257,12 @@ public class NodesV2ApiTest { "application1.json"); assertFile(new Request("http://localhost:8080/nodes/v2/application/tenant2.application2.instance2"), "application2.json"); + + // Update (PATCH) an application + assertResponse(new Request("http://localhost:8080/nodes/v2/application/tenant1.application1.instance1", + Utf8.toBytes("{\"currentReadShare\": 0.3, " + + "\"maxReadShare\": 0.5 }"), Request.Method.PATCH), + "{\"message\":\"Updated application 'tenant1.application1.instance1'\"}"); } @Test |