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 /node-repository | |
parent | 466a53ad422c819b81792f3ad682edfa65dc06b5 (diff) | |
parent | 9f87d959285f1d4b435df1c47c15c29c356980b8 (diff) |
Merge pull request #16577 from vespa-engine/bratseth/traffic-fraction
Bratseth/traffic fraction
Diffstat (limited to 'node-repository')
21 files changed, 327 insertions, 39 deletions
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 |