summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@gmail.com>2023-01-22 15:13:07 +0100
committerGitHub <noreply@github.com>2023-01-22 15:13:07 +0100
commit45577c7d7d2c70637d63f89702c3229df312edc3 (patch)
tree77d7095ffeb6a631ee701546a8f0b18735bf47bd
parent85bb4e8d16ec06bba6c3b45ea7cb1f084f5e988d (diff)
parent1d5806f068309e18b70fee03b7a22e111c180607 (diff)
Merge pull request #25667 from vespa-engine/bratseth/group-size
Support a group size constraint in content clusters
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/provision/InMemoryProvisioner.java18
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/NodesSpecification.java55
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java27
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/IndexedHierarchicDistributionValidator.java40
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/StorageGroup.java49
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/cluster/RedundancyBuilder.java9
-rw-r--r--config-model/src/main/resources/schema/content.rnc3
-rw-r--r--config-model/src/test/java/com/yahoo/config/model/provision/ModelProvisioningTest.java133
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/CloudAccountChangeValidatorTest.java2
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/IndexedHierarchicDistributionTest.java9
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/StorageClusterTest.java7
-rw-r--r--config-model/src/test/schema-test-files/services-hosted.xml2
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/Capacity.java36
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/ClusterResources.java5
-rw-r--r--config-provisioning/src/test/java/com/yahoo/config/provision/CapacityTest.java12
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-clusters.json2
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/applications/Cluster.java18
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/AllocationOptimizer.java5
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/autoscale/Limits.java27
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/ApplicationSerializer.java19
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/CapacityPolicies.java7
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java26
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/ApplicationSerializer.java8
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java7
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/AutoscalingTest.java62
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/AutoscalingTester.java1
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/AutoscalingMaintainerTest.java5
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/HostCapacityMaintainerTest.java5
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/ScalingSuggestionsMaintainerTest.java8
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/ApplicationSerializerTest.java5
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java7
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/application1.json2
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/application2.json2
-rw-r--r--vespajlib/src/main/java/com/yahoo/collections/IntRange.java132
-rw-r--r--vespajlib/src/test/java/com/yahoo/collections/IntRangeTestCase.java38
35 files changed, 633 insertions, 160 deletions
diff --git a/config-model/src/main/java/com/yahoo/config/model/provision/InMemoryProvisioner.java b/config-model/src/main/java/com/yahoo/config/model/provision/InMemoryProvisioner.java
index f44849e185f..105a9669d1f 100644
--- a/config-model/src/main/java/com/yahoo/config/model/provision/InMemoryProvisioner.java
+++ b/config-model/src/main/java/com/yahoo/config/model/provision/InMemoryProvisioner.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.config.model.provision;
+import com.yahoo.collections.IntRange;
import com.yahoo.collections.ListMap;
import com.yahoo.collections.Pair;
import com.yahoo.config.model.api.HostProvisioner;
@@ -153,13 +154,18 @@ public class InMemoryProvisioner implements HostProvisioner {
requested = requested.withLimits(requested.minResources().withNodes(1),
requested.maxResources().withNodes(1));
}
- if (useMaxResources)
- return prepare(cluster, requested.maxResources(), requested.isRequired(), requested.canFail());
- else
- return prepare(cluster, requested.minResources(), requested.isRequired(), requested.canFail());
+ IntRange groupRange = IntRange.of(requested.minResources().groups(), requested.maxResources().groups());
+ if (useMaxResources) {
+ int groups = groupRange.fit(requested.maxResources().nodes() / requested.groupSize().to().orElse(1));
+ return prepare(cluster, requested.maxResources(),groups, requested.isRequired(), requested.canFail());
+ }
+ else {
+ int groups = groupRange.fit(requested.minResources().nodes() / requested.groupSize().from().orElse(1));
+ return prepare(cluster, requested.minResources(), groups, requested.isRequired(), requested.canFail());
+ }
}
- public List<HostSpec> prepare(ClusterSpec cluster, ClusterResources requested, boolean required, boolean canFail) {
+ public List<HostSpec> prepare(ClusterSpec cluster, ClusterResources requested, int groups, boolean required, boolean canFail) {
if (cluster.group().isPresent() && requested.groups() > 1)
throw new IllegalArgumentException("Cannot both be specifying a group and ask for groups to be created");
@@ -169,7 +175,7 @@ public class InMemoryProvisioner implements HostProvisioner {
if (alwaysReturnOneNode)
nodes = 1;
- int groups = Math.min(requested.groups(), nodes);
+ groups = Math.min(groups, nodes);
List<HostSpec> allocation = new ArrayList<>();
if (groups == 1) {
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..352e08af4a3 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;
@@ -99,10 +104,11 @@ public class NodesSpecification {
Optional<CloudAccount> cloudAccount) {
var resolvedElement = resolveElement(nodesElement);
var combinedId = findCombinedId(nodesElement, resolvedElement);
- var resources = toResources(resolvedElement);
+ var resourceConstraints = toResourceConstraints(resolvedElement);
boolean hasCountAttribute = resolvedElement.stringAttribute("count") != null;
- return new NodesSpecification(resources.getFirst(),
- resources.getSecond(),
+ return new NodesSpecification(resourceConstraints.min,
+ resourceConstraints.max,
+ resourceConstraints.groupSize,
dedicated,
version,
resolvedElement.booleanAttribute("required", false),
@@ -114,14 +120,27 @@ public class NodesSpecification {
hasCountAttribute);
}
- private static Pair<ClusterResources, ClusterResources> toResources(ModelElement nodesElement) {
- Pair<Integer, Integer> nodes = toRange(nodesElement.stringAttribute("count"), 1, Integer::parseInt);
- Pair<Integer, Integer> groups = toRange(nodesElement.stringAttribute("groups"), 1, Integer::parseInt);
- var min = new ClusterResources(nodes.getFirst(), groups.getFirst(), nodeResources(nodesElement).getFirst());
- var max = new ClusterResources(nodes.getSecond(), groups.getSecond(), nodeResources(nodesElement).getSecond());
- return new Pair<>(min, max);
+ private static ResourceConstraints toResourceConstraints(ModelElement nodesElement) {
+ var nodes = rangeFrom(nodesElement, "count");
+ var groups = rangeFrom(nodesElement, "groups");
+ var groupSize = rangeFrom(nodesElement, "group-size");
+ int defaultMaxGroups = groupSize.isEmpty() ? 1 : nodes.to().orElse(1); // Don't constrain the number of groups if group size is set
+ var min = new ClusterResources(nodes.from().orElse(1), groups.from().orElse(1), nodeResources(nodesElement).getFirst());
+ var max = new ClusterResources(nodes.to().orElse(1), groups.to().orElse(defaultMaxGroups), nodeResources(nodesElement).getSecond());
+ return new ResourceConstraints(min, max, groupSize);
}
+ private static IntRange rangeFrom(ModelElement element, String name) {
+ try {
+ return IntRange.from(element.stringAttribute(name, ""));
+ }
+ catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Illegal " + name + " value", e);
+ }
+ }
+
+ private record ResourceConstraints(ClusterResources min, ClusterResources max, IntRange groupSize) {}
+
/** Returns the ID of the cluster referencing this node specification, if any */
private static Optional<String> findCombinedId(ModelElement nodesElement, ModelElement resolvedElement) {
if (resolvedElement != nodesElement) {
@@ -143,18 +162,6 @@ public class NodesSpecification {
}
/**
- * Returns a requirement for dedicated nodes taken from the <code>nodes</code> element
- * contained in the given parent element, or empty if the parent element is null, or the nodes elements
- * is not present.
- */
- public static Optional<NodesSpecification> fromParent(ModelElement parentElement, ConfigModelContext context) {
- if (parentElement == null) return Optional.empty();
- ModelElement nodesElement = parentElement.child("nodes");
- if (nodesElement == null) return Optional.empty();
- return Optional.of(from(nodesElement, context));
- }
-
- /**
* Returns a requirement for non-dedicated or dedicated nodes taken from the <code>nodes</code> element
* contained in the given parent element, or empty if the parent element is null, or the nodes elements
* is not present.
@@ -178,6 +185,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 +201,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 +228,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 +242,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 +286,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..62a3d004ac0 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());
@@ -945,16 +947,21 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> {
}
private List<ApplicationContainer> createNodesFromNodeCount(ApplicationContainerCluster cluster, Element containerElement, Element nodesElement, ConfigModelContext context) {
- NodesSpecification nodesSpecification = NodesSpecification.from(new ModelElement(nodesElement), context);
- ClusterSpec.Id clusterId = ClusterSpec.Id.from(cluster.name());
- ZoneEndpoint zoneEndpoint = zoneEndpoint(context, clusterId);
- Map<HostResource, ClusterMembership> hosts = nodesSpecification.provision(cluster.getRoot().hostSystem(),
- ClusterSpec.Type.container,
- clusterId,
- zoneEndpoint,
- log,
- getZooKeeper(containerElement) != null);
- return createNodesFromHosts(hosts, cluster, context.getDeployState());
+ try {
+ NodesSpecification nodesSpecification = NodesSpecification.from(new ModelElement(nodesElement), context);
+ ClusterSpec.Id clusterId = ClusterSpec.Id.from(cluster.name());
+ ZoneEndpoint zoneEndpoint = zoneEndpoint(context, clusterId);
+ Map<HostResource, ClusterMembership> hosts = nodesSpecification.provision(cluster.getRoot().hostSystem(),
+ ClusterSpec.Type.container,
+ clusterId,
+ zoneEndpoint,
+ log,
+ getZooKeeper(containerElement) != null);
+ return createNodesFromHosts(hosts, cluster, context.getDeployState());
+ }
+ catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("In " + cluster, e);
+ }
}
private List<ApplicationContainer> createNodesFromNodeType(ApplicationContainerCluster cluster, Element nodesElement, ConfigModelContext context) {
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/IndexedHierarchicDistributionValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/content/IndexedHierarchicDistributionValidator.java
index dbe813bfb2d..b4a83efd2aa 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/content/IndexedHierarchicDistributionValidator.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/IndexedHierarchicDistributionValidator.java
@@ -30,17 +30,18 @@ public class IndexedHierarchicDistributionValidator {
public void validate() {
validateThatWeHaveOneGroupLevel();
validateThatLeafGroupsHasEqualNumberOfNodes();
- validateThatLeafGroupsCountIsAFactorOfRedundancy(clusterName, redundancy.effectiveFinalRedundancy(), rootGroup.getSubgroups().size());
+ validateThatLeafGroupsCountIsAFactorOfRedundancy(redundancy.effectiveFinalRedundancy(), rootGroup.getSubgroups().size());
validateThatRedundancyPerGroupIsEqual();
- validateThatReadyCopiesIsCompatibleWithRedundancy(clusterName, redundancy.effectiveFinalRedundancy(), redundancy.effectiveReadyCopies(), rootGroup.getSubgroups().size());
+ validateThatReadyCopiesIsCompatibleWithRedundancy(redundancy.effectiveFinalRedundancy(), redundancy.effectiveReadyCopies(), rootGroup.getSubgroups().size());
}
private void validateThatWeHaveOneGroupLevel() {
for (StorageGroup group : rootGroup.getSubgroups()) {
if (group.getSubgroups().size() > 0) {
- throw new IllegalArgumentException(getErrorMsgPrefix(clusterName) + "Expected all groups under root group '" +
- rootGroup.getName() + "' to be leaf groups only containing nodes, but sub group '" + group.getName() + "' contains " +
- group.getSubgroups().size() + " sub groups.");
+ throw new IllegalArgumentException("Expected all groups under root group '" +
+ rootGroup.getName() + "' to be leaf groups only containing nodes, but sub group '" +
+ group.getName() + "' contains " +
+ group.getSubgroups().size() + " sub groups");
}
}
}
@@ -56,18 +57,19 @@ public class IndexedHierarchicDistributionValidator {
}
if (group.getNodes().size() != previousGroup.getNodes().size())
- throw new IllegalArgumentException(getErrorMsgPrefix(clusterName) + "Expected leaf groups to contain an equal number of nodes, but leaf group '" +
- previousGroup.getName() + "' contains " + previousGroup.getNodes().size() + " node(s) while leaf group '" +
- group.getName() + "' contains " + group.getNodes().size() + " node(s).");
+ throw new IllegalArgumentException("Expected leaf groups to contain an equal number of nodes, but leaf group '" +
+ previousGroup.getName() + "' contains " + previousGroup.getNodes().size() +
+ " node(s) while leaf group '" + group.getName() +
+ "' contains " + group.getNodes().size() + " node(s)");
previousGroup = group;
}
}
- static public void validateThatLeafGroupsCountIsAFactorOfRedundancy(String clusterName, int totalRedundancy, int subGroups) {
+ static public void validateThatLeafGroupsCountIsAFactorOfRedundancy(int totalRedundancy, int subGroups) {
if (totalRedundancy % subGroups != 0) {
- throw new IllegalArgumentException(getErrorMsgPrefix(clusterName) + "Expected number of leaf groups (" +
+ throw new IllegalArgumentException("Expected number of leaf groups (" +
subGroups + ") to be a factor of redundancy (" +
- totalRedundancy + "), but it is not.");
+ totalRedundancy + "), but it is not");
}
}
@@ -75,9 +77,10 @@ public class IndexedHierarchicDistributionValidator {
int redundancyPerGroup = redundancy.effectiveFinalRedundancy() / rootGroup.getSubgroups().size();
String expPartitions = createDistributionPartitions(redundancyPerGroup, rootGroup.getSubgroups().size());
if (!rootGroup.getPartitions().get().equals(expPartitions)) {
- throw new IllegalArgumentException(getErrorMsgPrefix(clusterName) + "Expected redundancy per leaf group to be " +
+ throw new IllegalArgumentException("Expected redundancy per leaf group to be " +
redundancyPerGroup + ", but it is not according to distribution partitions '" +
- rootGroup.getPartitions().get() + "'. Expected distribution partitions should be '" + expPartitions + "'.");
+ rootGroup.getPartitions().get() +
+ "'. Expected distribution partitions should be '" + expPartitions + "'");
}
}
@@ -91,20 +94,17 @@ public class IndexedHierarchicDistributionValidator {
return sb.toString();
}
- static public void validateThatReadyCopiesIsCompatibleWithRedundancy(String clusterName, int totalRedundancy, int totalReadyCopies, int groupCount) {
+ static public void validateThatReadyCopiesIsCompatibleWithRedundancy(int totalRedundancy, int totalReadyCopies, int groupCount) {
if (totalRedundancy % groupCount != 0) {
- throw new IllegalArgumentException(getErrorMsgPrefix(clusterName) + "Expected equal redundancy per group.");
+ throw new IllegalArgumentException("Expected equal redundancy per group");
}
if (totalReadyCopies % groupCount != 0) {
- throw new IllegalArgumentException(getErrorMsgPrefix(clusterName) + "Expected equal amount of ready copies per group, but " +
+ throw new IllegalArgumentException("Expected equal amount of ready copies per group, but " +
totalReadyCopies + " ready copies is specified with " + groupCount + " groups");
}
if (totalReadyCopies == 0) {
- throw new IllegalArgumentException(getErrorMsgPrefix(clusterName) + "Warning. No ready copies configured. At least one is required.");
+ throw new IllegalArgumentException("No ready copies configured. At least 1 is required.");
}
}
- static private String getErrorMsgPrefix(String clusterName) {
- return "In indexed content cluster '" + clusterName + "' using hierarchic distribution: ";
- }
}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/StorageGroup.java b/config-model/src/main/java/com/yahoo/vespa/model/content/StorageGroup.java
index aef00be5ea9..36090aa7349 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/content/StorageGroup.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/StorageGroup.java
@@ -204,30 +204,35 @@ public class StorageGroup {
}
public StorageGroup buildRootGroup(DeployState deployState, RedundancyBuilder redundancyBuilder, ContentCluster owner) {
- if (owner.isHosted())
- validateRedundancyAndGroups(deployState.zone().environment());
+ try {
+ if (owner.isHosted())
+ validateRedundancyAndGroups(deployState.zone().environment());
- Optional<ModelElement> group = Optional.ofNullable(clusterElement.child("group"));
- Optional<ModelElement> nodes = getNodes(clusterElement);
+ Optional<ModelElement> group = Optional.ofNullable(clusterElement.child("group"));
+ Optional<ModelElement> nodes = getNodes(clusterElement);
- if (group.isPresent() && nodes.isPresent())
- throw new IllegalArgumentException("Both <group> and <nodes> is specified: Only one of these tags can be used in the same configuration");
- if (group.isPresent() && (group.get().integerAttribute("distribution-key") != null)) {
- deployState.getDeployLogger().logApplicationPackage(Level.INFO, "'distribution-key' attribute on a content cluster's root group is ignored");
- }
+ if (group.isPresent() && nodes.isPresent())
+ throw new IllegalArgumentException("Both <group> and <nodes> is specified: Only one of these tags can be used in the same configuration");
+ if (group.isPresent() && (group.get().integerAttribute("distribution-key") != null)) {
+ deployState.getDeployLogger().logApplicationPackage(Level.INFO, "'distribution-key' attribute on a content cluster's root group is ignored");
+ }
- GroupBuilder groupBuilder = collectGroup(owner.isHosted(), group, nodes, null, null);
- StorageGroup storageGroup = owner.isHosted()
- ? groupBuilder.buildHosted(deployState, owner, Optional.empty())
- : groupBuilder.buildNonHosted(deployState, owner, Optional.empty());
+ GroupBuilder groupBuilder = collectGroup(owner.isHosted(), group, nodes, null, null);
+ StorageGroup storageGroup = owner.isHosted()
+ ? groupBuilder.buildHosted(deployState, owner, Optional.empty())
+ : groupBuilder.buildNonHosted(deployState, owner, Optional.empty());
- Redundancy redundancy = redundancyBuilder.build(owner.getName(), owner.isHosted(), storageGroup.subgroups.size(),
- storageGroup.getNumberOfLeafGroups(), storageGroup.countNodes(false));
- owner.setRedundancy(redundancy);
- if (storageGroup.partitions.isEmpty() && (redundancy.groups() > 1)) {
- storageGroup.partitions = Optional.of(computePartitions(redundancy.finalRedundancy(), redundancy.groups()));
+ Redundancy redundancy = redundancyBuilder.build(owner.isHosted(), storageGroup.subgroups.size(),
+ storageGroup.getNumberOfLeafGroups(), storageGroup.countNodes(false));
+ owner.setRedundancy(redundancy);
+ if (storageGroup.partitions.isEmpty() && (redundancy.groups() > 1)) {
+ storageGroup.partitions = Optional.of(computePartitions(redundancy.finalRedundancy(), redundancy.groups()));
+ }
+ return storageGroup;
+ }
+ catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("In " + owner, e);
}
- return storageGroup;
}
private void validateRedundancyAndGroups(Environment environment) {
@@ -242,11 +247,11 @@ public class StorageGroup {
// Allow dev deployment of self-hosted app (w/o count attribute): absent count => 1 node
if (!nodesSpec.hasCountAttribute() && environment == Environment.dev) return;
- int minNodesPerGroup = (int)Math.ceil((double)nodesSpec.minResources().nodes() / nodesSpec.minResources().groups());
+ int minNodesPerGroup = (int) Math.ceil((double) nodesSpec.minResources().nodes() / nodesSpec.minResources().groups());
if (minNodesPerGroup < redundancy) {
- throw new IllegalArgumentException("Cluster '" + clusterElement.stringAttribute("id") + "' " +
- "specifies redundancy " + redundancy + ", but it cannot be higher than " +
+ throw new IllegalArgumentException("This cluster specifies redundancy " + redundancy +
+ ", but this cannot be higher than " +
"the minimum nodes per group, which is " + minNodesPerGroup);
}
}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/RedundancyBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/RedundancyBuilder.java
index 1948cc1bd71..853a90cd8fa 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/RedundancyBuilder.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/RedundancyBuilder.java
@@ -37,14 +37,15 @@ public class RedundancyBuilder {
}
}
}
- public Redundancy build(String clusterName, boolean isHosted, int subGroups, int leafGroups, int totalNodes) {
+ public Redundancy build(boolean isHosted, int subGroups, int leafGroups, int totalNodes) {
if (isHosted) {
return new Redundancy(initialRedundancy, finalRedundancy, readyCopies, leafGroups, totalNodes);
} else {
subGroups = Math.max(1, subGroups);
- IndexedHierarchicDistributionValidator.validateThatLeafGroupsCountIsAFactorOfRedundancy(clusterName, finalRedundancy, subGroups);
- IndexedHierarchicDistributionValidator.validateThatReadyCopiesIsCompatibleWithRedundancy(clusterName, finalRedundancy, readyCopies, subGroups);
- return new Redundancy(initialRedundancy/subGroups, finalRedundancy/subGroups, readyCopies/subGroups, subGroups, totalNodes);
+ IndexedHierarchicDistributionValidator.validateThatLeafGroupsCountIsAFactorOfRedundancy(finalRedundancy, subGroups);
+ IndexedHierarchicDistributionValidator.validateThatReadyCopiesIsCompatibleWithRedundancy(finalRedundancy, readyCopies, subGroups);
+ return new Redundancy(initialRedundancy/subGroups, finalRedundancy/subGroups,
+ readyCopies/subGroups, subGroups, totalNodes);
}
}
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..6f8547c3701 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>";
@@ -595,6 +595,126 @@ public class ModelProvisioningTest {
}
@Test
+ public void testUsingGroups() {
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services>" +
+ " <admin version='4.0'/>" +
+ " <container version='1.0' id='foo'>" +
+ " <nodes count='10'/>" +
+ " </container>" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy>2</redundancy>" +
+ " <documents>" +
+ " <document type='type1' mode='index'/>" +
+ " </documents>" +
+ " <nodes count='30' groups='2'/>" +
+ " </content>" +
+ " <content version='1.0' id='baz'>" +
+ " <redundancy>1</redundancy>" +
+ " <documents>" +
+ " <document type='type1' mode='index'/>" +
+ " </documents>" +
+ " <nodes count='30' groups='30'/>" +
+ " </content>" +
+ "</services>";
+
+ int numberOfHosts = 73;
+ VespaModelTester tester = new VespaModelTester();
+ tester.addHosts(numberOfHosts);
+ VespaModel model = tester.createModel(services, true);
+ assertEquals(numberOfHosts, model.getRoot().hostSystem().getHosts().size());
+
+ ContentCluster cluster = model.getContentClusters().get("bar");
+ List<StorageGroup> subGroups = cluster.getRootGroup().getSubgroups();
+ assertEquals( 0, cluster.getRootGroup().getNodes().size());
+ assertEquals( 2, subGroups.size());
+ assertEquals(15, subGroups.get(0).getNodes().size());
+
+ cluster = model.getContentClusters().get("baz");
+ subGroups = cluster.getRootGroup().getSubgroups();
+ assertEquals( 0, cluster.getRootGroup().getNodes().size());
+ assertEquals(30, subGroups.size());
+ assertEquals( 1, subGroups.get(0).getNodes().size());
+ }
+
+ // Same as the test above but setting groupSize only
+ @Test
+ public void testUsingGroupSizeNotGroups() {
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services>" +
+ " <admin version='4.0'/>" +
+ " <container version='1.0' id='foo'>" +
+ " <nodes count='10'/>" +
+ " </container>" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy>2</redundancy>" +
+ " <documents>" +
+ " <document type='type1' mode='index'/>" +
+ " </documents>" +
+ " <nodes count='30' group-size='[15, 30]'/>" +
+ " </content>" +
+ " <content version='1.0' id='baz'>" +
+ " <redundancy>1</redundancy>" +
+ " <documents>" +
+ " <document type='type1' mode='index'/>" +
+ " </documents>" +
+ " <nodes count='30' group-size='1'/>" +
+ " </content>" +
+ "</services>";
+
+ int numberOfHosts = 73;
+ VespaModelTester tester = new VespaModelTester();
+ tester.addHosts(numberOfHosts);
+ VespaModel model = tester.createModel(services, true);
+ assertEquals(numberOfHosts, model.getRoot().hostSystem().getHosts().size());
+
+ ContentCluster cluster = model.getContentClusters().get("bar");
+ List<StorageGroup> subGroups = cluster.getRootGroup().getSubgroups();
+ assertEquals( 0, cluster.getRootGroup().getNodes().size());
+ assertEquals( 2, subGroups.size());
+ assertEquals(15, subGroups.get(0).getNodes().size());
+
+ cluster = model.getContentClusters().get("baz");
+ subGroups = cluster.getRootGroup().getSubgroups();
+ assertEquals( 0, cluster.getRootGroup().getNodes().size());
+ assertEquals(30, subGroups.size());
+ assertEquals( 1, subGroups.get(0).getNodes().size());
+ }
+
+ @Test
+ public void testIllegalGroupSize() {
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services>" +
+ " <admin version='4.0'/>" +
+ " <container version='1.0' id='foo'>" +
+ " <nodes count='2'/>" +
+ " </container>" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy>2</redundancy>" +
+ " <documents>" +
+ " <document type='type1' mode='index'/>" +
+ " </documents>" +
+ " <nodes count='5' group-size='[2, --]'/>" +
+ " </content>" +
+ "</services>";
+
+ int numberOfHosts = 10;
+ VespaModelTester tester = new VespaModelTester();
+ tester.addHosts(numberOfHosts);
+ try {
+ tester.createModel(services, true);
+ fail("Expected exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("In content cluster 'bar': Illegal group-size value: " +
+ "Expected a number or range on the form [min, max], but got '[2, --]': '--' is not an integer", Exceptions.toMessageString(e));
+ }
+ }
+
+ @Test
public void testSlobroksOnContainersIfNoContentClusters() {
String services =
"<?xml version='1.0' encoding='utf-8' ?>\n" +
@@ -1104,7 +1224,8 @@ public class ModelProvisioningTest {
fail("Expected exception");
}
catch (IllegalArgumentException e) {
- assertEquals("Cluster 'bar' specifies redundancy 2, but it cannot be higher than the minimum nodes per group, which is 1", Exceptions.toMessageString(e));
+ assertEquals("In content cluster 'bar': This cluster specifies redundancy 2, " +
+ "but this cannot be higher than the minimum nodes per group, which is 1", Exceptions.toMessageString(e));
}
}
@@ -1449,7 +1570,7 @@ public class ModelProvisioningTest {
tester.addHosts(new NodeResources(85, 200, 1000_000_000, 0.3), 20);
tester.addHosts(new NodeResources( 0.5, 2, 10, 0.3), 3);
VespaModel model = tester.createModel(services, true);
- assertEquals(totalHosts + 3, model.getRoot().hostSystem().getHosts().size());
+ assertEquals(4 + 6 + 1, model.getRoot().hostSystem().getHosts().size());
}
@Test
@@ -1700,7 +1821,9 @@ public class ModelProvisioningTest {
VespaModel model = tester.createModel(new Zone(Environment.staging, RegionName.from("us-central-1")), services, true);
fail("expected failure");
} catch (IllegalArgumentException e) {
- assertTrue(e.getMessage().startsWith("Clusters in hosted environments must have a <nodes count='N'> tag"));
+ assertEquals("In content cluster 'bar': Clusters in hosted environments must have a <nodes count='N'> tag\n" +
+ "matching all zones, and having no <node> subtags,\nsee https://cloud.vespa.ai/en/reference/services",
+ Exceptions.toMessageString(e));
}
}
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/java/com/yahoo/vespa/model/content/IndexedHierarchicDistributionTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/IndexedHierarchicDistributionTest.java
index 07c032a52a5..7fba5ba12e9 100644
--- a/config-model/src/test/java/com/yahoo/vespa/model/content/IndexedHierarchicDistributionTest.java
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/IndexedHierarchicDistributionTest.java
@@ -6,6 +6,7 @@ import com.yahoo.vespa.model.content.cluster.ContentCluster;
import com.yahoo.vespa.model.search.DispatchGroup;
import com.yahoo.vespa.model.search.SearchInterface;
import com.yahoo.vespa.model.search.SearchNode;
+import com.yahoo.yolean.Exceptions;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
@@ -176,7 +177,8 @@ public class IndexedHierarchicDistributionTest {
getIllegalMultipleGroupsLevelCluster();
fail("Did not get expected Exception");
} catch (Exception e) {
- assertTrue(e.getMessage().contains("sub group 'group0' contains 2 sub groups."));
+ assertEquals("Expected all groups under root group 'null' to be leaf groups only containing nodes, but sub group 'group0' contains 2 sub groups",
+ Exceptions.toMessageString(e));
}
}
@@ -220,7 +222,8 @@ public class IndexedHierarchicDistributionTest {
getTwoGroupsCluster(3, 3, "2|*");
fail("Did not get expected Exception");
} catch (Exception e) {
- assertTrue(e.getMessage().contains("Expected number of leaf groups (2) to be a factor of redundancy (3)"));
+ assertEquals("In content cluster 'mycluster': Expected number of leaf groups (2) to be a factor of redundancy (3), but it is not",
+ Exceptions.toMessageString(e));
}
}
@@ -240,7 +243,7 @@ public class IndexedHierarchicDistributionTest {
getTwoGroupsCluster(4, 3, "2|*");
fail("Did not get expected Exception");
} catch (Exception e) {
- assertTrue(e.getMessage().contains("Expected equal amount of ready copies per group"));
+ assertEquals("In content cluster 'mycluster': Expected equal amount of ready copies per group, but 3 ready copies is specified with 2 groups", Exceptions.toMessageString(e));
}
}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/StorageClusterTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/StorageClusterTest.java
index 9fb4eefba75..57ee15a1dc4 100644
--- a/config-model/src/test/java/com/yahoo/vespa/model/content/StorageClusterTest.java
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/StorageClusterTest.java
@@ -21,6 +21,7 @@ import static com.yahoo.config.model.test.TestUtil.joinLines;
import com.yahoo.vespa.model.content.cluster.ContentCluster;
import com.yahoo.vespa.model.content.storagecluster.StorageCluster;
import com.yahoo.vespa.model.content.utils.ContentClusterUtils;
+import com.yahoo.yolean.Exceptions;
import org.junit.jupiter.api.Test;
@@ -432,8 +433,8 @@ public class StorageClusterTest {
ContentClusterUtils.createCluster(xml, root);
fail("Did not fail when having both group and nodes");
} catch (RuntimeException e) {
- assertEquals("Both <group> and <nodes> is specified: Only one of these tags can be used in the same configuration",
- e.getMessage());
+ assertEquals("In content cluster 'storage': Both <group> and <nodes> is specified: Only one of these tags can be used in the same configuration",
+ Exceptions.toMessageString(e));
}
}
@@ -507,7 +508,7 @@ public class StorageClusterTest {
ContentClusterUtils.createCluster(xml, new MockRoot());
fail("Did not get exception with missing distribution element");
} catch (RuntimeException e) {
- assertEquals("'distribution' attribute is required with multiple subgroups", e.getMessage());
+ assertEquals("In content cluster 'storage': 'distribution' attribute is required with multiple subgroups", Exceptions.toMessageString(e));
}
}
}
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/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-clusters.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-clusters.json
index d9021b1d894..b5ae4efd752 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-clusters.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-clusters.json
@@ -88,7 +88,7 @@
{
"from": {
"nodes": 0,
- "groups": 0,
+ "groups": 1,
"nodeResources": {
"vcpu": 0.0,
"memoryGb": 0.0,
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..839e10ba096 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() / min.groups())
+ .toAtLeast(min.nodes() / max.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..b745322d458
--- /dev/null
+++ b/vespajlib/src/main/java/com/yahoo/collections/IntRange.java
@@ -0,0 +1,132 @@
+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;
+ }
+
+ /** Returns the given value adjusted minimally to fit within this range. */
+ public int fit(int value) {
+ if (from.isPresent() && value < from.getAsInt()) return from.getAsInt();
+ if (to.isPresent() && value > to.getAsInt()) return to.getAsInt();
+ return value;
+ }
+
+ @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) {
+ try {
+ s = s.trim();
+ if (s.isEmpty()) return OptionalInt.empty();
+ return OptionalInt.of(Integer.parseInt(s));
+ }
+ catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("'" + s + "' is not an integer");
+ }
+ }
+
+}
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));
+ }
+
+}