// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.config.model.provision; import com.yahoo.cloud.config.ZookeeperServerConfig; import com.yahoo.cloud.config.log.LogdConfig; import com.yahoo.config.application.api.ApplicationPackage; import com.yahoo.config.application.api.DeployLogger; import com.yahoo.config.model.api.ApplicationClusterEndpoint; import com.yahoo.config.model.api.ContainerEndpoint; import com.yahoo.config.model.api.container.ContainerServiceType; import com.yahoo.config.model.deploy.DeployState; import com.yahoo.config.model.deploy.TestProperties; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.Zone; import com.yahoo.container.core.ApplicationMetadataConfig; import com.yahoo.search.config.QrStartConfig; import com.yahoo.vespa.config.content.FleetcontrollerConfig; import com.yahoo.vespa.config.content.core.StorCommunicationmanagerConfig; import com.yahoo.vespa.config.content.core.StorStatusConfig; import com.yahoo.vespa.config.search.core.ProtonConfig; import com.yahoo.vespa.model.HostResource; import com.yahoo.vespa.model.HostSystem; import com.yahoo.vespa.model.VespaModel; import com.yahoo.vespa.model.admin.Admin; import com.yahoo.vespa.model.admin.Logserver; import com.yahoo.vespa.model.admin.Slobrok; import com.yahoo.vespa.model.admin.clustercontroller.ClusterControllerContainer; import com.yahoo.vespa.model.admin.clustercontroller.ClusterControllerContainerCluster; import com.yahoo.vespa.model.container.ApplicationContainerCluster; import com.yahoo.vespa.model.container.Container; import com.yahoo.vespa.model.container.ContainerCluster; import com.yahoo.vespa.model.content.ContentSearchCluster; import com.yahoo.vespa.model.content.StorageGroup; import com.yahoo.vespa.model.content.StorageNode; import com.yahoo.vespa.model.content.cluster.ContentCluster; import com.yahoo.vespa.model.content.storagecluster.StorageCluster; import com.yahoo.vespa.model.search.SearchNode; import com.yahoo.vespa.model.test.VespaModelTester; import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithMockPkg; import com.yahoo.yolean.Exceptions; import org.junit.jupiter.api.Test; import java.io.StringReader; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.logging.Level; import java.util.stream.Collectors; import static com.yahoo.config.model.test.TestUtil.joinLines; import static com.yahoo.config.provision.NodeResources.Architecture; import static com.yahoo.config.provision.NodeResources.DiskSpeed; import static com.yahoo.config.provision.NodeResources.StorageType; import static com.yahoo.vespa.defaults.Defaults.getDefaults; import static com.yahoo.vespa.model.Host.memoryOverheadGb; import static com.yahoo.vespa.model.search.NodeResourcesTuning.GB; import static com.yahoo.vespa.model.test.utils.ApplicationPackageUtils.generateSchemas; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; /** * Test cases for provisioning nodes to entire Vespa models. * * @author Vegard Havdal * @author bratseth */ public class ModelProvisioningTest { @Test public void testNodesJdisc() { String services = "\n" + "\n" + "\n" + "\n" + "" + " " + " " + " " + " " + "" + "" + " " + " " + " " + " " + " " + " " + " " + "" + ""; String hosts ="" + " " + " node0" + " " + " " + " node1" + " " + " " + " node2" + " " + " " + " node3" + " " + " " + " node4" + " " + " " + " node5" + " " + ""; VespaModelCreatorWithMockPkg creator = new VespaModelCreatorWithMockPkg(null, services); VespaModel model = creator.create(new DeployState.Builder().modelHostProvisioner(new InMemoryProvisioner(Hosts.readFrom(new StringReader(hosts)), true, false))); ApplicationContainerCluster mydisc = model.getContainerClusters().get("mydisc"); ApplicationContainerCluster mydisc2 = model.getContainerClusters().get("mydisc2"); assertEquals(3, mydisc.getContainers().size()); assertEquals("mydisc/container.0", (mydisc.getContainers().get(0).getConfigId())); assertTrue(mydisc.getContainers().get(0).isInitialized()); assertEquals("mydisc/container.1", mydisc.getContainers().get(1).getConfigId()); assertTrue(mydisc.getContainers().get(1).isInitialized()); assertEquals("mydisc/container.2", mydisc.getContainers().get(2).getConfigId()); assertTrue(mydisc.getContainers().get(2).isInitialized()); assertEquals(2, mydisc2.getContainers().size()); assertEquals("mydisc2/container.0", mydisc2.getContainers().get(0).getConfigId()); assertTrue(mydisc2.getContainers().get(0).isInitialized()); assertEquals("mydisc2/container.1", mydisc2.getContainers().get(1).getConfigId()); assertTrue(mydisc2.getContainers().get(1).isInitialized()); assertEquals("", mydisc.getContainers().get(0).getJvmOptions()); assertEquals("", mydisc.getContainers().get(1).getJvmOptions()); assertEquals("", mydisc.getContainers().get(2).getJvmOptions()); assertEquals(getDefaults().underVespaHome("lib64/vespa/malloc/libvespamalloc.so"), mydisc.getContainers().get(0).getPreLoad()); assertEquals(getDefaults().underVespaHome("lib64/vespa/malloc/libvespamalloc.so"), mydisc.getContainers().get(1).getPreLoad()); assertEquals(getDefaults().underVespaHome("lib64/vespa/malloc/libvespamalloc.so"), mydisc.getContainers().get(2).getPreLoad()); assertEquals(Optional.empty(), mydisc.getMemoryPercentage()); assertEquals("-Xlog:gc", mydisc2.getContainers().get(0).getJvmOptions()); assertEquals("-Xlog:gc", mydisc2.getContainers().get(1).getJvmOptions()); assertEquals("lib/blablamalloc.so", mydisc2.getContainers().get(0).getPreLoad()); assertEquals("lib/blablamalloc.so", mydisc2.getContainers().get(1).getPreLoad()); assertEquals(45, mydisc2.getMemoryPercentage().get().percentage()); assertEquals(Optional.of("-XX:+UseParNewGC"), mydisc2.getJvmGCOptions()); QrStartConfig.Builder qrStartBuilder = new QrStartConfig.Builder(); mydisc2.getConfig(qrStartBuilder); QrStartConfig qrsStartConfig = new QrStartConfig(qrStartBuilder); assertEquals(45, qrsStartConfig.jvm().heapSizeAsPercentageOfPhysicalMemory()); HostSystem hostSystem = model.hostSystem(); assertTrue(hostNameExists(hostSystem, "myhost0")); assertTrue(hostNameExists(hostSystem, "myhost1")); assertTrue(hostNameExists(hostSystem, "myhost2")); assertFalse(hostNameExists(hostSystem, "Nope")); } @Test public void testNodeCountForContentGroup() { String xmlWithNodes = "" + "" + "\n" + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); int numberOfHosts = 5; tester.addHosts(numberOfHosts); int numberOfContentNodes = 2; VespaModel model = tester.createModel(xmlWithNodes, true, deployStateWithClusterEndpoints("bar.indexing")); assertEquals(numberOfHosts, model.getRoot().hostSystem().getHosts().size()); Map contentClusters = model.getContentClusters(); ContentCluster cluster = contentClusters.get("bar"); assertEquals(numberOfContentNodes, cluster.getRootGroup().getNodes().size()); int i = 0; for (StorageNode node : cluster.getRootGroup().getNodes()) assertEquals(i++, node.getDistributionKey()); } @Test public void testSeparateClusters() { String xmlWithNodes = "" + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.addHosts(8); tester.addHosts(new NodeResources(20, 200, 2000, 1.0), 1); VespaModel model = tester.createModel(xmlWithNodes, true, deployStateWithClusterEndpoints("container1", "container2")); assertEquals(2, model.getContentClusters().get("content1").getRootGroup().getNodes().size(), "Nodes in content1"); assertEquals(1, model.getContainerClusters().get("container1").getContainers().size(), "Nodes in container1"); assertEquals(2, model.getContentClusters().get("content").getRootGroup().getNodes().size(), "Nodes in cluster without ID"); assertEquals(65, physicalMemoryPercentage(model.getContainerClusters().get("container1")), "Heap size for container1"); assertEquals(84, physicalMemoryPercentage(model.getContainerClusters().get("container2")), "Heap size for container2"); assertProvisioned(2, ClusterSpec.Id.from("content1"), ClusterSpec.Type.content, model); assertProvisioned(1, ClusterSpec.Id.from("container1"), ClusterSpec.Type.container, model); assertProvisioned(2, ClusterSpec.Id.from("content"), ClusterSpec.Type.content, model); } @Test public void testClusterMembership() { String xmlWithNodes = "" + "" + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.addHosts(1); VespaModel model = tester.createModel(xmlWithNodes, true, deployStateWithClusterEndpoints("container1")); assertEquals(1, model.hostSystem().getHosts().size()); HostResource host = model.hostSystem().getHosts().iterator().next(); assertTrue(host.spec().membership().isPresent()); assertEquals("container", host.spec().membership().get().cluster().type().name()); assertEquals("container1", host.spec().membership().get().cluster().id().value()); } @Test public void testCombinedCluster() { String xmlWithNodes = "" + "" + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.addHosts(5); TestLogger logger = new TestLogger(); VespaModel model = tester.createModel(xmlWithNodes, true, deployStateWithClusterEndpoints("container1").deployLogger(logger)); assertEquals(2, model.getContentClusters().get("content1").getRootGroup().getNodes().size(), "Nodes in content1"); assertEquals(2, model.getContainerClusters().get("container1").getContainers().size(), "Nodes in container1"); assertEquals(18, physicalMemoryPercentage(model.getContainerClusters().get("container1")), "Heap size is lowered with combined clusters"); assertEquals(2025077080L, protonMemorySize(model.getContentClusters().get("content1")), "Memory for proton is lowered to account for the jvm heap"); assertProvisioned(0, ClusterSpec.Id.from("container1"), ClusterSpec.Type.container, model); assertProvisioned(2, ClusterSpec.Id.from("content1"), ClusterSpec.Id.from("container1"), ClusterSpec.Type.combined, model); var msgs = logger.msgs().stream().filter(m -> m.level().equals(Level.WARNING)).toList(); assertEquals(1, msgs.size()); assertEquals("Declaring combined cluster with is deprecated without replacement, " + "and the feature will be removed in Vespa 9. Use separate container and content clusters instead", msgs.get(0).message); } @Test public void testCombinedClusterWithJvmHeapSizeOverride() { String xmlWithNodes = "" + "" + " " + " " + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.addHosts(5); VespaModel model = tester.createModel(xmlWithNodes, true, deployStateWithClusterEndpoints("container1")); assertEquals(2, model.getContentClusters().get("content1").getRootGroup().getNodes().size(), "Nodes in content1"); assertEquals(2, model.getContainerClusters().get("container1").getContainers().size(), "Nodes in container1"); assertEquals(30, physicalMemoryPercentage(model.getContainerClusters().get("container1")), "Heap size is lowered with combined clusters"); assertEquals((long) ((3 - memoryOverheadGb) * (Math.pow(1024, 3)) * (1 - 0.30)), protonMemorySize(model.getContentClusters() .get("content1")), "Memory for proton is lowered to account for the jvm heap"); assertProvisioned(0, ClusterSpec.Id.from("container1"), ClusterSpec.Type.container, model); assertProvisioned(2, ClusterSpec.Id.from("content1"), ClusterSpec.Id.from("container1"), ClusterSpec.Type.combined, model); } /** For comparison with the above */ @Test public void testNonCombinedCluster() { String xmlWithNodes = "" + "" + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.addHosts(7); VespaModel model = tester.createModel(xmlWithNodes, true, deployStateWithClusterEndpoints("container1")); assertEquals(2, model.getContentClusters().get("content1").getRootGroup().getNodes().size(), "Nodes in content1"); assertEquals(2, model.getContainerClusters().get("container1").getContainers().size(), "Nodes in container1"); assertEquals(65, physicalMemoryPercentage(model.getContainerClusters().get("container1")), "Heap size is normal"); assertEquals((long) ((3 - memoryOverheadGb) * (Math.pow(1024, 3))), protonMemorySize(model.getContentClusters().get("content1")), "Memory for proton is normal"); } @Test public void testCombinedClusterWithJvmOptions() { String xmlWithNodes = "" + "" + " " + " " + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.addHosts(5); VespaModel model = tester.createModel(xmlWithNodes, true, deployStateWithClusterEndpoints("container1")); assertEquals(2, model.getContentClusters().get("content1").getRootGroup().getNodes().size(), "Nodes in content1"); assertEquals(2, model.getContainerClusters().get("container1").getContainers().size(), "Nodes in container1"); for (Container container : model.getContainerClusters().get("container1").getContainers()) assertTrue(container.getJvmOptions().contains("testoption")); } @Test public void testMultipleCombinedClusters() { String xmlWithNodes = "" + "" + " " + " " + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.addHosts(8); VespaModel model = tester.createModel(xmlWithNodes, true, deployStateWithClusterEndpoints("container1", "container2")); assertEquals(2, model.getContentClusters().get("content1").getRootGroup().getNodes().size(), "Nodes in content1"); assertEquals(2, model.getContainerClusters().get("container1").getContainers().size(), "Nodes in container1"); assertEquals(3, model.getContentClusters().get("content2").getRootGroup().getNodes().size(), "Nodes in content2"); assertEquals(3, model.getContainerClusters().get("container2").getContainers().size(), "Nodes in container2"); } @Test public void testNonExistingCombinedClusterReference() { String xmlWithNodes = "" + "" + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.addHosts(2); try { tester.createModel(xmlWithNodes, true); fail("Expected exception"); } catch (IllegalArgumentException e) { assertEquals("container cluster 'container1' contains an invalid reference: referenced service 'container2' is not defined", Exceptions.toMessageString(e)); } } @Test public void testInvalidCombinedClusterReference() { String xmlWithNodes = "" + "" + " " + " " + " " + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.addHosts(2); try { tester.createModel(xmlWithNodes, true); fail("Expected exception"); } catch (IllegalArgumentException e) { assertEquals("container cluster 'container1' contains an invalid reference: service 'container2' is not a content service", Exceptions.toMessageString(e)); } } @Test public void testCombinedClusterWithZooKeeperFails() { String xmlWithNodes = "" + "" + " " + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.addHosts(2); try { tester.createModel(xmlWithNodes, true); fail("ZooKeeper should not be allowed on combined clusters"); } catch (IllegalArgumentException e) { assertEquals("A combined cluster cannot run ZooKeeper", e.getMessage()); } } @Test public void testUsingNodesAndGroupCountAttributes() { String services = "\n" + "" + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + " " + " 1" + " " + " " + " " + " " + " " + ""; int numberOfHosts = 67; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); VespaModel model = tester.createModel(services, true, deployStateWithClusterEndpoints("foo")); assertEquals(numberOfHosts, model.getRoot().hostSystem().getHosts().size()); // Check container cluster assertEquals(1, model.getContainerClusters().size()); Set containerHosts = model.getContainerClusters().get("foo").getContainers().stream() .map(Container::getHost) .collect(Collectors.toSet()); assertEquals(10, containerHosts.size()); // Check admin clusters Admin admin = model.getAdmin(); Set clusterControllerHosts = admin.getClusterControllers().getContainers() .stream().map(cc -> cc.getHostResource()).collect(Collectors.toSet()); Set slobrokHosts = admin.getSlobroks().stream().map(Slobrok::getHost).collect(Collectors.toSet()); assertEquals(3, slobrokHosts.size()); assertTrue(clusterControllerHosts.containsAll(slobrokHosts), "Slobroks are assigned on cluster controller nodes"); assertTrue(containerHosts.contains(admin.getLogserver().getHost()), "Logserver is assigned from container nodes"); assertEquals(0, admin.getConfigservers().size(), "No in-cluster config servers in a hosted environment"); assertEquals(3, admin.getClusterControllers().getContainers().size(), "Dedicated admin cluster controllers when hosted"); // Check content clusters ContentCluster cluster = model.getContentClusters().get("bar"); List subGroups = cluster.getRootGroup().getSubgroups(); assertEquals(0, cluster.getRootGroup().getNodes().size()); assertEquals(9, subGroups.size()); assertEquals("0", subGroups.get(0).getIndex()); assertEquals(3, subGroups.get(0).getNodes().size()); assertEquals(0, subGroups.get(0).getNodes().get(0).getDistributionKey()); assertEquals("bar/storage/0", subGroups.get(0).getNodes().get(0).getConfigId()); assertEquals("node-1-3-50-57", subGroups.get(0).getNodes().get(0).getHostName()); assertEquals(1, subGroups.get(0).getNodes().get(1).getDistributionKey()); assertEquals("bar/storage/1", subGroups.get(0).getNodes().get(1).getConfigId()); assertEquals(2, subGroups.get(0).getNodes().get(2).getDistributionKey()); assertEquals("bar/storage/2", subGroups.get(0).getNodes().get(2).getConfigId()); assertEquals("1", subGroups.get(1).getIndex()); assertEquals(3, subGroups.get(1).getNodes().size()); assertEquals(3, subGroups.get(1).getNodes().get(0).getDistributionKey()); assertEquals("bar/storage/3", subGroups.get(1).getNodes().get(0).getConfigId()); assertEquals("node-1-3-50-54", subGroups.get(1).getNodes().get(0).getHostName()); assertEquals(4, subGroups.get(1).getNodes().get(1).getDistributionKey()); assertEquals("bar/storage/4", subGroups.get(1).getNodes().get(1).getConfigId()); assertEquals(5, subGroups.get(1).getNodes().get(2).getDistributionKey()); assertEquals("bar/storage/5", subGroups.get(1).getNodes().get(2).getConfigId()); // ... assertEquals("node-1-3-50-51", subGroups.get(2).getNodes().get(0).getHostName()); // ... assertEquals("8", subGroups.get(8).getIndex()); assertEquals(3, subGroups.get(8).getNodes().size()); assertEquals(24, subGroups.get(8).getNodes().get(0).getDistributionKey()); assertEquals("bar/storage/24", subGroups.get(8).getNodes().get(0).getConfigId()); assertEquals(25, subGroups.get(8).getNodes().get(1).getDistributionKey()); assertEquals("bar/storage/25", subGroups.get(8).getNodes().get(1).getConfigId()); assertEquals(26, subGroups.get(8).getNodes().get(2).getDistributionKey()); assertEquals("bar/storage/26", subGroups.get(8).getNodes().get(2).getConfigId()); cluster = model.getContentClusters().get("baz"); subGroups = cluster.getRootGroup().getSubgroups(); assertEquals(0, cluster.getRootGroup().getNodes().size()); assertEquals(27, subGroups.size()); assertEquals("0", subGroups.get(0).getIndex()); assertEquals(1, subGroups.get(0).getNodes().size()); assertEquals(0, subGroups.get(0).getNodes().get(0).getDistributionKey()); assertEquals("baz/storage/0", subGroups.get(0).getNodes().get(0).getConfigId()); assertEquals("node-1-3-50-27", subGroups.get(0).getNodes().get(0).getHostName()); assertEquals("1", subGroups.get(1).getIndex()); assertEquals(1, subGroups.get(1).getNodes().size()); assertEquals(1, subGroups.get(1).getNodes().get(0).getDistributionKey()); assertEquals("baz/storage/1", subGroups.get(1).getNodes().get(0).getConfigId()); assertEquals("node-1-3-50-26", subGroups.get(1).getNodes().get(0).getHostName()); // ... assertEquals("node-1-3-50-25", subGroups.get(2).getNodes().get(0).getHostName()); // ... assertEquals("26", subGroups.get(26).getIndex()); assertEquals(1, subGroups.get(26).getNodes().size()); assertEquals(26, subGroups.get(26).getNodes().get(0).getDistributionKey()); assertEquals("baz/storage/26", subGroups.get(26).getNodes().get(0).getConfigId()); } @Test public void testUsingGroups() { String services = "\n" + "" + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + " " + " 1" + " " + " " + " " + " " + " " + ""; int numberOfHosts = 73; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); VespaModel model = tester.createModel(services, true, deployStateWithClusterEndpoints("foo")); assertEquals(numberOfHosts, model.getRoot().hostSystem().getHosts().size()); ContentCluster cluster = model.getContentClusters().get("bar"); List subGroups = cluster.getRootGroup().getSubgroups(); assertEquals( 0, cluster.getRootGroup().getNodes().size()); assertEquals( 2, subGroups.size()); assertEquals(15, subGroups.get(0).getNodes().size()); cluster = model.getContentClusters().get("baz"); subGroups = cluster.getRootGroup().getSubgroups(); assertEquals( 0, cluster.getRootGroup().getNodes().size()); assertEquals(30, subGroups.size()); assertEquals( 1, subGroups.get(0).getNodes().size()); } // Same as the test above but setting groupSize only @Test public void testUsingGroupSizeNotGroups() { String services = "\n" + "" + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + " " + " 1" + " " + " " + " " + " " + " " + ""; int numberOfHosts = 73; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); VespaModel model = tester.createModel(services, true, deployStateWithClusterEndpoints("foo")); assertEquals(numberOfHosts, model.getRoot().hostSystem().getHosts().size()); ContentCluster cluster = model.getContentClusters().get("bar"); List subGroups = cluster.getRootGroup().getSubgroups(); assertEquals( 0, cluster.getRootGroup().getNodes().size()); assertEquals( 2, subGroups.size()); assertEquals(15, subGroups.get(0).getNodes().size()); cluster = model.getContentClusters().get("baz"); subGroups = cluster.getRootGroup().getSubgroups(); assertEquals( 0, cluster.getRootGroup().getNodes().size()); assertEquals(30, subGroups.size()); assertEquals( 1, subGroups.get(0).getNodes().size()); } @Test public void testIllegalGroupSize() { String services = "\n" + "" + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + ""; int numberOfHosts = 10; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); try { tester.createModel(services, true); fail("Expected exception"); } catch (IllegalArgumentException e) { assertEquals("In content cluster 'bar': Illegal group-size value: " + "Expected a number or range on the form [min, max], but got '[2, --]': '--' is not an integer", Exceptions.toMessageString(e)); } } @Test public void testSlobroksOnContainersIfNoContentClusters() { String services = "\n" + "" + " " + " " + " " + " " + ""; int numberOfHosts = 10; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); VespaModel model = tester.createModel(services, true, deployStateWithClusterEndpoints("foo")); assertEquals(numberOfHosts, model.getRoot().hostSystem().getHosts().size()); // Check container cluster assertEquals(1, model.getContainerClusters().size()); Set containerHosts = model.getContainerClusters().get("foo").getContainers().stream() .map(Container::getHost) .collect(Collectors.toSet()); assertEquals(10, containerHosts.size()); // Check admin clusters Admin admin = model.getAdmin(); Set slobrokHosts = admin.getSlobroks().stream().map(Slobrok::getHost).collect(Collectors.toSet()); assertEquals(3, slobrokHosts.size()); assertTrue(containerHosts.containsAll(slobrokHosts), "Slobroks are assigned from container nodes"); assertTrue(containerHosts.contains(admin.getLogserver().getHost()), "Logserver is assigned from container nodes"); assertEquals(0, admin.getConfigservers().size(), "No in-cluster config servers in a hosted environment"); } @Test public void testUsingNodesAndGroupCountAttributesWithoutDedicatedClusterControllers() { String services = "\n" + "" + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + " " + " 1" + " " + " " + " " + " " + " " + ""; int numberOfHosts = 67; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); VespaModel model = tester.createModel(services, true, deployStateWithClusterEndpoints("foo")); assertEquals(numberOfHosts, model.getRoot().hostSystem().getHosts().size()); // Check container cluster assertEquals(1, model.getContainerClusters().size()); Set containerHosts = model.getContainerClusters().get("foo").getContainers().stream() .map(Container::getHost) .collect(Collectors.toSet()); assertEquals(10, containerHosts.size()); // Check admin clusters Admin admin = model.getAdmin(); Set clusterControllerHosts = admin.getClusterControllers().getContainers() .stream().map(cc -> cc.getHostResource()).collect(Collectors.toSet()); Set slobrokHosts = admin.getSlobroks().stream().map(Slobrok::getHost).collect(Collectors.toSet()); assertEquals(3, slobrokHosts.size()); assertTrue(clusterControllerHosts.containsAll(slobrokHosts), "Slobroks are assigned on cluster controller nodes"); assertTrue(containerHosts.contains(admin.getLogserver().getHost()), "Logserver is assigned from container nodes"); assertEquals(0, admin.getConfigservers().size(), "No in-cluster config servers in a hosted environment"); assertEquals(3, admin.getClusterControllers().getContainers().size()); // Check content clusters ContentCluster cluster = model.getContentClusters().get("bar"); List subGroups = cluster.getRootGroup().getSubgroups(); assertEquals(0, cluster.getRootGroup().getNodes().size()); assertEquals(9, subGroups.size()); assertEquals("0", subGroups.get(0).getIndex()); assertEquals(3, subGroups.get(0).getNodes().size()); assertEquals(0, subGroups.get(0).getNodes().get(0).getDistributionKey()); assertEquals("bar/storage/0", subGroups.get(0).getNodes().get(0).getConfigId()); assertEquals("node-1-3-50-57", subGroups.get(0).getNodes().get(0).getHostName()); assertEquals(1, subGroups.get(0).getNodes().get(1).getDistributionKey()); assertEquals("bar/storage/1", subGroups.get(0).getNodes().get(1).getConfigId()); assertEquals(2, subGroups.get(0).getNodes().get(2).getDistributionKey()); assertEquals("bar/storage/2", subGroups.get(0).getNodes().get(2).getConfigId()); assertEquals("1", subGroups.get(1).getIndex()); assertEquals(3, subGroups.get(1).getNodes().size()); assertEquals(3, subGroups.get(1).getNodes().get(0).getDistributionKey()); assertEquals("bar/storage/3", subGroups.get(1).getNodes().get(0).getConfigId()); assertEquals("node-1-3-50-54", subGroups.get(1).getNodes().get(0).getHostName()); assertEquals(4, subGroups.get(1).getNodes().get(1).getDistributionKey()); assertEquals("bar/storage/4", subGroups.get(1).getNodes().get(1).getConfigId()); assertEquals(5, subGroups.get(1).getNodes().get(2).getDistributionKey()); assertEquals("bar/storage/5", subGroups.get(1).getNodes().get(2).getConfigId()); // ... assertEquals("node-1-3-50-51", subGroups.get(2).getNodes().get(0).getHostName()); // ... assertEquals("8", subGroups.get(8).getIndex()); assertEquals(3, subGroups.get(8).getNodes().size()); assertEquals(24, subGroups.get(8).getNodes().get(0).getDistributionKey()); assertEquals("bar/storage/24", subGroups.get(8).getNodes().get(0).getConfigId()); assertEquals(25, subGroups.get(8).getNodes().get(1).getDistributionKey()); assertEquals("bar/storage/25", subGroups.get(8).getNodes().get(1).getConfigId()); assertEquals(26, subGroups.get(8).getNodes().get(2).getDistributionKey()); assertEquals("bar/storage/26", subGroups.get(8).getNodes().get(2).getConfigId()); cluster = model.getContentClusters().get("baz"); subGroups = cluster.getRootGroup().getSubgroups(); assertEquals(0, cluster.getRootGroup().getNodes().size()); assertEquals(27, subGroups.size()); assertEquals("0", subGroups.get(0).getIndex()); assertEquals(1, subGroups.get(0).getNodes().size()); assertEquals(0, subGroups.get(0).getNodes().get(0).getDistributionKey()); assertEquals("baz/storage/0", subGroups.get(0).getNodes().get(0).getConfigId()); assertEquals("node-1-3-50-27", subGroups.get(0).getNodes().get(0).getHostName()); assertEquals("1", subGroups.get(1).getIndex()); assertEquals(1, subGroups.get(1).getNodes().size()); assertEquals(1, subGroups.get(1).getNodes().get(0).getDistributionKey()); assertEquals("baz/storage/1", subGroups.get(1).getNodes().get(0).getConfigId()); assertEquals("node-1-3-50-26", subGroups.get(1).getNodes().get(0).getHostName()); // ... assertEquals("node-1-3-50-25", subGroups.get(2).getNodes().get(0).getHostName()); // ... assertEquals("26", subGroups.get(26).getIndex()); assertEquals(1, subGroups.get(26).getNodes().size()); assertEquals(26, subGroups.get(26).getNodes().get(0).getDistributionKey()); assertEquals("baz/storage/26", subGroups.get(26).getNodes().get(0).getConfigId()); } @Test public void testGroupsOfSize1() { String services = "\n" + "" + " " + " " + " " + " " + " " + " 1" + " " + " " + " " + " " + " " + ""; int numberOfHosts = 21; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); VespaModel model = tester.createModel(services, true, deployStateWithClusterEndpoints("foo")); assertEquals(numberOfHosts, model.getRoot().hostSystem().getHosts().size()); ClusterControllerContainerCluster clusterControllers = model.getAdmin().getClusterControllers(); assertEquals(3, clusterControllers.getContainers().size()); assertEquals("cluster-controllers", clusterControllers.getName()); assertEquals("node-1-3-50-03", clusterControllers.getContainers().get(0).getHostName()); assertEquals("node-1-3-50-02", clusterControllers.getContainers().get(1).getHostName()); assertEquals("node-1-3-50-01", clusterControllers.getContainers().get(2).getHostName()); // Check content cluster ContentCluster cluster = model.getContentClusters().get("bar"); List subGroups = cluster.getRootGroup().getSubgroups(); assertEquals(0, cluster.getRootGroup().getNodes().size()); assertEquals(8, subGroups.size()); assertEquals(8, cluster.distributionBits()); // first group assertEquals("0", subGroups.get(0).getIndex()); assertEquals(1, subGroups.get(0).getNodes().size()); assertEquals(0, subGroups.get(0).getNodes().get(0).getDistributionKey()); assertEquals("bar/storage/0", subGroups.get(0).getNodes().get(0).getConfigId()); assertEquals("node-1-3-50-11", subGroups.get(0).getNodes().get(0).getHostName()); // second group assertEquals("1", subGroups.get(1).getIndex()); assertEquals(1, subGroups.get(1).getNodes().size()); assertEquals(1, subGroups.get(1).getNodes().get(0).getDistributionKey()); assertEquals("bar/storage/1", subGroups.get(1).getNodes().get(0).getConfigId()); assertEquals("node-1-3-50-10", subGroups.get(1).getNodes().get(0).getHostName()); // ... last group assertEquals("7", subGroups.get(7).getIndex()); assertEquals(1, subGroups.get(7).getNodes().size()); assertEquals(7, subGroups.get(7).getNodes().get(0).getDistributionKey()); assertEquals("bar/storage/7", subGroups.get(7).getNodes().get(0).getConfigId()); assertEquals("node-1-3-50-04", subGroups.get(7).getNodes().get(0).getHostName()); } @Test public void testSlobroksClustersAreExpandedToIncludeRetiredNodes() { String services = "\n" + "" + " " + " " + " " + " " + ""; int numberOfHosts = 11; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); VespaModel model = tester.createModel(Zone.defaultZone(), services, true, deployStateWithClusterEndpoints("foo"), "node-1-3-50-09"); assertEquals(numberOfHosts, model.getRoot().hostSystem().getHosts().size()); // Check slobroks clusters assertEquals(1+3, model.getAdmin().getSlobroks().size(), "Includes retired node"); assertEquals("node-1-3-50-11", model.getAdmin().getSlobroks().get(0).getHostName()); assertEquals("node-1-3-50-10", model.getAdmin().getSlobroks().get(1).getHostName()); assertEquals("node-1-3-50-08", model.getAdmin().getSlobroks().get(2).getHostName()); assertEquals("node-1-3-50-09", model.getAdmin().getSlobroks().get(3).getHostName(), "Included in addition because it is retired"); } @Test public void testSlobroksClustersAreExpandedToIncludeRetiredNodesWhenRetiredComesLast() { String services = "\n" + "" + " " + " " + " " + " " + ""; int numberOfHosts = 12; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); VespaModel model = tester.createModel(Zone.defaultZone(), services, true, deployStateWithClusterEndpoints("foo"), "node-1-3-50-03", "node-1-3-50-04"); assertEquals(10+2, model.getRoot().hostSystem().getHosts().size()); // Check slobroks clusters assertEquals(3+2, model.getAdmin().getSlobroks().size(), "Includes retired node"); assertEquals("node-1-3-50-12", model.getAdmin().getSlobroks().get(0).getHostName()); assertEquals("node-1-3-50-11", model.getAdmin().getSlobroks().get(1).getHostName()); assertEquals("node-1-3-50-10", model.getAdmin().getSlobroks().get(2).getHostName()); assertEquals("node-1-3-50-04", model.getAdmin().getSlobroks().get(3).getHostName(), "Included in addition because it is retired"); assertEquals("node-1-3-50-03", model.getAdmin().getSlobroks().get(4).getHostName(), "Included in addition because it is retired"); } @Test public void testSlobroksAreSpreadOverAllContainerClusters() { String services = "\n" + "" + " " + " " + " " + " " + " " + " " + " " + ""; int numberOfHosts = 16; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); VespaModel model = tester.createModel(Zone.defaultZone(), services, true, deployStateWithClusterEndpoints("foo", "bar"), "node-1-3-50-15", "node-1-3-50-05", "node-1-3-50-04"); assertEquals(numberOfHosts, model.getRoot().hostSystem().getHosts().size()); // Check slobroks clusters // ... from cluster default assertEquals(7, model.getAdmin().getSlobroks().size(), "Includes retired node"); assertEquals("node-1-3-50-16", model.getAdmin().getSlobroks().get(0).getHostName()); assertEquals("node-1-3-50-14", model.getAdmin().getSlobroks().get(1).getHostName()); assertEquals("node-1-3-50-15", model.getAdmin().getSlobroks().get(2).getHostName(), "Included in addition because it is retired"); // ... from cluster bar assertEquals("node-1-3-50-03", model.getAdmin().getSlobroks().get(3).getHostName()); assertEquals("node-1-3-50-05", model.getAdmin().getSlobroks().get(5).getHostName(), "Included in addition because it is retired"); assertEquals("node-1-3-50-04", model.getAdmin().getSlobroks().get(6).getHostName(), "Included in addition because it is retired"); } @Test public void testDedicatedClusterControllers() { String services = "\n" + "" + " " + " 2" + " " + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + ""; int numberOfHosts = 7; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); VespaModel model = tester.createModel(services, true, deployStateWithClusterEndpoints("foo.indexing", "bar.indexing")); assertEquals(7, model.getRoot().hostSystem().getHosts().size()); // Check cluster controllers ClusterControllerContainerCluster clusterControllers = model.getAdmin().getClusterControllers(); assertEquals(3, clusterControllers.getContainers().size()); assertEquals("cluster-controllers", clusterControllers.getName()); clusterControllers.getContainers().stream().map(ClusterControllerContainer::getHost).forEach(host -> { assertTrue(host.spec().membership().get().cluster().isStateful()); assertEquals(ClusterSpec.Type.admin, host.spec().membership().get().cluster().type()); }); } @Test public void testLogserverContainerWhenDedicatedLogserver() { String services = "\n" + "" + " " + " " + " " + " " + " " + " " + " " + " " + ""; boolean useDedicatedNodeForLogserver = false; testContainerOnLogserverHost(services, useDedicatedNodeForLogserver); } @Test public void testLogForwarderNotInAdminCluster() { String services = "\n" + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; int numberOfHosts = 2; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts+1); VespaModel model = tester.createModel(Zone.defaultZone(), services, true, deployStateWithClusterEndpoints("foo")); assertEquals(numberOfHosts, model.getRoot().hostSystem().getHosts().size()); Admin admin = model.getAdmin(); Logserver logserver = admin.getLogserver(); HostResource hostResource = logserver.getHostResource(); assertNotNull(hostResource.getService("logserver")); assertNull(hostResource.getService("container")); assertNull(hostResource.getService("logforwarder")); var clist = model.getContainerClusters().get("foo").getContainers(); assertEquals(1, clist.size()); hostResource = clist.get(0).getHostResource(); assertNull(hostResource.getService("logserver")); assertNotNull(hostResource.getService("container")); assertNotNull(hostResource.getService("logforwarder")); var lfs = hostResource.getService("logforwarder"); String shutdown = lfs.getPreShutdownCommand().orElse(""); assertTrue(shutdown.startsWith("$ROOT/bin/vespa-logforwarder-start -S -c hosts/")); } @Test public void testLogForwarderInAdminCluster() { String services = "\n" + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; int numberOfHosts = 2; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts+1); VespaModel model = tester.createModel(Zone.defaultZone(), services, true, deployStateWithClusterEndpoints("foo")); assertEquals(numberOfHosts, model.getRoot().hostSystem().getHosts().size()); Admin admin = model.getAdmin(); Logserver logserver = admin.getLogserver(); HostResource hostResource = logserver.getHostResource(); assertNotNull(hostResource.getService("logserver")); assertNull(hostResource.getService("container")); assertNotNull(hostResource.getService("logforwarder")); var clist = model.getContainerClusters().get("foo").getContainers(); assertEquals(1, clist.size()); hostResource = clist.get(0).getHostResource(); assertNull(hostResource.getService("logserver")); assertNotNull(hostResource.getService("container")); assertNotNull(hostResource.getService("logforwarder")); } @Test public void testImplicitLogserverContainer() { String services = "\n" + "" + " " + " " + " " + ""; boolean useDedicatedNodeForLogserver = true; testContainerOnLogserverHost(services, useDedicatedNodeForLogserver); } @Test public void testUsingNodesAndGroupCountAttributesAndGettingTooFewNodes() { String services = "" + "" + " " + " " + // Ignored " " + " " + " 4" + " " + " " + " " + " " + " 3" + " " + ""; int numberOfHosts = 6; // We only have 6 content nodes -> 3 groups with redundancy 2 in each VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); VespaModel model = tester.createModel(services, false, deployStateWithClusterEndpoints("bar.indexing")); assertEquals(numberOfHosts, model.getRoot().hostSystem().getHosts().size()); ContentCluster cluster = model.getContentClusters().get("bar"); List subGroups = cluster.getRootGroup().getSubgroups(); assertEquals(2*3, cluster.getRedundancy().effectiveInitialRedundancy()); // Reduced from 3*3 assertEquals(2*3, cluster.getRedundancy().effectiveFinalRedundancy()); // Reduced from 3*4 assertEquals(2*3, cluster.getRedundancy().effectiveReadyCopies()); // Reduced from 3*3 assertEquals("2|2|*", cluster.getRootGroup().getPartitions().get()); // Reduced from 4|4|* assertEquals(0, cluster.getRootGroup().getNodes().size()); assertEquals(3, subGroups.size()); assertEquals("0", subGroups.get(0).getIndex()); assertEquals(2, subGroups.get(0).getNodes().size()); assertEquals(0, subGroups.get(0).getNodes().get(0).getDistributionKey()); assertEquals("bar/storage/0", subGroups.get(0).getNodes().get(0).getConfigId()); assertEquals(1, subGroups.get(0).getNodes().get(1).getDistributionKey()); assertEquals("bar/storage/1", subGroups.get(0).getNodes().get(1).getConfigId()); assertEquals("1", subGroups.get(1).getIndex()); assertEquals(2, subGroups.get(1).getNodes().size()); assertEquals(2, subGroups.get(1).getNodes().get(0).getDistributionKey()); assertEquals("bar/storage/2", subGroups.get(1).getNodes().get(0).getConfigId()); assertEquals(3, subGroups.get(1).getNodes().get(1).getDistributionKey()); assertEquals("bar/storage/3", subGroups.get(1).getNodes().get(1).getConfigId()); assertEquals("2", subGroups.get(2).getIndex()); assertEquals(2, subGroups.get(2).getNodes().size()); assertEquals(4, subGroups.get(2).getNodes().get(0).getDistributionKey()); assertEquals("bar/storage/4", subGroups.get(2).getNodes().get(0).getConfigId()); assertEquals(5, subGroups.get(2).getNodes().get(1).getDistributionKey()); assertEquals("bar/storage/5", subGroups.get(2).getNodes().get(1).getConfigId()); } @Test public void testRedundancyWithGroupsTooHighRedundancyAndOneRetiredNode() { String services = "" + "" + " " + " 2" + // Should have been illegal since we only have 1 node per group " " + " " + " " + " " + " " + ""; int numberOfHosts = 3; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); try { VespaModel model = tester.createModel(services, false, "node-1-3-50-03"); fail("Expected exception"); } catch (IllegalArgumentException e) { assertEquals("In content cluster 'bar': This cluster specifies redundancy 2, " + "but this cannot be higher than the minimum nodes per group, which is 1", Exceptions.toMessageString(e)); } } @Test public void testRedundancyWithGroupsAndThreeRetiredNodes() { String services = "" + "" + " " + " 1" + " " + " " + " " + " " + " " + ""; int numberOfHosts = 5; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); VespaModel model = tester.createModel(Zone.defaultZone(), services, false, deployStateWithClusterEndpoints("bar.indexing"), "node-1-3-50-05", "node-1-3-50-04", "node-1-3-50-03"); assertEquals(numberOfHosts, model.getRoot().hostSystem().getHosts().size()); ContentCluster cluster = model.getContentClusters().get("bar"); assertEquals(2, cluster.getRedundancy().effectiveInitialRedundancy()); assertEquals(2, cluster.getRedundancy().effectiveFinalRedundancy()); assertEquals(2, cluster.getRedundancy().effectiveReadyCopies()); assertEquals("1|*", cluster.getRootGroup().getPartitions().get()); assertEquals(0, cluster.getRootGroup().getNodes().size()); assertEquals(2, cluster.getRootGroup().getSubgroups().size()); } @Test public void testRedundancy2DownscaledToOneNodeButOneRetired() { String services = "" + "" + " " + " 2" + " " + " " + " " + " " + " " + ""; int numberOfHosts = 3; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); VespaModel model = tester.createModel(Zone.defaultZone(), services, false, false, true, NodeResources.unspecified(), 0, Optional.empty(), deployStateWithClusterEndpoints("bar.indexing"), "node-1-3-50-03"); assertEquals(numberOfHosts, model.getRoot().hostSystem().getHosts().size()); ContentCluster cluster = model.getContentClusters().get("bar"); assertEquals(2, cluster.getStorageCluster().getChildren().size()); assertEquals(1, cluster.getRedundancy().effectiveInitialRedundancy()); assertEquals(1, cluster.getRedundancy().effectiveFinalRedundancy()); assertEquals(1, cluster.getRedundancy().effectiveReadyCopies()); assertEquals(2, cluster.getRootGroup().getNodes().size()); assertEquals(0, cluster.getRootGroup().getSubgroups().size()); } @Test public void testUsingNodesCountAttributesAndGettingTooFewNodes() { String services = "" + "" + " " + " " + // Ignored " " + " " + " " + " " + " " + " " + " 12" + " " + " " + " " + " " + " 5" + " 7" + // TODO: Allowed, but ignored, remove in Vespa 9 " " + ""; int numberOfHosts = 6; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); VespaModel model = tester.createModel(services, false, deployStateWithClusterEndpoints("container")); assertEquals(numberOfHosts, model.getRoot().hostSystem().getHosts().size()); ContentCluster cluster = model.getContentClusters().get("bar"); assertEquals(4, cluster.getRedundancy().effectiveInitialRedundancy()); assertEquals(4, cluster.getRedundancy().effectiveFinalRedundancy()); assertEquals(4, cluster.getRedundancy().effectiveReadyCopies()); assertEquals(4, cluster.getRedundancy().readyCopies()); assertFalse(cluster.getRootGroup().getPartitions().isPresent()); assertEquals(4, cluster.getRootGroup().getNodes().size()); assertEquals(0, cluster.getRootGroup().getSubgroups().size()); assertEquals(4, cluster.getRootGroup().getNodes().size()); assertEquals(0, cluster.getRootGroup().getNodes().get(0).getDistributionKey()); assertEquals("bar/storage/0", cluster.getRootGroup().getNodes().get(0).getConfigId()); assertEquals(1, cluster.getRootGroup().getNodes().get(1).getDistributionKey()); assertEquals("bar/storage/1", cluster.getRootGroup().getNodes().get(1).getConfigId()); assertEquals(2, cluster.getRootGroup().getNodes().get(2).getDistributionKey()); assertEquals("bar/storage/2", cluster.getRootGroup().getNodes().get(2).getConfigId()); assertEquals(3, cluster.getRootGroup().getNodes().get(3).getDistributionKey()); assertEquals("bar/storage/3", cluster.getRootGroup().getNodes().get(3).getConfigId()); } @Test public void testUsingNodesAndGroupCountAttributesAndGettingJustOneNode() { String services = "\n" + "" + " " + " " + // Ignored " " + " " + " 4" + " " + " " + " " + " " + " 3" + " " + ""; int numberOfHosts = 1; // We only have 1 content node -> 1 groups with redundancy 1 VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); VespaModel model = tester.createModel(services, false, deployStateWithClusterEndpoints("bar.indexing")); assertEquals(numberOfHosts, model.getRoot().hostSystem().getHosts().size()); ContentCluster cluster = model.getContentClusters().get("bar"); assertEquals(1, cluster.getRedundancy().effectiveInitialRedundancy()); // Reduced from 3*3 assertEquals(1, cluster.getRedundancy().effectiveFinalRedundancy()); // Reduced from 3*4 assertEquals(1, cluster.getRedundancy().effectiveReadyCopies()); // Reduced from 3*3 assertFalse(cluster.getRootGroup().getPartitions().isPresent()); // 1 group - > flattened -> no distribution assertEquals(1, cluster.getRootGroup().getNodes().size()); assertEquals(0, cluster.getRootGroup().getSubgroups().size()); assertEquals(1, cluster.getRootGroup().getNodes().size()); assertEquals(0, cluster.getRootGroup().getNodes().get(0).getDistributionKey()); assertEquals("bar/storage/0", cluster.getRootGroup().getNodes().get(0).getConfigId()); } @Test public void testRequiringMoreNodesThanAreAvailable() { assertThrows(IllegalArgumentException.class, () -> { String services = "\n" + "" + " " + " 1" + " " + " " + " " + " " + " " + ""; int numberOfHosts = 2; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); tester.createModel(services, false); }); } @Test public void testRequiredNodesAndDedicatedClusterControllers() { assertThrows(IllegalArgumentException.class, () -> { String services = "\n" + "" + " " + " 1" + " " + " " + " " + " " + " " + ""; int numberOfHosts = 4; // needs 2 for foo and 3 for cluster controllers. VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); tester.createModel(services, false); }); } @Test public void testExclusiveNodes() { String services = "\n" + "" + "" + " " + " " + " " + " 1" + " " + " " + " " + " " + " " + ""; int numberOfHosts = 5; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); VespaModel model = tester.createModel(services, false, deployStateWithClusterEndpoints("container")); model.hostSystem().getHosts().forEach(host -> assertTrue(host.spec().membership().get().cluster().isExclusive())); } @Test public void testUsingNodesCountAttributesAndGettingJustOneNode() { String services = "\n" + "" + " " + " " + // Ignored " " + " " + " 12" + " " + " " + " " + " " + " 5" + " 7" + " " + ""; int numberOfHosts = 1; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); VespaModel model = tester.createModel(services, false, deployStateWithClusterEndpoints("bar.indexing")); assertEquals(numberOfHosts, model.getRoot().hostSystem().getHosts().size()); ContentCluster cluster = model.getContentClusters().get("bar"); assertEquals(1, cluster.getRedundancy().effectiveInitialRedundancy()); assertEquals(1, cluster.getRedundancy().effectiveFinalRedundancy()); assertEquals(1, cluster.getRedundancy().effectiveReadyCopies()); assertFalse(cluster.getRootGroup().getPartitions().isPresent()); assertEquals(1, cluster.getRootGroup().getNodes().size()); assertEquals(0, cluster.getRootGroup().getSubgroups().size()); assertEquals(1, cluster.getRootGroup().getNodes().size()); assertEquals(0, cluster.getRootGroup().getNodes().get(0).getDistributionKey()); assertEquals("bar/storage/0", cluster.getRootGroup().getNodes().get(0).getConfigId()); } @Test public void testRequestingSpecificNodeResources() { String services = """ 3 3 """; int totalHosts = 23; VespaModelTester tester = new VespaModelTester(); tester.addHosts(new NodeResources(0.1, 0.2, 300, 0.3, NodeResources.DiskSpeed.slow), 1);// Logserver tester.addHosts(new NodeResources(0.1, 0.3, 1, 0.5), 2); // Slobrok tester.addHosts(new NodeResources(12, 10, 30, 0.3, NodeResources.DiskSpeed.fast, NodeResources.StorageType.local, NodeResources.Architecture.arm64), 4); // Container tester.addHosts(new NodeResources(4, 16, 125, 10, NodeResources.DiskSpeed.fast, NodeResources.StorageType.local, Architecture.x86_64, new NodeResources.GpuResources(1, 16)), 4); // Container 2 tester.addHosts(new NodeResources(8, 200, 1000000, 0.3), 5); // Content-foo tester.addHosts(new NodeResources(10, 64, 200, 0.3), 6); // Content-bar tester.addHosts(new NodeResources(0.5, 2, 10, 0.3), 6); // Cluster-controller VespaModel model = tester.createModel(Zone.defaultZone(), services, true, false, false, NodeResources.unspecified(), 0, Optional.empty(), deployStateWithClusterEndpoints("container", "container2")); assertEquals(totalHosts, model.getRoot().hostSystem().getHosts().size()); } @Test public void testRequestingRangesMin() { String services = "" + "" + " " + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + " " + " " + ""; int totalHosts = 10; VespaModelTester tester = new VespaModelTester(); tester.addHosts(new NodeResources(11.5, 10, 30, 0.3), 6); tester.addHosts(new NodeResources(85, 200, 1000_000_000, 0.3), 20); tester.addHosts(new NodeResources( 0.5, 2, 10, 0.3), 3); VespaModel model = tester.createModel(services, true); assertEquals(4 + 6 + 1, model.getRoot().hostSystem().getHosts().size()); } @Test public void testRequestingRangesMax() { String services = "" + "" + " " + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + " " + " " + ""; int totalHosts = 29; VespaModelTester tester = new VespaModelTester(); tester.addHosts(new NodeResources(13.5, 100, 1000, 0.3), 6); tester.addHosts(new NodeResources(85, 200, 1000_000_000, 0.3), 20); tester.addHosts(new NodeResources( 0.5, 2, 10, 0.3), 3); VespaModel model = tester.createModel(services, true, true); assertEquals(totalHosts, model.getRoot().hostSystem().getHosts().size()); } @Test public void testUseArm64NodesForAdminCluster() { String services = "" + "" + " " + " " + " " + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.setHosted(true); tester.useDedicatedNodeForLogserver(true); tester.addHosts(new NodeResources(13.5, 100, 1000, 0.3), 4); tester.addHosts(new NodeResources(0.5, 2, 50, 0.3, DiskSpeed.fast, StorageType.any, Architecture.arm64), 4); // 3 ccs, 1 logserver VespaModel model = tester.createModel(services, true, true); List hosts = model.getRoot().hostSystem().getHosts(); assertEquals(8, hosts.size()); Set clusterControllerResources = getHostResourcesForService(hosts, "container-clustercontroller"); assertEquals(3, clusterControllerResources.size()); assertTrue(clusterControllerResources.stream().allMatch(host -> host.realResources().architecture() == Architecture.arm64)); Set logserverResources = getHostResourcesForService(hosts, "logserver-container"); assertEquals(1, logserverResources.size()); assertTrue(logserverResources.stream().allMatch(host -> host.realResources().architecture() == Architecture.arm64)); // Other hosts should be default assertTrue(hosts.stream() .filter(host -> !clusterControllerResources.contains(host)) .filter(host -> !logserverResources.contains(host)) .allMatch(host -> host.realResources().architecture() == Architecture.getDefault())); } private Set getHostResourcesForService(List hosts, String service) { return hosts.stream() .filter(host -> host.getHostInfo().getServices().stream() .anyMatch(s -> s.getServiceType().equals(service))) .collect(Collectors.toSet()); } @Test public void testContainerOnly() { String services = "\n" + "" + " " + " " + ""; int numberOfHosts = 3; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); VespaModel model = tester.createModel(services, true, deployStateWithClusterEndpoints("container")); assertEquals(numberOfHosts, model.getRoot().hostSystem().getHosts().size()); assertEquals(3, model.getContainerClusters().get("container").getContainers().size()); assertNotNull(model.getAdmin().getLogserver()); assertEquals(3, model.getAdmin().getSlobroks().size()); } @Test public void testJvmOptions() { String services = "\n" + "" + " " + " " + " " + " " + ""; int numberOfHosts = 3; VespaModelTester tester = new VespaModelTester(); tester.addHosts(numberOfHosts); VespaModel model = tester.createModel(services, true, deployStateWithClusterEndpoints("container")); assertEquals(numberOfHosts, model.getRoot().hostSystem().getHosts().size()); assertEquals("-DfooOption=xyz", model.getContainerClusters().get("container").getContainers().get(0).getAssignedJvmOptions()); } @Test public void testUsingHostaliasWithProvisioner() { String services = "\n" + "" + "" + " \n"+ "\n" + "" + " " + " " + " " + " " + " " + " " + "" + ""; VespaModelTester tester = new VespaModelTester(); tester.setHosted(false); tester.addHosts(1); VespaModel model = tester.createModel(services, true); assertEquals(1, model.getRoot().hostSystem().getHosts().size()); assertEquals(1, model.getAdmin().getSlobroks().size()); } @Test public void testThatStandaloneSyntaxWorksOnHostedVespa() { String services = "" + "" + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.addHosts(2); VespaModel model = tester.createModel(services, true, deployStateWithClusterEndpoints("foo")); assertEquals(2, model.getHosts().size()); assertEquals(1, model.getContainerClusters().size()); assertEquals(2, model.getContainerClusters().get("foo").getContainers().size()); } @Test public void testThatStandaloneSyntaxOnHostedVespaRequiresDefaultPort() { try { String services = "" + "" + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.addHosts(1); VespaModel model = tester.createModel(services, true); fail("Expected exception"); } catch (IllegalArgumentException e) { // Success assertEquals("Illegal port 8095 in http server 'server1': Port must be set to " + getDefaults().vespaWebServicePort(), e.getMessage()); } } @Test public void testThatStandaloneSyntaxWorksOnHostedManuallyDeployed() { String services = "" + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.setHosted(true); tester.addHosts(4); VespaModel model = tester.createModel(new Zone(Environment.dev, RegionName.from("us-central-1")), services, true, deployStateWithClusterEndpoints("foo")); assertEquals(3, model.getHosts().size(), "We get 1 node per cluster and no admin node apart from the dedicated cluster controller"); assertEquals(1, model.getContainerClusters().size()); assertEquals(1, model.getContainerClusters().get("foo").getContainers().size()); assertEquals(1, model.getContentClusters().get("bar").getRootGroup().countNodes(true)); assertEquals(1, model.getAdmin().getClusterControllers().getContainers().size()); } @Test public void testThatStandaloneSyntaxWithClusterControllerWorksOnHostedManuallyDeployed() { String services = "" + "" + " " + " " + " " + " " + " " + " " + " " + " 1" + " " + " " + " " + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.setHosted(true); tester.addHosts(4); try { VespaModel model = tester.createModel(new Zone(Environment.staging, RegionName.from("us-central-1")), services, true); fail("expected failure"); } catch (IllegalArgumentException e) { assertEquals("In content cluster 'bar': Clusters in hosted environments must have a tag\n" + "matching all zones, and having no subtags,\nsee https://cloud.vespa.ai/en/reference/services", Exceptions.toMessageString(e)); } } /** Deploying an application with "nodes count" standalone should give a single-node deployment */ @Test public void testThatHostedSyntaxWorksOnStandalone() { String services = "" + "" + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.setHosted(false); tester.addHosts(3); VespaModel model = tester.createModel(services, true); assertEquals(1, model.getContainerClusters().get("container1").getContainers().size(), "Nodes in container cluster"); assertEquals(1, model.getContentClusters().get("content").getRootGroup().getNodes().size(), "Nodes in content cluster (downscaled)"); assertEquals(1, model.getAdmin().getSlobroks().size()); model.getConfig(new StorStatusConfig.Builder(), "default"); StorageCluster storage = model.getContentClusters().get("content").getStorageCluster(); StorCommunicationmanagerConfig.Builder builder = new StorCommunicationmanagerConfig.Builder(); storage.getChildren().get("0").getConfig(builder); } @Test public void testMinRedundancyMetByGroups() { String services = "" + "" + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.setHosted(true); tester.addHosts(6); VespaModel model = tester.createModel(services, true, deployStateWithClusterEndpoints("container1")); var contentCluster = model.getContentClusters().get("content"); ProtonConfig.Builder protonBuilder = new ProtonConfig.Builder(); contentCluster.getSearch().getConfig(protonBuilder); ProtonConfig protonConfig = new ProtonConfig(protonBuilder); assertEquals(1, protonConfig.distribution().searchablecopies()); assertEquals(1, protonConfig.distribution().redundancy()); } @Test public void testMinRedundancyMetWithinGroup() { String services = "" + "" + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.setHosted(true); tester.addHosts(6); VespaModel model = tester.createModel(services, true, deployStateWithClusterEndpoints("container1")); var contentCluster = model.getContentClusters().get("content"); ProtonConfig.Builder protonBuilder = new ProtonConfig.Builder(); contentCluster.getSearch().getConfig(protonBuilder); ProtonConfig protonConfig = new ProtonConfig(protonBuilder); assertEquals(2, protonConfig.distribution().searchablecopies()); assertEquals(2, protonConfig.distribution().redundancy()); } @Test public void testRedundancy1() { String services = "" + "" + " " + " " + " " + " " + " 1" + " " + " " + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.setHosted(true); tester.addHosts(6); VespaModel model = tester.createModel(services, true, deployStateWithClusterEndpoints("container1")); var contentCluster = model.getContentClusters().get("content"); ProtonConfig.Builder protonBuilder = new ProtonConfig.Builder(); contentCluster.getSearch().getConfig(protonBuilder); ProtonConfig protonConfig = new ProtonConfig(protonBuilder); assertEquals(1, protonConfig.distribution().searchablecopies()); assertEquals(1, protonConfig.distribution().redundancy()); } /** * Deploying an application with "nodes count" standalone should give a single-node deployment, * also if the user has a lingering hosts file from running self-hosted. * * NOTE: This does *not* work (but gives an understandable error message), * but the current code does not get provoke the error that is thrown from HostsXmlProvisioner.prepare */ @Test public void testThatHostedSyntaxWorksOnStandaloneAlsoWithAHostedFile() { String services = "" + "" + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + ""; String hosts = "\n" + "\n" + " \n" + " vespa-1\n" + " \n" + " \n" + " vespa-2\n" + " \n" + " \n" + " vespa-3\n" + " \n" + ""; VespaModelTester tester = new VespaModelTester(); tester.setHosted(false); tester.addHosts(3); VespaModel model = tester.createModel(services, hosts, true); assertEquals(1, model.getContainerClusters().get("container1").getContainers().size(), "Nodes in container cluster"); assertEquals(1, model.getContentClusters().get("content").getRootGroup().getNodes().size(), "Nodes in content cluster (downscaled)"); assertEquals(1, model.getAdmin().getSlobroks().size()); model.getConfig(new StorStatusConfig.Builder(), "default"); StorageCluster storage = model.getContentClusters().get("content").getStorageCluster(); StorCommunicationmanagerConfig.Builder builder = new StorCommunicationmanagerConfig.Builder(); storage.getChildren().get("0").getConfig(builder); } @Test public void testNoNodeTagMeansTwoNodes() { String services = "\n" + "" + " " + " " + " " + " " + " " + " 3" + " " + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.addHosts(6); VespaModel model = tester.createModel(services, true, deployStateWithClusterEndpoints("foo")); assertEquals(6, model.getRoot().hostSystem().getHosts().size()); assertEquals(3, model.getAdmin().getSlobroks().size()); assertEquals(2, model.getContainerClusters().get("foo").getContainers().size()); assertEquals(1, model.getContentClusters().get("bar").getRootGroup().countNodes(true)); } @Test public void testNoNodeTagMeansTwoNodesNoContent() { String services = "\n" + "" + " " + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.addHosts(2); VespaModel model = tester.createModel(services, true, deployStateWithClusterEndpoints("foo")); assertEquals(2, model.getRoot().hostSystem().getHosts().size()); assertEquals(2, model.getAdmin().getSlobroks().size()); assertEquals(2, model.getContainerClusters().get("foo").getContainers().size()); } @Test public void testNoNodeTagMeans1NodeNonHosted() { String services = "\n" + "" + " " + " " + " " + " " + " " + " 3" + " " + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.setHosted(false); tester.addHosts(1); VespaModel model = tester.createModel(services, true); assertEquals(1, model.getRoot().hostSystem().getHosts().size()); assertEquals(1, model.getAdmin().getSlobroks().size()); assertEquals(1, model.getContainerClusters().get("foo").getContainers().size()); assertEquals(1, model.getContentClusters().get("bar").getRootGroup().recursiveGetNodes().size()); } @Test public void testSingleNodeNonHosted() { String services = "\n" + "" + " " + " " + " " + " "+ " " + " " + " 3" + " " + " " + " " + " "+ " " + ""; VespaModelTester tester = new VespaModelTester(); tester.setHosted(false); tester.addHosts(1); VespaModel model = tester.createModel(services, true); assertEquals(1, model.getRoot().hostSystem().getHosts().size()); assertEquals(1, model.getAdmin().getSlobroks().size()); assertEquals(1, model.getContainerClusters().get("foo").getContainers().size()); assertEquals(1, model.getContentClusters().get("bar").getRootGroup().countNodes(true)); } /** Recreate the combination used in some factory tests */ @Test public void testMultitenantButNotHosted() { String services = "" + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + " " + " 0" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " "; VespaModel model = createNonProvisionedMultitenantModel(services); assertEquals(1, model.getRoot().hostSystem().getHosts().size()); ContentCluster content = model.getContentClusters().get("storage"); assertEquals(2, content.getRootGroup().getNodes().size()); ContainerCluster controller = model.getAdmin().getClusterControllers(); assertEquals(1, controller.getContainers().size()); } @Test public void testModelWithReferencedIndexingCluster() { String services = "\n" + "\n" + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n" + " \n" + " \n" + " 1.0\n" + " \n" + " 2\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n" + ""; VespaModel model = createNonProvisionedMultitenantModel(services); assertEquals(1, model.getRoot().hostSystem().getHosts().size()); ContentCluster content = model.getContentClusters().get("storage"); assertEquals(1, content.getRootGroup().getNodes().size()); ContainerCluster controller = model.getAdmin().getClusterControllers(); assertEquals(1, controller.getContainers().size()); } @Test public void testSharedNodesNotHosted() { String hosts = "\n" + "\n" + " \n" + " vespa-1\n" + " \n" + " \n" + " vespa-2\n" + " \n" + " \n" + " vespa-3\n" + " \n" + ""; String services = "\n" + "\n" + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n" + " \n" + " \n" + " 1.0\n" + " \n" + " 2\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n" + ""; VespaModel model = createNonProvisionedModel(false, hosts, services); assertEquals(3, model.getRoot().hostSystem().getHosts().size()); ContentCluster content = model.getContentClusters().get("storage"); assertEquals(3, content.getRootGroup().getNodes().size()); } @Test public void testMultitenantButNotHostedSharedContentNode() { String services = "" + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + " " + " 0" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + " " + " " + " "; VespaModel model = createNonProvisionedMultitenantModel(services); assertEquals(1, model.getRoot().hostSystem().getHosts().size()); ContentCluster content = model.getContentClusters().get("storage"); assertEquals(2, content.getRootGroup().getNodes().size()); ContainerCluster controller = model.getAdmin().getClusterControllers(); assertEquals(1, controller.getContainers().size()); } @Test public void testStatefulProperty() { String servicesXml = "" + "" + " " + " " + " " + " " + " " + " " + " " + " " + " 2" + " " + " " + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.addHosts(9); VespaModel model = tester.createModel(servicesXml, true, deployStateWithClusterEndpoints("qrs", "zk")); Map tests = Map.of("qrs", false, "zk", true, "content", true); Map> hostsByCluster = model.hostSystem().getHosts().stream() .collect(Collectors.groupingBy(h -> h.spec().membership().get().cluster().id().value())); tests.forEach((clusterId, stateful) -> { List hosts = hostsByCluster.getOrDefault(clusterId, List.of()); assertFalse(hosts.isEmpty(), "Hosts are provisioned for '" + clusterId + "'"); assertEquals(stateful, hosts.stream().allMatch(h -> h.spec().membership().get().cluster().isStateful()), "Hosts in cluster '" + clusterId + "' are " + (stateful ? "" : "not ") + "stateful"); }); } @Test public void testAllow2ContentGroupsDown() { String servicesXml = "" + "" + " " + " " + " " + " " + " 1" + " " + " " + " " + " " + " " + " " + " 0.5" + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.addHosts(9); VespaModel model = tester.createModel(servicesXml, true, deployStateWithClusterEndpoints("qrs").properties(new TestProperties())); var fleetControllerConfigBuilder = new FleetcontrollerConfig.Builder(); model.getConfig(fleetControllerConfigBuilder, "admin/standalone/cluster-controllers/0/components/clustercontroller-content-configurer"); assertEquals(2, fleetControllerConfigBuilder.build().max_number_of_groups_allowed_to_be_down()); } @Test public void containerWithZooKeeperSuboptimalNodeCountDuringRetirement() { String servicesXml = "" + "" + " " + " " + " " + " " + ""; VespaModelTester tester = new VespaModelTester(); tester.addHosts(4); VespaModel model = tester.createModel(Zone.defaultZone(), servicesXml, true, deployStateWithClusterEndpoints("zk"), "node-1-3-50-04"); ApplicationContainerCluster cluster = model.getContainerClusters().get("zk"); assertEquals(1, cluster.getContainers().stream().filter(Container::isRetired).count()); assertEquals(3, cluster.getContainers().stream().filter(c -> !c.isRetired()).count()); } @Test public void containerWithZooKeeperJoiningServers() { Function servicesXml = (nodeCount) -> { return "" + "" + " " + " " + " " + " " + ""; }; VespaModelTester tester = new VespaModelTester(); tester.addHosts(5); VespaModel model = tester.createModel(servicesXml.apply(3), true, deployStateWithClusterEndpoints("zk")); { ApplicationContainerCluster cluster = model.getContainerClusters().get("zk"); ZookeeperServerConfig.Builder config = new ZookeeperServerConfig.Builder(); cluster.getContainers().forEach(c -> c.getConfig(config)); cluster.getConfig(config); assertTrue(config.build().server().stream().noneMatch(ZookeeperServerConfig.Server::joining), "Initial servers are not joining"); } { VespaModel nextModel = tester.createModel(Zone.defaultZone(), servicesXml.apply(3), true, false, false, NodeResources.unspecified(), 0, Optional.of(model), deployStateWithClusterEndpoints("zk"), "node-1-3-50-04", "node-1-3-50-03"); ApplicationContainerCluster cluster = nextModel.getContainerClusters().get("zk"); ZookeeperServerConfig.Builder config = new ZookeeperServerConfig.Builder(); cluster.getContainers().forEach(c -> c.getConfig(config)); cluster.getConfig(config); assertEquals(Map.of(0, false, 1, false, 2, false, 3, true, 4, true), config.build().server().stream().collect(Collectors.toMap(ZookeeperServerConfig.Server::id, ZookeeperServerConfig.Server::joining)), "New nodes are joining"); assertEquals(Map.of(0, false, 1, true, 2, true, 3, false, 4, false), config.build().server().stream().collect(Collectors.toMap(ZookeeperServerConfig.Server::id, ZookeeperServerConfig.Server::retired)), "Retired nodes are retired"); } } private VespaModel createNonProvisionedMultitenantModel(String services) { return createNonProvisionedModel(true, null, services); } private VespaModel createNonProvisionedModel(boolean multitenant, String hosts, String services) { VespaModelCreatorWithMockPkg modelCreatorWithMockPkg = new VespaModelCreatorWithMockPkg(hosts, services, generateSchemas("type1")); ApplicationPackage appPkg = modelCreatorWithMockPkg.appPkg; DeployState deployState = new DeployState.Builder().applicationPackage(appPkg). properties((new TestProperties()).setMultitenant(multitenant)). build(); return modelCreatorWithMockPkg.create(false, deployState); } private int physicalMemoryPercentage(ContainerCluster cluster) { QrStartConfig.Builder b = new QrStartConfig.Builder(); cluster.getConfig(b); return b.build().jvm().heapSizeAsPercentageOfPhysicalMemory(); } private long protonMemorySize(ContentCluster cluster) { ProtonConfig.Builder b = new ProtonConfig.Builder(); cluster.getSearch().getSearchCluster().getSearchNode(0).getConfig(b); return b.build().hwinfo().memory().size(); } @Test public void require_that_proton_config_is_tuned_based_on_node_resources() { String services = joinLines("", "", " ", " 2" + " ", " ", " ", " ", " ", " ", " ", ""); VespaModelTester tester = new VespaModelTester(); tester.addHosts(new NodeResources(1, 3, 10, 5, NodeResources.DiskSpeed.slow), 5); VespaModel model = tester.createModel(Zone.defaultZone(), services, true, false, false, NodeResources.unspecified(), 0, Optional.empty(), deployStateWithClusterEndpoints("test.indexing")); ContentSearchCluster cluster = model.getContentClusters().get("test").getSearch(); assertEquals(2, cluster.getSearchNodes().size()); assertEquals(40, getProtonConfig(cluster, 0).hwinfo().disk().writespeed(), 0.001); assertEquals(40, getProtonConfig(cluster, 1).hwinfo().disk().writespeed(), 0.001); } @Test public void require_that_resources_can_be_partially_specified() { String services = joinLines("", "", " ", " 2" + " ", " ", " ", " ", " ", " ", " ", ""); VespaModelTester tester = new VespaModelTester(); tester.addHosts(new NodeResources(1, 3, 10, 5), 5); VespaModel model = tester.createModel(Zone.defaultZone(), services, true, false, false, new NodeResources(1.0, 3.0, 9.0, 1.0), 0, Optional.empty(), deployStateWithClusterEndpoints("test.indexing")); ContentSearchCluster cluster = model.getContentClusters().get("test").getSearch(); assertEquals(2, cluster.getSearchNodes().size()); } private static ProtonConfig getProtonConfig(ContentSearchCluster cluster, int searchNodeIdx) { ProtonConfig.Builder builder = new ProtonConfig.Builder(); List searchNodes = cluster.getSearchNodes(); assertTrue(searchNodeIdx < searchNodes.size()); searchNodes.get(searchNodeIdx).getConfig(builder); return new ProtonConfig(builder); } @Test public void require_that_config_override_and_explicit_proton_tuning_and_resource_limits_have_precedence_over_default_node_resource_tuning() { String services = joinLines("", "", " ", " 1" + " ", " 2000", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " 1000", " ", " ", " ", " ", " ", " ", " ", " ", ""); VespaModelTester tester = new VespaModelTester(); tester.addHosts(new NodeResources(1, 3, 10, 1), 4); tester.addHosts(new NodeResources(1, 128, 100, 0.3), 1); VespaModel model = tester.createModel(Zone.defaultZone(), services, true, false, false, NodeResources.unspecified(), 0, Optional.empty(), deployStateWithClusterEndpoints("test.indexing")); ContentSearchCluster cluster = model.getContentClusters().get("test").getSearch(); ProtonConfig cfg = getProtonConfig(model, cluster.getSearchNodes().get(0).getConfigId()); assertEquals(2000, cfg.flush().memory().maxtlssize()); // from config override assertEquals(1000, cfg.flush().memory().maxmemory()); // from explicit tuning assertEquals((long) ((128 - memoryOverheadGb) * GB * 0.08), cfg.flush().memory().each().maxmemory()); // from default node flavor tuning } private static ProtonConfig getProtonConfig(VespaModel model, String configId) { ProtonConfig.Builder builder = new ProtonConfig.Builder(); model.getConfig(builder, configId); return new ProtonConfig(builder); } // Tests that a container is allocated on logserver host and that // it is able to get config private void testContainerOnLogserverHost(String services, boolean useDedicatedNodeForLogserver) { int numberOfHosts = 2; VespaModelTester tester = new VespaModelTester(); tester.useDedicatedNodeForLogserver(useDedicatedNodeForLogserver); tester.addHosts(numberOfHosts); VespaModel model = tester.createModel(Zone.defaultZone(), services, true, deployStateWithClusterEndpoints("foo")); assertEquals(numberOfHosts, model.getRoot().hostSystem().getHosts().size()); Admin admin = model.getAdmin(); Logserver logserver = admin.getLogserver(); HostResource hostResource = logserver.getHostResource(); assertNotNull(hostResource.getService("logserver")); String containerServiceType = ContainerServiceType.LOGSERVER_CONTAINER.serviceName; assertNotNull(hostResource.getService(containerServiceType)); // Test that the container gets config String configId = admin.getLogserver().getHostResource().getService(containerServiceType).getConfigId(); ApplicationMetadataConfig.Builder builder = new ApplicationMetadataConfig.Builder(); model.getConfig(builder, configId); ApplicationMetadataConfig cfg = new ApplicationMetadataConfig(builder); assertEquals(1, cfg.generation()); LogdConfig.Builder logdConfigBuilder = new LogdConfig.Builder(); model.getConfig(logdConfigBuilder, configId); LogdConfig logdConfig = new LogdConfig(logdConfigBuilder); // Logd should use logserver (forward logs to it) assertTrue(logdConfig.logserver().use()); } private static void assertProvisioned(int nodeCount, ClusterSpec.Id id, ClusterSpec.Id combinedId, ClusterSpec.Type type, VespaModel model) { assertEquals(nodeCount, model.hostSystem().getHosts().stream() .map(h -> h.spec().membership().get().cluster()) .filter(spec -> spec.id().equals(id) && spec.type().equals(type) && spec.combinedId().equals(Optional.ofNullable(combinedId))) .count(), "Nodes in cluster " + id + " with type " + type + (combinedId != null ? ", combinedId " + combinedId : "")); } private static void assertProvisioned(int nodeCount, ClusterSpec.Id id, ClusterSpec.Type type, VespaModel model) { assertProvisioned(nodeCount, id, null, type, model); } private static boolean hostNameExists(HostSystem hostSystem, String hostname) { return hostSystem.getHosts().stream().map(HostResource::getHost).anyMatch(host -> host.getHostname().equals(hostname)); } private static DeployState.Builder deployStateWithClusterEndpoints(String... cluster) { Set endpoints = Arrays.stream(cluster) .map(c -> new ContainerEndpoint(c, ApplicationClusterEndpoint.Scope.zone, List.of(c + ".example.com"))) .collect(Collectors.toSet()); return new DeployState.Builder().endpoints(endpoints); } record TestLogger(List msgs) implements DeployLogger { public TestLogger() { this(new ArrayList<>()); } @Override public void log(Level level, String message) { msgs.add(new LogMessage(level, message)); } record LogMessage(Level level, String message) {} } }