summaryrefslogtreecommitdiffstats
path: root/config-model
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@verizonmedia.com>2019-05-06 10:02:49 +0200
committerJon Bratseth <bratseth@verizonmedia.com>2019-05-06 10:02:49 +0200
commit1add32ea899b62a38008cc460a42437e15f31b15 (patch)
treea8d5ecaa20880676be7af49617319eb88cfefa36 /config-model
parent21a212f66f4491ad1ae42349139ec9ec16973fa2 (diff)
Allow node allocation by resource spec
Diffstat (limited to 'config-model')
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/provision/InMemoryProvisioner.java48
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/ModelElement.java8
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/NodesSpecification.java63
-rw-r--r--config-model/src/main/resources/schema/common.rnc9
-rw-r--r--config-model/src/main/resources/schema/containercluster.rnc1
-rw-r--r--config-model/src/main/resources/schema/content.rnc1
-rw-r--r--config-model/src/main/resources/schema/docproc.rnc1
-rw-r--r--config-model/src/test/java/com/yahoo/config/model/provision/ModelProvisioningTest.java4
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/test/VespaModelTester.java21
-rw-r--r--config-model/src/test/schema-test-files/services-hosted.xml8
10 files changed, 118 insertions, 46 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 c744c509b9a..2439475e95c 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
@@ -8,6 +8,7 @@ import com.yahoo.config.provision.Capacity;
import com.yahoo.config.provision.ClusterMembership;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.HostSpec;
+import com.yahoo.config.provision.NodeResources;
import com.yahoo.config.provision.ProvisionLogger;
import java.util.ArrayList;
@@ -42,42 +43,47 @@ public class InMemoryProvisioner implements HostProvisioner {
/** Hosts which should be returned as retired */
private final Set<String> retiredHostNames;
- /** Free hosts of each flavor */
- private final ListMap<String, Host> freeNodes = new ListMap<>();
+ /** Free hosts of each resource size */
+ private final ListMap<NodeResources, Host> freeNodes = new ListMap<>();
private final Map<String, HostSpec> legacyMapping = new LinkedHashMap<>();
private final Map<ClusterSpec, List<HostSpec>> allocations = new LinkedHashMap<>();
/** Indexes must be unique across all groups in a cluster */
- private final Map<Pair<ClusterSpec.Type,ClusterSpec.Id>, Integer> nextIndexInCluster = new HashMap<>();
+ private final Map<Pair<ClusterSpec.Type, ClusterSpec.Id>, Integer> nextIndexInCluster = new HashMap<>();
/** Use this index as start index for all clusters */
private final int startIndexForClusters;
/** Creates this with a number of nodes of the flavor 'default' */
public InMemoryProvisioner(int nodeCount) {
- this(Collections.singletonMap("default", createHostInstances(nodeCount)), true, 0);
+ this(Collections.singletonMap(NodeResources.fromLegacyName("default"),
+ createHostInstances(nodeCount)), true, 0);
}
/** Creates this with a set of host names of the flavor 'default' */
public InMemoryProvisioner(boolean failOnOutOfCapacity, String... hosts) {
- this(Collections.singletonMap("default", toHostInstances(hosts)), failOnOutOfCapacity, 0);
+ this(Collections.singletonMap(NodeResources.fromLegacyName("default"),
+ toHostInstances(hosts)), failOnOutOfCapacity, 0);
}
/** Creates this with a set of hosts of the flavor 'default' */
public InMemoryProvisioner(Hosts hosts, boolean failOnOutOfCapacity, String ... retiredHostNames) {
- this(Collections.singletonMap("default", hosts.asCollection()), failOnOutOfCapacity, 0, retiredHostNames);
+ this(Collections.singletonMap(NodeResources.fromLegacyName("default"),
+ hosts.asCollection()), failOnOutOfCapacity, 0, retiredHostNames);
}
/** Creates this with a set of hosts of the flavor 'default' */
public InMemoryProvisioner(Hosts hosts, boolean failOnOutOfCapacity, int startIndexForClusters, String ... retiredHostNames) {
- this(Collections.singletonMap("default", hosts.asCollection()), failOnOutOfCapacity, startIndexForClusters, retiredHostNames);
+ this(Collections.singletonMap(NodeResources.fromLegacyName("default"),
+ hosts.asCollection()), failOnOutOfCapacity, startIndexForClusters, retiredHostNames);
}
- public InMemoryProvisioner(Map<String, Collection<Host>> hosts, boolean failOnOutOfCapacity, int startIndexForClusters, String ... retiredHostNames) {
+ public InMemoryProvisioner(Map<NodeResources, Collection<Host>> hosts, boolean failOnOutOfCapacity,
+ int startIndexForClusters, String ... retiredHostNames) {
this.failOnOutOfCapacity = failOnOutOfCapacity;
- for (Map.Entry<String, Collection<Host>> hostsOfFlavor : hosts.entrySet())
- for (Host host : hostsOfFlavor.getValue())
- freeNodes.put(hostsOfFlavor.getKey(), host);
+ for (Map.Entry<NodeResources, Collection<Host>> hostsWithResources : hosts.entrySet())
+ for (Host host : hostsWithResources.getValue())
+ freeNodes.put(hostsWithResources.getKey(), host);
this.retiredHostNames = new HashSet<>(Arrays.asList(retiredHostNames));
this.startIndexForClusters = startIndexForClusters;
}
@@ -104,9 +110,9 @@ public class InMemoryProvisioner implements HostProvisioner {
@Override
public HostSpec allocateHost(String alias) {
if (legacyMapping.containsKey(alias)) return legacyMapping.get(alias);
- List<Host> defaultHosts = freeNodes.get("default");
+ List<Host> defaultHosts = freeNodes.get(NodeResources.fromLegacyName("default"));
if (defaultHosts.isEmpty()) throw new IllegalArgumentException("No more hosts of default flavor available");
- Host newHost = freeNodes.removeValue("default", 0);
+ Host newHost = freeNodes.removeValue(NodeResources.fromLegacyName("default"), 0);
HostSpec hostSpec = new HostSpec(newHost.hostname(), newHost.aliases(), newHost.flavor(), Optional.empty(), newHost.version());
legacyMapping.put(alias, hostSpec);
return hostSpec;
@@ -122,16 +128,16 @@ public class InMemoryProvisioner implements HostProvisioner {
int capacity = failOnOutOfCapacity || requestedCapacity.isRequired()
? requestedCapacity.nodeCount()
- : Math.min(requestedCapacity.nodeCount(), freeNodes.get("default").size() + totalAllocatedTo(cluster));
+ : Math.min(requestedCapacity.nodeCount(), freeNodes.get(NodeResources.fromLegacyName("default")).size() + totalAllocatedTo(cluster));
if (groups > capacity)
groups = capacity;
- String flavor = requestedCapacity.flavor().orElse("default");
+ NodeResources nodeResources = requestedCapacity.nodeResources().orElse(NodeResources.fromLegacyName("default"));
List<HostSpec> allocation = new ArrayList<>();
if (groups == 1) {
allocation.addAll(allocateHostGroup(cluster.with(Optional.of(ClusterSpec.Group.from(0))),
- flavor,
+ nodeResources,
capacity,
startIndexForClusters,
requestedCapacity.canFail()));
@@ -139,7 +145,7 @@ public class InMemoryProvisioner implements HostProvisioner {
else {
for (int i = 0; i < groups; i++) {
allocation.addAll(allocateHostGroup(cluster.with(Optional.of(ClusterSpec.Group.from(i))),
- flavor,
+ nodeResources,
capacity / groups,
allocation.size(),
requestedCapacity.canFail()));
@@ -161,19 +167,19 @@ public class InMemoryProvisioner implements HostProvisioner {
host.version());
}
- private List<HostSpec> allocateHostGroup(ClusterSpec clusterGroup, String flavor, int nodesInGroup, int startIndex, boolean canFail) {
+ private List<HostSpec> allocateHostGroup(ClusterSpec clusterGroup, NodeResources nodeResources, int nodesInGroup, int startIndex, boolean canFail) {
List<HostSpec> allocation = allocations.getOrDefault(clusterGroup, new ArrayList<>());
allocations.put(clusterGroup, allocation);
int nextIndex = nextIndexInCluster.getOrDefault(new Pair<>(clusterGroup.type(), clusterGroup.id()), startIndex);
while (allocation.size() < nodesInGroup) {
- if (freeNodes.get(flavor).isEmpty()) {
+ if (freeNodes.get(nodeResources).isEmpty()) {
if (canFail)
- throw new IllegalArgumentException("Insufficient capacity of flavor '" + flavor + "'");
+ throw new IllegalArgumentException("Insufficient capacity of for " + nodeResources);
else
break;
}
- Host newHost = freeNodes.removeValue(flavor, 0);
+ Host newHost = freeNodes.removeValue(nodeResources, 0);
ClusterMembership membership = ClusterMembership.from(clusterGroup, nextIndex++);
allocation.add(new HostSpec(newHost.hostname(), newHost.aliases(), newHost.flavor(), Optional.of(membership), newHost.version()));
}
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 1f649b122fa..d34a11abdf4 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
@@ -176,6 +176,14 @@ public class ModelElement {
return xml.getAttribute(name);
}
+ /** Returns the content of the attribute with the given name or throws IllegalArgumentException if not present */
+ public String requiredStringAttribute(String name) {
+ if (stringAttribute(name) == null)
+ throw new IllegalArgumentException("Required attribute '" + name + "' is missing");
+ return stringAttribute(name);
+ }
+
+
public List<ModelElement> subElements(String name) {
List<Element> elements = XML.getChildren(xml, 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 1031ae7b787..7864623f251 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
@@ -7,7 +7,7 @@ import com.yahoo.config.model.ConfigModelContext;
import com.yahoo.config.provision.Capacity;
import com.yahoo.config.provision.ClusterMembership;
import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.FlavorSpec;
+import com.yahoo.config.provision.NodeResources;
import com.yahoo.config.provision.RotationName;
import com.yahoo.vespa.model.HostResource;
import com.yahoo.vespa.model.HostSystem;
@@ -44,15 +44,15 @@ public class NodesSpecification {
private final boolean exclusive;
- /** The flavor the nodes should have, or empty to use the default */
- private final Optional<FlavorSpec> flavor;
+ /** The resources each node should have, or empty to use the default */
+ private final Optional<NodeResources> resources;
/** The identifier of the custom docker image layer to use (not supported yet) */
private final Optional<String> dockerImage;
private NodesSpecification(boolean dedicated, int count, int groups, Version version,
boolean required, boolean canFail, boolean exclusive,
- Optional<FlavorSpec> flavor, Optional<String> dockerImage) {
+ Optional<NodeResources> resources, Optional<String> dockerImage) {
this.dedicated = dedicated;
this.count = count;
this.groups = groups;
@@ -60,7 +60,7 @@ public class NodesSpecification {
this.required = required;
this.canFail = canFail;
this.exclusive = exclusive;
- this.flavor = flavor;
+ this.resources = resources;
this.dockerImage = dockerImage;
}
@@ -170,28 +170,65 @@ public class NodesSpecification {
DeployLogger logger,
Set<RotationName> rotations) {
ClusterSpec cluster = ClusterSpec.request(clusterType, clusterId, version, exclusive, rotations);
- return hostSystem.allocateHosts(cluster, Capacity.fromCount(count, flavor, required, canFail), groups, logger);
+ return hostSystem.allocateHosts(cluster, Capacity.fromCount(count, resources, required, canFail), groups, logger);
}
- private static Optional<FlavorSpec> getFlavor(ModelElement nodesElement) {
- ModelElement flavor = nodesElement.child("flavor");
+ private static Optional<NodeResources> getFlavor(ModelElement nodesElement) {
+ ModelElement flavor = nodesElement.child("resources");
if (flavor != null) {
- return Optional.of(new FlavorSpec(flavor.requiredDoubleAttribute("cpus"),
- flavor.requiredDoubleAttribute("memory"),
- flavor.requiredDoubleAttribute("disk")));
+ return Optional.of(new NodeResources(flavor.requiredDoubleAttribute("vcpu"),
+ parseGbAmount(flavor.requiredStringAttribute("memory")),
+ parseGbAmount(flavor.requiredStringAttribute("disk"))));
}
else if (nodesElement.stringAttribute("flavor") != null) { // legacy fallback
- return Optional.of(FlavorSpec.fromLegacyFlavorName(nodesElement.stringAttribute("flavor")));
+ return Optional.of(NodeResources.fromLegacyName(nodesElement.stringAttribute("flavor")));
}
else { // Get the default
return Optional.empty();
}
}
+ private static double parseGbAmount(String byteAmount) {
+ byteAmount = byteAmount.strip();
+ byteAmount = byteAmount.toUpperCase();
+ if (byteAmount.endsWith("B"))
+ byteAmount = byteAmount.substring(0, byteAmount.length() -1);
+
+ double multiplier = 1/1000^3;
+ if (byteAmount.endsWith("K"))
+ multiplier = 1/1000^2;
+ else if (byteAmount.endsWith("M"))
+ multiplier = 1/1000;
+ else if (byteAmount.endsWith("G"))
+ multiplier = 1;
+ else if (byteAmount.endsWith("T"))
+ multiplier = 1000;
+ else if (byteAmount.endsWith("P"))
+ multiplier = 1000^2;
+ else if (byteAmount.endsWith("E"))
+ multiplier = 1000^3;
+ else if (byteAmount.endsWith("Z"))
+ multiplier = 1000^4;
+ else if (byteAmount.endsWith("Y"))
+ multiplier = 1000^5;
+ else
+ throw new IllegalArgumentException("Invalid byte amount '" + byteAmount +
+ "': Must end with k, M, G, T, P, E, Z or Y");
+
+ byteAmount = byteAmount.substring(0, byteAmount.length() -1 ).strip();
+ try {
+ return Double.parseDouble(byteAmount) * multiplier;
+ }
+ catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Invalid byte amount '" + byteAmount +
+ "': Must be a floating point number followed by k, M, G, T, P, E, Z or Y");
+ }
+ }
+
@Override
public String toString() {
return "specification of " + count + (dedicated ? " dedicated " : " ") + "nodes" +
- (flavor.isPresent() ? " of flavor " + flavor.get() : "") +
+ (resources.isPresent() ? " with resources " + resources.get() : "") +
(groups > 1 ? " in " + groups + " groups" : "");
}
diff --git a/config-model/src/main/resources/schema/common.rnc b/config-model/src/main/resources/schema/common.rnc
index 73882da2b01..59b40f433b5 100644
--- a/config-model/src/main/resources/schema/common.rnc
+++ b/config-model/src/main/resources/schema/common.rnc
@@ -19,7 +19,14 @@ 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 flavor { xsd:string }? &
- attribute docker-image { xsd:string }?
+ attribute docker-image { xsd:string }? &
+ Resources?
+}
+
+Resources = element resources {
+ attribute vcpu { xsd:double { minExclusive = "0.0" } } &
+ attribute memory { xsd:string } &
+ attribute disk { xsd:string }
}
OptionalDedicatedNodes = element nodes {
diff --git a/config-model/src/main/resources/schema/containercluster.rnc b/config-model/src/main/resources/schema/containercluster.rnc
index 6bc54c433f3..d3a3b26c635 100644
--- a/config-model/src/main/resources/schema/containercluster.rnc
+++ b/config-model/src/main/resources/schema/containercluster.rnc
@@ -215,6 +215,7 @@ NodesOfContainerCluster = element nodes {
attribute preload { text }? &
attribute allocated-memory { text }? &
attribute cpu-socket-affinity { xsd:boolean }? &
+ Resources? &
element environment-variables {
anyElement +
} ? &
diff --git a/config-model/src/main/resources/schema/content.rnc b/config-model/src/main/resources/schema/content.rnc
index c23f99518cb..0686708a8a1 100644
--- a/config-model/src/main/resources/schema/content.rnc
+++ b/config-model/src/main/resources/schema/content.rnc
@@ -208,6 +208,7 @@ ContentNode = element node {
}
ContentNodes = element nodes {
+ Resources? &
attribute cpu-socket-affinity { xsd:string }? &
attribute mmap-core-limit { xsd:nonNegativeInteger }? &
attribute core-on-oom { xsd:boolean }? &
diff --git a/config-model/src/main/resources/schema/docproc.rnc b/config-model/src/main/resources/schema/docproc.rnc
index b5bd85ba095..3ee249c89d4 100644
--- a/config-model/src/main/resources/schema/docproc.rnc
+++ b/config-model/src/main/resources/schema/docproc.rnc
@@ -37,6 +37,7 @@ ClusterV3 = element cluster {
DocprocClusterAttributes? &
element nodes {
+ Resources? &
attribute jvmargs { text }? &
attribute preload { text }? &
element node {
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 6c9b9fdc084..06c9f78456c 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
@@ -704,7 +704,7 @@ public class ModelProvisioningTest {
}
@Test
- public void testSlobroksClustersAreExpandedToIncludeRetiredNodesWhenRetiredComesLast() throws ParseException {
+ public void testSlobroksClustersAreExpandedToIncludeRetiredNodesWhenRetiredComesLast() {
String services =
"<?xml version='1.0' encoding='utf-8' ?>\n" +
"<services>" +
@@ -718,7 +718,7 @@ public class ModelProvisioningTest {
VespaModelTester tester = new VespaModelTester();
tester.addHosts(numberOfHosts);
VespaModel model = tester.createModel(services, true, "default09", "default08");
- assertThat(model.getRoot().getHostSystem().getHosts().size(), is(numberOfHosts));
+ assertEquals(numberOfHosts, model.getRoot().getHostSystem().getHosts().size());
// Check slobroks clusters
assertEquals("Includes retired node", 3+2, model.getAdmin().getSlobroks().size());
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 801e138f3c7..866c4027711 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
@@ -14,6 +14,7 @@ import com.yahoo.config.model.provision.InMemoryProvisioner;
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.Zone;
import com.yahoo.vespa.model.VespaModel;
import com.yahoo.vespa.model.test.utils.ApplicationPackageUtils;
@@ -44,7 +45,7 @@ public class VespaModelTester {
private final ConfigModelRegistry configModelRegistry;
private boolean hosted = true;
- private Map<String, Collection<Host>> hostsByFlavor = new HashMap<>();
+ private Map<NodeResources, Collection<Host>> hostsByResources = new HashMap<>();
private ApplicationId applicationId = ApplicationId.defaultId();
private boolean useDedicatedNodeForLogserver = false;
private boolean enableMetricsProxyContainer = false;
@@ -62,24 +63,30 @@ public class VespaModelTester {
/** Adds some hosts to this system */
public Hosts addHosts(String flavor, int count) {
- return addHosts(Optional.empty(), flavor, count);
+ return addHosts(Optional.empty(), NodeResources.fromLegacyName(flavor), count);
}
public void addHosts(Flavor flavor, int count) {
- addHosts(Optional.of(flavor), flavor.name(), count);
+ addHosts(Optional.of(flavor), NodeResources.fromLegacyName(flavor.name()), count);
}
- private Hosts addHosts(Optional<Flavor> flavor, String flavorName, int count) {
+ public void addHosts(NodeResources resources, int count) {
+ addHosts(Optional.of(new Flavor(resources)), resources, count);
+ }
+
+ private Hosts addHosts(Optional<Flavor> flavor, NodeResources resources, int count) {
List<Host> hosts = new ArrayList<>();
for (int i = 0; i < count; ++i) {
// Let host names sort in the opposite order of the order the hosts are added
// This allows us to test index vs. name order selection when subsets of hosts are selected from a cluster
// (for e.g cluster controllers and slobrok nodes)
- String hostname = String.format("%s%02d", flavorName, count - i);
+ String hostname = String.format("%s%02d",
+ resources.allocateByLegacyName() ? resources.legacyName().get() : resources.toString(),
+ count - i);
hosts.add(new Host(hostname, ImmutableList.of(), flavor));
}
- this.hostsByFlavor.put(flavorName, hosts);
+ this.hostsByResources.put(resources, hosts);
if (hosts.size() > 100)
throw new IllegalStateException("The host naming scheme is nameNN. To test more than 100 hosts, change to nameNNN");
@@ -135,7 +142,7 @@ public class VespaModelTester {
ApplicationPackage appPkg = modelCreatorWithMockPkg.appPkg;
HostProvisioner provisioner = hosted ?
- new InMemoryProvisioner(hostsByFlavor, failOnOutOfCapacity, startIndexForClusters, retiredHostNames) :
+ new InMemoryProvisioner(hostsByResources, failOnOutOfCapacity, 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 e9b1672ce7d..f7f20d003ee 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,9 @@
</admin>
<jdisc id="container1" version="1.0">
- <nodes count="5" flavor="medium" required="true"/>
+ <nodes count="5" required="true">
+ <resources vcpu="1.2" memory="10Gb" disk="0.3 TB"/>
+ </nodes>
</jdisc>
<jdisc id="container1" version="1.0">
@@ -28,7 +30,9 @@
<content id="search" version="1.0">
<redundancy>2</redundancy>
- <nodes count="7" flavor="large" groups="12"/>
+ <nodes count="7" flavor="large" groups="12">
+ <resources vcpu="3.0" memory="32000.0Mb" disk="300 Gb"/>
+ </nodes>
</content>
</services>