diff options
author | Jon Bratseth <bratseth@oath.com> | 2020-03-26 18:45:58 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-03-26 18:45:58 +0100 |
commit | fa9c389e779851c563ccef77a2a37a7277c24e1c (patch) | |
tree | 431b52e861dcec849399b2ab12c1345148e72641 | |
parent | 0b5095cac4970fd6e4c7312e4616ff0aaa5f3750 (diff) | |
parent | a82614e0ae27b6aa0875c1d92b2b3116db1bbc63 (diff) |
Merge pull request #12725 from vespa-engine/bratseth/autoscaling-ranges-2
Bratseth/autoscaling ranges 2
14 files changed, 264 insertions, 97 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 2a21af606ef..298517b85f6 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 @@ -6,6 +6,7 @@ import com.yahoo.collections.Pair; import com.yahoo.config.model.api.HostProvisioner; import com.yahoo.config.provision.Capacity; import com.yahoo.config.provision.ClusterMembership; +import com.yahoo.config.provision.ClusterResources; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Flavor; import com.yahoo.config.provision.HostSpec; @@ -57,6 +58,8 @@ public class InMemoryProvisioner implements HostProvisioner { /** Use this index as start index for all clusters */ private final int startIndexForClusters; + private final boolean useMaxResources; + /** Creates this with a number of nodes with resources 1, 3, 9, 1 */ public InMemoryProvisioner(int nodeCount) { this(nodeCount, defaultResources); @@ -64,27 +67,29 @@ public class InMemoryProvisioner implements HostProvisioner { /** Creates this with a number of nodes with given resources */ public InMemoryProvisioner(int nodeCount, NodeResources resources) { - this(Map.of(resources, createHostInstances(nodeCount)), true, 0); + this(Map.of(resources, createHostInstances(nodeCount)), true, false, 0); } /** Creates this with a set of host names of the flavor 'default' */ public InMemoryProvisioner(boolean failOnOutOfCapacity, String... hosts) { - this(Map.of(defaultResources, toHostInstances(hosts)), failOnOutOfCapacity, 0); + this(Map.of(defaultResources, toHostInstances(hosts)), failOnOutOfCapacity, false, 0); } /** Creates this with a set of hosts of the flavor 'default' */ public InMemoryProvisioner(Hosts hosts, boolean failOnOutOfCapacity, String ... retiredHostNames) { - this(Map.of(defaultResources, hosts.asCollection()), failOnOutOfCapacity, 0, retiredHostNames); + this(Map.of(defaultResources, hosts.asCollection()), failOnOutOfCapacity, false, 0, retiredHostNames); } /** Creates this with a set of hosts of the flavor 'default' */ public InMemoryProvisioner(Hosts hosts, boolean failOnOutOfCapacity, int startIndexForClusters, String ... retiredHostNames) { - this(Map.of(defaultResources, hosts.asCollection()), failOnOutOfCapacity, startIndexForClusters, retiredHostNames); + this(Map.of(defaultResources, hosts.asCollection()), failOnOutOfCapacity, false, startIndexForClusters, retiredHostNames); } public InMemoryProvisioner(Map<NodeResources, Collection<Host>> hosts, boolean failOnOutOfCapacity, + boolean useMaxResources, int startIndexForClusters, String ... retiredHostNames) { this.failOnOutOfCapacity = failOnOutOfCapacity; + this.useMaxResources = useMaxResources; for (Map.Entry<NodeResources, Collection<Host>> hostsWithResources : hosts.entrySet()) for (Host host : hostsWithResources.getValue()) freeNodes.put(hostsWithResources.getKey(), host); @@ -119,30 +124,37 @@ public class InMemoryProvisioner implements HostProvisioner { } @Override - public List<HostSpec> prepare(ClusterSpec cluster, Capacity requestedCapacity, ProvisionLogger logger) { - if (cluster.group().isPresent() && requestedCapacity.minResources().groups() > 1) + public List<HostSpec> prepare(ClusterSpec cluster, Capacity requested, ProvisionLogger logger) { + if (useMaxResources) + return prepare(cluster, requested.maxResources(), requested.isRequired(), requested.canFail()); + else + return prepare(cluster, requested.minResources(), requested.isRequired(), requested.canFail()); + } + + public List<HostSpec> prepare(ClusterSpec cluster, ClusterResources requested, 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"); - int capacity = failOnOutOfCapacity || requestedCapacity.isRequired() - ? requestedCapacity.minResources().nodes() - : Math.min(requestedCapacity.minResources().nodes(), freeNodes.get(defaultResources).size() + totalAllocatedTo(cluster)); - int groups = requestedCapacity.minResources().groups() > capacity ? capacity : requestedCapacity.minResources().groups(); + int capacity = failOnOutOfCapacity || required + ? requested.nodes() + : Math.min(requested.nodes(), freeNodes.get(defaultResources).size() + totalAllocatedTo(cluster)); + int groups = requested.groups() > capacity ? capacity : requested.groups(); List<HostSpec> allocation = new ArrayList<>(); if (groups == 1) { allocation.addAll(allocateHostGroup(cluster.with(Optional.of(ClusterSpec.Group.from(0))), - requestedCapacity.minResources().nodeResources(), + requested.nodeResources(), capacity, startIndexForClusters, - requestedCapacity.canFail())); + canFail)); } else { for (int i = 0; i < groups; i++) { allocation.addAll(allocateHostGroup(cluster.with(Optional.of(ClusterSpec.Group.from(i))), - requestedCapacity.minResources().nodeResources(), + requested.nodeResources(), capacity / groups, allocation.size(), - requestedCapacity.canFail())); + canFail)); } } for (ListIterator<HostSpec> i = allocation.listIterator(); i.hasNext(); ) { diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminV4Builder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminV4Builder.java index 5629956e8b9..804a5442608 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminV4Builder.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminV4Builder.java @@ -65,12 +65,12 @@ public class DomAdminV4Builder extends DomAdminBuilderBase { createSlobroks(deployLogger, admin, allocateHosts(admin.hostSystem(), "slobroks", nodesSpecification)); } else { - createSlobroks(deployLogger, admin, pickContainerHostsForSlobrok(nodesSpecification.count(), 2)); + createSlobroks(deployLogger, admin, pickContainerHostsForSlobrok(nodesSpecification.minResources().nodes(), 2)); } } private void assignLogserver(DeployState deployState, NodesSpecification nodesSpecification, Admin admin) { - if (nodesSpecification.count() > 1) throw new IllegalArgumentException("You can only request a single log server"); + if (nodesSpecification.minResources().nodes() > 1) throw new IllegalArgumentException("You can only request a single log server"); if (deployState.getProperties().applicationId().instance().isTester()) return; // No logserver is needed on tester applications if (nodesSpecification.isDedicated()) { Collection<HostResource> hosts = allocateHosts(admin.hostSystem(), "logserver", nodesSpecification); @@ -79,7 +79,7 @@ public class DomAdminV4Builder extends DomAdminBuilderBase { Logserver logserver = createLogserver(deployState.getDeployLogger(), admin, hosts); createContainerOnLogserverHost(deployState, admin, logserver.getHostResource()); } else if (containerModels.iterator().hasNext()) { - List<HostResource> hosts = sortedContainerHostsFrom(containerModels.iterator().next(), nodesSpecification.count(), false); + List<HostResource> hosts = sortedContainerHostsFrom(containerModels.iterator().next(), nodesSpecification.minResources().nodes(), false); if (hosts.isEmpty()) return; // No log server can be created (and none is needed) createLogserver(deployState.getDeployLogger(), admin, hosts); diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/ModelElement.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/ModelElement.java index d34a11abdf4..80c95ad6b59 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/ModelElement.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/ModelElement.java @@ -172,7 +172,12 @@ public class ModelElement { /** Returns the content of the attribute with the given name, or null if none */ public String stringAttribute(String name) { - if ( ! xml.hasAttribute(name)) return null; + return stringAttribute(name, null); + } + + /** Returns the content of the attribute with the given name, or the default value if none */ + public String stringAttribute(String name, String defaultValue) { + if ( ! xml.hasAttribute(name)) return defaultValue; return xml.getAttribute(name); } 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 20d101b07d9..6a52ff4f051 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 2017 Yahoo Holdings. 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.Pair; import com.yahoo.component.Version; import com.yahoo.config.application.api.DeployLogger; import com.yahoo.config.model.ConfigModelContext; @@ -19,6 +20,8 @@ import org.w3c.dom.Node; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Function; +import java.util.regex.Pattern; /** * A common utility class to represent a requirement for nodes during model building. @@ -28,11 +31,9 @@ import java.util.Optional; */ public class NodesSpecification { - private final boolean dedicated; - - private final int count; + private final ClusterResources min, max; - private final int groups; + private final boolean dedicated; /** The Vespa version we want the nodes to run */ private Version version; @@ -47,43 +48,51 @@ public class NodesSpecification { private final boolean exclusive; - /** The resources each node should have, or empty to use the default */ - private final Optional<NodeResources> resources; - /** The repo part of a docker image (without tag), optional */ private final Optional<String> dockerImageRepo; /** The ID of the cluster referencing this node specification, if any */ private final Optional<String> combinedId; - private NodesSpecification(boolean dedicated, int count, int groups, Version version, + private NodesSpecification(ClusterResources min, + ClusterResources max, + boolean dedicated, Version version, boolean required, boolean canFail, boolean exclusive, - Optional<NodeResources> resources, Optional<String> dockerImageRepo, + Optional<String> dockerImageRepo, Optional<String> combinedId) { + this.min = min; + this.max = max; this.dedicated = dedicated; - this.count = count; - this.groups = groups; this.version = version; this.required = required; this.canFail = canFail; this.exclusive = exclusive; - this.resources = resources; this.dockerImageRepo = dockerImageRepo; this.combinedId = combinedId; } - private NodesSpecification(boolean dedicated, boolean canFail, Version version, ModelElement nodesElement, - Optional<String> combinedId, Optional<String> dockerImageRepo) { - this(dedicated, - nodesElement.integerAttribute("count", 1), - nodesElement.integerAttribute("groups", 1), - version, - nodesElement.booleanAttribute("required", false), - canFail, - nodesElement.booleanAttribute("exclusive", false), - getResources(nodesElement), - dockerImageToUse(nodesElement, dockerImageRepo), - combinedId); + private static NodesSpecification create(boolean dedicated, boolean canFail, Version version, + ModelElement nodesElement, Optional<String> dockerImageRepo) { + var resolvedElement = resolveElement(nodesElement); + var combinedId = findCombinedId(nodesElement, resolvedElement); + var resources = toResources(resolvedElement); + return new NodesSpecification(resources.getFirst(), + resources.getSecond(), + dedicated, + version, + resolvedElement.booleanAttribute("required", false), + canFail, + resolvedElement.booleanAttribute("exclusive", false), + dockerImageToUse(resolvedElement, dockerImageRepo), + combinedId); + } + + 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); } /** Returns the ID of the cluster referencing this node specification, if any */ @@ -96,13 +105,6 @@ public class NodesSpecification { return containerIdReferencing(nodesElement); } - private static NodesSpecification create(boolean dedicated, boolean canFail, Version version, - ModelElement nodesElement, Optional<String> dockerImage) { - var resolvedElement = resolveElement(nodesElement); - var combinedId = findCombinedId(nodesElement, resolvedElement); - return new NodesSpecification(dedicated, canFail, version, resolvedElement, combinedId, dockerImage); - } - /** Returns a requirement for dedicated nodes taken from the given <code>nodes</code> element */ public static NodesSpecification from(ModelElement nodesElement, ConfigModelContext context) { return create(true, @@ -145,32 +147,33 @@ public class NodesSpecification { * Returns a requirement from <code>count</code> non-dedicated nodes in one group */ public static NodesSpecification nonDedicated(int count, ConfigModelContext context) { - return new NodesSpecification(false, - count, - 1, + return new NodesSpecification(new ClusterResources(count, 1, NodeResources.unspecified), + new ClusterResources(count, 1, NodeResources.unspecified), + false, context.getDeployState().getWantedNodeVespaVersion(), false, ! context.getDeployState().getProperties().isBootstrap(), false, - Optional.empty(), context.getDeployState().getWantedDockerImageRepo(), Optional.empty()); } /** Returns a requirement from <code>count</code> dedicated nodes in one group */ public static NodesSpecification dedicated(int count, ConfigModelContext context) { - return new NodesSpecification(true, - count, - 1, + return new NodesSpecification(new ClusterResources(count, 1, NodeResources.unspecified), + new ClusterResources(count, 1, NodeResources.unspecified), + true, context.getDeployState().getWantedNodeVespaVersion(), false, ! context.getDeployState().getProperties().isBootstrap(), false, - Optional.empty(), context.getDeployState().getWantedDockerImageRepo(), Optional.empty()); } + public ClusterResources minResources() { return min; } + public ClusterResources maxResources() { return max; } + /** * Returns whether this requires dedicated nodes. * Otherwise the model encountering this request should reuse nodes requested for other purposes whenever possible. @@ -184,12 +187,6 @@ public class NodesSpecification { */ public boolean isExclusive() { return exclusive; } - /** Returns the number of nodes required */ - public int count() { return count; } - - /** Returns the number of host groups this specifies. Default is 1 */ - public int groups() { return groups; } - public Map<HostResource, ClusterMembership> provision(HostSystem hostSystem, ClusterSpec.Type clusterType, ClusterSpec.Id clusterId, @@ -202,31 +199,36 @@ public class NodesSpecification { .combinedId(combinedId.map(ClusterSpec.Id::from)) .dockerImageRepo(dockerImageRepo) .build(); - return hostSystem.allocateHosts(cluster, Capacity.from(new ClusterResources(count, groups, resources.orElse(NodeResources.unspecified)), - required, canFail), - logger); + return hostSystem.allocateHosts(cluster, Capacity.from(min, max, required, canFail), logger); } - private static Optional<NodeResources> getResources(ModelElement nodesElement) { + private static Pair<NodeResources, NodeResources> nodeResources(ModelElement nodesElement) { ModelElement resources = nodesElement.child("resources"); if (resources != null) { - return Optional.of(new NodeResources(resources.requiredDoubleAttribute("vcpu"), - parseGbAmount(resources.requiredStringAttribute("memory"), "B"), - parseGbAmount(resources.requiredStringAttribute("disk"), "B"), - Optional.ofNullable(resources.stringAttribute("bandwidth")) - .map(b -> parseGbAmount(b, "BPS")) - .orElse(0.3), - parseOptionalDiskSpeed(resources.stringAttribute("disk-speed")), - parseOptionalStorageType(resources.stringAttribute("storage-type")))); + return nodeResourcesFromResorcesElement(resources); } else if (nodesElement.stringAttribute("flavor") != null) { // legacy fallback - return Optional.of(NodeResources.fromLegacyName(nodesElement.stringAttribute("flavor"))); + var flavorResources = NodeResources.fromLegacyName(nodesElement.stringAttribute("flavor")); + return new Pair<>(flavorResources, flavorResources); } - else { // Get the default - return Optional.empty(); + else { + return new Pair<>(NodeResources.unspecified, NodeResources.unspecified); } } + private static Pair<NodeResources, NodeResources> nodeResourcesFromResorcesElement(ModelElement element) { + Pair<Double, Double> vcpu = toRange(element.requiredStringAttribute("vcpu"), .0, Double::parseDouble); + Pair<Double, Double> memory = toRange(element.requiredStringAttribute("memory"), .0, s -> parseGbAmount(s, "B")); + Pair<Double, Double> disk = toRange(element.requiredStringAttribute("disk"), .0, s -> parseGbAmount(s, "B")); + Pair<Double, Double> bandwith = toRange(element.stringAttribute("bandwith"), .3, s -> parseGbAmount(s, "BPS")); + NodeResources.DiskSpeed diskSpeed = parseOptionalDiskSpeed(element.stringAttribute("disk-speed")); + NodeResources.StorageType storageType = parseOptionalStorageType(element.stringAttribute("storage-type")); + + var min = new NodeResources(vcpu.getFirst(), memory.getFirst(), disk.getFirst(), bandwith.getFirst(), diskSpeed, storageType); + var max = new NodeResources(vcpu.getSecond(), memory.getSecond(), disk.getSecond(), bandwith.getSecond(), diskSpeed, storageType); + return new Pair<>(min, max); + } + private static double parseGbAmount(String byteAmount, String unit) { byteAmount = byteAmount.strip(); byteAmount = byteAmount.toUpperCase(); @@ -361,11 +363,28 @@ public class NodesSpecification { return dockerImageFromElement == null ? dockerImage : Optional.of(dockerImageFromElement); } + /** Parses a value ("value") or value range ("[min-value, max-value]") */ + private static <T> Pair<T, T> toRange(String s, T defaultValue, Function<String, T> valueParser) { + try { + if (s == null) return new Pair<>(defaultValue, defaultValue); + s = s.trim(); + if (s.startsWith("[") && s.endsWith("]")) { + String[] numbers = s.substring(1, s.length() - 1).split(","); + if (numbers.length != 2) throw new IllegalArgumentException(); + return new Pair<>(valueParser.apply(numbers[0].trim()), valueParser.apply(numbers[1].trim())); + } else { + return new Pair<>(valueParser.apply(s), valueParser.apply(s)); + } + } + catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Expected a number or range on the form [min, max], but got '" + s + "'", e); + } + } + @Override public String toString() { - return "specification of " + count + (dedicated ? " dedicated " : " ") + "nodes" + - (resources.map(nodeResources -> " with resources " + nodeResources).orElse("")) + - (groups > 1 ? " in " + groups + " groups" : ""); + return "specification of " + (dedicated ? "dedicated " : "") + + (min.equals(max) ? min : "min " + min + " max " + max); } } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/ContentCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/ContentCluster.java index 066fef727c5..6dd3e619ec2 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/ContentCluster.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/ContentCluster.java @@ -286,7 +286,7 @@ public class ContentCluster extends AbstractConfigProducer implements .orElse(NodesSpecification.nonDedicated(3, context)); Collection<HostResource> hosts = nodesSpecification.isDedicated() ? getControllerHosts(nodesSpecification, admin, clusterName, context) : - drawControllerHosts(nodesSpecification.count(), rootGroup, containers); + drawControllerHosts(nodesSpecification.minResources().nodes(), rootGroup, containers); clusterControllers = createClusterControllers(new ClusterControllerCluster(contentCluster, "standalone"), hosts, clusterName, diff --git a/config-model/src/main/resources/schema/common.rnc b/config-model/src/main/resources/schema/common.rnc index c47983adc12..878faabfec1 100644 --- a/config-model/src/main/resources/schema/common.rnc +++ b/config-model/src/main/resources/schema/common.rnc @@ -17,14 +17,14 @@ anyElement = element * { JavaId = xsd:string { pattern = "([a-zA-Z_$][a-zA-Z\d_$]*\.)*[a-zA-Z_$][a-zA-Z\d_$]*" } Nodes = element nodes { - attribute count { xsd:positiveInteger } & + attribute count { xsd:positiveInteger | xsd:string } & attribute flavor { xsd:string }? & attribute docker-image { xsd:string }? & Resources? } Resources = element resources { - attribute vcpu { xsd:double { minExclusive = "0.0" } } & + attribute vcpu { xsd:double { minExclusive = "0.0" } | xsd:string } & attribute memory { xsd:string } & attribute disk { xsd:string } & attribute disk-speed { xsd:string }? & @@ -32,7 +32,7 @@ Resources = element resources { } OptionalDedicatedNodes = element nodes { - attribute count { xsd:positiveInteger } & + attribute count { xsd:positiveInteger | xsd:string } & attribute flavor { xsd:string }? & attribute required { xsd:boolean }? & attribute docker-image { xsd:string }? & diff --git a/config-model/src/main/resources/schema/containercluster.rnc b/config-model/src/main/resources/schema/containercluster.rnc index 726fa849c00..3c8b60fb84b 100644 --- a/config-model/src/main/resources/schema/containercluster.rnc +++ b/config-model/src/main/resources/schema/containercluster.rnc @@ -239,7 +239,7 @@ NodesOfContainerCluster = element nodes { attribute type { xsd:string } | ( - attribute count { xsd:positiveInteger } & + attribute count { xsd:positiveInteger | xsd:string } & attribute flavor { xsd:string }? & attribute required { xsd:boolean }? & attribute exclusive { xsd:boolean }? & diff --git a/config-model/src/main/resources/schema/content.rnc b/config-model/src/main/resources/schema/content.rnc index ee451185415..b1821680b14 100644 --- a/config-model/src/main/resources/schema/content.rnc +++ b/config-model/src/main/resources/schema/content.rnc @@ -221,11 +221,11 @@ ContentNodes = element nodes { attribute vespamalloc-debug-stacktrace { xsd:string }? & ( ( - attribute count { xsd:positiveInteger } & + attribute count { xsd:positiveInteger | xsd:string } & attribute flavor { xsd:string }? & attribute required { xsd:boolean }? & attribute docker-image { xsd:string }? & - attribute groups { xsd:positiveInteger }? + attribute groups { xsd:positiveInteger | xsd:string }? ) | ContentNode + @@ -266,12 +266,12 @@ Group = element group { | ( element nodes { - attribute count { xsd:positiveInteger } & + attribute count { xsd:positiveInteger | xsd:string } & attribute flavor { xsd:string }? & attribute required { xsd:boolean }? & attribute exclusive { xsd:boolean }? & attribute docker-image { xsd:string }? & - attribute groups { xsd:positiveInteger }? + attribute groups { xsd:positiveInteger | xsd:string }? } ) | 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 1670ac23ba4..ebafe9dd45b 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 @@ -1205,6 +1205,62 @@ public class ModelProvisioningTest { } @Test + public void testRequestingRangesMin() { + String services = + "<?xml version='1.0' encoding='utf-8' ?>" + + "<services>" + + " <container version='1.0' id='container'>" + + " <nodes count='[4, 6]'>" + + " <resources vcpu='[11.5, 13.5]' memory='[10Gb, 100Gb]' disk='[30Gb, 1Tb]'/>" + + " </nodes>" + + " </container>" + + " <content version='1.0' id='foo'>" + + " <documents>" + + " <document type='type1' mode='index'/>" + + " </documents>" + + " <nodes count='[6, 20]' groups='[3,4]'>" + + " <resources vcpu='8' memory='200Gb' disk='1Pb'/>" + + " </nodes>" + + " </content>" + + "</services>"; + + int totalHosts = 10; + VespaModelTester tester = new VespaModelTester(); + tester.addHosts(new NodeResources(11.5, 10, 30, 0.3), 6); + tester.addHosts(new NodeResources(85, 200, 1000_000_000, 0.3), 20); + VespaModel model = tester.createModel(services, true); + assertEquals(totalHosts, model.getRoot().hostSystem().getHosts().size()); + } + + @Test + public void testRequestingRangesMax() { + String services = + "<?xml version='1.0' encoding='utf-8' ?>" + + "<services>" + + " <container version='1.0' id='container'>" + + " <nodes count='[4, 6]'>" + + " <resources vcpu='[11.5, 13.5]' memory='[10Gb, 100Gb]' disk='[30Gb, 1Tb]'/>" + + " </nodes>" + + " </container>" + + " <content version='1.0' id='foo'>" + + " <documents>" + + " <document type='type1' mode='index'/>" + + " </documents>" + + " <nodes count='[6, 20]' groups='[3,4]'>" + + " <resources vcpu='8' memory='200Gb' disk='1Pb'/>" + + " </nodes>" + + " </content>" + + "</services>"; + + int totalHosts = 26; + VespaModelTester tester = new VespaModelTester(); + tester.addHosts(new NodeResources(13.5, 100, 1000, 0.3), 6); + tester.addHosts(new NodeResources(85, 200, 1000_000_000, 0.3), 20); + VespaModel model = tester.createModel(services, true, true); + assertEquals(totalHosts, model.getRoot().hostSystem().getHosts().size()); + } + + @Test public void testContainerOnly() { String services = "<?xml version='1.0' encoding='utf-8' ?>\n" + diff --git a/config-model/src/test/java/com/yahoo/vespa/model/test/VespaModelTester.java b/config-model/src/test/java/com/yahoo/vespa/model/test/VespaModelTester.java index fd837c6dea3..17c7b3a308d 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/test/VespaModelTester.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/test/VespaModelTester.java @@ -15,6 +15,7 @@ import com.yahoo.config.model.provision.SingleNodeProvisioner; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Flavor; import com.yahoo.config.provision.NodeResources; +import com.yahoo.config.provision.Provisioner; import com.yahoo.config.provision.Zone; import com.yahoo.vespa.model.VespaModel; import com.yahoo.vespa.model.test.utils.ApplicationPackageUtils; @@ -109,33 +110,41 @@ public class VespaModelTester { /** Creates a model which uses 0 as start index */ public VespaModel createModel(String services, boolean failOnOutOfCapacity, String ... retiredHostNames) { - return createModel(Zone.defaultZone(), services, failOnOutOfCapacity, 0, retiredHostNames); + return createModel(Zone.defaultZone(), services, failOnOutOfCapacity, false, 0, retiredHostNames); + } + + /** Creates a model which uses 0 as start index */ + public VespaModel createModel(String services, boolean failOnOutOfCapacity, boolean useMaxResources, String ... retiredHostNames) { + return createModel(Zone.defaultZone(), services, failOnOutOfCapacity, useMaxResources, 0, retiredHostNames); } /** Creates a model which uses 0 as start index */ public VespaModel createModel(String services, boolean failOnOutOfCapacity, int startIndexForClusters, String ... retiredHostNames) { - return createModel(Zone.defaultZone(), services, failOnOutOfCapacity, startIndexForClusters, retiredHostNames); + return createModel(Zone.defaultZone(), services, failOnOutOfCapacity, false, startIndexForClusters, retiredHostNames); } /** Creates a model which uses 0 as start index */ public VespaModel createModel(Zone zone, String services, boolean failOnOutOfCapacity, String ... retiredHostNames) { - return createModel(zone, services, failOnOutOfCapacity, 0, retiredHostNames); + return createModel(zone, services, failOnOutOfCapacity, false, 0, retiredHostNames); } /** * Creates a model using the hosts already added to this * * @param services the services xml string + * @param useMaxResources false to use the minmal resources (when given a range), true to use max * @param failOnOutOfCapacity whether we should get an exception when not enough hosts of the requested flavor * is available or if we should just silently receive a smaller allocation * @return the resulting model */ - public VespaModel createModel(Zone zone, String services, boolean failOnOutOfCapacity, int startIndexForClusters, String ... retiredHostNames) { + public VespaModel createModel(Zone zone, String services, boolean failOnOutOfCapacity, boolean useMaxResources, + int startIndexForClusters, String ... retiredHostNames) { VespaModelCreatorWithMockPkg modelCreatorWithMockPkg = new VespaModelCreatorWithMockPkg(null, services, ApplicationPackageUtils.generateSearchDefinition("type1")); ApplicationPackage appPkg = modelCreatorWithMockPkg.appPkg; - HostProvisioner provisioner = hosted ? - new InMemoryProvisioner(hostsByResources, failOnOutOfCapacity, startIndexForClusters, retiredHostNames) : + HostProvisioner provisioner = hosted ? + new InMemoryProvisioner(hostsByResources, failOnOutOfCapacity, useMaxResources, + startIndexForClusters, retiredHostNames) : new SingleNodeProvisioner(); TestProperties properties = new TestProperties() 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 07839239c81..71a07926240 100644 --- a/config-model/src/test/schema-test-files/services-hosted.xml +++ b/config-model/src/test/schema-test-files/services-hosted.xml @@ -7,7 +7,7 @@ </admin> <container id="container1" version="1.0"> - <nodes count="5" required="true"> + <nodes count="[5,7]" required="true"> <resources vcpu="1.2" memory="10Gb" disk="0.3 TB"/> </nodes> </container> @@ -27,4 +27,11 @@ </nodes> </content> + <content id="ml" version="1.0"> + <redundancy>2</redundancy> + <nodes count="[10,20]" flavor="large" groups="[1,3]"> + <resources vcpu="[3.0, 4]" memory="[32000.0Mb, 33Gb]" disk="[300 Gb, 1Tb]"/> + </nodes> + </content> + </services> 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 37bea40e932..48b4e9d91bc 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 @@ -21,6 +21,9 @@ public final class Capacity { private final NodeType type; private Capacity(ClusterResources min, ClusterResources max, boolean required, boolean canFail, NodeType type) { + if (max.smallerThan(min)) + throw new IllegalArgumentException("The max capacity must be larger than the min capacity, but got min " + + min + " and max " + max); this.min = min; this.max = max; this.required = required; 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 49b635aa859..11873bc908c 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 @@ -36,6 +36,14 @@ public class ClusterResources { public ClusterResources with(NodeResources resources) { return new ClusterResources(nodes, groups, resources); } public ClusterResources withGroups(int groups) { return new ClusterResources(nodes, groups, nodeResources); } + /** Returns true if this is smaller than the given resources in any dimension */ + public boolean smallerThan(ClusterResources other) { + if (this.nodes < other.nodes) return true; + if (this.groups < other.groups) return true; + if ( ! this.nodeResources.justNumbers().satisfies(other.nodeResources.justNumbers())) return true; + return false; + } + @Override public boolean equals(Object o) { if (o == this) return true; 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 new file mode 100644 index 00000000000..326ed7317f6 --- /dev/null +++ b/config-provisioning/src/test/java/com/yahoo/config/provision/CapacityTest.java @@ -0,0 +1,48 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config.provision; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author bratseth + */ +public class CapacityTest { + + @Test + public 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); + 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)), + new ClusterResources(4, 2, new NodeResources(1,2,3,4))); + assertValidationFailure(new ClusterResources(4, 2, new NodeResources(2,2,3,4)), + new ClusterResources(4, 2, new NodeResources(1,2,3,4))); + assertValidationFailure(new ClusterResources(4, 2, new NodeResources(1,3,3,4)), + new ClusterResources(4, 2, new NodeResources(1,2,3,4))); + assertValidationFailure(new ClusterResources(4, 2, new NodeResources(1,2,4,4)), + new ClusterResources(4, 2, new NodeResources(1,2,3,4))); + assertValidationFailure(new ClusterResources(4, 2, new NodeResources(1,2,3,5)), + new ClusterResources(4, 2, new NodeResources(1,2,3,4))); + // It's enough than one dimension is smaller also when the others are larger + assertValidationFailure(new ClusterResources(4, 2, new NodeResources(1,2,3,4)), + new ClusterResources(8, 4, new NodeResources(2,1,6,8))); + } + + private void assertValidationFailure(ClusterResources min, ClusterResources max) { + try { + Capacity.from(min, max, false, true); + fail("Expected exception with min " + min + " and max " + max); + } + catch (IllegalArgumentException e) { + assertEquals("The max capacity must be larger than the min capacity, but got min " + min + " and max " + max, + e.getMessage()); + } + } + +} |