diff options
author | Jon Bratseth <bratseth@gmail.com> | 2023-01-21 17:50:10 +0100 |
---|---|---|
committer | Jon Bratseth <bratseth@gmail.com> | 2023-01-21 17:50:10 +0100 |
commit | 7a6af9caa065b3ab63b094d78b7347d7df6bea0f (patch) | |
tree | 48e80650a9cd88016ff8e618d85d727a28f3542c | |
parent | 00d86602a88c66486c8f4c68a1c8bdff096c7273 (diff) |
Support a group size constraint in content clusters
28 files changed, 383 insertions, 67 deletions
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/NodesSpecification.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/NodesSpecification.java index aac968f9038..d3cf5c531e1 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/NodesSpecification.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/NodesSpecification.java @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.model.builder.xml.dom; +import com.yahoo.collections.IntRange; import com.yahoo.collections.Pair; import com.yahoo.component.Version; import com.yahoo.config.application.api.DeployLogger; @@ -34,6 +35,8 @@ public class NodesSpecification { private final ClusterResources min, max; + private final IntRange groupSize; + private final boolean dedicated; /** The Vespa version we want the nodes to run */ @@ -63,6 +66,7 @@ public class NodesSpecification { private NodesSpecification(ClusterResources min, ClusterResources max, + IntRange groupSize, boolean dedicated, Version version, boolean required, boolean canFail, boolean exclusive, Optional<DockerImage> dockerImageRepo, @@ -83,6 +87,7 @@ public class NodesSpecification { this.min = min; this.max = max; + this.groupSize = groupSize; this.dedicated = dedicated; this.version = version; this.required = required; @@ -103,6 +108,7 @@ public class NodesSpecification { boolean hasCountAttribute = resolvedElement.stringAttribute("count") != null; return new NodesSpecification(resources.getFirst(), resources.getSecond(), + IntRange.from(resolvedElement.stringAttribute("group-size", "")), dedicated, version, resolvedElement.booleanAttribute("required", false), @@ -178,6 +184,7 @@ public class NodesSpecification { public static NodesSpecification nonDedicated(int count, ConfigModelContext context) { return new NodesSpecification(new ClusterResources(count, 1, NodeResources.unspecified()), new ClusterResources(count, 1, NodeResources.unspecified()), + IntRange.empty(), false, context.getDeployState().getWantedNodeVespaVersion(), false, @@ -193,6 +200,7 @@ public class NodesSpecification { public static NodesSpecification dedicated(int count, ConfigModelContext context) { return new NodesSpecification(new ClusterResources(count, 1, NodeResources.unspecified()), new ClusterResources(count, 1, NodeResources.unspecified()), + IntRange.empty(), true, context.getDeployState().getWantedNodeVespaVersion(), false, @@ -219,6 +227,7 @@ public class NodesSpecification { .toList(); return new NodesSpecification(new ClusterResources(count, 1, resources), new ClusterResources(count, 1, resources), + IntRange.empty(), true, context.getDeployState().getWantedNodeVespaVersion(), allContent.stream().anyMatch(content -> content.required), @@ -232,6 +241,7 @@ public class NodesSpecification { public ClusterResources minResources() { return min; } public ClusterResources maxResources() { return max; } + public IntRange groupSize() { return groupSize; } /** * Returns whether this requires dedicated nodes. @@ -275,7 +285,7 @@ public class NodesSpecification { .loadBalancerSettings(zoneEndpoint) .stateful(stateful) .build(); - return hostSystem.allocateHosts(cluster, Capacity.from(min, max, required, canFail, cloudAccount), logger); + return hostSystem.allocateHosts(cluster, Capacity.from(min, max, groupSize, required, canFail, cloudAccount), logger); } private static Pair<NodeResources, NodeResources> nodeResources(ModelElement nodesElement) { diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java index 4c7bad575d2..7b1876db457 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.model.container.xml; +import com.yahoo.collections.IntRange; import com.yahoo.component.ComponentSpecification; import com.yahoo.component.Version; import com.yahoo.component.chain.dependencies.Dependencies; @@ -926,6 +927,7 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { ClusterResources resources = new ClusterResources(nodeCount, 1, NodeResources.unspecified()); Capacity capacity = Capacity.from(resources, resources, + IntRange.empty(), false, !deployState.getProperties().isBootstrap(), context.getDeployState().getProperties().cloudAccount()); diff --git a/config-model/src/main/resources/schema/content.rnc b/config-model/src/main/resources/schema/content.rnc index 3d1873507ce..703001f0107 100644 --- a/config-model/src/main/resources/schema/content.rnc +++ b/config-model/src/main/resources/schema/content.rnc @@ -226,7 +226,8 @@ ContentNodes = element nodes { attribute required { xsd:boolean }? & attribute exclusive { xsd:boolean }? & attribute docker-image { xsd:string }? & - attribute groups { xsd:positiveInteger | xsd:string }? + attribute groups { xsd:positiveInteger | xsd:string }? & + attribute group-size { xsd:positiveInteger | xsd:string }? ) | ContentNode + diff --git a/config-model/src/test/java/com/yahoo/config/model/provision/ModelProvisioningTest.java b/config-model/src/test/java/com/yahoo/config/model/provision/ModelProvisioningTest.java index 635ed4c8659..a6ddce4cec5 100644 --- a/config-model/src/test/java/com/yahoo/config/model/provision/ModelProvisioningTest.java +++ b/config-model/src/test/java/com/yahoo/config/model/provision/ModelProvisioningTest.java @@ -501,14 +501,14 @@ public class ModelProvisioningTest { " <documents>" + " <document type='type1' mode='index'/>" + " </documents>" + - " <nodes count='27' groups='9'/>" + + " <nodes count='27' groups='9' group-size='[2, 3]'/>" + " </content>" + " <content version='1.0' id='baz'>" + " <redundancy>1</redundancy>" + " <documents>" + " <document type='type1' mode='index'/>" + " </documents>" + - " <nodes count='27' groups='27'/>" + + " <nodes count='27' groups='27' group-size='1'/>" + " </content>" + "</services>"; diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/CloudAccountChangeValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/CloudAccountChangeValidatorTest.java index 46d0fcb3123..91493bab480 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/CloudAccountChangeValidatorTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/CloudAccountChangeValidatorTest.java @@ -1,5 +1,6 @@ package com.yahoo.vespa.model.application.validation.change; +import com.yahoo.collections.IntRange; import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.model.api.Provisioned; import com.yahoo.config.model.deploy.DeployState; @@ -55,6 +56,7 @@ class CloudAccountChangeValidatorTest { NodeResources nodeResources = new NodeResources(4, 8, 100, 10); return Capacity.from(new ClusterResources(2, 1, nodeResources), new ClusterResources(2, 1, nodeResources), + IntRange.empty(), false, false, Optional.of(cloudAccount).filter(account -> !account.isUnspecified())); diff --git a/config-model/src/test/schema-test-files/services-hosted.xml b/config-model/src/test/schema-test-files/services-hosted.xml index caf56881475..2697cc871e2 100644 --- a/config-model/src/test/schema-test-files/services-hosted.xml +++ b/config-model/src/test/schema-test-files/services-hosted.xml @@ -31,7 +31,7 @@ <content id="ml" version="1.0"> <redundancy>2</redundancy> - <nodes count="[10,20]" flavor="large" groups="[1,3]" vespamalloc-debug-stacktrace="proton"> + <nodes count="[10,20]" flavor="large" groups="[1,3]" group-size="[1,2]" vespamalloc-debug-stacktrace="proton"> <resources vcpu="[3.0, 4]" memory="[32000.0Mb, 33Gb]" disk="[300 Gb, 1Tb]"/> </nodes> </content> diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/Capacity.java b/config-provisioning/src/main/java/com/yahoo/config/provision/Capacity.java index e63750e0e11..993ab686675 100644 --- a/config-provisioning/src/main/java/com/yahoo/config/provision/Capacity.java +++ b/config-provisioning/src/main/java/com/yahoo/config/provision/Capacity.java @@ -1,6 +1,8 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.config.provision; +import com.yahoo.collections.IntRange; + import java.util.Objects; import java.util.Optional; import java.util.stream.Stream; @@ -15,12 +17,19 @@ public final class Capacity { /** Resources should stay between these values, inclusive */ private final ClusterResources min, max; + private final IntRange groupSize; private final boolean required; private final boolean canFail; private final NodeType type; private final Optional<CloudAccount> cloudAccount; - private Capacity(ClusterResources min, ClusterResources max, boolean required, boolean canFail, NodeType type, Optional<CloudAccount> cloudAccount) { + private Capacity(ClusterResources min, + ClusterResources max, + IntRange groupSize, + boolean required, + boolean canFail, + NodeType type, + Optional<CloudAccount> cloudAccount) { validate(min); validate(max); if (max.smallerThan(min)) @@ -30,6 +39,7 @@ public final class Capacity { throw new IllegalArgumentException("Capacity range does not allow GPU, got min " + min + " and max " + max); this.min = min; this.max = max; + this.groupSize = groupSize; this.required = required; this.canFail = canFail; this.type = type; @@ -45,6 +55,7 @@ public final class Capacity { public ClusterResources minResources() { return min; } public ClusterResources maxResources() { return max; } + public IntRange groupSize() { return groupSize; } /** Returns whether the requested number of nodes must be met exactly for a request for this to succeed */ public boolean isRequired() { return required; } @@ -69,7 +80,11 @@ public final class Capacity { } public Capacity withLimits(ClusterResources min, ClusterResources max) { - return new Capacity(min, max, required, canFail, type, cloudAccount); + return withLimits(min, max, IntRange.empty()); + } + + public Capacity withLimits(ClusterResources min, ClusterResources max, IntRange groupSize) { + return new Capacity(min, max, groupSize, required, canFail, type, cloudAccount); } @Override @@ -85,29 +100,34 @@ public final class Capacity { /** Create a non-required, failable capacity request */ public static Capacity from(ClusterResources min, ClusterResources max) { - return from(min, max, false, true); + return from(min, max, IntRange.empty(), false, true, Optional.empty()); } public static Capacity from(ClusterResources resources, boolean required, boolean canFail) { return from(resources, required, canFail, NodeType.tenant); } - // TODO(mpolden): Remove when config models < 7.590 are gone + // TODO: Remove after February 2023 public static Capacity from(ClusterResources min, ClusterResources max, boolean required, boolean canFail) { - return from(min, max, required, canFail, Optional.empty()); + return new Capacity(min, max, IntRange.empty(), required, canFail, NodeType.tenant, Optional.empty()); } + // TODO: Remove after February 2023 public static Capacity from(ClusterResources min, ClusterResources max, boolean required, boolean canFail, Optional<CloudAccount> cloudAccount) { - return new Capacity(min, max, required, canFail, NodeType.tenant, cloudAccount); + return new Capacity(min, max, IntRange.empty(), required, canFail, NodeType.tenant, cloudAccount); + } + + public static Capacity from(ClusterResources min, ClusterResources max, IntRange groupSize, boolean required, boolean canFail, Optional<CloudAccount> cloudAccount) { + return new Capacity(min, max, groupSize, required, canFail, NodeType.tenant, cloudAccount); } /** Creates this from a node type */ public static Capacity fromRequiredNodeType(NodeType type) { - return from(new ClusterResources(0, 0, NodeResources.unspecified()), true, false, type); + return from(new ClusterResources(0, 1, NodeResources.unspecified()), true, false, type); } private static Capacity from(ClusterResources resources, boolean required, boolean canFail, NodeType type) { - return new Capacity(resources, resources, required, canFail, type, Optional.empty()); + return new Capacity(resources, resources, IntRange.empty(), required, canFail, type, Optional.empty()); } } diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterResources.java b/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterResources.java index 9938823768b..5511a9ed1ed 100644 --- a/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterResources.java +++ b/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterResources.java @@ -21,7 +21,7 @@ public class ClusterResources { public ClusterResources(int nodes, int groups, NodeResources nodeResources) { this.nodes = nodes; - this.groups = groups; + this.groups = groups == 0 ? 1 : groups; // TODO: Throw on groups == 0 after February 2023 this.nodeResources = Objects.requireNonNull(nodeResources); } @@ -73,9 +73,8 @@ public class ClusterResources { @Override public boolean equals(Object o) { if (o == this) return true; - if ( ! (o instanceof ClusterResources)) return false; + if ( ! (o instanceof ClusterResources other)) return false; - ClusterResources other = (ClusterResources)o; if (other.nodes != this.nodes) return false; if (other.groups != this.groups) return false; if ( ! other.nodeResources.equals(this.nodeResources)) return false; diff --git a/config-provisioning/src/test/java/com/yahoo/config/provision/CapacityTest.java b/config-provisioning/src/test/java/com/yahoo/config/provision/CapacityTest.java index a6ddb401b2f..ca552003f7a 100644 --- a/config-provisioning/src/test/java/com/yahoo/config/provision/CapacityTest.java +++ b/config-provisioning/src/test/java/com/yahoo/config/provision/CapacityTest.java @@ -1,8 +1,11 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.config.provision; +import com.yahoo.collections.IntRange; import org.junit.jupiter.api.Test; +import java.util.Optional; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; @@ -15,8 +18,11 @@ public class CapacityTest { void testCapacityValidation() { // Equal min and max is allowed Capacity.from(new ClusterResources(4, 2, new NodeResources(1, 2, 3, 4)), - new ClusterResources(4, 2, new NodeResources(1, 2, 3, 4)), - false, true); + new ClusterResources(4, 2, new NodeResources(1, 2, 3, 4)), + IntRange.empty(), + false, + true, + Optional.empty()); assertValidationFailure(new ClusterResources(4, 2, new NodeResources(1, 2, 3, 4)), new ClusterResources(2, 2, new NodeResources(1, 2, 3, 4))); assertValidationFailure(new ClusterResources(4, 4, new NodeResources(1, 2, 3, 4)), @@ -36,7 +42,7 @@ public class CapacityTest { private void assertValidationFailure(ClusterResources min, ClusterResources max) { try { - Capacity.from(min, max, false, true); + Capacity.from(min, max, IntRange.empty(), false, true, Optional.empty()); fail("Expected exception with min " + min + " and max " + max); } catch (IllegalArgumentException e) { diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Cluster.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Cluster.java index 0a731f66418..60a5682101f 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Cluster.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Cluster.java @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.provision.applications; +import com.yahoo.collections.IntRange; import com.yahoo.config.provision.Capacity; import com.yahoo.config.provision.ClusterResources; import com.yahoo.config.provision.ClusterSpec; @@ -28,6 +29,7 @@ public class Cluster { private final ClusterSpec.Id id; private final boolean exclusive; private final ClusterResources min, max; + private final IntRange groupSize; private final boolean required; private final Autoscaling suggested; private final Autoscaling target; @@ -39,6 +41,7 @@ public class Cluster { boolean exclusive, ClusterResources minResources, ClusterResources maxResources, + IntRange groupSize, boolean required, Autoscaling suggested, Autoscaling target, @@ -47,6 +50,7 @@ public class Cluster { this.exclusive = exclusive; this.min = Objects.requireNonNull(minResources); this.max = Objects.requireNonNull(maxResources); + this.groupSize = Objects.requireNonNull(groupSize); this.required = required; this.suggested = Objects.requireNonNull(suggested); Objects.requireNonNull(target); @@ -68,6 +72,9 @@ public class Cluster { /** Returns the configured maximal resources in this cluster */ public ClusterResources maxResources() { return max; } + /** Returns the configured group size range in this cluster */ + public IntRange groupSize() { return groupSize; } + /** * Returns whether the resources of this cluster are required to be within the specified min and max. * Otherwise they may be adjusted by capacity policies. @@ -105,16 +112,16 @@ public class Cluster { public Cluster withConfiguration(boolean exclusive, Capacity capacity) { return new Cluster(id, exclusive, - capacity.minResources(), capacity.maxResources(), capacity.isRequired(), + capacity.minResources(), capacity.maxResources(), capacity.groupSize(), capacity.isRequired(), suggested, target, scalingEvents); } public Cluster withSuggested(Autoscaling suggested) { - return new Cluster(id, exclusive, min, max, required, suggested, target, scalingEvents); + return new Cluster(id, exclusive, min, max, groupSize, required, suggested, target, scalingEvents); } public Cluster withTarget(Autoscaling target) { - return new Cluster(id, exclusive, min, max, required, suggested, target, scalingEvents); + return new Cluster(id, exclusive, min, max, groupSize, required, suggested, target, scalingEvents); } /** Add or update (based on "at" time) a scaling event */ @@ -128,7 +135,7 @@ public class Cluster { scalingEvents.add(scalingEvent); prune(scalingEvents); - return new Cluster(id, exclusive, min, max, required, suggested, target, scalingEvents); + return new Cluster(id, exclusive, min, max, groupSize, required, suggested, target, scalingEvents); } @Override @@ -158,7 +165,8 @@ public class Cluster { } public static Cluster create(ClusterSpec.Id id, boolean exclusive, Capacity requested) { - return new Cluster(id, exclusive, requested.minResources(), requested.maxResources(), requested.isRequired(), + return new Cluster(id, exclusive, + requested.minResources(), requested.maxResources(), requested.groupSize(), requested.isRequired(), Autoscaling.empty(), Autoscaling.empty(), List.of()); } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/AllocationOptimizer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/AllocationOptimizer.java index 56a6d61ebee..c3dc0ff250f 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/AllocationOptimizer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/AllocationOptimizer.java @@ -1,6 +1,7 @@ // Copyright Yahoo. 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.collections.IntRange; import com.yahoo.config.provision.ClusterResources; import com.yahoo.config.provision.NodeResources; import com.yahoo.vespa.hosted.provision.NodeRepository; @@ -38,7 +39,8 @@ public class AllocationOptimizer { int minimumNodes = AllocationOptimizer.minimumNodes; if (limits.isEmpty()) limits = Limits.of(new ClusterResources(minimumNodes, 1, NodeResources.unspecified()), - new ClusterResources(maximumNodes, maximumNodes, NodeResources.unspecified())); + new ClusterResources(maximumNodes, maximumNodes, NodeResources.unspecified()), + IntRange.empty()); else limits = atLeast(minimumNodes, limits).fullySpecified(current.clusterSpec(), nodeRepository, clusterModel.application().id()); Optional<AllocatableClusterResources> bestAllocation = Optional.empty(); @@ -50,6 +52,7 @@ public class AllocationOptimizer { for (int groups = limits.min().groups(); groups <= limits.max().groups(); groups++) { for (int nodes = limits.min().nodes(); nodes <= limits.max().nodes(); nodes++) { if (nodes % groups != 0) continue; + if ( ! limits.groupSize().includes(nodes / groups)) continue; var resources = new ClusterResources(nodes, groups, diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Limits.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Limits.java index cb5d8dd5042..9de7134a6d7 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Limits.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Limits.java @@ -1,6 +1,7 @@ // Copyright Yahoo. 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.collections.IntRange; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Capacity; import com.yahoo.config.provision.ClusterResources; @@ -19,13 +20,15 @@ import java.util.Objects; */ public class Limits { - private static final Limits empty = new Limits(null, null); + private static final Limits empty = new Limits(null, null, IntRange.empty()); private final ClusterResources min, max; + private final IntRange groupSize; - private Limits(ClusterResources min, ClusterResources max) { + private Limits(ClusterResources min, ClusterResources max, IntRange groupSize) { this.min = min; this.max = max; + this.groupSize = groupSize; } public static Limits empty() { return empty; } @@ -42,12 +45,14 @@ public class Limits { return max; } + public IntRange groupSize() { return groupSize; } + public Limits withMin(ClusterResources min) { - return new Limits(min, max); + return new Limits(min, max, groupSize); } public Limits withMax(ClusterResources max) { - return new Limits(min, max); + return new Limits(min, max, groupSize); } /** Caps the given resources at the limits of this. If it is empty the node resources are returned as-is */ @@ -66,7 +71,7 @@ public class Limits { var defaultResources = new CapacityPolicies(nodeRepository).defaultNodeResources(clusterSpec, applicationId); var specifiedMin = min.nodeResources().isUnspecified() ? min.with(defaultResources) : min; var specifiedMax = max.nodeResources().isUnspecified() ? max.with(defaultResources) : max; - return new Limits(specifiedMin, specifiedMax); + return new Limits(specifiedMin, specifiedMax, groupSize); } private double between(double min, double max, double value) { @@ -76,21 +81,23 @@ public class Limits { } public static Limits of(Cluster cluster) { - return new Limits(cluster.minResources(), cluster.maxResources()); + return new Limits(cluster.minResources(), cluster.maxResources(), cluster.groupSize()); } public static Limits of(Capacity capacity) { - return new Limits(capacity.minResources(), capacity.maxResources()); + return new Limits(capacity.minResources(), capacity.maxResources(), capacity.groupSize()); } - public static Limits of(ClusterResources min, ClusterResources max) { - return new Limits(Objects.requireNonNull(min, "min"), Objects.requireNonNull(max, "max")); + public static Limits of(ClusterResources min, ClusterResources max, IntRange groupSize) { + return new Limits(Objects.requireNonNull(min, "min"), + Objects.requireNonNull(max, "max"), + Objects.requireNonNull(groupSize, "groupSize")); } @Override public String toString() { if (isEmpty()) return "no limits"; - return "limits: from " + min + " to " + max; + return "limits: from " + min + " to " + max + ( groupSize.isEmpty() ? "" : " with group size " + groupSize); } } 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 2f5b057e927..4d2fae8a4d7 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 @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.provision.persistence; +import com.yahoo.collections.IntRange; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ClusterResources; import com.yahoo.config.provision.ClusterSpec; @@ -23,6 +24,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.OptionalInt; /** * Application JSON serializer @@ -48,6 +50,7 @@ public class ApplicationSerializer { private static final String exclusiveKey = "exclusive"; private static final String minResourcesKey = "min"; private static final String maxResourcesKey = "max"; + private static final String groupSizeKey = "groupSize"; private static final String requiredKey = "required"; private static final String suggestedKey = "suggested"; private static final String resourcesKey = "resources"; @@ -122,6 +125,7 @@ public class ApplicationSerializer { clusterObject.setBool(exclusiveKey, cluster.exclusive()); toSlime(cluster.minResources(), clusterObject.setObject(minResourcesKey)); toSlime(cluster.maxResources(), clusterObject.setObject(maxResourcesKey)); + toSlime(cluster.groupSize(), clusterObject.setObject(groupSizeKey)); clusterObject.setBool(requiredKey, cluster.required()); toSlime(cluster.suggested(), clusterObject.setObject(suggestedKey)); toSlime(cluster.target(), clusterObject.setObject(targetKey)); @@ -133,6 +137,7 @@ public class ApplicationSerializer { clusterObject.field(exclusiveKey).asBool(), clusterResourcesFromSlime(clusterObject.field(minResourcesKey)), clusterResourcesFromSlime(clusterObject.field(maxResourcesKey)), + intRangeFromSlime(clusterObject.field(groupSizeKey)), clusterObject.field(requiredKey).asBool(), autoscalingFromSlime(clusterObject.field(suggestedKey), clusterObject.field("nonExisting")), autoscalingFromSlime(clusterObject.field(targetKey), clusterObject.field(autoscalingStatusObjectKey)), @@ -165,6 +170,16 @@ public class ApplicationSerializer { NodeResourcesSerializer.resourcesFromSlime(clusterResourcesObject.field(nodeResourcesKey))); } + private static void toSlime(IntRange range, Cursor rangeObject) { + range.from().ifPresent(from -> rangeObject.setLong(fromKey, from)); + range.to().ifPresent(from -> rangeObject.setLong(toKey, from)); + } + + private static IntRange intRangeFromSlime(Inspector rangeObject) { + if ( ! rangeObject.valid()) return IntRange.empty(); + return new IntRange(optionalInt(rangeObject.field(fromKey)), optionalInt(rangeObject.field(toKey))); + } + private static void toSlime(Load load, Cursor loadObject) { loadObject.setDouble(cpuKey, load.cpu()); loadObject.setDouble(memoryKey, load.memory()); @@ -257,4 +272,8 @@ public class ApplicationSerializer { return inspector.valid() ? Optional.of(Instant.ofEpochMilli(inspector.asLong())) : Optional.empty(); } + private static OptionalInt optionalInt(Inspector inspector) { + return inspector.valid() ? OptionalInt.of((int)inspector.asLong()) : OptionalInt.empty(); + } + } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/CapacityPolicies.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/CapacityPolicies.java index a1400626658..081de1fd8dc 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/CapacityPolicies.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/CapacityPolicies.java @@ -39,8 +39,11 @@ public class CapacityPolicies { } public Capacity applyOn(Capacity capacity, ApplicationId application, boolean exclusive) { - return capacity.withLimits(applyOn(capacity.minResources(), capacity, application, exclusive), - applyOn(capacity.maxResources(), capacity, application, exclusive)); + var min = applyOn(capacity.minResources(), capacity, application, exclusive); + var max = applyOn(capacity.maxResources(), capacity, application, exclusive); + var groupSize = capacity.groupSize().fromAtMost(max.nodes() / max.groups()) + .toAtLeast(min.nodes() / min.groups()); + return capacity.withLimits(min, max, groupSize); } private ClusterResources applyOn(ClusterResources resources, Capacity capacity, ApplicationId application, boolean exclusive) { 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 59e67f65f8f..bc623b495c8 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 @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.provision.provisioning; +import com.yahoo.collections.IntRange; import com.yahoo.component.annotation.Inject; import com.yahoo.config.provision.ActivationContext; import com.yahoo.config.provision.ApplicationId; @@ -98,7 +99,7 @@ public class NodeRepositoryProvisioner implements Provisioner { cluster = capacityPolicies.decideExclusivity(requested, cluster); Capacity actual = capacityPolicies.applyOn(requested, application, cluster.isExclusive()); ClusterResources target = decideTargetResources(application, cluster, actual); - ensureRedundancy(target.nodes(), cluster, actual.canFail(), application); + validate(actual, target, cluster, application); logIfDownscaled(requested.minResources().nodes(), actual.minResources().nodes(), cluster, logger); groups = target.groups(); @@ -204,18 +205,17 @@ public class NodeRepositoryProvisioner implements Provisioner { .advertisedResources(); } - /** - * Throw if the node count is 1 for container and content clusters and we're in a production zone - * - * @throws IllegalArgumentException if only one node is requested and we can fail - */ - private void ensureRedundancy(int nodeCount, ClusterSpec cluster, boolean canFail, ApplicationId application) { - if (! application.instance().isTester() && - canFail && - nodeCount == 1 && - requiresRedundancy(cluster.type()) && - zone.environment().isProduction()) - throw new IllegalArgumentException("Deployments to prod require at least 2 nodes per cluster for redundancy. Not fulfilled for " + cluster); + private void validate(Capacity actual, ClusterResources target, ClusterSpec cluster, ApplicationId application) { + if ( ! actual.canFail()) return; + + if (! application.instance().isTester() && zone.environment().isProduction() && + requiresRedundancy(cluster.type()) && target.nodes() == 1) + throw new IllegalArgumentException("In " + cluster + + ": Deployments to prod require at least 2 nodes per cluster for redundancy."); + + if ( ! actual.groupSize().includes(target.nodes() / target.groups())) + throw new IllegalArgumentException("In " + cluster + ": Group size with " + target + + " is not within allowed group size " + actual.groupSize()); } private static boolean requiresRedundancy(ClusterSpec.Type clusterType) { diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/ApplicationSerializer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/ApplicationSerializer.java index cb927c72eb5..6936a767936 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/ApplicationSerializer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/ApplicationSerializer.java @@ -1,6 +1,7 @@ // Copyright Yahoo. 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.collections.IntRange; import com.yahoo.config.provision.ClusterResources; import com.yahoo.slime.Cursor; import com.yahoo.slime.Slime; @@ -62,6 +63,8 @@ public class ApplicationSerializer { Limits limits = Limits.of(cluster).fullySpecified(nodes.clusterSpec(), nodeRepository, application.id()); toSlime(limits.min(), clusterObject.setObject("min")); toSlime(limits.max(), clusterObject.setObject("max")); + if ( ! cluster.groupSize().isEmpty()) + toSlime(cluster.groupSize(), clusterObject.setObject("groupSize")); toSlime(currentResources, clusterObject.setObject("current")); if (cluster.shouldSuggestResources(currentResources)) toSlime(cluster.suggested(), clusterObject.setObject("suggested")); @@ -85,6 +88,11 @@ public class ApplicationSerializer { NodeResourcesSerializer.toSlime(resources.nodeResources(), clusterResourcesObject.setObject("resources")); } + private static void toSlime(IntRange range, Cursor rangeObject) { + range.from().ifPresent(from -> rangeObject.setLong("from", range.from().getAsInt())); + range.to().ifPresent(to -> rangeObject.setLong("to", range.to().getAsInt())); + } + private static void toSlime(Load load, Cursor utilizationObject) { utilizationObject.setDouble("cpu", load.cpu()); utilizationObject.setDouble("memory", load.memory()); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java index 92ffe9828c3..e50db5c0d55 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.provision.testutils; +import com.yahoo.collections.IntRange; import com.yahoo.component.Version; import com.yahoo.config.provision.ActivationContext; import com.yahoo.config.provision.ApplicationId; @@ -193,7 +194,11 @@ public class MockNodeRepository extends NodeRepository { activate(provisioner.prepare(app1Id, cluster1Id, Capacity.from(new ClusterResources(2, 1, new NodeResources(2, 8, 50, 1)), - new ClusterResources(8, 2, new NodeResources(4, 16, 1000, 1)), false, true), + new ClusterResources(8, 2, new NodeResources(4, 16, 1000, 1)), + IntRange.empty(), + false, + true, + Optional.empty()), null), app1Id, provisioner); Application app1 = applications().get(app1Id).get(); Cluster cluster1 = app1.cluster(cluster1Id.id()).get(); 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 e1b32726070..35197da4315 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 @@ -1,6 +1,7 @@ // Copyright Yahoo. 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.collections.IntRange; import com.yahoo.config.provision.Capacity; import com.yahoo.config.provision.ClusterResources; import com.yahoo.config.provision.ClusterSpec; @@ -353,6 +354,23 @@ public class AutoscalingTest { } @Test + public void autoscaling_respects_group_size_limit() { + var min = new ClusterResources( 2, 2, new NodeResources(1, 1, 1, 1)); + var now = new ClusterResources(5, 5, new NodeResources(3.0, 10, 10, 1)); + var max = new ClusterResources(18, 6, new NodeResources(100, 1000, 1000, 1)); + var fixture = AutoscalingTester.fixture() + .awsProdSetup(true) + .initialResources(Optional.of(now)) + .capacity(Capacity.from(min, max, IntRange.of(2, 3), false, true, Optional.empty())) + .build(); + fixture.tester().clock().advance(Duration.ofDays(2)); + fixture.loader().applyCpuLoad(0.4, 240); + fixture.tester().assertResources("Scaling cpu up", + 12, 6, 2.8, 4.3, 10.0, + fixture.autoscale()); + } + + @Test public void test_autoscaling_limits_when_min_equals_max() { ClusterResources min = new ClusterResources( 2, 1, new NodeResources(1, 1, 1, 1)); var fixture = AutoscalingTester.fixture().awsProdSetup(true).capacity(Capacity.from(min, min)).build(); @@ -427,7 +445,7 @@ public class AutoscalingTest { } @Test - public void test_autoscaling_group_size_1() { + public void test_autoscaling_group_size_unconstrained() { var min = new ClusterResources( 2, 2, new NodeResources(1, 1, 1, 1)); var now = new ClusterResources(5, 5, new NodeResources(3, 100, 100, 1)); var max = new ClusterResources(20, 20, new NodeResources(10, 1000, 1000, 1)); @@ -444,6 +462,23 @@ public class AutoscalingTest { } @Test + public void test_autoscaling_group_size_1() { + var min = new ClusterResources( 2, 2, new NodeResources(1, 1, 1, 1)); + var now = new ClusterResources(5, 5, new NodeResources(3, 100, 100, 1)); + var max = new ClusterResources(20, 20, new NodeResources(10, 1000, 1000, 1)); + var fixture = AutoscalingTester.fixture() + .awsProdSetup(true) + .initialResources(Optional.of(now)) + .capacity(Capacity.from(min, max, IntRange.of(1), false, true, Optional.empty())) + .build(); + fixture.tester().clock().advance(Duration.ofDays(2)); + fixture.loader().applyCpuLoad(0.9, 120); + fixture.tester().assertResources("Scaling up to 2 nodes, scaling memory and disk down at the same time", + 7, 7, 9.4, 80.8, 85.2, + fixture.autoscale()); + } + + @Test public void test_autoscaling_groupsize_by_cpu_read_dominated() { var min = new ClusterResources( 3, 1, new NodeResources(1, 1, 1, 1)); var now = new ClusterResources(6, 2, new NodeResources(3, 100, 100, 1)); @@ -672,6 +707,23 @@ public class AutoscalingTest { fixture.autoscale().resources().isEmpty()); } + @Test + public void test_autoscaling_in_dev_with_cluster_size_constraint() { + var min = new ClusterResources(4, 1, + new NodeResources(1, 4, 10, 1, NodeResources.DiskSpeed.any)); + var max = new ClusterResources(20, 20, + new NodeResources(100, 1000, 1000, 1, NodeResources.DiskSpeed.any)); + var fixture = AutoscalingTester.fixture() + .awsSetup(true, Environment.dev) + .capacity(Capacity.from(min, max, IntRange.of(3, 5), false, true, Optional.empty())) + .build(); + fixture.tester().clock().advance(Duration.ofDays(2)); + fixture.loader().applyLoad(new Load(1.0, 1.0, 1.0), 200); + fixture.tester().assertResources("Scale only to a single node and group since this is dev", + 1, 1, 0.1, 24.8, 131.1, + fixture.autoscale()); + } + /** Same setup as test_autoscaling_in_dev(), just with required = true */ @Test public void test_autoscaling_in_dev_with_required_resources_preprovisioned() { @@ -680,8 +732,10 @@ public class AutoscalingTest { new NodeResources(1, 1, 1, 1, NodeResources.DiskSpeed.any)), new ClusterResources(20, 1, new NodeResources(100, 1000, 1000, 1, NodeResources.DiskSpeed.any)), + IntRange.empty(), true, - true); + true, + Optional.empty()); var fixture = AutoscalingTester.fixture() .hostCount(5) @@ -700,8 +754,10 @@ public class AutoscalingTest { var requiredCapacity = Capacity.from(new ClusterResources(1, 1, NodeResources.unspecified()), new ClusterResources(3, 1, NodeResources.unspecified()), + IntRange.empty(), + true, true, - true); + Optional.empty()); var fixture = AutoscalingTester.fixture() .hostCount(5) 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 4e6b8dec9ef..05d0822758d 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 @@ -131,6 +131,7 @@ class AutoscalingTester { cluster.exclusive(), cluster.minResources(), cluster.maxResources(), + cluster.groupSize(), cluster.required(), cluster.suggested(), cluster.target(), diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/AutoscalingMaintainerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/AutoscalingMaintainerTest.java index 2da65fc1a2f..af47001cd84 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/AutoscalingMaintainerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/AutoscalingMaintainerTest.java @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.provision.maintenance; +import com.yahoo.collections.IntRange; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Capacity; import com.yahoo.config.provision.ClusterResources; @@ -61,10 +62,10 @@ public class AutoscalingMaintainerTest { tester.deploy(app1, cluster1, Capacity.from(new ClusterResources(5, 1, new NodeResources(4, 4, 10, 0.1)), new ClusterResources(5, 1, new NodeResources(4, 4, 10, 0.1)), - false, true)); + IntRange.empty(), false, true, Optional.empty())); tester.deploy(app2, cluster2, Capacity.from(new ClusterResources(5, 1, new NodeResources(4, 4, 10, 0.1)), new ClusterResources(10, 1, new NodeResources(6.5, 9, 20, 0.1)), - false, true)); + IntRange.empty(), false, true, Optional.empty())); tester.clock().advance(Duration.ofMinutes(10)); tester.maintainer().maintain(); // noop diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/HostCapacityMaintainerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/HostCapacityMaintainerTest.java index 27a944a471e..58589f540cf 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/HostCapacityMaintainerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/HostCapacityMaintainerTest.java @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.provision.maintenance; +import com.yahoo.collections.IntRange; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Capacity; @@ -466,7 +467,7 @@ public class HostCapacityMaintainerTest { ClusterSpec spec = ProvisioningTester.contentClusterSpec(); ClusterResources resources = new ClusterResources(2, 1, new NodeResources(16, 24, 100, 1)); CloudAccount cloudAccount0 = CloudAccount.from("000000000000"); - Capacity capacity0 = Capacity.from(resources, resources, false, true, Optional.of(cloudAccount0)); + Capacity capacity0 = Capacity.from(resources, resources, IntRange.empty(), false, true, Optional.of(cloudAccount0)); List<HostSpec> prepared = provisioningTester.prepare(applicationId, spec, capacity0); // Hosts are provisioned in requested account @@ -476,7 +477,7 @@ public class HostCapacityMaintainerTest { // Redeployment in different account provisions a new set of hosts CloudAccount cloudAccount1 = CloudAccount.from("100000000000"); - Capacity capacity1 = Capacity.from(resources, resources, false, true, Optional.of(cloudAccount1)); + Capacity capacity1 = Capacity.from(resources, resources, IntRange.empty(), false, true, Optional.of(cloudAccount1)); prepared = provisioningTester.prepare(applicationId, spec, capacity1); provisionHostsIn(cloudAccount1, 2, tester); assertEquals(2, provisioningTester.activate(applicationId, prepared).size()); 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 c94d864ba9d..36c9819e10d 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 @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.provision.maintenance; +import com.yahoo.collections.IntRange; import com.yahoo.collections.Pair; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Capacity; @@ -56,10 +57,10 @@ public class ScalingSuggestionsMaintainerTest { tester.deploy(app1, cluster1, Capacity.from(new ClusterResources(5, 1, new NodeResources(4, 4, 10, 0.1)), new ClusterResources(5, 1, new NodeResources(4, 4, 10, 0.1)), - false, true)); + IntRange.empty(), false, true, Optional.empty())); tester.deploy(app2, cluster2, Capacity.from(new ClusterResources(5, 1, new NodeResources(4, 4, 10, 0.1)), new ClusterResources(10, 1, new NodeResources(6.5, 5, 15, 0.1)), - false, true)); + IntRange.empty(), false, true, Optional.empty())); tester.clock().advance(Duration.ofHours(13)); Duration timeAdded = addMeasurements(0.90f, 0.90f, 0.90f, 0, 500, app1, tester.nodeRepository()); @@ -96,7 +97,8 @@ public class ScalingSuggestionsMaintainerTest { addMeasurements(0.7f, 0.7f, 0.7f, 0, 500, app1, tester.nodeRepository()); maintainer.maintain(); var suggested = tester.nodeRepository().applications().get(app1).get().cluster(cluster1.id()).get().suggested().resources().get(); - tester.deploy(app1, cluster1, Capacity.from(suggested, suggested, false, true)); + tester.deploy(app1, cluster1, Capacity.from(suggested, suggested, + IntRange.empty(), false, true, Optional.empty())); tester.clock().advance(Duration.ofDays(2)); addMeasurements(0.2f, 0.65f, 0.6f, 0, 500, app1, tester.nodeRepository()); 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 a05cc388bea..51f775c7286 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 @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.provision.persistence; +import com.yahoo.collections.IntRange; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ClusterResources; import com.yahoo.config.provision.ClusterSpec; @@ -34,6 +35,7 @@ public class ApplicationSerializerTest { false, new ClusterResources( 8, 4, new NodeResources(1, 2, 3, 4)), new ClusterResources(12, 6, new NodeResources(3, 6, 21, 24)), + IntRange.empty(), true, Autoscaling.empty(), Autoscaling.empty(), @@ -43,6 +45,7 @@ public class ApplicationSerializerTest { true, new ClusterResources( 8, 4, minResources), new ClusterResources(14, 7, new NodeResources(3, 6, 21, 24)), + IntRange.of(3, 5), false, new Autoscaling(Autoscaling.Status.unavailable, "", @@ -79,11 +82,11 @@ public class ApplicationSerializerTest { Cluster serializedCluster = serialized.clusters().get(originalCluster.id()); assertNotNull(serializedCluster); assertNotSame(originalCluster, serializedCluster); - assertEquals(originalCluster, serializedCluster); assertEquals(originalCluster.id(), serializedCluster.id()); assertEquals(originalCluster.exclusive(), serializedCluster.exclusive()); assertEquals(originalCluster.minResources(), serializedCluster.minResources()); assertEquals(originalCluster.maxResources(), serializedCluster.maxResources()); + assertEquals(originalCluster.groupSize(), serializedCluster.groupSize()); assertEquals(originalCluster.required(), serializedCluster.required()); assertEquals(originalCluster.suggested(), serializedCluster.suggested()); assertEquals(originalCluster.target(), serializedCluster.target()); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java index 3653e20d848..9a18bbb76ee 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.provision.provisioning; import ai.vespa.http.DomainName; import com.google.common.collect.Iterators; +import com.yahoo.collections.IntRange; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Capacity; import com.yahoo.config.provision.CloudAccount; @@ -316,7 +317,7 @@ public class LoadBalancerProvisionerTest { @Test public void load_balancer_with_custom_settings() { ClusterResources resources = new ClusterResources(3, 1, nodeResources); - Capacity capacity = Capacity.from(resources, resources, false, true, Optional.of(CloudAccount.empty)); + Capacity capacity = Capacity.from(resources, resources, IntRange.empty(), false, true, Optional.of(CloudAccount.empty)); tester.activate(app1, prepare(app1, capacity, clusterRequest(ClusterSpec.Type.container, ClusterSpec.Id.from("c1")))); LoadBalancerList loadBalancers = tester.nodeRepository().loadBalancers().list(); assertEquals(1, loadBalancers.size()); @@ -336,7 +337,7 @@ public class LoadBalancerProvisionerTest { ClusterResources resources = new ClusterResources(3, 1, nodeResources); CloudAccount cloudAccount0 = CloudAccount.empty; { - Capacity capacity = Capacity.from(resources, resources, false, true, Optional.of(cloudAccount0)); + Capacity capacity = Capacity.from(resources, resources, IntRange.empty(), false, true, Optional.of(cloudAccount0)); tester.activate(app1, prepare(app1, capacity, clusterRequest(ClusterSpec.Type.container, ClusterSpec.Id.from("c1")))); } LoadBalancerList loadBalancers = tester.nodeRepository().loadBalancers().list(); @@ -345,7 +346,7 @@ public class LoadBalancerProvisionerTest { // Changing account fails if there is an existing LB in the previous account. CloudAccount cloudAccount1 = CloudAccount.from("111111111111"); - Capacity capacity = Capacity.from(resources, resources, false, true, Optional.of(cloudAccount1)); + Capacity capacity = Capacity.from(resources, resources, IntRange.empty(), false, true, Optional.of(cloudAccount1)); try { prepare(app1, capacity, clusterRequest(ClusterSpec.Type.container, ClusterSpec.Id.from("c1"))); fail("Expected exception"); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/application1.json b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/application1.json index ead4e5fd06a..c9046998e91 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/application1.json +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/application1.json @@ -103,7 +103,7 @@ { "from": { "nodes": 0, - "groups": 0, + "groups": 1, "resources": { "vcpu" : 0.0, "memoryGb": 0.0, diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/application2.json b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/application2.json index f60fdf3e602..4166d20ad8d 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/application2.json +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/application2.json @@ -62,7 +62,7 @@ { "from": { "nodes": 0, - "groups": 0, + "groups": 1, "resources": { "vcpu" : 0.0, "memoryGb": 0.0, diff --git a/vespajlib/src/main/java/com/yahoo/collections/IntRange.java b/vespajlib/src/main/java/com/yahoo/collections/IntRange.java new file mode 100644 index 00000000000..3c3815589ab --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/collections/IntRange.java @@ -0,0 +1,120 @@ +package com.yahoo.collections; + +import java.util.Objects; +import java.util.OptionalInt; + +/** + * An integer range. + * + * @author bratseth + */ +public class IntRange { + + private static final IntRange empty = new IntRange(OptionalInt.empty(), OptionalInt.empty()); + + private final OptionalInt from, to; + + public IntRange(OptionalInt from, OptionalInt to) { + if (from.isPresent() && to.isPresent() && from.getAsInt() > to.getAsInt()) + throw new IllegalArgumentException("from " + from.getAsInt() + " is greater than to " + to.getAsInt()); + this.from = from; + this.to = to; + } + + /** Returns the minimum value which is in this range, or empty if it is open downwards. */ + public OptionalInt from() { return from; } + + /** Returns the maximum value which is in this range, or empty if it is open upwards. */ + public OptionalInt to() { return to; } + + public boolean isEmpty() { + return from.isEmpty() && to.isEmpty(); + } + + /** Returns whether the given value is in this range. */ + public boolean includes(int value) { + if (from.isPresent() && value < from.getAsInt()) return false; + if (to.isPresent() && value > to.getAsInt()) return false; + return true; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if ( ! (o instanceof IntRange other)) return false; + if ( ! this.from.equals(other.from)) return false; + if ( ! this.to.equals(other.to)) return false; + return true; + } + + @Override + public int hashCode() { + return Objects.hash(from, to); + } + + @Override + public String toString() { + if (isEmpty()) return "[]"; + if (from.equals(to)) return String.valueOf(from.getAsInt()); + return "[" + (from.isPresent() ? from.getAsInt() : "") + ", " + (to.isPresent() ? to.getAsInt() : "") + "]"; + } + + public static IntRange empty() { return empty; } + + public static IntRange from(int from) { + return new IntRange(OptionalInt.of(from), OptionalInt.empty()); + } + + public static IntRange to(int to) { + return new IntRange(OptionalInt.empty(), OptionalInt.of(to)); + } + + public static IntRange of(int fromTo) { + return new IntRange(OptionalInt.of(fromTo), OptionalInt.of(fromTo)); + } + + public static IntRange of(int from, int to) { + return new IntRange(OptionalInt.of(from), OptionalInt.of(to)); + } + + /** Returns this with a from limit which is at most the given value */ + public IntRange fromAtMost(int minLimit) { + if (from.isEmpty()) return this; + if (from.getAsInt() <= minLimit) return this; + return new IntRange(OptionalInt.of(minLimit), to); + } + + /** Returns this with a to limit which is at least the given value */ + public IntRange toAtLeast(int maxLimit) { + if (to.isEmpty()) return this; + if (to.getAsInt() >= maxLimit) return this; + return new IntRange(from, OptionalInt.of(maxLimit)); + } + + /** Parses a value ("value"), value range ("[min-value?, max-value?]"), or empty. */ + public static IntRange from(String s) { + try { + s = s.trim(); + if (s.startsWith("[") && s.endsWith("]")) { + String innards = s.substring(1, s.length() - 1).trim(); + if (innards.isEmpty()) return empty(); + String[] numbers = (" " + innards + " ").split(","); // pad to make sure we get 2 elements + if (numbers.length != 2) throw new IllegalArgumentException("Expected two numbers"); + return new IntRange(parseOptionalInt(numbers[0]), parseOptionalInt(numbers[1])); + } else { + var fromTo = parseOptionalInt(s); + return new IntRange(fromTo, fromTo); + } + } + catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Expected a number or range on the form [min, max], but got '" + s + "'", e); + } + } + + private static OptionalInt parseOptionalInt(String s) { + s = s.trim(); + if (s.isEmpty()) return OptionalInt.empty(); + return OptionalInt.of(Integer.parseInt(s)); + } + +} diff --git a/vespajlib/src/test/java/com/yahoo/collections/IntRangeTestCase.java b/vespajlib/src/test/java/com/yahoo/collections/IntRangeTestCase.java new file mode 100644 index 00000000000..dc3c39ea19b --- /dev/null +++ b/vespajlib/src/test/java/com/yahoo/collections/IntRangeTestCase.java @@ -0,0 +1,38 @@ +package com.yahoo.collections; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author bratseth + */ +public class IntRangeTestCase { + + @Test + public void testStringAndEquals() { + assertEquals(IntRange.empty(), IntRange.from(IntRange.from("[]").toString())); + assertEquals(IntRange.from(1), IntRange.from(IntRange.from("[1,]").toString())); + assertEquals(IntRange.to(3), IntRange.from(IntRange.from("[,3]").toString())); + assertEquals(IntRange.of(1, 3), IntRange.from(IntRange.from("[1,3]").toString())); + assertEquals(IntRange.of(1, 3), IntRange.from(IntRange.from("[1, 3]").toString())); + } + + @Test + public void testInclusion() { + assertFalse(IntRange.of(3, 5).includes(2)); + assertTrue(IntRange.of(3, 5).includes(3)); + assertTrue(IntRange.of(3, 5).includes(4)); + assertTrue(IntRange.of(3, 5).includes(5)); + assertFalse(IntRange.of(3, 5).includes(6)); + + assertTrue(IntRange.from(3).includes(1000)); + assertFalse(IntRange.from(3).includes(2)); + + assertTrue(IntRange.to(5).includes(-1000)); + assertFalse(IntRange.to(3).includes(4)); + } + +} |