diff options
author | Jon Bratseth <bratseth@verizonmedia.com> | 2020-03-26 17:19:55 +0100 |
---|---|---|
committer | Jon Bratseth <bratseth@verizonmedia.com> | 2020-03-26 17:19:55 +0100 |
commit | 77fa3b877833b8dc5957039ee02b1068cf0b0be6 (patch) | |
tree | bf0b58465e2ee002b100fb000d0eefa0170b7afe /config-model/src/main | |
parent | a1d7251d0b27a5903ac4c700b5bbb80f9c989599 (diff) |
Support specifying resource ranges
Diffstat (limited to 'config-model/src/main')
8 files changed, 103 insertions, 48 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 338679d314b..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.resources().nodes(), 2)); + createSlobroks(deployLogger, admin, pickContainerHostsForSlobrok(nodesSpecification.minResources().nodes(), 2)); } } private void assignLogserver(DeployState deployState, NodesSpecification nodesSpecification, Admin admin) { - if (nodesSpecification.resources().nodes() > 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.resources().nodes(), 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 71c04dcbca0..ef37481d0fc 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,7 +31,7 @@ import java.util.Optional; */ public class NodesSpecification { - private final ClusterResources resources; + private final ClusterResources min, max; private final boolean dedicated; @@ -51,12 +54,14 @@ public class NodesSpecification { /** The ID of the cluster referencing this node specification, if any */ private final Optional<String> combinedId; - private NodesSpecification(ClusterResources resources, + private NodesSpecification(ClusterResources min, + ClusterResources max, boolean dedicated, Version version, boolean required, boolean canFail, boolean exclusive, Optional<String> dockerImageRepo, Optional<String> combinedId) { - this.resources = resources; + this.min = min; + this.max = max; this.dedicated = dedicated; this.version = version; this.required = required; @@ -70,7 +75,9 @@ public class NodesSpecification { ModelElement nodesElement, Optional<String> dockerImageRepo) { var resolvedElement = resolveElement(nodesElement); var combinedId = findCombinedId(nodesElement, resolvedElement); - return new NodesSpecification(toMinResources(resolvedElement), + var resources = toResources(resolvedElement); + return new NodesSpecification(resources.getFirst(), + resources.getSecond(), dedicated, version, resolvedElement.booleanAttribute("required", false), @@ -80,10 +87,12 @@ public class NodesSpecification { combinedId); } - private static ClusterResources toMinResources(ModelElement nodesElement) { - return new ClusterResources(nodesElement.integerAttribute("count", 1), - nodesElement.integerAttribute("groups", 1), - getResources(nodesElement)); + 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 */ @@ -139,6 +148,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), false, context.getDeployState().getWantedNodeVespaVersion(), false, @@ -151,6 +161,7 @@ public class NodesSpecification { /** Returns a requirement from <code>count</code> dedicated nodes in one group */ public static NodesSpecification dedicated(int count, ConfigModelContext context) { return new NodesSpecification(new ClusterResources(count, 1, NodeResources.unspecified), + new ClusterResources(count, 1, NodeResources.unspecified), true, context.getDeployState().getWantedNodeVespaVersion(), false, @@ -160,7 +171,8 @@ public class NodesSpecification { Optional.empty()); } - public ClusterResources resources() { return resources; } + public ClusterResources minResources() { return min; } + public ClusterResources maxResources() { return max; } /** * Returns whether this requires dedicated nodes. @@ -187,29 +199,36 @@ public class NodesSpecification { .combinedId(combinedId.map(ClusterSpec.Id::from)) .dockerImageRepo(dockerImageRepo) .build(); - return hostSystem.allocateHosts(cluster, Capacity.from(resources, required, canFail), logger); + return hostSystem.allocateHosts(cluster, Capacity.from(min, max, required, canFail), logger); } - private static NodeResources getResources(ModelElement nodesElement) { + private static Pair<NodeResources, NodeResources> nodeResources(ModelElement nodesElement) { ModelElement resources = nodesElement.child("resources"); if (resources != null) { - return 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 NodeResources.fromLegacyName(nodesElement.stringAttribute("flavor")); + var flavorResources = NodeResources.fromLegacyName(nodesElement.stringAttribute("flavor")); + return new Pair<>(flavorResources, flavorResources); } else { - return NodeResources.unspecified; + 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(); @@ -344,9 +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 + "'"); + } + } + @Override public String toString() { - return "specification of " + (dedicated ? "dedicated " : "") + resources; + 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 d5d9f6b51bb..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.resources().nodes(), 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..51a1d5f36b9 100644 --- a/config-model/src/main/resources/schema/common.rnc +++ b/config-model/src/main/resources/schema/common.rnc @@ -17,7 +17,7 @@ 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? @@ -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 }? } ) | |