diff options
35 files changed, 771 insertions, 244 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 448bd91c374..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 @@ -33,8 +33,6 @@ import java.util.Set; */ public class InMemoryProvisioner implements HostProvisioner { - private static final NodeResources defaultResources = new NodeResources(1, 3, 9); - /** * If this is true an exception is thrown when all nodes are used. * If false this will simply return nodes best effort, preferring to satisfy the @@ -56,27 +54,27 @@ public class InMemoryProvisioner implements HostProvisioner { /** Use this index as start index for all clusters */ private final int startIndexForClusters; - /** Creates this with a number of nodes with resources 1, 3, 9 */ + /** Creates this with a number of nodes of the flavor 'default' */ public InMemoryProvisioner(int nodeCount) { - this(Collections.singletonMap(defaultResources, + 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(defaultResources, + 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(defaultResources, + 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(defaultResources, + this(Collections.singletonMap(NodeResources.fromLegacyName("default"), hosts.asCollection()), failOnOutOfCapacity, startIndexForClusters, retiredHostNames); } @@ -112,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(defaultResources); - if (defaultHosts.isEmpty()) throw new IllegalArgumentException("No more hosts with default resources available"); - Host newHost = freeNodes.removeValue(defaultResources, 0); + 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(NodeResources.fromLegacyName("default"), 0); HostSpec hostSpec = new HostSpec(newHost.hostname(), newHost.aliases(), newHost.flavor(), Optional.empty(), newHost.version()); legacyMapping.put(alias, hostSpec); return hostSpec; @@ -130,11 +128,11 @@ public class InMemoryProvisioner implements HostProvisioner { int capacity = failOnOutOfCapacity || requestedCapacity.isRequired() ? requestedCapacity.nodeCount() - : Math.min(requestedCapacity.nodeCount(), freeNodes.get(defaultResources).size() + totalAllocatedTo(cluster)); + : Math.min(requestedCapacity.nodeCount(), freeNodes.get(NodeResources.fromLegacyName("default")).size() + totalAllocatedTo(cluster)); if (groups > capacity) groups = capacity; - NodeResources nodeResources = requestedCapacity.nodeResources().orElse(defaultResources); + NodeResources nodeResources = requestedCapacity.nodeResources().orElse(NodeResources.fromLegacyName("default")); List<HostSpec> allocation = new ArrayList<>(); if (groups == 1) { 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 eae9330a9dd..edc7f8691b2 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 @@ -414,16 +414,16 @@ public class ModelProvisioningTest { ClusterControllerContainerCluster clusterControllers = cluster.getClusterControllers(); assertEquals(3, clusterControllers.getContainers().size()); assertEquals("bar-controllers", clusterControllers.getName()); - assertEquals("node-1-3-9-54", clusterControllers.getContainers().get(0).getHostName()); - assertEquals("node-1-3-9-51", clusterControllers.getContainers().get(1).getHostName()); - assertEquals("node-1-3-9-48", clusterControllers.getContainers().get(2).getHostName()); + assertEquals("default54", clusterControllers.getContainers().get(0).getHostName()); + assertEquals("default51", clusterControllers.getContainers().get(1).getHostName()); + assertEquals("default48", clusterControllers.getContainers().get(2).getHostName()); assertEquals(0, cluster.getRootGroup().getNodes().size()); assertEquals(9, cluster.getRootGroup().getSubgroups().size()); assertThat(cluster.getRootGroup().getSubgroups().get(0).getIndex(), is("0")); assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().size(), is(3)); assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getDistributionKey(), is(0)); assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getConfigId(), is("bar/storage/0")); - assertEquals("node-1-3-9-54", cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getHostName()); + assertEquals("default54", cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getHostName()); assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(1).getDistributionKey(), is(1)); assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(1).getConfigId(), is("bar/storage/1")); assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(2).getDistributionKey(), is(2)); @@ -432,13 +432,13 @@ public class ModelProvisioningTest { assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().size(), is(3)); assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getDistributionKey(), is(3)); assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getConfigId(), is("bar/storage/3")); - assertEquals("node-1-3-9-51", cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getHostName()); + assertEquals("default51", cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getHostName()); assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(1).getDistributionKey(), is(4)); assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(1).getConfigId(), is("bar/storage/4")); assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(2).getDistributionKey(), is(5)); assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(2).getConfigId(), is("bar/storage/5")); // ... - assertEquals("node-1-3-9-48", cluster.getRootGroup().getSubgroups().get(2).getNodes().get(0).getHostName()); + assertEquals("default48", cluster.getRootGroup().getSubgroups().get(2).getNodes().get(0).getHostName()); // ... assertThat(cluster.getRootGroup().getSubgroups().get(8).getIndex(), is("8")); assertThat(cluster.getRootGroup().getSubgroups().get(8).getNodes().size(), is(3)); @@ -453,23 +453,23 @@ public class ModelProvisioningTest { clusterControllers = cluster.getClusterControllers(); assertEquals(3, clusterControllers.getContainers().size()); assertEquals("baz-controllers", clusterControllers.getName()); - assertEquals("node-1-3-9-27", clusterControllers.getContainers().get(0).getHostName()); - assertEquals("node-1-3-9-26", clusterControllers.getContainers().get(1).getHostName()); - assertEquals("node-1-3-9-25", clusterControllers.getContainers().get(2).getHostName()); + assertEquals("default27", clusterControllers.getContainers().get(0).getHostName()); + assertEquals("default26", clusterControllers.getContainers().get(1).getHostName()); + assertEquals("default25", clusterControllers.getContainers().get(2).getHostName()); assertEquals(0, cluster.getRootGroup().getNodes().size()); assertEquals(27, cluster.getRootGroup().getSubgroups().size()); assertThat(cluster.getRootGroup().getSubgroups().get(0).getIndex(), is("0")); assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().size(), is(1)); assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getDistributionKey(), is(0)); assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getConfigId(), is("baz/storage/0")); - assertEquals("node-1-3-9-27", cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getHostName()); + assertEquals("default27", cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getHostName()); assertThat(cluster.getRootGroup().getSubgroups().get(1).getIndex(), is("1")); assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().size(), is(1)); assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getDistributionKey(), is(1)); assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getConfigId(), is("baz/storage/1")); - assertEquals("node-1-3-9-26", cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getHostName()); + assertEquals("default26", cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getHostName()); // ... - assertEquals("node-1-3-9-25", cluster.getRootGroup().getSubgroups().get(2).getNodes().get(0).getHostName()); + assertEquals("default25", cluster.getRootGroup().getSubgroups().get(2).getNodes().get(0).getHostName()); // ... assertThat(cluster.getRootGroup().getSubgroups().get(26).getIndex(), is("26")); assertThat(cluster.getRootGroup().getSubgroups().get(26).getNodes().size(), is(1)); @@ -506,9 +506,9 @@ public class ModelProvisioningTest { ClusterControllerContainerCluster clusterControllers = cluster.getClusterControllers(); assertEquals(3, clusterControllers.getContainers().size()); assertEquals("bar-controllers", clusterControllers.getName()); - assertEquals("node-1-3-9-08", clusterControllers.getContainers().get(0).getHostName()); - assertEquals("node-1-3-9-07", clusterControllers.getContainers().get(1).getHostName()); - assertEquals("node-1-3-9-06", clusterControllers.getContainers().get(2).getHostName()); + assertEquals("default08", clusterControllers.getContainers().get(0).getHostName()); + assertEquals("default07", clusterControllers.getContainers().get(1).getHostName()); + assertEquals("default06", clusterControllers.getContainers().get(2).getHostName()); assertEquals(0, cluster.getRootGroup().getNodes().size()); assertEquals(8, cluster.getRootGroup().getSubgroups().size()); assertEquals(8, cluster.distributionBits()); @@ -517,19 +517,19 @@ public class ModelProvisioningTest { assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().size(), is(1)); assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getDistributionKey(), is(0)); assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getConfigId(), is("bar/storage/0")); - assertEquals("node-1-3-9-08", cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getHostName()); + assertEquals("default08", cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getHostName()); // second group assertThat(cluster.getRootGroup().getSubgroups().get(1).getIndex(), is("1")); assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().size(), is(1)); assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getDistributionKey(), is(1)); assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getConfigId(), is("bar/storage/1")); - assertEquals("node-1-3-9-07", cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getHostName()); + assertEquals("default07", cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getHostName()); // ... last group assertThat(cluster.getRootGroup().getSubgroups().get(7).getIndex(), is("7")); assertThat(cluster.getRootGroup().getSubgroups().get(7).getNodes().size(), is(1)); assertThat(cluster.getRootGroup().getSubgroups().get(7).getNodes().get(0).getDistributionKey(), is(7)); assertThat(cluster.getRootGroup().getSubgroups().get(7).getNodes().get(0).getConfigId(), is("bar/storage/7")); - assertEquals("node-1-3-9-01", cluster.getRootGroup().getSubgroups().get(7).getNodes().get(0).getHostName()); + assertEquals("default01", cluster.getRootGroup().getSubgroups().get(7).getNodes().get(0).getHostName()); } @Test @@ -563,15 +563,15 @@ public class ModelProvisioningTest { assertEquals( 8, cluster.distributionBits()); assertEquals("We get the closest odd number", 5, clusterControllers.getContainers().size()); assertEquals("bar-controllers", clusterControllers.getName()); - assertEquals("node-1-3-9-09", clusterControllers.getContainers().get(0).getHostName()); - assertEquals("node-1-3-9-08", clusterControllers.getContainers().get(1).getHostName()); - assertEquals("node-1-3-9-06", clusterControllers.getContainers().get(2).getHostName()); - assertEquals("node-1-3-9-05", clusterControllers.getContainers().get(3).getHostName()); - assertEquals("node-1-3-9-03", clusterControllers.getContainers().get(4).getHostName()); - assertEquals("node-1-3-9-09", cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getHostName()); - assertEquals("node-1-3-9-08", cluster.getRootGroup().getSubgroups().get(0).getNodes().get(1).getHostName()); - assertEquals("node-1-3-9-06", cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getHostName()); - assertEquals("node-1-3-9-03", cluster.getRootGroup().getSubgroups().get(2).getNodes().get(0).getHostName()); + assertEquals("default09", clusterControllers.getContainers().get(0).getHostName()); + assertEquals("default08", clusterControllers.getContainers().get(1).getHostName()); + assertEquals("default06", clusterControllers.getContainers().get(2).getHostName()); + assertEquals("default05", clusterControllers.getContainers().get(3).getHostName()); + assertEquals("default03", clusterControllers.getContainers().get(4).getHostName()); + assertEquals("default09", cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getHostName()); + assertEquals("default08", cluster.getRootGroup().getSubgroups().get(0).getNodes().get(1).getHostName()); + assertEquals("default06", cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getHostName()); + assertEquals("default03", cluster.getRootGroup().getSubgroups().get(2).getNodes().get(0).getHostName()); } @Test @@ -603,9 +603,9 @@ public class ModelProvisioningTest { ClusterControllerContainerCluster clusterControllers = cluster.getClusterControllers(); assertEquals("We get the closest odd number", 3, clusterControllers.getContainers().size()); assertEquals("bar-controllers", clusterControllers.getName()); - assertEquals("node-1-3-9-08", clusterControllers.getContainers().get(0).getHostName()); - assertEquals("node-1-3-9-06", clusterControllers.getContainers().get(1).getHostName()); - assertEquals("node-1-3-9-04", clusterControllers.getContainers().get(2).getHostName()); + assertEquals("default08", clusterControllers.getContainers().get(0).getHostName()); + assertEquals("default06", clusterControllers.getContainers().get(1).getHostName()); + assertEquals("default04", clusterControllers.getContainers().get(2).getHostName()); } @Test @@ -662,7 +662,7 @@ public class ModelProvisioningTest { int numberOfHosts = 19; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); - VespaModel model = tester.createModel(services, true, "node-1-3-9-09", "node-1-3-9-06", "node-1-3-9-03"); + VespaModel model = tester.createModel(services, true, "default09", "default06", "default03"); assertThat(model.getRoot().getHostSystem().getHosts().size(), is(numberOfHosts)); // Check content clusters @@ -670,9 +670,9 @@ public class ModelProvisioningTest { ClusterControllerContainerCluster clusterControllers = cluster.getClusterControllers(); assertEquals(3, clusterControllers.getContainers().size()); assertEquals("bar-controllers", clusterControllers.getName()); - assertEquals("Skipping retired default09", "node-1-3-9-08", clusterControllers.getContainers().get(0).getHostName()); - assertEquals("Skipping retired default06", "node-1-3-9-05", clusterControllers.getContainers().get(1).getHostName()); - assertEquals("Skipping retired default03", "node-1-3-9-02", clusterControllers.getContainers().get(2).getHostName()); + assertEquals("Skipping retired default09", "default08", clusterControllers.getContainers().get(0).getHostName()); + assertEquals("Skipping retired default06", "default05", clusterControllers.getContainers().get(1).getHostName()); + assertEquals("Skipping retired default03", "default02", clusterControllers.getContainers().get(2).getHostName()); } @Test @@ -689,15 +689,15 @@ public class ModelProvisioningTest { int numberOfHosts = 10; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); - VespaModel model = tester.createModel(services, true, "node-1-3-9-09"); + VespaModel model = tester.createModel(services, true, "default09"); assertThat(model.getRoot().getHostSystem().getHosts().size(), is(numberOfHosts)); // Check slobroks clusters assertEquals("Includes retired node", 1+3, model.getAdmin().getSlobroks().size()); - assertEquals("node-1-3-9-01", model.getAdmin().getSlobroks().get(0).getHostName()); - assertEquals("node-1-3-9-02", model.getAdmin().getSlobroks().get(1).getHostName()); - assertEquals("node-1-3-9-10", model.getAdmin().getSlobroks().get(2).getHostName()); - assertEquals("Included in addition because it is retired", "node-1-3-9-09", model.getAdmin().getSlobroks().get(3).getHostName()); + assertEquals("default01", model.getAdmin().getSlobroks().get(0).getHostName()); + assertEquals("default02", model.getAdmin().getSlobroks().get(1).getHostName()); + assertEquals("default10", model.getAdmin().getSlobroks().get(2).getHostName()); + assertEquals("Included in addition because it is retired", "default09", model.getAdmin().getSlobroks().get(3).getHostName()); } @Test @@ -714,16 +714,16 @@ public class ModelProvisioningTest { int numberOfHosts = 10; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); - VespaModel model = tester.createModel(services, true, "node-1-3-9-09", "node-1-3-9-08"); + VespaModel model = tester.createModel(services, true, "default09", "default08"); assertEquals(numberOfHosts, model.getRoot().getHostSystem().getHosts().size()); // Check slobroks clusters assertEquals("Includes retired node", 3+2, model.getAdmin().getSlobroks().size()); - assertEquals("node-1-3-9-01", model.getAdmin().getSlobroks().get(0).getHostName()); - assertEquals("node-1-3-9-02", model.getAdmin().getSlobroks().get(1).getHostName()); - assertEquals("node-1-3-9-10", model.getAdmin().getSlobroks().get(2).getHostName()); - assertEquals("Included in addition because it is retired", "node-1-3-9-08", model.getAdmin().getSlobroks().get(3).getHostName()); - assertEquals("Included in addition because it is retired", "node-1-3-9-09", model.getAdmin().getSlobroks().get(4).getHostName()); + assertEquals("default01", model.getAdmin().getSlobroks().get(0).getHostName()); + assertEquals("default02", model.getAdmin().getSlobroks().get(1).getHostName()); + assertEquals("default10", model.getAdmin().getSlobroks().get(2).getHostName()); + assertEquals("Included in addition because it is retired", "default08", model.getAdmin().getSlobroks().get(3).getHostName()); + assertEquals("Included in addition because it is retired", "default09", model.getAdmin().getSlobroks().get(4).getHostName()); } @Test @@ -743,19 +743,29 @@ public class ModelProvisioningTest { int numberOfHosts = 13; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); - VespaModel model = tester.createModel(services, true, "node-1-3-9-12", "node-1-3-9-03", "node-1-3-9-02"); + VespaModel model = tester.createModel(services, true, "default12", "default03", "default02"); assertThat(model.getRoot().getHostSystem().getHosts().size(), is(numberOfHosts)); // Check slobroks clusters // ... from cluster default assertEquals("Includes retired node", 3+3, model.getAdmin().getSlobroks().size()); - assertEquals("node-1-3-9-04", model.getAdmin().getSlobroks().get(0).getHostName()); - assertEquals("node-1-3-9-13", model.getAdmin().getSlobroks().get(1).getHostName()); - assertEquals("Included in addition because it is retired", "node-1-3-9-12", model.getAdmin().getSlobroks().get(2).getHostName()); + assertEquals("default04", model.getAdmin().getSlobroks().get(0).getHostName()); + assertEquals("default13", model.getAdmin().getSlobroks().get(1).getHostName()); + assertEquals("Included in addition because it is retired", "default12", model.getAdmin().getSlobroks().get(2).getHostName()); // ... from cluster bar - assertEquals("node-1-3-9-01", model.getAdmin().getSlobroks().get(3).getHostName()); - assertEquals("Included in addition because it is retired", "node-1-3-9-02", model.getAdmin().getSlobroks().get(4).getHostName()); - assertEquals("Included in addition because it is retired", "node-1-3-9-03", model.getAdmin().getSlobroks().get(5).getHostName()); + assertEquals("default01", model.getAdmin().getSlobroks().get(3).getHostName()); + assertEquals("Included in addition because it is retired", "default02", model.getAdmin().getSlobroks().get(4).getHostName()); + assertEquals("Included in addition because it is retired", "default03", model.getAdmin().getSlobroks().get(5).getHostName()); + } + + private Set<String> getClusterHostnames(VespaModel model, String clusterId) { + return model.getHosts().stream() + .filter(host -> host.getServices().stream() + .anyMatch(serviceInfo -> Objects.equals( + serviceInfo.getProperty("clustername"), + Optional.of(clusterId)))) + .map(HostInfo::getHostname) + .collect(Collectors.toSet()); } @Test @@ -886,10 +896,10 @@ public class ModelProvisioningTest { ClusterControllerContainerCluster clusterControllers = cluster.getClusterControllers(); assertEquals(4, clusterControllers.getContainers().size()); assertEquals("bar-controllers", clusterControllers.getName()); - assertEquals("node-1-3-9-04", clusterControllers.getContainers().get(0).getHostName()); - assertEquals("node-1-3-9-03", clusterControllers.getContainers().get(1).getHostName()); - assertEquals("node-1-3-9-02", clusterControllers.getContainers().get(2).getHostName()); - assertEquals("node-1-3-9-01", clusterControllers.getContainers().get(3).getHostName()); + assertEquals("default04", clusterControllers.getContainers().get(0).getHostName()); + assertEquals("default03", clusterControllers.getContainers().get(1).getHostName()); + assertEquals("default02", clusterControllers.getContainers().get(2).getHostName()); + assertEquals("default01", clusterControllers.getContainers().get(3).getHostName()); } @Test @@ -1055,7 +1065,7 @@ public class ModelProvisioningTest { ClusterControllerContainerCluster clusterControllers = cluster.getClusterControllers(); assertEquals(1, clusterControllers.getContainers().size()); assertEquals("bar-controllers", clusterControllers.getName()); - assertEquals("node-1-3-9-01", clusterControllers.getContainers().get(0).getHostName()); + assertEquals("default01", clusterControllers.getContainers().get(0).getHostName()); assertEquals(1, cluster.redundancy().effectiveInitialRedundancy()); // Reduced from 3*3 assertEquals(1, cluster.redundancy().effectiveFinalRedundancy()); // Reduced from 3*4 assertEquals(1, cluster.redundancy().effectiveReadyCopies()); // Reduced from 3*3 @@ -1127,6 +1137,47 @@ public class ModelProvisioningTest { } @Test + public void testRequestingSpecificFlavors() { + String services = + "<?xml version='1.0' encoding='utf-8' ?>\n" + + "<services>" + + " <admin version='4.0'>" + + " <logservers><nodes count='1' dedicated='true' flavor='logserver-flavor'/></logservers>" + + " <slobroks><nodes count='2' dedicated='true' flavor='slobrok-flavor'/></slobroks>" + + " </admin>" + + " <container version='1.0' id='container'>" + + " <nodes count='4' flavor='container-flavor'/>" + + " </container>" + + " <content version='1.0' id='foo'>" + + " <documents>" + + " <document type='type1' mode='index'/>" + + " </documents>" + + " <controllers><nodes count='2' dedicated='true' flavor='controller-foo-flavor'/></controllers>" + + " <nodes count='5' flavor='content-foo-flavor'/>" + + " </content>" + + " <content version='1.0' id='bar'>" + + " <documents>" + + " <document type='type1' mode='index'/>" + + " </documents>" + + " <controllers><nodes count='3' dedicated='true' flavor='controller-bar-flavor'/></controllers>" + + " <nodes count='6' flavor='content-bar-flavor'/>" + + " </content>" + + "</services>"; + + int totalHosts = 23; + VespaModelTester tester = new VespaModelTester(); + tester.addHosts("logserver-flavor", 1); + tester.addHosts("slobrok-flavor", 2); + tester.addHosts("container-flavor", 4); + tester.addHosts("controller-foo-flavor", 2); + tester.addHosts("content-foo-flavor", 5); + tester.addHosts("controller-bar-flavor", 3); + tester.addHosts("content-bar-flavor", 6); + VespaModel model = tester.createModel(services, true, 0); // fails unless the right flavors+counts are requested + assertEquals(totalHosts, model.getRoot().getHostSystem().getHosts().size()); + } + + @Test public void testRequestingSpecificNodeResources() { String services = "<?xml version='1.0' encoding='utf-8' ?>" + @@ -1705,21 +1756,19 @@ public class ModelProvisioningTest { } @Test - public void require_that_proton_config_is_tuned_based_on_node_resources() { + public void require_that_proton_config_is_tuned_based_on_node_flavor() { String services = joinLines("<?xml version='1.0' encoding='utf-8' ?>", "<services>", " <content version='1.0' id='test'>", " <documents>", " <document type='type1' mode='index'/>", " </documents>", - " <nodes count='2'>", - " <resources vcpu='1' memory='3Gb' disk='9Gb' disk-speed='slow'/>", - " </nodes>", + " <nodes count='2' flavor='content-test-flavor'/>", " </content>", "</services>"); VespaModelTester tester = new VespaModelTester(); - tester.addHosts(new NodeResources(1, 3, 9, NodeResources.DiskSpeed.slow), 2); + tester.addHosts(createFlavorFromDiskSetting("content-test-flavor", false), 2); VespaModel model = tester.createModel(services, true, 0); ContentSearchCluster cluster = model.getContentClusters().get("test").getSearch(); assertEquals(2, cluster.getSearchNodes().size()); @@ -1741,7 +1790,7 @@ public class ModelProvisioningTest { } @Test - public void require_that_config_override_and_explicit_proton_tuning_and_resource_limits_have_precedence_over_default_node_resource_tuning() { + public void require_that_config_override_and_explicit_proton_tuning_and_resource_limits_have_precedence_over_default_node_flavor_tuning() { String services = joinLines("<?xml version='1.0' encoding='utf-8' ?>", "<services>", " <content version='1.0' id='test'>", @@ -1751,9 +1800,7 @@ public class ModelProvisioningTest { " <documents>", " <document type='type1' mode='index'/>", " </documents>", - " <nodes count='1'>", - " <resources vcpu='1' memory='128Gb' disk='100Gb'/>", - " </nodes>", + " <nodes count='1' flavor='content-test-flavor'/>", " <engine>", " <proton>", " <resource-limits>", @@ -1776,8 +1823,8 @@ public class ModelProvisioningTest { "</services>"); VespaModelTester tester = new VespaModelTester(); - tester.addHosts(new NodeResources(1, 3, 9), 1); - tester.addHosts(new NodeResources(1, 128, 100), 1); + tester.addHosts("default", 1); + tester.addHosts(createFlavorFromMemoryAndDisk("content-test-flavor", 128, 100), 1); VespaModel model = tester.createModel(services, true, 0); ContentSearchCluster cluster = model.getContentClusters().get("test").getSearch(); ProtonConfig cfg = getProtonConfig(model, cluster.getSearchNodes().get(0).getConfigId()); 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 3f82a668fd0..2e5acb9025d 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 @@ -57,20 +57,20 @@ public class VespaModelTester { this.configModelRegistry = configModelRegistry; } - /** Adds some nodes with resources 1, 3, 9 */ - public Hosts addHosts(int count) { return addHosts(new NodeResources(1, 3, 9), count); } + /** Adds some hosts of the 'default' flavor to this system */ + public Hosts addHosts(int count) { return addHosts("default", count); } /** Adds some hosts to this system */ public Hosts addHosts(String flavor, int count) { return addHosts(Optional.empty(), NodeResources.fromLegacyName(flavor), count); } - public Hosts addHosts(Flavor flavor, int count) { - return addHosts(Optional.of(flavor), NodeResources.fromLegacyName(flavor.name()), count); + public void addHosts(Flavor flavor, int count) { + addHosts(Optional.of(flavor), NodeResources.fromLegacyName(flavor.name()), count); } - public Hosts addHosts(NodeResources resources, int count) { - return addHosts(Optional.of(new Flavor(resources)), resources, 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) { @@ -80,10 +80,8 @@ public class VespaModelTester { // 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", - "node" + "-" + Math.round(resources.vcpu()) + - "-" + Math.round(resources.memoryGb()) + - "-" + Math.round(resources.diskGb()), + String hostname = String.format("%s%02d", + resources.allocateByLegacyName() ? resources.legacyName().get() : resources.toString(), count - i); hosts.add(new Host(hostname, ImmutableList.of(), flavor)); } 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 dfa9ab7f6b8..f8535eda44f 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 @@ -41,7 +41,7 @@ public final class Capacity { @Deprecated public Optional<String> flavor() { if (nodeResources().isEmpty()) return Optional.empty(); - return nodeResources.map(n -> n.toString()); + return nodeResources.get().legacyName(); } /** Returns the resources requested for each node, or empty to leave this decision to provisioning */ diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/Flavor.java b/config-provisioning/src/main/java/com/yahoo/config/provision/Flavor.java index 2bc70efbc15..48c84b8ecb7 100644 --- a/config-provisioning/src/main/java/com/yahoo/config/provision/Flavor.java +++ b/config-provisioning/src/main/java/com/yahoo/config/provision/Flavor.java @@ -3,14 +3,15 @@ package com.yahoo.config.provision; import com.yahoo.config.provisioning.FlavorsConfig; -import java.util.Collections; +import java.util.ArrayList; import java.util.List; import java.util.Objects; /** * A host or node flavor. * *Host* flavors come from a configured set which corresponds to the actual flavors available in a zone. - * *Node* flavors are simply a wrapper of a NodeResources object. + * *Node* flavors are simply a wrapper of a NodeResources object (for now (May 2019) with the exception of some + * legacy behavior where nodes are allocated by specifying a physical host flavor directly). * * @author bratseth */ @@ -19,8 +20,11 @@ public class Flavor { private boolean configured; private final String name; private final int cost; + private final boolean isStock; private final Type type; private final double bandwidth; + private final boolean retired; + private List<Flavor> replacesFlavors; /** The hardware resources of this flavor */ private NodeResources resources; @@ -30,22 +34,31 @@ public class Flavor { this.configured = true; this.name = flavorConfig.name(); this.cost = flavorConfig.cost(); + this.isStock = flavorConfig.stock(); this.type = Type.valueOf(flavorConfig.environment()); this.resources = new NodeResources(flavorConfig.minCpuCores(), flavorConfig.minMainMemoryAvailableGb(), flavorConfig.minDiskAvailableGb(), flavorConfig.fastDisk() ? NodeResources.DiskSpeed.fast : NodeResources.DiskSpeed.slow); this.bandwidth = flavorConfig.bandwidth(); + this.retired = flavorConfig.retired(); + this.replacesFlavors = new ArrayList<>(); } /** Creates a *node* flavor from a node resources spec */ public Flavor(NodeResources resources) { Objects.requireNonNull(resources, "Resources cannot be null"); + if (resources.allocateByLegacyName()) + throw new IllegalArgumentException("Can not create flavor '" + resources.legacyName() + "' from a flavor: " + + "Non-docker flavors must be of a configured flavor"); this.configured = false; - this.name = resources.toString(); + this.name = resources.legacyName().orElse(resources.toString()); this.cost = 0; + this.isStock = true; this.type = Type.DOCKER_CONTAINER; this.bandwidth = 1; + this.retired = false; + this.replacesFlavors = List.of(); this.resources = resources; } @@ -60,6 +73,8 @@ public class Flavor { */ public int cost() { return cost; } + public boolean isStock() { return isStock; } + /** * True if this is a configured flavor used for hosts, * false if it is a virtual flavor created on the fly from node resources @@ -78,31 +93,65 @@ public class Flavor { public double getMinCpuCores() { return resources.vcpu(); } + /** Returns whether the flavor is retired */ + public boolean isRetired() { + return retired; + } + public Type getType() { return type; } /** Convenience, returns getType() == Type.DOCKER_CONTAINER */ public boolean isDocker() { return type == Type.DOCKER_CONTAINER; } - // TODO: Remove after August 2019 - public String canonicalName() { return name; } - - // TODO: Remove after August 2019 - public boolean satisfies(Flavor flavor) { return this.equals(flavor); } - - // TODO: Remove after August 2019 - public boolean isStock() { return false; } - - // TODO: Remove after August 2019 - public boolean isRetired() { return false; } + /** + * Returns the canonical name of this flavor - which is the name which should be used as an interface to users. + * The canonical name of this flavor is: + * <ul> + * <li>If it replaces one flavor, the canonical name of the flavor it replaces + * <li>If it replaces multiple or no flavors - itself + * </ul> + * + * The logic is that we can use this to capture the gritty details of configurations in exact flavor names + * but also encourage users to refer to them by a common name by letting such flavor variants declare that they + * replace the canonical name we want. However, if a node replaces multiple names, we have no basis for choosing one + * of them as the canonical, so we return the current as canonical. + */ + public String canonicalName() { + return isCanonical() ? name : replacesFlavors.get(0).canonicalName(); + } + + /** Returns whether this is a canonical flavor */ + public boolean isCanonical() { + return replacesFlavors.size() != 1; + } - // TODO: Remove after August 2019 - public boolean isCanonical() { return false; } + /** + * The flavors this (directly) replaces. + * This is immutable if this is frozen, and a mutable list otherwise. + */ + public List<Flavor> replaces() { return replacesFlavors; } - // TODO: Remove after August 2019 - public List<Flavor> replaces() { return Collections.emptyList(); } + /** + * Returns whether this flavor satisfies the requested flavor, either directly + * (by being the same), or by directly or indirectly replacing it + */ + public boolean satisfies(Flavor flavor) { + if (this.equals(flavor)) { + return true; + } + if (this.retired) { + return false; + } + for (Flavor replaces : replacesFlavors) + if (replaces.satisfies(flavor)) + return true; + return false; + } - // TODO: Remove after August 2019 - public void freeze() {} + /** Irreversibly freezes the content of this */ + public void freeze() { + replacesFlavors = List.copyOf(replacesFlavors); + } @Override public int hashCode() { return name.hashCode(); } diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/NodeFlavors.java b/config-provisioning/src/main/java/com/yahoo/config/provision/NodeFlavors.java index a9f031cae70..4d4d3c8cf86 100644 --- a/config-provisioning/src/main/java/com/yahoo/config/provision/NodeFlavors.java +++ b/config-provisioning/src/main/java/com/yahoo/config/provision/NodeFlavors.java @@ -41,7 +41,10 @@ public class NodeFlavors { return Optional.of(configuredFlavors.get(name)); NodeResources nodeResources = NodeResources.fromLegacyName(name); - return Optional.of(new Flavor(nodeResources)); + if (nodeResources.allocateByLegacyName()) + return Optional.empty(); + else + return Optional.of(new Flavor(nodeResources)); } /** @@ -49,7 +52,8 @@ public class NodeFlavors { * and cannot be created on the fly. */ public Flavor getFlavorOrThrow(String flavorName) { - return getFlavor(flavorName).orElseThrow(() -> new IllegalArgumentException("Unknown flavor '" + flavorName + "'")); + return getFlavor(flavorName).orElseThrow(() -> new IllegalArgumentException("Unknown flavor '" + flavorName + + "'. Flavors are " + canonicalFlavorNames())); } /** Returns true if this flavor is configured or can be created on the fly */ @@ -57,8 +61,43 @@ public class NodeFlavors { return getFlavor(flavorName).isPresent(); } + private List<String> canonicalFlavorNames() { + return configuredFlavors.values().stream().map(Flavor::canonicalName).distinct().sorted().collect(Collectors.toList()); + } + private static Collection<Flavor> toFlavors(FlavorsConfig config) { - return config.flavor().stream().map(Flavor::new).collect(Collectors.toList()); + Map<String, Flavor> flavors = new HashMap<>(); + // First pass, create all flavors, but do not include flavorReplacesConfig. + for (FlavorsConfig.Flavor flavorConfig : config.flavor()) { + flavors.put(flavorConfig.name(), new Flavor(flavorConfig)); + } + // Second pass, set flavorReplacesConfig to point to correct flavor. + for (FlavorsConfig.Flavor flavorConfig : config.flavor()) { + Flavor flavor = flavors.get(flavorConfig.name()); + for (FlavorsConfig.Flavor.Replaces flavorReplacesConfig : flavorConfig.replaces()) { + if (! flavors.containsKey(flavorReplacesConfig.name())) { + throw new IllegalStateException("Replaces for " + flavor.name() + + " pointing to a non existing flavor: " + flavorReplacesConfig.name()); + } + flavor.replaces().add(flavors.get(flavorReplacesConfig.name())); + } + flavor.freeze(); + } + // Third pass, ensure that retired flavors have a replacement + for (Flavor flavor : flavors.values()) { + if (flavor.isRetired() && !hasReplacement(flavors.values(), flavor)) { + throw new IllegalStateException( + String.format("Flavor '%s' is retired, but has no replacement", flavor.name()) + ); + } + } + return flavors.values(); + } + + private static boolean hasReplacement(Collection<Flavor> flavors, Flavor flavor) { + return flavors.stream() + .filter(f -> !f.equals(flavor)) + .anyMatch(f -> f.satisfies(flavor)); } } diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/NodeResources.java b/config-provisioning/src/main/java/com/yahoo/config/provision/NodeResources.java index 8ef48f7048f..7e90767c9c5 100644 --- a/config-provisioning/src/main/java/com/yahoo/config/provision/NodeResources.java +++ b/config-provisioning/src/main/java/com/yahoo/config/provision/NodeResources.java @@ -1,6 +1,7 @@ // Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.config.provision; +import java.util.Objects; import java.util.Optional; /** @@ -21,6 +22,11 @@ public class NodeResources { private final double diskGb; private final DiskSpeed diskSpeed; + private final boolean allocateByLegacyName; + + /** The legacy (flavor) name of this, or null if none */ + private final String legacyName; + /** Create node resources requiring fast disk */ public NodeResources(double vcpu, double memoryGb, double diskGb) { this(vcpu, memoryGb, diskGb, DiskSpeed.fast); @@ -31,6 +37,18 @@ public class NodeResources { this.memoryGb = memoryGb; this.diskGb = diskGb; this.diskSpeed = diskSpeed; + this.allocateByLegacyName = false; + this.legacyName = null; + } + + private NodeResources(double vcpu, double memoryGb, double diskGb, DiskSpeed diskSpeed, + boolean allocateByLegacyName, String legacyName) { + this.vcpu = vcpu; + this.memoryGb = memoryGb; + this.diskGb = diskGb; + this.diskSpeed = diskSpeed; + this.allocateByLegacyName = allocateByLegacyName; + this.legacyName = legacyName; } public double vcpu() { return vcpu; } @@ -64,17 +82,24 @@ public class NodeResources { combine(this.diskSpeed, other.diskSpeed)); } - // TODO: Remove after August 2019 + /** + * If this is true, a non-docker legacy name was used to specify this and we'll respect that by mapping directly. + * The other getters of this will return 0. + */ + public boolean allocateByLegacyName() { return allocateByLegacyName; } + + /** Returns the legacy name of this, or empty if none. */ public Optional<String> legacyName() { - return Optional.of(toString()); + return Optional.ofNullable(legacyName); } - // TODO: Remove after August 2019 - public boolean allocateByLegacyName() { return false; } - private boolean isInterchangeableWith(NodeResources other) { + if (this.allocateByLegacyName != other.allocateByLegacyName) return false; + if (this.allocateByLegacyName) return legacyName.equals(other.legacyName); + if (this.diskSpeed != DiskSpeed.any && other.diskSpeed != DiskSpeed.any && this.diskSpeed != other.diskSpeed) return false; + return true; } @@ -90,26 +115,40 @@ public class NodeResources { if (o == this) return true; if ( ! (o instanceof NodeResources)) return false; NodeResources other = (NodeResources)o; - if (this.vcpu != other.vcpu) return false; - if (this.memoryGb != other.memoryGb) return false; - if (this.diskGb != other.diskGb) return false; - if (this.diskSpeed != other.diskSpeed) return false; - return true; + if (allocateByLegacyName) { + return this.legacyName.equals(other.legacyName); + } + else { + if (this.vcpu != other.vcpu) return false; + if (this.memoryGb != other.memoryGb) return false; + if (this.diskGb != other.diskGb) return false; + if (this.diskSpeed != other.diskSpeed) return false; + return true; + } } @Override public int hashCode() { - return (int)(2503 * vcpu + 22123 * memoryGb + 26987 * diskGb + diskSpeed.hashCode()); + if (allocateByLegacyName) + return legacyName.hashCode(); + else + return (int)(2503 * vcpu + 22123 * memoryGb + 26987 * diskGb + diskSpeed.hashCode()); } @Override public String toString() { - return "[vcpu: " + vcpu + ", memory: " + memoryGb + " Gb, disk " + diskGb + " Gb" + - (diskSpeed != DiskSpeed.fast ? ", disk speed: " + diskSpeed : "") + "]"; + if (allocateByLegacyName) + return "flavor '" + legacyName + "'"; + else + return "[vcpu: " + vcpu + ", memory: " + memoryGb + " Gb, disk " + diskGb + " Gb" + + (diskSpeed != DiskSpeed.fast ? ", disk speed: " + diskSpeed : "") + "]"; } /** Returns true if all the resources of this are the same or larger than the given resources */ public boolean satisfies(NodeResources other) { + if (this.allocateByLegacyName || other.allocateByLegacyName) // resources are not available + return Objects.equals(this.legacyName, other.legacyName); + if (this.vcpu < other.vcpu) return false; if (this.memoryGb < other.memoryGb) return false; if (this.diskGb < other.diskGb) return false; @@ -124,6 +163,9 @@ public class NodeResources { /** Returns true if all the resources of this are the same as or compatible with the given resources */ public boolean compatibleWith(NodeResources other) { + if (this.allocateByLegacyName || other.allocateByLegacyName) // resources are not available + return Objects.equals(this.legacyName, other.legacyName); + if (this.vcpu != other.vcpu) return false; if (this.memoryGb != other.memoryGb) return false; if (this.diskGb != other.diskGb) return false; @@ -137,20 +179,20 @@ public class NodeResources { * * @throws IllegalArgumentException if the given string cannot be parsed as a serial form of this */ - public static NodeResources fromLegacyName(String name) { - if ( ! name.startsWith("d-")) - throw new IllegalArgumentException("A node specification string must start by 'd-' but was '" + name + "'"); - String[] parts = name.split("-"); - if (parts.length != 4) - throw new IllegalArgumentException("A node specification string must contain three numbers separated by '-' but was '" + name + "'"); - - double cpu = Integer.parseInt(parts[1]); - double mem = Integer.parseInt(parts[2]); - double dsk = Integer.parseInt(parts[3]); - if (cpu == 0) cpu = 0.5; - if (cpu == 2 && mem == 8 ) cpu = 1.5; - if (cpu == 2 && mem == 12 ) cpu = 2.3; - return new NodeResources(cpu, mem, dsk, DiskSpeed.fast); + public static NodeResources fromLegacyName(String flavorString) { + if (flavorString.startsWith("d-")) { // A legacy docker flavor: We still allocate by numbers + String[] parts = flavorString.split("-"); + double cpu = Integer.parseInt(parts[1]); + double mem = Integer.parseInt(parts[2]); + double dsk = Integer.parseInt(parts[3]); + if (cpu == 0) cpu = 0.5; + if (cpu == 2 && mem == 8 ) cpu = 1.5; + if (cpu == 2 && mem == 12 ) cpu = 2.3; + return new NodeResources(cpu, mem, dsk, DiskSpeed.fast, false, flavorString); + } + else { // Another legacy flavor: Allocate by direct matching + return new NodeResources(0, 0, 0, DiskSpeed.fast, true, flavorString); + } } } diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/serialization/AllocatedHostsSerializer.java b/config-provisioning/src/main/java/com/yahoo/config/provision/serialization/AllocatedHostsSerializer.java index 9fcee6b60ed..9e01718bfc6 100644 --- a/config-provisioning/src/main/java/com/yahoo/config/provision/serialization/AllocatedHostsSerializer.java +++ b/config-provisioning/src/main/java/com/yahoo/config/provision/serialization/AllocatedHostsSerializer.java @@ -154,6 +154,7 @@ public class AllocatedHostsSerializer { } private static NodeResources.DiskSpeed diskSpeedFromSlime(Inspector diskSpeed) { + if ( ! diskSpeed.valid()) return NodeResources.DiskSpeed.fast; // TODO: Remove this line after June 2019 switch (diskSpeed.asString()) { case "fast" : return NodeResources.DiskSpeed.fast; case "slow" : return NodeResources.DiskSpeed.slow; diff --git a/config-provisioning/src/main/resources/configdefinitions/flavors.def b/config-provisioning/src/main/resources/configdefinitions/flavors.def index 131c23054a2..1cfb18d2cd2 100644 --- a/config-provisioning/src/main/resources/configdefinitions/flavors.def +++ b/config-provisioning/src/main/resources/configdefinitions/flavors.def @@ -7,14 +7,22 @@ namespace=config.provisioning # If a certain flavor has no config it is not necessary to list it here to use it. flavor[].name string -# NOT USED: TODO: Remove after August 2019 +# Names of other flavors (whether mentioned in this config or not) which this flavor +# is a replacement for: If one of these flavor names are requested, this flavor may +# be assigned instead. +# Replacements are transitive: If flavor a replaces b replaces c, then a request for flavor +# c may be satisfied by assigning nodes of flavor a. flavor[].replaces[].name string # The monthly Total Cost of Ownership (TCO) in USD. Typically calculated as TCO divided by # the expected lifetime of the node (usually three years). flavor[].cost int default=0 -# NOT USED: TODO: Remove after August 2019 +# A stock flavor is any flavor which we expect to buy more of in the future. +# Stock flavors are assigned to applications by cost priority. +# +# Non-stock flavors are used for nodes for which a fixed amount has already been purchased +# for some historical reason. These nodes are assigned to applications by exact match and ignoring cost. flavor[].stock bool default=true # The type of node: BARE_METAL, VIRTUAL_MACHINE or DOCKER_CONTAINER @@ -35,6 +43,6 @@ flavor[].fastDisk bool default=true # Expected network interface bandwidth available for this flavor, in Mbit/s. flavor[].bandwidth double default=0.0 -# NOT USED: TODO: Remove after August 2019 +# The flavor is retired and should no longer be used. flavor[].retired bool default=false diff --git a/config-provisioning/src/test/java/com/yahoo/config/provision/NodeFlavorsTest.java b/config-provisioning/src/test/java/com/yahoo/config/provision/NodeFlavorsTest.java index ec3f73a8194..55ffa821e26 100644 --- a/config-provisioning/src/test/java/com/yahoo/config/provision/NodeFlavorsTest.java +++ b/config-provisioning/src/test/java/com/yahoo/config/provision/NodeFlavorsTest.java @@ -2,22 +2,47 @@ package com.yahoo.config.provision; import com.yahoo.config.provisioning.FlavorsConfig; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import java.util.ArrayList; import java.util.List; -import static org.junit.Assert.assertEquals; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + public class NodeFlavorsTest { + @Rule + public final ExpectedException exception = ExpectedException.none(); + + @Test + public void testReplacesWithBadValue() { + FlavorsConfig.Builder builder = new FlavorsConfig.Builder(); + List<FlavorsConfig.Flavor.Builder> flavorBuilderList = new ArrayList<>(); + FlavorsConfig.Flavor.Builder flavorBuilder = new FlavorsConfig.Flavor.Builder(); + FlavorsConfig.Flavor.Replaces.Builder flavorReplacesBuilder = new FlavorsConfig.Flavor.Replaces.Builder(); + flavorReplacesBuilder.name("non-existing-config"); + flavorBuilder.name("strawberry").cost(2).replaces.add(flavorReplacesBuilder); + flavorBuilderList.add(flavorBuilder); + builder.flavor(flavorBuilderList); + FlavorsConfig config = new FlavorsConfig(builder); + exception.expect(IllegalStateException.class); + exception.expectMessage("Replaces for strawberry pointing to a non existing flavor: non-existing-config"); + new NodeFlavors(config); + } + @Test public void testConfigParsing() { FlavorsConfig.Builder builder = new FlavorsConfig.Builder(); List<FlavorsConfig.Flavor.Builder> flavorBuilderList = new ArrayList<>(); { FlavorsConfig.Flavor.Builder flavorBuilder = new FlavorsConfig.Flavor.Builder(); - flavorBuilder.name("strawberry").cost(2); + FlavorsConfig.Flavor.Replaces.Builder flavorReplacesBuilder = new FlavorsConfig.Flavor.Replaces.Builder(); + flavorReplacesBuilder.name("banana"); + flavorBuilder.name("strawberry").cost(2).replaces.add(flavorReplacesBuilder); flavorBuilderList.add(flavorBuilder); } { @@ -28,7 +53,28 @@ public class NodeFlavorsTest { builder.flavor(flavorBuilderList); FlavorsConfig config = new FlavorsConfig(builder); NodeFlavors nodeFlavors = new NodeFlavors(config); - assertEquals(3, nodeFlavors.getFlavor("banana").get().cost()); + assertThat(nodeFlavors.getFlavor("banana").get().cost(), is(3)); + } + + @Test + public void testRetiredFlavorWithoutReplacement() { + FlavorsConfig.Builder builder = new FlavorsConfig.Builder(); + List<FlavorsConfig.Flavor.Builder> flavorBuilderList = new ArrayList<>(); + { + FlavorsConfig.Flavor.Builder flavorBuilder = new FlavorsConfig.Flavor.Builder(); + flavorBuilder.name("retired").retired(true); + flavorBuilderList.add(flavorBuilder); + } + { + FlavorsConfig.Flavor.Builder flavorBuilder = new FlavorsConfig.Flavor.Builder(); + flavorBuilder.name("chocolate"); + flavorBuilderList.add(flavorBuilder); + } + builder.flavor(flavorBuilderList); + FlavorsConfig config = new FlavorsConfig(builder); + exception.expect(IllegalStateException.class); + exception.expectMessage("Flavor 'retired' is retired, but has no replacement"); + new NodeFlavors(config); } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java index d83dea7007b..327f37335bf 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java @@ -182,7 +182,7 @@ public final class Node { /** Returns a copy of this node which is retired */ public Node retire(Instant retiredAt) { - if (status.wantToRetire()) + if (flavor.isRetired() || status.wantToRetire()) return retire(Agent.system, retiredAt); else return retire(Agent.application, retiredAt); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeList.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeList.java index 4ec2bac0159..2e17c2d6f67 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeList.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeList.java @@ -49,9 +49,6 @@ public class NodeList implements Iterable<Node> { /** Returns the subset of nodes having exactly the given resources */ public NodeList resources(NodeResources resources) { return filter(node -> node.flavor().resources().equals(resources)); } - /** Returns the subset of nodes not having exactly the given resources */ - public NodeList notResources(NodeResources resources) { return filter(node -> ! node.flavor().resources().equals(resources)); } - /** Returns the subset of nodes of the given flavor */ public NodeList flavor(String flavor) { return filter(node -> node.flavor().name().equals(flavor)); 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 64e2df78642..6cee3005e91 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 @@ -7,6 +7,7 @@ import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.Zone; +import com.yahoo.config.provision.NodeFlavors; import com.yahoo.vespa.flags.FlagSource; import com.yahoo.vespa.flags.Flags; @@ -21,10 +22,12 @@ import java.util.Optional; public class CapacityPolicies { private final Zone zone; + private final NodeFlavors flavors; private final FlagSource flagSource; - public CapacityPolicies(Zone zone, FlagSource flagSource) { + public CapacityPolicies(Zone zone, NodeFlavors flavors, FlagSource flagSource) { this.zone = zone; + this.flavors = flavors; this.flagSource = flagSource; } @@ -42,7 +45,9 @@ public class CapacityPolicies { } public NodeResources decideNodeResources(Optional<NodeResources> requestedResources, ClusterSpec cluster) { - NodeResources resources = requestedResources.orElse(defaultNodeResources(cluster.type())); + NodeResources resources = specifiedOrDefaultNodeResources(requestedResources, cluster); + + if (resources.allocateByLegacyName()) return resources; // Modification not possible // Allow slow disks in zones which are not performance sensitive if (zone.system().isCd() || zone.environment() == Environment.dev || zone.environment() == Environment.test) @@ -55,6 +60,24 @@ public class CapacityPolicies { return resources; } + private NodeResources specifiedOrDefaultNodeResources(Optional<NodeResources> requestedResources, ClusterSpec cluster) { + if (requestedResources.isPresent() && ! requestedResources.get().allocateByLegacyName()) + return requestedResources.get(); + + if (requestedResources.isEmpty()) + return defaultNodeResources(cluster.type()); + + switch (zone.environment()) { + case dev: case test: case staging: return defaultNodeResources(cluster.type()); + default: + flavors.getFlavorOrThrow(requestedResources.get().legacyName().get()); // verify existence + // Return this spec containing the legacy flavor name, not the flavor's capacity object + // which describes the flavors capacity, as the point of legacy allocation is to match + // by name, not by resources + return requestedResources.get(); + } + } + private NodeResources defaultNodeResources(ClusterSpec.Type clusterType) { if (clusterType == ClusterSpec.Type.admin) return nodeResourcesForAdminCluster(); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/FlavorConfigBuilder.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/FlavorConfigBuilder.java index 4d3f2308c03..fbf97ba25d9 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/FlavorConfigBuilder.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/FlavorConfigBuilder.java @@ -30,6 +30,12 @@ public class FlavorConfigBuilder { return flavor; } + public void addReplaces(String replaces, FlavorsConfig.Flavor.Builder flavor) { + FlavorsConfig.Flavor.Replaces.Builder flavorReplaces = new FlavorsConfig.Flavor.Replaces.Builder(); + flavorReplaces.name(replaces); + flavor.replaces(flavorReplaces); + } + public void addCost(int cost, FlavorsConfig.Flavor.Builder flavor) { flavor.cost(cost); } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeAllocation.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeAllocation.java index 8b79a303dbd..d4527452e9c 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeAllocation.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeAllocation.java @@ -111,6 +111,7 @@ class NodeAllocation { // conditions on which we want to retire nodes that were allocated previously if ( violatesParentHostPolicy(this.nodes, offered)) wantToRetireNode = true; if ( ! hasCompatibleFlavor(offered)) wantToRetireNode = true; + if ( offered.flavor().isRetired()) wantToRetireNode = true; if ( offered.status().wantToRetire()) wantToRetireNode = true; if ( requestedNodes.isExclusive() && ! hostsOnly(application.tenant(), offered.parentHostname())) wantToRetireNode = true; @@ -132,6 +133,9 @@ class NodeAllocation { ++rejectedDueToExclusivity; continue; } + if (offered.flavor().isRetired()) { + continue; + } if (offered.status().wantToRetire()) { continue; } @@ -283,6 +287,7 @@ class NodeAllocation { .filter(NodeSpec.CountNodeSpec.class::isInstance) .map(NodeSpec.CountNodeSpec.class::cast) .map(spec -> new FlavorCount(spec.resources(), spec.fulfilledDeficitCount(acceptedOfRequestedFlavor))) + .filter(flavorCount -> ! flavorCount.getFlavor().allocateByLegacyName()) .filter(flavorCount -> flavorCount.getCount() > 0); } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodePrioritizer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodePrioritizer.java index f6554b2dede..6b27662448c 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodePrioritizer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodePrioritizer.java @@ -215,7 +215,8 @@ class NodePrioritizer { private PrioritizableNode toNodePriority(Node node, boolean isSurplusNode, boolean isNewNode) { PrioritizableNode.Builder builder = new PrioritizableNode.Builder(node) .withSurplusNode(isSurplusNode) - .withNewNode(isNewNode); + .withNewNode(isNewNode) + .withPreferredOnFlavor(preferredOnLegacyFlavor(node)); allNodes.parentOf(node).ifPresent(parent -> { builder.withParent(parent).withFreeParentCapacity(capacity.freeCapacityOf(parent, false)); @@ -228,6 +229,18 @@ class NodePrioritizer { return builder.build(); } + /** Needed to handle requests for legacy non-docker nodes only */ + private boolean preferredOnLegacyFlavor(Node node) { + if (requestedNodes instanceof NodeSpec.CountNodeSpec) { + NodeResources requestedNodeResources = ((NodeSpec.CountNodeSpec)requestedNodes).resources(); + if (requestedNodeResources.allocateByLegacyName()) { + Flavor requestedFlavor = flavors.getFlavorOrThrow(requestedNodeResources.legacyName().get()); + return ! requestedFlavor.isStock() && node.flavor().equals(requestedFlavor); + } + } + return false; + } + static boolean isPreferredNodeToBeRelocated(List<Node> nodes, Node node, Node parent) { NodeList list = new NodeList(nodes); return list.childrenOf(parent).asList().stream() @@ -248,7 +261,8 @@ class NodePrioritizer { } private boolean isDocker() { - return resources(requestedNodes) != null; + NodeResources flavor = resources(requestedNodes); + return (flavor != null) && ! flavor.allocateByLegacyName(); } private static int compareForRelocation(Node a, Node b) { 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 29e37b76ac3..1cf5cfbb4f3 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 @@ -57,10 +57,10 @@ public class NodeRepositoryProvisioner implements Provisioner { } @Inject - public NodeRepositoryProvisioner(NodeRepository nodeRepository, Zone zone, + public NodeRepositoryProvisioner(NodeRepository nodeRepository, NodeFlavors flavors, Zone zone, ProvisionServiceProvider provisionServiceProvider, FlagSource flagSource) { this.nodeRepository = nodeRepository; - this.capacityPolicies = new CapacityPolicies(zone, flagSource); + this.capacityPolicies = new CapacityPolicies(zone, flavors, flagSource); this.zone = zone; this.loadBalancerProvisioner = provisionServiceProvider.getLoadBalancerService().map(lbService -> new LoadBalancerProvisioner(nodeRepository, lbService)); this.preparer = new Preparer(nodeRepository, diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java index 865b643b93b..66a8f2f8f6d 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java @@ -92,13 +92,19 @@ public interface NodeSpec { @Override public boolean isCompatible(Flavor flavor, NodeFlavors flavors) { - if (flavor.isDocker()) { // Docker nodes can satisfy a request for parts of their resources - if (flavor.resources().compatibleWith(requestedNodeResources)) + if (requestedNodeResources.allocateByLegacyName() && flavor.isConfigured()) { + if (flavor.satisfies(flavors.getFlavorOrThrow(requestedNodeResources.legacyName().get()))) return true; } - else { // Other nodes must be matched exactly - if (requestedNodeResources.equals(flavor.resources())) - return true; + else { + if (flavor.isDocker()) { // Docker nodes can satisfy a request for parts of their resources + if (flavor.resources().compatibleWith(requestedNodeResources)) + return true; + } + else { // Other nodes must be matched exactly + if (requestedNodeResources.equals(flavor.resources())) + return true; + } } return requestedFlavorCanBeAchievedByResizing(flavor); } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodesResponse.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodesResponse.java index df817ebb8ad..be34e11f585 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodesResponse.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodesResponse.java @@ -145,7 +145,7 @@ class NodesResponse extends HttpResponse { } object.setString("openStackId", node.id()); object.setString("flavor", node.flavor().name()); - object.setString("canonicalFlavor", node.flavor().name()); + object.setString("canonicalFlavor", node.flavor().canonicalName()); object.setDouble("minDiskAvailableGb", node.flavor().getMinDiskAvailableGb()); object.setDouble("minMainMemoryAvailableGb", node.flavor().getMinMainMemoryAvailableGb()); object.setDouble("minCpuCores", node.flavor().getMinCpuCores()); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeFlavors.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeFlavors.java index a87f5af715c..0992fbc75ca 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeFlavors.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeFlavors.java @@ -24,8 +24,11 @@ public class MockNodeFlavors extends NodeFlavors { b.addFlavor("docker", 0.2, 0.5, 100, Flavor.Type.DOCKER_CONTAINER); b.addFlavor("d-2-8-100", 2, 8, 100, Flavor.Type.DOCKER_CONTAINER); b.addFlavor("v-4-8-100", 4.0, 8.0, 100, Flavor.Type.VIRTUAL_MACHINE); - b.addFlavor("large-variant", 64, 128, 2000, Flavor.Type.BARE_METAL); - b.addFlavor("expensive", 6, 12, 500, Flavor.Type.BARE_METAL); + FlavorsConfig.Flavor.Builder largeVariant = b.addFlavor("large-variant", 64, 128, 2000, Flavor.Type.BARE_METAL); + b.addReplaces("large", largeVariant); + FlavorsConfig.Flavor.Builder expensiveFlavor = b.addFlavor("expensive", 6, 12, 500, Flavor.Type.BARE_METAL); + b.addReplaces("default", expensiveFlavor); + b.addCost(200, expensiveFlavor); return b.build(); } 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 3da1d14541e..a586bfa15c2 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 @@ -58,6 +58,7 @@ public class MockNodeRepository extends NodeRepository { private void populate() { NodeRepositoryProvisioner provisioner = new NodeRepositoryProvisioner(this, + flavors, Zone.defaultZone(), new MockProvisionServiceProvider(), new InMemoryFlagSource()); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/FailedExpirerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/FailedExpirerTest.java index 89c6ed6aa0d..6be03e7969a 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/FailedExpirerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/FailedExpirerTest.java @@ -49,13 +49,8 @@ import static org.junit.Assert.assertEquals; public class FailedExpirerTest { private static final ApplicationId tenantHostApplicationId = ApplicationId.from("vespa", "zone-app", "default"); - - private static final ClusterSpec tenantHostApplicationClusterSpec = - ClusterSpec.request(ClusterSpec.Type.container, - ClusterSpec.Id.from("node-admin"), - Version.fromString("6.42"), - false); - + private static final ClusterSpec tenantHostApplicationClusterSpec = ClusterSpec.request( + ClusterSpec.Type.container, ClusterSpec.Id.from("node-admin"), Version.fromString("6.42"), false); private static final Capacity tenantHostApplicationCapacity = Capacity.fromRequiredNodeType(NodeType.host); @Test @@ -280,7 +275,7 @@ public class FailedExpirerTest { new MockNameResolver().mockAnyLookup(), DockerImage.fromString("docker-image"), true); - this.provisioner = new NodeRepositoryProvisioner(nodeRepository, Zone.defaultZone(), new MockProvisionServiceProvider(), new InMemoryFlagSource()); + this.provisioner = new NodeRepositoryProvisioner(nodeRepository, nodeFlavors, Zone.defaultZone(), new MockProvisionServiceProvider(), new InMemoryFlagSource()); this.expirer = new FailedExpirer(nodeRepository, zone, clock, Duration.ofMinutes(30)); } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailTester.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailTester.java index f4b36b12bff..4e82bdbfafe 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailTester.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailTester.java @@ -76,7 +76,7 @@ public class NodeFailTester { curator = new MockCurator(); nodeRepository = new NodeRepository(nodeFlavors, curator, clock, zone, new MockNameResolver().mockAnyLookup(), DockerImage.fromString("docker-registry.domain.tld:8080/dist/vespa"), true); - provisioner = new NodeRepositoryProvisioner(nodeRepository, zone, new MockProvisionServiceProvider(), new InMemoryFlagSource()); + provisioner = new NodeRepositoryProvisioner(nodeRepository, nodeFlavors, zone, new MockProvisionServiceProvider(), new InMemoryFlagSource()); hostLivenessTracker = new TestHostLivenessTracker(clock); orchestrator = new OrchestratorMock(); } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/OperatorChangeApplicationMaintainerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/OperatorChangeApplicationMaintainerTest.java index 95f62521628..e1ac0430ee4 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/OperatorChangeApplicationMaintainerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/OperatorChangeApplicationMaintainerTest.java @@ -58,7 +58,7 @@ public class OperatorChangeApplicationMaintainerTest { new MockNameResolver().mockAnyLookup(), DockerImage.fromString("docker-registry.domain.tld:8080/dist/vespa"), true); - this.fixture = new Fixture(zone, nodeRepository); + this.fixture = new Fixture(zone, nodeRepository, nodeFlavors); createReadyNodes(15, this.fixture.nodeResources, nodeRepository); createHostNodes(2, nodeRepository, nodeFlavors); @@ -126,12 +126,10 @@ public class OperatorChangeApplicationMaintainerTest { final int wantedNodesApp1 = 5; final int wantedNodesApp2 = 7; - Fixture(Zone zone, NodeRepository nodeRepository) { + Fixture(Zone zone, NodeRepository nodeRepository, NodeFlavors flavors) { this.nodeRepository = nodeRepository; - NodeRepositoryProvisioner provisioner = new NodeRepositoryProvisioner(nodeRepository, - zone, - new MockProvisionServiceProvider(), - new InMemoryFlagSource()); + NodeRepositoryProvisioner provisioner = new NodeRepositoryProvisioner( + nodeRepository, flavors, zone, new MockProvisionServiceProvider(), new InMemoryFlagSource()); Map<ApplicationId, MockDeployer.ApplicationContext> apps = Map.of( app1, new MockDeployer.ApplicationContext(app1, clusterApp1, Capacity.fromCount(wantedNodesApp1, nodeResources), 1), diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/PeriodicApplicationMaintainerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/PeriodicApplicationMaintainerTest.java index 94e43e4f99e..211b4a4472f 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/PeriodicApplicationMaintainerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/PeriodicApplicationMaintainerTest.java @@ -64,7 +64,7 @@ public class PeriodicApplicationMaintainerTest { new MockNameResolver().mockAnyLookup(), DockerImage.fromString("docker-registry.domain.tld:8080/dist/vespa"), true); - this.fixture = new Fixture(zone, nodeRepository); + this.fixture = new Fixture(zone, nodeRepository, nodeFlavors); createReadyNodes(15, fixture.nodeResources, nodeRepository); createHostNodes(2, nodeRepository, nodeFlavors); @@ -252,12 +252,10 @@ public class PeriodicApplicationMaintainerTest { private final TestablePeriodicApplicationMaintainer maintainer; - Fixture(Zone zone, NodeRepository nodeRepository) { + Fixture(Zone zone, NodeRepository nodeRepository, NodeFlavors flavors) { this.nodeRepository = nodeRepository; - NodeRepositoryProvisioner provisioner = new NodeRepositoryProvisioner(nodeRepository, - zone, - new MockProvisionServiceProvider(), - new InMemoryFlagSource()); + NodeRepositoryProvisioner provisioner = new NodeRepositoryProvisioner( + nodeRepository, flavors, zone, new MockProvisionServiceProvider(), new InMemoryFlagSource()); Map<ApplicationId, MockDeployer.ApplicationContext> apps = Map.of( app1, new MockDeployer.ApplicationContext(app1, clusterApp1, Capacity.fromCount(wantedNodesApp1, nodeResources), 1), diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/ReservationExpirerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/ReservationExpirerTest.java index 3861e4ff98c..f8efb4fdea1 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/ReservationExpirerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/ReservationExpirerTest.java @@ -48,7 +48,7 @@ public class ReservationExpirerTest { new MockNameResolver().mockAnyLookup(), DockerImage.fromString("docker-registry.domain.tld:8080/dist/vespa"), true); - NodeRepositoryProvisioner provisioner = new NodeRepositoryProvisioner(nodeRepository, Zone.defaultZone(), new MockProvisionServiceProvider(), new InMemoryFlagSource()); + NodeRepositoryProvisioner provisioner = new NodeRepositoryProvisioner(nodeRepository, flavors, Zone.defaultZone(), new MockProvisionServiceProvider(), new InMemoryFlagSource()); List<Node> nodes = new ArrayList<>(2); nodes.add(nodeRepository.createNode(UUID.randomUUID().toString(), UUID.randomUUID().toString(), Optional.empty(), new Flavor(new NodeResources(2, 8, 50)), NodeType.tenant)); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/RetiredExpirerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/RetiredExpirerTest.java index 7e471a81cf8..5d3485ab447 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/RetiredExpirerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/RetiredExpirerTest.java @@ -66,7 +66,7 @@ public class RetiredExpirerTest { private final NodeRepository nodeRepository = new NodeRepository(nodeFlavors, curator, clock, zone, new MockNameResolver().mockAnyLookup(), DockerImage.fromString("docker-registry.domain.tld:8080/dist/vespa"), true); - private final NodeRepositoryProvisioner provisioner = new NodeRepositoryProvisioner(nodeRepository, zone, new MockProvisionServiceProvider(), new InMemoryFlagSource()); + private final NodeRepositoryProvisioner provisioner = new NodeRepositoryProvisioner(nodeRepository, nodeFlavors, zone, new MockProvisionServiceProvider(), new InMemoryFlagSource()); private final Orchestrator orchestrator = mock(Orchestrator.class); private static final Duration RETIRED_EXPIRATION = Duration.ofHours(12); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/SerializationTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/SerializationTest.java index 2094a68148e..b2966ccb91a 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/SerializationTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/SerializationTest.java @@ -154,7 +154,7 @@ public class SerializationTest { Node node = nodeSerializer.fromJson(Node.State.provisioned, Utf8.toBytes(nodeData)); - assertEquals("large", node.flavor().name()); + assertEquals("large", node.flavor().canonicalName()); assertEquals(1, node.status().reboot().wanted()); assertEquals(2, node.status().reboot().current()); assertEquals(3, node.allocation().get().restartGeneration().wanted()); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DockerProvisioningTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DockerProvisioningTest.java index eeb90a06951..0ecbf389825 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DockerProvisioningTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DockerProvisioningTest.java @@ -225,7 +225,7 @@ public class DockerProvisioningTest { NodeList nodes = tester.getNodes(application1, Node.State.active); assertEquals(1, nodes.size()); - assertEquals("[vcpu: 1.0, memory: 1.0 Gb, disk 1.0 Gb]", nodes.asList().get(0).flavor().name()); + assertEquals("[vcpu: 1.0, memory: 1.0 Gb, disk 1.0 Gb]", nodes.asList().get(0).flavor().canonicalName()); } private Set<String> hostsOf(NodeList nodes) { diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicDockerAllocationTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicDockerAllocationTest.java index d543856f71c..1f474a8e07e 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicDockerAllocationTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicDockerAllocationTest.java @@ -285,6 +285,22 @@ public class DynamicDockerAllocationTest { } @Test + public void legacy_bare_metal_allocations_are_not_altered() { + ProvisioningTester tester = new ProvisioningTester.Builder().zone(new Zone(SystemName.cd, Environment.prod, RegionName.from("us-east"))).flavorsConfig(flavorsConfig()).build(); + tester.makeReadyNodes(5, "host-large", NodeType.tenant); + deployZoneApp(tester); + + ApplicationId application = tester.makeApplicationId(); + ClusterSpec cluster = ClusterSpec.request(ClusterSpec.Type.container, ClusterSpec.Id.from("test"), Version.fromString("1"), false); + NodeResources resources = NodeResources.fromLegacyName("host-large"); + + List<HostSpec> hosts = tester.prepare(application, cluster, 2, 1, resources); + assertEquals(2, hosts.size()); + assertEquals("host-large", hosts.get(0).flavor().get().name()); + tester.activate(application, hosts); + } + + @Test public void provisioning_fast_disk_speed_do_not_get_slow_nodes() { provisionFastAndSlowThenDeploy(NodeResources.DiskSpeed.fast, true); } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/MultigroupProvisioningTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/MultigroupProvisioningTest.java index 0523a1f7f72..a5d5fb81147 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/MultigroupProvisioningTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/MultigroupProvisioningTest.java @@ -7,7 +7,6 @@ import com.yahoo.config.provision.Capacity; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.HostSpec; -import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.Zone; import com.yahoo.vespa.hosted.provision.Node; @@ -39,25 +38,25 @@ public class MultigroupProvisioningTest { ApplicationId application1 = tester.makeApplicationId(); - tester.makeReadyNodes(21, "d-1-3-9"); - - deploy(application1, 6, 1, "d-1-3-9", tester); - deploy(application1, 6, 2, "d-1-3-9", tester); - deploy(application1, 6, 3, "d-1-3-9", tester); - deploy(application1, 6, 6, "d-1-3-9", tester); - deploy(application1, 6, 1, "d-1-3-9", tester); - deploy(application1, 6, 6, "d-1-3-9", tester); - deploy(application1, 6, 6, "d-1-3-9", tester); - deploy(application1, 6, 2, "d-1-3-9", tester); - deploy(application1, 8, 2, "d-1-3-9", tester); - deploy(application1, 9, 3, "d-1-3-9", tester); - deploy(application1, 9, 3, "d-1-3-9", tester); - deploy(application1, 9, 3, "d-1-3-9", tester); - deploy(application1,12, 4, "d-1-3-9", tester); - deploy(application1, 8, 4, "d-1-3-9", tester); - deploy(application1,12, 4, "d-1-3-9", tester); - deploy(application1, 8, 2, "d-1-3-9", tester); - deploy(application1, 6, 3, "d-1-3-9", tester); + tester.makeReadyNodes(21, "default"); + + deploy(application1, 6, 1, tester); + deploy(application1, 6, 2, tester); + deploy(application1, 6, 3, tester); + deploy(application1, 6, 6, tester); + deploy(application1, 6, 1, tester); + deploy(application1, 6, 6, tester); + deploy(application1, 6, 6, tester); + deploy(application1, 6, 2, tester); + deploy(application1, 8, 2, tester); + deploy(application1, 9, 3, tester); + deploy(application1, 9, 3, tester); + deploy(application1, 9, 3, tester); + deploy(application1,12, 4, tester); + deploy(application1, 8, 4, tester); + deploy(application1,12, 4, tester); + deploy(application1, 8, 2, tester); + deploy(application1, 6, 3, tester); } /** @@ -71,7 +70,7 @@ public class MultigroupProvisioningTest { ApplicationId application1 = tester.makeApplicationId(); - tester.makeReadyNodes(21, "d-1-3-9"); + tester.makeReadyNodes(21, "default"); deploy(application1, 12, 2, tester); deploy(application1, 9, 3, tester); @@ -84,12 +83,12 @@ public class MultigroupProvisioningTest { ApplicationId application1 = tester.makeApplicationId(); - tester.makeReadyNodes(10, "d-1-1-1"); - tester.makeReadyNodes(10, "d-3-3-3"); + tester.makeReadyNodes(10, "small"); + tester.makeReadyNodes(10, "large"); - deploy(application1, 8, 1, "d-1-1-1", tester); - deploy(application1, 8, 1, "d-3-3-3", tester); - deploy(application1, 8, 8, "d-3-3-3", tester); + deploy(application1, 8, 1, "small", tester); + deploy(application1, 8, 1, "large", tester); + deploy(application1, 8, 8, "large", tester); } @Test @@ -98,10 +97,10 @@ public class MultigroupProvisioningTest { ApplicationId application1 = tester.makeApplicationId(); - tester.makeReadyNodes(10, "d-1-1-1"); + tester.makeReadyNodes(10, "small"); - deploy(application1, Capacity.fromNodeCount(1, Optional.of("d-1-1-1"), true, true), 1, tester); - deploy(application1, Capacity.fromNodeCount(2, Optional.of("d-1-1-1"), true, true), 2, tester); + deploy(application1, Capacity.fromNodeCount(1, Optional.of("small"), true, true), 1, tester); + deploy(application1, Capacity.fromNodeCount(2, Optional.of("small"), true, true), 2, tester); } @Test @@ -110,11 +109,11 @@ public class MultigroupProvisioningTest { ApplicationId application1 = tester.makeApplicationId(); - tester.makeReadyNodes(10, "d-1-1-1"); - tester.makeReadyNodes(10, "d-3-3-3"); + tester.makeReadyNodes(10, "small"); + tester.makeReadyNodes(10, "large"); - deploy(application1, Capacity.fromNodeCount(1, Optional.of("d-1-1-1"), true, true), 1, tester); - deploy(application1, Capacity.fromNodeCount(2, Optional.of("d-3-3-3"), true, true), 2, tester); + deploy(application1, Capacity.fromNodeCount(1, Optional.of("small"), true, true), 1, tester); + deploy(application1, Capacity.fromNodeCount(2, Optional.of("large"), true, true), 2, tester); } @Test @@ -123,11 +122,11 @@ public class MultigroupProvisioningTest { ApplicationId application1 = tester.makeApplicationId(); - tester.makeReadyNodes(10, "d-1-1-1"); - tester.makeReadyNodes(10, "d-3-3-3"); + tester.makeReadyNodes(10, "small"); + tester.makeReadyNodes(10, "large"); - deploy(application1, 8, 1, "d-1-1-1", tester); - deploy(application1, 8, 1, "d-3-3-3", tester); + deploy(application1, 8, 1, "small", tester); + deploy(application1, 8, 1, "large", tester); // Expire small nodes tester.advanceTime(Duration.ofDays(7)); @@ -136,36 +135,35 @@ public class MultigroupProvisioningTest { tester.clock(), Collections.singletonMap(application1, new MockDeployer.ApplicationContext(application1, cluster(), - Capacity.fromNodeCount(8, Optional.of("d-3-3-3"), false, true), 1))); + Capacity.fromNodeCount(8, Optional.of("large"), false, true), 1))); new RetiredExpirer(tester.nodeRepository(), tester.orchestrator(), deployer, tester.clock(), Duration.ofDays(30), Duration.ofHours(12)).run(); - assertEquals(8, tester.getNodes(application1, Node.State.inactive).resources(new NodeResources(1, 1, 1)).size()); - deploy(application1, 8, 8, "d-3-3-3", tester); + assertEquals(8, tester.getNodes(application1, Node.State.inactive).flavor("small").size()); + deploy(application1, 8, 8, "large", tester); } private void deploy(ApplicationId application, int nodeCount, int groupCount, String flavor, ProvisioningTester tester) { deploy(application, Capacity.fromNodeCount(nodeCount, Optional.of(flavor), false, true), groupCount, tester); } private void deploy(ApplicationId application, int nodeCount, int groupCount, ProvisioningTester tester) { - deploy(application, Capacity.fromNodeCount(nodeCount, Optional.of("d-3-3-3"), false, true), groupCount, tester); + deploy(application, Capacity.fromNodeCount(nodeCount, Optional.of("default"), false, true), groupCount, tester); } + @SuppressWarnings("deprecation") // TODO: Remove private void deploy(ApplicationId application, Capacity capacity, int wantedGroups, ProvisioningTester tester) { int nodeCount = capacity.nodeCount(); - NodeResources nodeResources = capacity.nodeResources().get(); + String flavor = capacity.flavor().get(); - int previousActiveNodeCount = tester.getNodes(application, Node.State.active).resources(nodeResources).size(); + int previousActiveNodeCount = tester.getNodes(application, Node.State.active).flavor(flavor).size(); tester.activate(application, prepare(application, capacity, wantedGroups, tester)); - System.out.println("Active nodes ---------------"); - tester.getNodes(application, Node.State.active).forEach(n -> System.out.println(" " + n.hostname() + ": Flavor : " + n.flavor() + " retired " + n.status().wantToRetire())); assertEquals("Superfluous nodes are retired, but no others - went from " + previousActiveNodeCount + " to " + nodeCount + " nodes", Math.max(0, previousActiveNodeCount - capacity.nodeCount()), - tester.getNodes(application, Node.State.active).retired().resources(nodeResources).size()); + tester.getNodes(application, Node.State.active).retired().flavor(flavor).size()); assertEquals("Other flavors are retired", - 0, tester.getNodes(application, Node.State.active).nonretired().notResources(nodeResources).size()); + 0, tester.getNodes(application, Node.State.active).nonretired().notFlavor(capacity.flavor().get()).size()); // Check invariants for all nodes Set<Integer> allIndexes = new HashSet<>(); @@ -181,7 +179,7 @@ public class MultigroupProvisioningTest { // Count unretired nodes and groups of the requested flavor Set<Integer> indexes = new HashSet<>(); Map<ClusterSpec.Group, Integer> nonretiredGroups = new HashMap<>(); - for (Node node : tester.getNodes(application, Node.State.active).nonretired().resources(nodeResources)) { + for (Node node : tester.getNodes(application, Node.State.active).nonretired().flavor(flavor)) { indexes.add(node.allocation().get().membership().index()); ClusterSpec.Group group = node.allocation().get().membership().cluster().group().get(); @@ -196,7 +194,7 @@ public class MultigroupProvisioningTest { assertEquals("Group size", (long)nodeCount / wantedGroups, (long)groupSize); Map<ClusterSpec.Group, Integer> allGroups = new HashMap<>(); - for (Node node : tester.getNodes(application, Node.State.active).resources(nodeResources)) { + for (Node node : tester.getNodes(application, Node.State.active).flavor(flavor)) { ClusterSpec.Group group = node.allocation().get().membership().cluster().group().get(); allGroups.put(group, nonretiredGroups.getOrDefault(group, 0) + 1); } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTest.java index 2f97cd13234..7ad8dbbf7bb 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTest.java @@ -290,6 +290,54 @@ public class ProvisioningTest { } @Test + public void application_deployment_multiple_flavors_with_replacement() { + ProvisioningTester tester = new ProvisioningTester.Builder().zone(new Zone(Environment.prod, RegionName.from("us-east"))).build(); + + ApplicationId application1 = tester.makeApplicationId(); + + tester.makeReadyNodes(8, "large"); + tester.makeReadyNodes(8, "large-variant"); + + // deploy with flavor which will be fulfilled by some old and new nodes + SystemState state1 = prepare(application1, 2, 2, 4, 4, + NodeResources.fromLegacyName("old-large1"), tester); + tester.activate(application1, state1.allHosts); + + // redeploy with increased sizes, this will map to the remaining old/new nodes + SystemState state2 = prepare(application1, 3, 4, 4, 5, + NodeResources.fromLegacyName("old-large2"), tester); + assertEquals("New nodes are reserved", 4, tester.getNodes(application1, Node.State.reserved).size()); + tester.activate(application1, state2.allHosts); + assertEquals("All nodes are used", + 16, tester.getNodes(application1, Node.State.active).size()); + assertEquals("No nodes are retired", + 0, tester.getNodes(application1, Node.State.active).retired().size()); + + // This is a noop as we are already using large nodes and nodes which replace large + SystemState state3 = prepare(application1, 3, 4, 4, 5, + NodeResources.fromLegacyName("large"), tester); + assertEquals("Noop", 0, tester.getNodes(application1, Node.State.reserved).size()); + tester.activate(application1, state3.allHosts); + + try { + SystemState state4 = prepare(application1, 3, 4, 4, 5, + NodeResources.fromLegacyName("large-variant"), tester); + fail("Should fail as we don't have that many large-variant nodes"); + } + catch (OutOfCapacityException expected) { + } + + // make enough nodes to complete the switch to large-variant + tester.makeReadyNodes(8, "large-variant"); + SystemState state4 = prepare(application1, 3, 4, 4, 5, + NodeResources.fromLegacyName("large-variant"), tester); + assertEquals("New 'large-variant' nodes are reserved", 8, tester.getNodes(application1, Node.State.reserved).size()); + tester.activate(application1, state4.allHosts); + // (we can not check for the precise state here without carrying over from earlier as the distribution of + // old and new on different clusters is unknown) + } + + @Test public void application_deployment_above_then_at_capacity_limit() { ProvisioningTester tester = new ProvisioningTester.Builder().zone(new Zone(Environment.prod, RegionName.from("us-east"))).build(); @@ -459,6 +507,47 @@ public class ProvisioningTest { } @Test + public void out_of_desired_flavor() { + ProvisioningTester tester = new ProvisioningTester.Builder().zone(new Zone(Environment.prod, RegionName.from("us-east"))).build(); + + tester.makeReadyNodes(10, "small"); // need 2+2+3+3=10 + tester.makeReadyNodes( 9, "large"); // need 2+2+3+3=10 + ApplicationId application = tester.makeApplicationId(); + try { + prepare(application, 2, 2, 3, 3, + NodeResources.fromLegacyName("large"), tester); + fail("Expected exception"); + } + catch (OutOfCapacityException e) { + assertTrue(e.getMessage().startsWith("Could not satisfy request for 3 nodes with flavor 'large'")); + } + } + + @Test + public void out_of_capacity_no_replacements_for_retired_flavor() { + String flavorToRetire = "default"; + String replacementFlavor = "new-default"; + + FlavorConfigBuilder b = new FlavorConfigBuilder(); + b.addFlavor(flavorToRetire, 1., 1., 10, Flavor.Type.BARE_METAL).cost(2).retired(true); + FlavorsConfig.Flavor.Builder newDefault = b.addFlavor(replacementFlavor, 2., 2., 20, + Flavor.Type.BARE_METAL).cost(2); + b.addReplaces(flavorToRetire, newDefault); + + ProvisioningTester tester = new ProvisioningTester.Builder() + .zone(new Zone(Environment.prod, RegionName.from("us-east"))).flavorsConfig(b.build()).build(); + ApplicationId application = tester.makeApplicationId(); + + try { + prepare(application, 2, 0, 2, 0, + NodeResources.fromLegacyName(flavorToRetire), tester); + fail("Expected exception"); + } catch (OutOfCapacityException e) { + assertTrue(e.getMessage().startsWith("Could not satisfy request")); + } + } + + @Test public void out_of_capacity_all_nodes_want_to_retire() { ProvisioningTester tester = new ProvisioningTester.Builder().zone(new Zone(Environment.prod, RegionName.from("us-east"))).build(); @@ -477,6 +566,21 @@ public class ProvisioningTest { } @Test + public void nonexisting_flavor() { + ProvisioningTester tester = new ProvisioningTester.Builder().zone(new Zone(Environment.prod, RegionName.from("us-east"))).build(); + + ApplicationId application = tester.makeApplicationId(); + try { + prepare(application, 2, 2, 3, 3, + NodeResources.fromLegacyName("nonexisting"), tester); + fail("Expected exception"); + } + catch (IllegalArgumentException e) { + assertEquals("Unknown flavor 'nonexisting'. Flavors are [default, dockerLarge, dockerSmall, large, old-large1, old-large2, small, v-4-8-100]", e.getMessage()); + } + } + + @Test public void highest_node_indexes_are_retired_first() { ProvisioningTester tester = new ProvisioningTester.Builder().zone(new Zone(Environment.prod, RegionName.from("us-east"))).build(); @@ -506,6 +610,96 @@ public class ProvisioningTest { } @Test + public void application_deployment_prefers_cheapest_stock_nodes() { + assertCorrectBareMetalFlavorPreferences(true); + } + + @Test + public void application_deployment_prefers_exact_nonstock_nodes() { + assertCorrectBareMetalFlavorPreferences(false); + } + + @Test + public void application_deployment_retires_nodes_having_retired_flavor() { + String flavorToRetire = "default"; + String replacementFlavor = "new-default"; + ApplicationId application = ApplicationId.from( + TenantName.from(UUID.randomUUID().toString()), + ApplicationName.from(UUID.randomUUID().toString()), + InstanceName.from(UUID.randomUUID().toString())); + Curator curator = new MockCurator(); + NameResolver nameResolver = new MockNameResolver().mockAnyLookup(); + + // Deploy with flavor that will eventually be retired + { + FlavorConfigBuilder b = new FlavorConfigBuilder(); + b.addFlavor(flavorToRetire, 1., 1., 10, Flavor.Type.BARE_METAL).cost(2); + + ProvisioningTester tester = new ProvisioningTester.Builder() + .flavorsConfig(b.build()).curator(curator).nameResolver(nameResolver).build(); + tester.makeReadyNodes(4, flavorToRetire); + SystemState state = prepare(application, 2, 0, 2, 0, + NodeResources.fromLegacyName(flavorToRetire), tester); + tester.activate(application, state.allHosts); + } + + // Re-deploy with same flavor, which is now retired + { + // Retire "default" flavor and add "new-default" as replacement + FlavorConfigBuilder b = new FlavorConfigBuilder(); + b.addFlavor(flavorToRetire, 1., 1., 10, Flavor.Type.BARE_METAL).cost(2).retired(true); + FlavorsConfig.Flavor.Builder newDefault = b.addFlavor(replacementFlavor, 2., 2., 20, + Flavor.Type.BARE_METAL).cost(2); + b.addReplaces(flavorToRetire, newDefault); + + ProvisioningTester tester = new ProvisioningTester.Builder() + .flavorsConfig(b.build()).curator(curator).nameResolver(nameResolver).build(); + + // Add nodes with "new-default" flavor + tester.makeReadyNodes(4, replacementFlavor); + + SystemState state = prepare(application, 2, 0, 2, 0, + NodeResources.fromLegacyName(flavorToRetire), tester); + + tester.activate(application, state.allHosts); + + // Nodes with retired flavor are retired + NodeList retired = tester.getNodes(application).retired(); + assertEquals(4, retired.size()); + assertTrue("Nodes are retired by system", retired.asList().stream().allMatch(retiredBy(Agent.system))); + } + } + + @Test + public void application_deployment_is_not_given_unallocated_nodes_having_retired_flavor() { + String flavorToRetire = "default"; + String replacementFlavor = "new-default"; + + FlavorConfigBuilder b = new FlavorConfigBuilder(); + b.addFlavor(flavorToRetire, 1., 1., 10, Flavor.Type.BARE_METAL).cost(2).retired(true); + FlavorsConfig.Flavor.Builder newDefault = b.addFlavor(replacementFlavor, 2., 2., 20, + Flavor.Type.BARE_METAL).cost(2); + b.addReplaces(flavorToRetire, newDefault); + + ProvisioningTester tester = new ProvisioningTester.Builder() + .zone(new Zone(Environment.prod, RegionName.from("us-east"))).flavorsConfig(b.build()).build(); + ApplicationId application = tester.makeApplicationId(); + + // Add nodes + tester.makeReadyNodes(4, flavorToRetire); + tester.makeReadyNodes(4, replacementFlavor); + + SystemState state = prepare(application, 2, 0, 2, 0, + NodeResources.fromLegacyName(flavorToRetire), tester); + + tester.activate(application, state.allHosts); + + List<Node> nodes = tester.getNodes(application).asList(); + assertTrue("Allocated nodes have flavor " + replacementFlavor, + nodes.stream().allMatch(n -> n.flavor().name().equals(replacementFlavor))); + } + + @Test public void application_deployment_retires_nodes_that_want_to_retire() { ProvisioningTester tester = new ProvisioningTester.Builder().zone(new Zone(Environment.prod, RegionName.from("us-east"))).build(); @@ -587,6 +781,43 @@ public class ProvisioningTest { } catch (IllegalArgumentException ignored) {} } + private void assertCorrectBareMetalFlavorPreferences(boolean largeIsStock) { + FlavorConfigBuilder b = new FlavorConfigBuilder(); + b.addFlavor("large", 4., 8., 100, Flavor.Type.BARE_METAL).cost(10).stock(largeIsStock); + FlavorsConfig.Flavor.Builder largeVariant = b.addFlavor("large-variant", 3., 9., 101, Flavor.Type.BARE_METAL).cost(9); + b.addReplaces("large", largeVariant); + FlavorsConfig.Flavor.Builder largeVariantVariant = b.addFlavor("large-variant-variant", 4., 9., 101, Flavor.Type.BARE_METAL).cost(11); + b.addReplaces("large-variant", largeVariantVariant); + + ProvisioningTester tester = new ProvisioningTester.Builder() + .zone(new Zone(Environment.prod, RegionName.from("us-east"))).flavorsConfig(b.build()).build(); + tester.makeReadyNodes(6, "large"); //cost = 10 + tester.makeReadyNodes(6, "large-variant"); //cost = 9 + tester.makeReadyNodes(6, "large-variant-variant"); //cost = 11 + + ApplicationId applicationId = tester.makeApplicationId(); + ClusterSpec contentClusterSpec = ClusterSpec.request(ClusterSpec.Type.content, ClusterSpec.Id.from("myContent"), Version.fromString("6.42"), false); + ClusterSpec containerClusterSpec = ClusterSpec.request(ClusterSpec.Type.container, ClusterSpec.Id.from("myContainer"), Version.fromString("6.42"), false); + + List<HostSpec> containerNodes = tester.prepare(applicationId, containerClusterSpec, 5, 1, + NodeResources.fromLegacyName("large")); + List<HostSpec> contentNodes = tester.prepare(applicationId, contentClusterSpec, 10, 1, + NodeResources.fromLegacyName("large")); + + if (largeIsStock) { // 'large' is replaced by 'large-variant' when possible, as it is cheaper + tester.assertNumberOfNodesWithFlavor(containerNodes, "large-variant", 5); + tester.assertNumberOfNodesWithFlavor(contentNodes, "large-variant", 1); + tester.assertNumberOfNodesWithFlavor(contentNodes, "large", 6); + } + else { // 'large' is preferred when available, as it is what is exactly specified + tester.assertNumberOfNodesWithFlavor(containerNodes, "large", 5); + tester.assertNumberOfNodesWithFlavor(contentNodes, "large", 1); + tester.assertNumberOfNodesWithFlavor(contentNodes, "large-variant", 6); + } + // in both cases the most expensive, never exactly specified is least preferred + tester.assertNumberOfNodesWithFlavor(contentNodes, "large-variant-variant", 3); + } + private SystemState prepare(ApplicationId application, int container0Size, int container1Size, int content0Size, int content1Size, NodeResources flavor, ProvisioningTester tester) { return prepare(application, container0Size, container1Size, content0Size, content1Size, flavor, diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java index 942492bb790..9f0b4faff01 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java @@ -90,8 +90,8 @@ public class ProvisioningTester { DockerImage.fromString("docker-registry.domain.tld:8080/dist/vespa"), true); this.orchestrator = orchestrator; ProvisionServiceProvider provisionServiceProvider = new MockProvisionServiceProvider(loadBalancerService, hostProvisioner); - this.provisioner = new NodeRepositoryProvisioner(nodeRepository, zone, provisionServiceProvider, flagSource); - this.capacityPolicies = new CapacityPolicies(zone, new InMemoryFlagSource()); + this.provisioner = new NodeRepositoryProvisioner(nodeRepository, nodeFlavors, zone, provisionServiceProvider, flagSource); + this.capacityPolicies = new CapacityPolicies(zone, nodeFlavors, new InMemoryFlagSource()); this.provisionLogger = new NullProvisionLogger(); this.loadBalancerService = loadBalancerService; } @@ -103,7 +103,15 @@ public class ProvisioningTester { b.addFlavor("dockerSmall", 1., 1., 10, Flavor.Type.DOCKER_CONTAINER).cost(1); b.addFlavor("dockerLarge", 2., 1., 20, Flavor.Type.DOCKER_CONTAINER).cost(3); b.addFlavor("v-4-8-100", 4., 8., 100, Flavor.Type.VIRTUAL_MACHINE).cost(4); - b.addFlavor("large", 4., 8., 100, Flavor.Type.BARE_METAL).cost(10); + b.addFlavor("old-large1", 2., 4., 100, Flavor.Type.BARE_METAL).cost(6); + b.addFlavor("old-large2", 2., 5., 100, Flavor.Type.BARE_METAL).cost(14); + FlavorsConfig.Flavor.Builder large = b.addFlavor("large", 4., 8., 100, Flavor.Type.BARE_METAL).cost(10); + b.addReplaces("old-large1", large); + b.addReplaces("old-large2", large); + FlavorsConfig.Flavor.Builder largeVariant = b.addFlavor("large-variant", 3., 9., 101, Flavor.Type.BARE_METAL).cost(9); + b.addReplaces("large", largeVariant); + FlavorsConfig.Flavor.Builder largeVariantVariant = b.addFlavor("large-variant-variant", 4., 9., 101, Flavor.Type.BARE_METAL).cost(11); + b.addReplaces("large-variant", largeVariantVariant); return b.build(); } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/node9.json b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/node9.json index dad1e45c1b8..561cab22f85 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/node9.json +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/node9.json @@ -6,7 +6,7 @@ "hostname": "host9.yahoo.com", "openStackId": "host9.yahoo.com", "flavor": "large-variant", - "canonicalFlavor": "large-variant", + "canonicalFlavor": "large", "minDiskAvailableGb": 2000.0, "minMainMemoryAvailableGb": 128.0, "minCpuCores": 64.0, diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/parent2.json b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/parent2.json index 7d8b48232b5..28bb960eb14 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/parent2.json +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/parent2.json @@ -6,7 +6,7 @@ "hostname": "parent2.yahoo.com", "openStackId": "parent2.yahoo.com", "flavor": "large-variant", - "canonicalFlavor": "large-variant", + "canonicalFlavor": "large", "minDiskAvailableGb": 2000.0, "minMainMemoryAvailableGb": 128.0, "minCpuCores": 64.0, |