// 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().getIndexed().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) {}
}
}