summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorValerij Fredriksen <freva@users.noreply.github.com>2021-02-18 19:21:45 +0100
committerGitHub <noreply@github.com>2021-02-18 19:21:45 +0100
commitdbde8cfb86537f35fab9418dd78c7baf060fe597 (patch)
tree989b602c5f726b8c28c5b3b56e0e4e1f0acfafa0
parent466a53ad422c819b81792f3ad682edfa65dc06b5 (diff)
parent9f87d959285f1d4b435df1c47c15c29c356980b8 (diff)
Merge pull request #16577 from vespa-engine/bratseth/traffic-fraction
Bratseth/traffic fraction
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Application.java4
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/NodeRepository.java3
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ApplicationPatch.java34
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ProvisionResource.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/TrafficShareUpdater.java66
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/NodeRepositoryMock.java12
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/TrafficShareUpdaterTest.java95
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Application.java25
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Applications.java5
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Status.java67
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Autoscaler.java13
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Resource.java2
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/ResourceTarget.java19
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/AutoscalingMaintainer.java4
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ScalingSuggestionsMaintainer.java4
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/Nodes.java1
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/ApplicationSerializer.java22
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java2
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/ApplicationPatcher.java79
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java15
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/applications/ApplicationsTest.java8
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/AutoscalingIntegrationTest.java4
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/AutoscalingTest.java33
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/AutoscalingTester.java17
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/ScalingSuggestionsMaintainerTest.java2
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/ApplicationSerializerTest.java4
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/ApplicationPatcherTest.java34
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiTest.java6
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