// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.model.content;
import com.yahoo.config.model.api.ModelContext;
import com.yahoo.config.model.deploy.DeployState;
import com.yahoo.config.model.deploy.TestProperties;
import com.yahoo.config.model.provision.SingleNodeProvisioner;
import com.yahoo.config.model.test.MockRoot;
import com.yahoo.config.model.test.TestDriver;
import com.yahoo.config.model.test.TestRoot;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.Flavor;
import com.yahoo.config.provision.RegionName;
import com.yahoo.config.provision.Zone;
import com.yahoo.config.provisioning.FlavorsConfig;
import com.yahoo.container.ComponentsConfig;
import com.yahoo.messagebus.routing.RoutingTableSpec;
import com.yahoo.metrics.MetricsmanagerConfig;
import com.yahoo.vespa.config.content.AllClustersBucketSpacesConfig;
import com.yahoo.vespa.config.content.DistributionConfig;
import com.yahoo.vespa.config.content.FleetcontrollerConfig;
import com.yahoo.vespa.config.content.StorDistributionConfig;
import com.yahoo.vespa.config.content.StorFilestorConfig;
import com.yahoo.vespa.config.content.core.StorDistributormanagerConfig;
import com.yahoo.vespa.config.content.core.StorServerConfig;
import com.yahoo.vespa.config.search.DispatchConfig;
import com.yahoo.vespa.config.search.core.ProtonConfig;
import com.yahoo.vespa.model.VespaModel;
import com.yahoo.vespa.model.admin.clustercontroller.ClusterControllerContainer;
import com.yahoo.vespa.model.admin.clustercontroller.ClusterControllerContainerCluster;
import com.yahoo.vespa.model.container.ContainerCluster;
import com.yahoo.vespa.model.content.cluster.ContentCluster;
import com.yahoo.vespa.model.content.engines.ProtonEngine;
import com.yahoo.vespa.model.content.utils.ContentClusterBuilder;
import com.yahoo.vespa.model.content.utils.ContentClusterUtils;
import com.yahoo.vespa.model.content.utils.SchemaBuilder;
import com.yahoo.vespa.model.routing.DocumentProtocol;
import com.yahoo.vespa.model.routing.Routing;
import com.yahoo.vespa.model.test.utils.ApplicationPackageUtils;
import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithMockPkg;
import com.yahoo.yolean.Exceptions;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import static org.junit.jupiter.api.Assertions.*;
public class ContentClusterTest extends ContentBaseTest {
private final static String HOSTS = "";
ContentCluster parse(String xml) {
xml = HOSTS + xml;
TestRoot root = new TestDriver().buildModel(xml);
return root.getConfigModels(Content.class).get(0).getCluster();
}
@Test
void testHierarchicRedundancy() {
ContentCluster cc = parse("" +
"\n" +
" " +
" " +
" " +
" 3" +
" " +
" " +
" 15\n" +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
""
);
DistributionConfig.Builder distributionBuilder = new DistributionConfig.Builder();
cc.getConfig(distributionBuilder);
DistributionConfig distributionConfig = distributionBuilder.build();
assertEquals(3, distributionConfig.cluster("storage").ready_copies());
assertEquals(15, distributionConfig.cluster("storage").initial_redundancy());
assertEquals(15, distributionConfig.cluster("storage").redundancy());
assertEquals(4, distributionConfig.cluster("storage").group().size());
assertEquals(1, distributionConfig.cluster().size());
StorDistributionConfig.Builder storBuilder = new StorDistributionConfig.Builder();
cc.getConfig(storBuilder);
StorDistributionConfig storConfig = new StorDistributionConfig(storBuilder);
assertEquals(15, storConfig.initial_redundancy());
assertEquals(15, storConfig.redundancy());
assertEquals(3, storConfig.ready_copies());
ProtonConfig.Builder protonBuilder = new ProtonConfig.Builder();
cc.getSearch().getConfig(protonBuilder);
ProtonConfig protonConfig = new ProtonConfig(protonBuilder);
assertEquals(1, protonConfig.distribution().searchablecopies());
assertEquals(5, protonConfig.distribution().redundancy());
}
@Test
void testRedundancy() {
ContentCluster cc = parse("" +
"\n" +
" " +
" " +
" " +
" 3" +
" " +
" " +
" 5\n" +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
""
);
DistributionConfig.Builder distributionBuilder = new DistributionConfig.Builder();
cc.getConfig(distributionBuilder);
DistributionConfig distributionConfig = distributionBuilder.build();
assertEquals(3, distributionConfig.cluster("storage").ready_copies());
assertEquals(4, distributionConfig.cluster("storage").initial_redundancy());
assertEquals(5, distributionConfig.cluster("storage").redundancy());
StorDistributionConfig.Builder storBuilder = new StorDistributionConfig.Builder();
cc.getConfig(storBuilder);
StorDistributionConfig storConfig = new StorDistributionConfig(storBuilder);
assertEquals(4, storConfig.initial_redundancy());
assertEquals(5, storConfig.redundancy());
assertEquals(3, storConfig.ready_copies());
ProtonConfig.Builder protonBuilder = new ProtonConfig.Builder();
cc.getSearch().getConfig(protonBuilder);
ProtonConfig protonConfig = new ProtonConfig(protonBuilder);
assertEquals(3, protonConfig.distribution().searchablecopies());
assertEquals(5, protonConfig.distribution().redundancy());
}
@Test
void testImplicitSearchableCopies() {
ContentCluster cc = parse("" +
"\n" +
" " +
" 3\n" +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
""
);
DistributionConfig.Builder distributionBuilder = new DistributionConfig.Builder();
cc.getConfig(distributionBuilder);
DistributionConfig distributionConfig = distributionBuilder.build();
assertEquals(3, distributionConfig.cluster("storage").ready_copies());
StorDistributionConfig.Builder storBuilder = new StorDistributionConfig.Builder();
cc.getConfig(storBuilder);
StorDistributionConfig storConfig = new StorDistributionConfig(storBuilder);
assertEquals(3, storConfig.ready_copies());
ProtonConfig.Builder protonBuilder = new ProtonConfig.Builder();
cc.getSearch().getConfig(protonBuilder);
ProtonConfig protonConfig = new ProtonConfig(protonBuilder);
assertEquals(1, protonConfig.distribution().searchablecopies());
}
@Test
void testMinRedundancy() {
{ // Groups ensures redundancy
ContentCluster cc = parse("""
2
"
"""
);
ProtonConfig.Builder protonBuilder = new ProtonConfig.Builder();
cc.getSearch().getConfig(protonBuilder);
ProtonConfig protonConfig = new ProtonConfig(protonBuilder);
assertEquals(1, protonConfig.distribution().redundancy());
assertEquals(1, protonConfig.distribution().searchablecopies());
}
{ // Redundancy must be within group
ContentCluster cc = parse("""
2
"""
);
ProtonConfig.Builder protonBuilder = new ProtonConfig.Builder();
cc.getSearch().getConfig(protonBuilder);
ProtonConfig protonConfig = new ProtonConfig(protonBuilder);
assertEquals(2, protonConfig.distribution().redundancy());
assertEquals(2, protonConfig.distribution().searchablecopies());
}
{ // Multiple gropups but they do not ensure redundancy
ContentCluster cc = parse("""
4
"
"""
);
ProtonConfig.Builder protonBuilder = new ProtonConfig.Builder();
cc.getSearch().getConfig(protonBuilder);
ProtonConfig protonConfig = new ProtonConfig(protonBuilder);
assertEquals(2, protonConfig.distribution().redundancy());
assertEquals(1, protonConfig.distribution().searchablecopies());
}
}
@Test
void testNoId() {
ContentCluster c = parse(
"\n" +
" 1\n" +
" " +
" 5\n" +
" " +
" \"" +
" " +
""
);
assertEquals("content", c.getName());
}
@Test
void testEndToEnd() {
String xml =
"\n" +
"\n" +
"\n" +
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
" " +
" " +
" " +
" \n" +
" \n" +
" " +
" 1\n" +
" " +
" \n" +
" \n" +
" \n" +
" " +
" " +
" " +
" " +
" \n" +
" 34567" +
" " +
" " +
" " +
"\n" +
"";
List sds = ApplicationPackageUtils.generateSchemas("type1", "type2");
VespaModel model = new VespaModelCreatorWithMockPkg(null, xml, sds).create();
assertEquals(2, model.getContentClusters().get("bar").getDocumentDefinitions().size());
ContainerCluster> cluster = model.getAdmin().getClusterControllers();
assertEquals(3, cluster.getContainers().size());
}
VespaModel createEnd2EndOneNode(ModelContext.Properties properties) {
String services =
"" +
"" +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" 2" +
" " +
" " +
" " +
" " +
" " +
" " +
" 0" +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" ";
return createEnd2EndOneNode(properties, services);
}
VespaModel createEnd2EndOneNode(ModelContext.Properties properties, String services) {
DeployState.Builder deployStateBuilder = new DeployState.Builder().properties(properties);
List sds = ApplicationPackageUtils.generateSchemas("type1");
return (new VespaModelCreatorWithMockPkg(null, services, sds)).create(deployStateBuilder);
}
@Test
void testEndToEndOneNode() {
VespaModel model = createEnd2EndOneNode(new TestProperties());
assertEquals(1, model.getContentClusters().get("storage").getDocumentDefinitions().size());
ContainerCluster> cluster = model.getAdmin().getClusterControllers();
assertEquals(1, cluster.getContainers().size());
}
@Test
void testSearchTuning() {
String xml =
"\n" +
"\n" +
"\n" +
" \n" +
" \n" +
" \n" +
" " +
" \n" +
" \n" +
" " +
" 1\n" +
" " +
" \n" +
" \n" +
" \n" +
" " +
" " +
" " +
" \n" +
" " +
" 34567" +
" " +
" " +
" " +
"\n" +
"";
List sds = ApplicationPackageUtils.generateSchemas("type1", "type2");
VespaModel model = new VespaModelCreatorWithMockPkg(getHosts(), xml, sds).create();
assertTrue(model.getContentClusters().get("bar").getPersistence() instanceof ProtonEngine.Factory);
{
StorDistributormanagerConfig.Builder builder = new StorDistributormanagerConfig.Builder();
model.getConfig(builder, "bar/distributor/0");
StorDistributormanagerConfig config = new StorDistributormanagerConfig(builder);
assertFalse(config.inlinebucketsplitting());
assertTrue(config.enable_two_phase_garbage_collection());
}
{
StorFilestorConfig.Builder builder = new StorFilestorConfig.Builder();
model.getConfig(builder, "bar/storage/0");
StorFilestorConfig config = new StorFilestorConfig(builder);
assertFalse(config.enable_multibit_split_optimalization());
}
}
@Test
void testRedundancyRequired() {
String xml =
"\n" +
"\n" +
"\n" +
" \n" +
" \n" +
" \n" +
" " +
" " +
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
"\n";
List sds = ApplicationPackageUtils.generateSchemas("type1", "type2");
try {
new VespaModelCreatorWithMockPkg(getHosts(), xml, sds).create();
fail("Deploying without redundancy should fail");
} catch (IllegalArgumentException e) {
assertEquals("In content cluster 'bar': Either or must be set",
Exceptions.toMessageString(e));
}
}
@Test
void testRedundancyFinalLessThanInitial() {
try {
parse(
"""
2
"""
);
fail("no exception thrown");
} catch (Exception e) { /* ignore */
}
}
@Test
void testReadyTooHigh() {
try {
parse(
"""
3
2
"""
);
fail("no exception thrown");
} catch (Exception e) { /* ignore */
}
}
FleetcontrollerConfig getFleetControllerConfig(String xml) {
ContentCluster cluster = parse(xml);
FleetcontrollerConfig.Builder builder = new FleetcontrollerConfig.Builder();
cluster.getConfig(builder);
cluster.getClusterControllerConfig().getConfig(builder);
return new FleetcontrollerConfig(builder);
}
@Test
void testFleetControllerOverride()
{
{
FleetcontrollerConfig config = getFleetControllerConfig(
"\n" +
" 3" +
" " +
" \n" +
" \n" +
" \n" +
""
);
assertEquals(0, config.min_storage_up_ratio(), 0.01);
assertEquals(0, config.min_distributor_up_ratio(), 0.01);
assertEquals(1, config.min_storage_up_count());
assertEquals(1, config.min_distributors_up_count());
}
{
FleetcontrollerConfig config = getFleetControllerConfig(
"\n" +
" 3" +
" " +
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
""
);
assertNotSame(0, config.min_storage_up_ratio());
}
}
@Test
void testImplicitDistributionBits()
{
ContentCluster cluster = parse(
"\n" +
" 3" +
" " +
" \n" +
" \n" +
" \n" +
""
);
assertDistributionBitsInConfig(cluster, 8);
cluster = parse(
"\n" +
" 3" +
" " +
" \n" +
" \n" +
" \n" +
""
);
assertDistributionBitsInConfig(cluster, 8);
}
@Test
void testExplicitDistributionBits()
{
ContentCluster cluster = parse(
"\n" +
" 3" +
" " +
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
""
);
assertDistributionBitsInConfig(cluster, 8);
cluster = parse(
"\n" +
" 2" +
" " +
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
""
);
assertDistributionBitsInConfig(cluster, 8);
}
@Test
void testZoneDependentDistributionBits() throws Exception {
String xml = new ContentClusterBuilder().docTypes("test").getXml();
ContentCluster prodWith16Bits = createWithZone(xml, new Zone(Environment.prod, RegionName.from("us-east-3")));
assertDistributionBitsInConfig(prodWith16Bits, 16);
ContentCluster perfWith16Bits = createWithZone(xml, new Zone(Environment.perf, RegionName.from("us-east-3")));
assertDistributionBitsInConfig(perfWith16Bits, 16);
ContentCluster stagingNot16Bits = createWithZone(xml, new Zone(Environment.staging, RegionName.from("us-east-3")));
assertDistributionBitsInConfig(stagingNot16Bits, 8);
}
@Test
void testGenerateSearchNodes()
{
ContentCluster cluster = parse(
"\n" +
" 3" +
" " +
" " +
" " +
" " +
" \n" +
" \n" +
" \n" +
" \n" +
""
);
{
StorServerConfig.Builder builder = new StorServerConfig.Builder();
cluster.getStorageCluster().getConfig(builder);
cluster.getStorageCluster().getChildren().get("0").getConfig(builder);
StorServerConfig config = new StorServerConfig(builder);
}
{
StorServerConfig.Builder builder = new StorServerConfig.Builder();
cluster.getStorageCluster().getConfig(builder);
cluster.getStorageCluster().getChildren().get("1").getConfig(builder);
StorServerConfig config = new StorServerConfig(builder);
}
}
@Test
void testAlternativeNodeSyntax()
{
ContentCluster cluster = parse(
"\n" +
" 3" +
" " +
" " +
" " +
" " +
" \n" +
" \n" +
" \n" +
" \n" +
""
);
DistributionConfig.Builder bob = new DistributionConfig.Builder();
cluster.getConfig(bob);
DistributionConfig.Cluster.Group group = bob.build().cluster("test").group(0);
assertEquals("invalid", group.name());
assertEquals("invalid", group.index());
assertEquals(2, group.nodes().size());
StorDistributionConfig.Builder builder = new StorDistributionConfig.Builder();
cluster.getConfig(builder);
StorDistributionConfig config = new StorDistributionConfig(builder);
assertEquals("invalid", config.group(0).name());
assertEquals("invalid", config.group(0).index());
assertEquals(2, config.group(0).nodes().size());
}
@Test
void testReadyWhenInitialOne() {
StorDistributionConfig.Builder builder = new StorDistributionConfig.Builder();
parse(
"\n" +
" " +
" 1\n" +
" \n" +
" " +
" " +
""
).getConfig(builder);
StorDistributionConfig config = new StorDistributionConfig(builder);
assertEquals(1, config.initial_redundancy());
assertEquals(1, config.redundancy());
assertEquals(1, config.ready_copies());
}
public void testProvider(String tagName, StorServerConfig.Persistence_provider.Type.Enum type) {
ContentCluster cluster = parse(
"\n" +
" " +
" 3" +
" \n" +
" <" + tagName + "/>\n" +
" \n" +
" \n" +
" " +
" " +
""
);
{
StorServerConfig.Builder builder = new StorServerConfig.Builder();
cluster.getStorageCluster().getConfig(builder);
cluster.getStorageCluster().getChildren().get("0").getConfig(builder);
StorServerConfig config = new StorServerConfig(builder);
assertEquals(type, config.persistence_provider().type());
}
{
StorServerConfig.Builder builder = new StorServerConfig.Builder();
cluster.getDistributorNodes().getConfig(builder);
cluster.getDistributorNodes().getChildren().get("0").getConfig(builder);
StorServerConfig config = new StorServerConfig(builder);
assertEquals(type, config.persistence_provider().type());
}
}
@Test
void testProviders() {
testProvider("proton", StorServerConfig.Persistence_provider.Type.RPC);
testProvider("dummy", StorServerConfig.Persistence_provider.Type.DUMMY);
}
@Test
void testMetrics() {
MetricsmanagerConfig.Builder builder = new MetricsmanagerConfig.Builder();
ContentCluster cluster = parse("\n" +
" 3" +
" " +
" \n" +
" \n" +
" \n" +
""
);
cluster.getConfig(builder);
MetricsmanagerConfig config = new MetricsmanagerConfig(builder);
assertEquals(5, config.consumer().size());
var status = config.consumer(0);
assertEquals("status", status.name());
assertEquals("*", status.addedmetrics(0));
assertEquals("partofsum", status.removedtags(0));
var log = config.consumer(1);
assertEquals("log", log.name());
assertEquals("logdefault", log.tags().get(0));
assertEquals("loadtype", log.removedtags(0));
var yamas = config.consumer(2);
assertEquals("yamas", yamas.name());
assertEquals("yamasdefault", yamas.tags().get(0));
assertEquals("loadtype", yamas.removedtags(0));
assertEquals("health", config.consumer(3).name());
var stateReporter = config.consumer(4);
assertEquals("statereporter", stateReporter.name());
assertEquals("*", stateReporter.addedmetrics(0));
assertEquals("thread", stateReporter.removedtags(0));
assertEquals("partofsum", stateReporter.removedtags(1));
assertEquals(0, stateReporter.tags().size());
cluster.getStorageCluster().getConfig(builder);
config = new MetricsmanagerConfig(builder);
assertEquals(5, config.consumer().size());
}
public MetricsmanagerConfig.Consumer getConsumer(String consumer, MetricsmanagerConfig config) {
for (MetricsmanagerConfig.Consumer c : config.consumer()) {
if (c.name().equals(consumer)) {
return c;
}
}
return null;
}
@Test
void testConfiguredMetrics() {
String xml = "" +
"" +
"\n" +
" 1\n" +
" " +
" \n" +
" \n" +
" " +
" \n" +
" \n" +
" \n" +
"" +
"" +
" " +
" " +
"" +
"";
List sds = ApplicationPackageUtils.generateSchemas("type1", "type2");
VespaModel model = new VespaModelCreatorWithMockPkg(getHosts(), xml, sds).create();
{
MetricsmanagerConfig.Builder builder = new MetricsmanagerConfig.Builder();
model.getConfig(builder, "storage/storage/0");
MetricsmanagerConfig config = new MetricsmanagerConfig(builder);
String expected =
"[vds.filestor.allthreads.put\n" +
"vds.filestor.allthreads.get\n" +
"vds.filestor.allthreads.remove\n" +
"vds.filestor.allthreads.update\n" +
"vds.datastored.alldisks.docs\n" +
"vds.datastored.alldisks.bytes\n" +
"vds.filestor.queuesize\n" +
"vds.filestor.averagequeuewait\n" +
"vds.visitor.cv_queuewaittime\n" +
"vds.visitor.allthreads.averagequeuewait\n" +
"vds.visitor.allthreads.averagevisitorlifetime\n" +
"vds.visitor.allthreads.created]";
String actual = getConsumer("log", config).addedmetrics().toString().replaceAll(", ", "\n");
assertEquals(expected, actual);
assertEquals("[logdefault]", getConsumer("log", config).tags().toString());
}
{
MetricsmanagerConfig.Builder builder = new MetricsmanagerConfig.Builder();
model.getConfig(builder, "storage/distributor/0");
MetricsmanagerConfig config = new MetricsmanagerConfig(builder);
assertEquals("[logdefault]", getConsumer("log", config).tags().toString());
}
}
@Test
void flush_on_shutdown_is_default_on_for_non_hosted() throws Exception {
assertPrepareRestartCommand(createOneNodeCluster(false));
}
@Test
void flush_on_shutdown_can_be_turned_off_for_non_hosted() throws Exception {
assertNoPreShutdownCommand(createClusterWithFlushOnShutdownOverride(false, false));
}
@Test
void flush_on_shutdown_is_default_on_for_hosted() throws Exception {
assertPrepareRestartCommand(createOneNodeCluster(true));
}
@Test
void flush_on_shutdown_can_be_turned_on_for_hosted() throws Exception {
assertPrepareRestartCommand(createClusterWithFlushOnShutdownOverride(true, true));
}
private static String oneNodeClusterXml() {
return "" +
" 3" +
" " +
" " +
" " +
" " +
"";
}
private static ContentCluster createOneNodeCluster(boolean isHostedVespa) throws Exception {
return createOneNodeCluster(oneNodeClusterXml(), new TestProperties().setHostedVespa(isHostedVespa));
}
private static ContentCluster createOneNodeCluster(TestProperties props) throws Exception {
return createOneNodeCluster(oneNodeClusterXml(), props);
}
private static ContentCluster createOneNodeCluster(TestProperties props, Optional flavor) throws Exception {
return createOneNodeCluster(oneNodeClusterXml(), props, flavor);
}
private static ContentCluster createClusterWithFlushOnShutdownOverride(boolean flushOnShutdown, boolean isHostedVespa) throws Exception {
return createOneNodeCluster("" +
" 1" +
" " +
" " +
" " +
" " + flushOnShutdown + "" +
" " +
" " +
" " +
" " +
" " +
"", new TestProperties().setHostedVespa(isHostedVespa));
}
private static ContentCluster createOneNodeCluster(String clusterXml, TestProperties props) throws Exception {
return createOneNodeCluster(clusterXml, props, Optional.empty());
}
private static ContentCluster createOneNodeCluster(String clusterXml, TestProperties props, Optional flavor) throws Exception {
DeployState.Builder deployStateBuilder = new DeployState.Builder()
.properties(props);
MockRoot root = flavor.isPresent() ?
ContentClusterUtils.createMockRoot(new SingleNodeProvisioner(flavor.get()),
Collections.emptyList(), deployStateBuilder) :
ContentClusterUtils.createMockRoot(Collections.emptyList(), deployStateBuilder);
ContentCluster cluster = ContentClusterUtils.createCluster(clusterXml, root);
root.freezeModelTopology();
cluster.validate();
return cluster;
}
private static void assertPrepareRestartCommand(ContentCluster cluster) {
Optional command = cluster.getSearch().getSearchNodes().get(0).getPreShutdownCommand();
assertTrue(command.isPresent());
assertTrue(command.get().matches(".*vespa-proton-cmd [0-9]+ prepareRestart"));
}
private static void assertNoPreShutdownCommand(ContentCluster cluster) {
Optional command = cluster.getSearch().getSearchNodes().get(0).getPreShutdownCommand();
assertFalse(command.isPresent());
}
@Test
void reserved_document_name_throws_exception() {
String xml = """
1
""";
List sds = ApplicationPackageUtils.generateSchemas("true");
try {
new VespaModelCreatorWithMockPkg(null, xml, sds).create();
fail();
} catch (IllegalArgumentException e) {
assertTrue(e.getMessage().startsWith("The following document types conflict with reserved keyword names: 'true'."));
}
}
@Test
void default_searchable_copies_indexing() {
String services = """
3
""";
var model = new VespaModelCreatorWithMockPkg(null, services, ApplicationPackageUtils.generateSchemas("music")).create();
assertEquals(2, model.getContentClusters().get("storage").getRedundancy().readyCopies());
}
@Test
void default_searchable_copies_streaming() {
String services = """
3
""";
var model = new VespaModelCreatorWithMockPkg(null, services, ApplicationPackageUtils.generateSchemas("mail")).create();
assertEquals(3, model.getContentClusters().get("storage").getRedundancy().readyCopies());
}
/** Here there is no good choice. */
@Test
void default_searchable_copies_mixed() {
String services = """
3
""";
var model = new VespaModelCreatorWithMockPkg(null, services, ApplicationPackageUtils.generateSchemas("music", "mail")).create();
assertEquals(2, model.getContentClusters().get("storage").getRedundancy().readyCopies());
}
private void assertClusterHasBucketSpaceMappings(AllClustersBucketSpacesConfig config, String clusterId,
List defaultSpaceTypes, List globalSpaceTypes) {
AllClustersBucketSpacesConfig.Cluster cluster = config.cluster(clusterId);
assertNotNull(cluster);
assertEquals(defaultSpaceTypes.size() + globalSpaceTypes.size(), cluster.documentType().size());
assertClusterHasTypesInBucketSpace(cluster, "default", defaultSpaceTypes);
assertClusterHasTypesInBucketSpace(cluster, "global", globalSpaceTypes);
}
private void assertClusterHasTypesInBucketSpace(AllClustersBucketSpacesConfig.Cluster cluster,
String bucketSpace, List expectedTypes) {
for (String type : expectedTypes) {
assertNotNull(cluster.documentType(type));
assertEquals(bucketSpace, cluster.documentType(type).bucketSpace());
}
}
private VespaModel createDualContentCluster() {
String xml =
"" +
"" +
" " +
"" +
"" +
" 1" +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
"" +
"" +
" 1" +
" " +
" " +
" " +
" " +
" " +
" " +
"" +
"";
List sds = ApplicationPackageUtils.generateSchemas("bunnies", "hares", "rabbits");
return new VespaModelCreatorWithMockPkg(getHosts(), xml, sds).create();
}
@Test
void all_clusters_bucket_spaces_config_contains_mappings_across_all_clusters() {
VespaModel model = createDualContentCluster();
AllClustersBucketSpacesConfig.Builder builder = new AllClustersBucketSpacesConfig.Builder();
model.getConfig(builder, "client");
AllClustersBucketSpacesConfig config = builder.build();
assertEquals(2, config.cluster().size());
assertClusterHasBucketSpaceMappings(config, "foo_c", Arrays.asList("bunnies", "hares"), Collections.emptyList());
assertClusterHasBucketSpaceMappings(config, "bar_c", Collections.emptyList(), Collections.singletonList("rabbits"));
}
@Test
void test_routing_with_multiple_clusters() {
VespaModel model = createDualContentCluster();
Routing routing = model.getRouting();
assertNotNull(routing);
assertEquals("[]", routing.getErrors().toString());
assertEquals(1, routing.getProtocols().size());
DocumentProtocol protocol = (DocumentProtocol) routing.getProtocols().get(0);
RoutingTableSpec spec = protocol.getRoutingTableSpec();
assertEquals(3, spec.getNumHops());
assertEquals("docproc/cluster.bar_c.indexing/chain.indexing", spec.getHop(0).getName());
assertEquals("docproc/cluster.foo_c.indexing/chain.indexing", spec.getHop(1).getName());
assertEquals("indexing", spec.getHop(2).getName());
assertEquals(10, spec.getNumRoutes());
assertRoute(spec.getRoute(0), "bar_c", "[MessageType:bar_c]");
assertRoute(spec.getRoute(1), "bar_c-direct", "[Content:cluster=bar_c]");
assertRoute(spec.getRoute(2), "bar_c-index", "docproc/cluster.bar_c.indexing/chain.indexing", "[Content:cluster=bar_c]");
assertRoute(spec.getRoute(3), "default", "indexing");
assertRoute(spec.getRoute(4), "default-get", "indexing");
assertRoute(spec.getRoute(5), "foo_c", "[MessageType:foo_c]");
assertRoute(spec.getRoute(6), "foo_c-direct", "[Content:cluster=foo_c]");
assertRoute(spec.getRoute(7), "foo_c-index", "docproc/cluster.foo_c.indexing/chain.indexing", "[Content:cluster=foo_c]");
assertRoute(spec.getRoute(8), "storage/cluster.bar_c", "route:bar_c");
assertRoute(spec.getRoute(9), "storage/cluster.foo_c", "route:foo_c");
}
private ContentCluster createWithZone(String clusterXml, Zone zone) throws Exception {
DeployState.Builder deployStateBuilder = new DeployState.Builder()
.zone(zone)
.properties(new TestProperties().setHostedVespa(true));
List schemas = SchemaBuilder.createSchemas("test");
MockRoot root = ContentClusterUtils.createMockRoot(schemas, deployStateBuilder);
ContentCluster cluster = ContentClusterUtils.createCluster(clusterXml, root);
root.freezeModelTopology();
cluster.validate();
return cluster;
}
private void assertDistributionBitsInConfig(ContentCluster cluster, int distributionBits) {
FleetcontrollerConfig.Builder builder = new FleetcontrollerConfig.Builder();
cluster.getConfig(builder);
cluster.getClusterControllerConfig().getConfig(builder);
FleetcontrollerConfig config = new FleetcontrollerConfig(builder);
assertEquals(distributionBits, config.ideal_distribution_bits());
StorDistributormanagerConfig.Builder sdBuilder = new StorDistributormanagerConfig.Builder();
cluster.getConfig(sdBuilder);
StorDistributormanagerConfig storDistributormanagerConfig = new StorDistributormanagerConfig(sdBuilder);
assertEquals(distributionBits, storDistributormanagerConfig.minsplitcount());
}
private void verifyTopKProbabilityPropertiesControl() {
VespaModel model = createEnd2EndOneNode(new TestProperties());
ContentCluster cc = model.getContentClusters().get("storage");
DispatchConfig.Builder builder = new DispatchConfig.Builder();
cc.getSearch().getConfig(builder);
DispatchConfig cfg = new DispatchConfig(builder);
assertEquals(0.9999, cfg.topKProbability(), 0.0);
}
@Test
void default_topKprobability_controlled_by_properties() {
verifyTopKProbabilityPropertiesControl();
}
private void verifyQueryDispatchPolicy(String policy, DispatchConfig.DistributionPolicy.Enum expected) {
TestProperties properties = new TestProperties();
if (policy != null) {
properties.setQueryDispatchPolicy(policy);
}
VespaModel model = createEnd2EndOneNode(properties);
ContentCluster cc = model.getContentClusters().get("storage");
DispatchConfig.Builder builder = new DispatchConfig.Builder();
cc.getSearch().getConfig(builder);
DispatchConfig cfg = new DispatchConfig(builder);
assertEquals(expected, cfg.distributionPolicy());
}
@Test
public void default_dispatch_controlled_by_properties() {
verifyQueryDispatchPolicy(null, DispatchConfig.DistributionPolicy.ADAPTIVE);
verifyQueryDispatchPolicy("adaptive", DispatchConfig.DistributionPolicy.ADAPTIVE);
verifyQueryDispatchPolicy("round-robin", DispatchConfig.DistributionPolicy.ROUNDROBIN);
verifyQueryDispatchPolicy("best-of-random-2", DispatchConfig.DistributionPolicy.BEST_OF_RANDOM_2);
verifyQueryDispatchPolicy("latency-amortized-over-requests", DispatchConfig.DistributionPolicy.LATENCY_AMORTIZED_OVER_REQUESTS);
verifyQueryDispatchPolicy("latency-amortized-over-time", DispatchConfig.DistributionPolicy.LATENCY_AMORTIZED_OVER_TIME);
try {
verifyQueryDispatchPolicy("unknown", DispatchConfig.DistributionPolicy.ADAPTIVE);
fail();
} catch (IllegalArgumentException e) {
assertEquals("Unknown dispatch policy 'unknown'", e.getMessage());
}
}
private void verifySummaryDecodeType(String policy, DispatchConfig.SummaryDecodePolicy.Enum expected) {
TestProperties properties = new TestProperties();
if (policy != null) {
properties.setSummaryDecodePolicy(policy);
}
VespaModel model = createEnd2EndOneNode(properties);
ContentCluster cc = model.getContentClusters().get("storage");
DispatchConfig.Builder builder = new DispatchConfig.Builder();
cc.getSearch().getConfig(builder);
DispatchConfig cfg = new DispatchConfig(builder);
assertEquals(expected, cfg.summaryDecodePolicy());
}
@Test
public void verify_summary_decoding_controlled_by_properties() {
verifySummaryDecodeType(null, DispatchConfig.SummaryDecodePolicy.EAGER);
verifySummaryDecodeType("illegal-config", DispatchConfig.SummaryDecodePolicy.EAGER);
verifySummaryDecodeType("eager", DispatchConfig.SummaryDecodePolicy.EAGER);
verifySummaryDecodeType("ondemand", DispatchConfig.SummaryDecodePolicy.ONDEMAND);
verifySummaryDecodeType("on-demand", DispatchConfig.SummaryDecodePolicy.ONDEMAND);
}
private int resolveMaxCompactBuffers(OptionalInt maxCompactBuffers) {
TestProperties testProperties = new TestProperties();
if (maxCompactBuffers.isPresent()) {
testProperties.maxCompactBuffers(maxCompactBuffers.getAsInt());
}
VespaModel model = createEnd2EndOneNode(testProperties);
ContentCluster cc = model.getContentClusters().get("storage");
ProtonConfig.Builder protonBuilder = new ProtonConfig.Builder();
cc.getSearch().getConfig(protonBuilder);
ProtonConfig protonConfig = new ProtonConfig(protonBuilder);
assertEquals(1, protonConfig.documentdb().size());
return protonConfig.documentdb(0).allocation().max_compact_buffers();
}
@Test
void default_max_compact_buffers_config_controlled_by_properties() {
assertEquals(1, resolveMaxCompactBuffers(OptionalInt.empty()));
assertEquals(2, resolveMaxCompactBuffers(OptionalInt.of(2)));
assertEquals(7, resolveMaxCompactBuffers(OptionalInt.of(7)));
}
private long resolveMaxTLSSize(Optional flavor) throws Exception {
TestProperties testProperties = new TestProperties();
ContentCluster cc = createOneNodeCluster(testProperties, flavor);
ProtonConfig.Builder protonBuilder = new ProtonConfig.Builder();
cc.getSearch().getSearchNodes().get(0).getConfig(protonBuilder);
ProtonConfig protonConfig = new ProtonConfig(protonBuilder);
return protonConfig.flush().memory().maxtlssize();
}
@Test
void verifyt_max_tls_size() throws Exception {
var flavor = new Flavor(new FlavorsConfig.Flavor(new FlavorsConfig.Flavor.Builder().name("test").minDiskAvailableGb(100)));
assertEquals(21474836480L, resolveMaxTLSSize(Optional.empty()));
assertEquals(2147483648L, resolveMaxTLSSize(Optional.of(flavor)));
}
void assertZookeeperServerImplementation(String expectedClassName,
ClusterControllerContainerCluster clusterControllerCluster) {
for (ClusterControllerContainer c : clusterControllerCluster.getContainers()) {
var builder = new ComponentsConfig.Builder();
c.getConfig(builder);
assertEquals(1, new ComponentsConfig(builder).components().stream()
.filter(component -> component.classId().equals(expectedClassName))
.count());
}
}
private StorDistributormanagerConfig resolveStorDistributormanagerConfig(TestProperties props) throws Exception {
var cc = createOneNodeCluster(props);
var builder = new StorDistributormanagerConfig.Builder();
cc.getDistributorNodes().getConfig(builder);
return (new StorDistributormanagerConfig(builder));
}
private int resolveMaxInhibitedGroupsConfigWithFeatureFlag(int maxGroups) throws Exception {
var cfg = resolveStorDistributormanagerConfig(new TestProperties().maxActivationInhibitedOutOfSyncGroups(maxGroups));
return cfg.max_activation_inhibited_out_of_sync_groups();
}
@Test
void default_distributor_max_inhibited_group_activation_config_controlled_by_properties() throws Exception {
assertEquals(0, resolveMaxInhibitedGroupsConfigWithFeatureFlag(0));
assertEquals(2, resolveMaxInhibitedGroupsConfigWithFeatureFlag(2));
}
private int resolveNumDistributorStripesConfig(Optional flavor) throws Exception {
var cc = createOneNodeCluster(new TestProperties(), flavor);
var builder = new StorDistributormanagerConfig.Builder();
cc.getDistributorNodes().getChildren().get("0").getConfig(builder);
return (new StorDistributormanagerConfig(builder)).num_distributor_stripes();
}
private int resolveTunedNumDistributorStripesConfig(int numCpuCores) throws Exception {
var flavor = new Flavor(new FlavorsConfig.Flavor(new FlavorsConfig.Flavor.Builder().name("test").minCpuCores(numCpuCores)));
return resolveNumDistributorStripesConfig(Optional.of(flavor));
}
@Test
void num_distributor_stripes_config_defaults_to_zero() throws Exception {
// This triggers tuning when starting the distributor process, based on CPU core sampling on the node.
assertEquals(0, resolveNumDistributorStripesConfig(Optional.empty()));
}
@Test
void num_distributor_stripes_config_tuned_by_flavor() throws Exception {
assertEquals(1, resolveTunedNumDistributorStripesConfig(1));
assertEquals(1, resolveTunedNumDistributorStripesConfig(16));
assertEquals(2, resolveTunedNumDistributorStripesConfig(17));
assertEquals(2, resolveTunedNumDistributorStripesConfig(64));
assertEquals(4, resolveTunedNumDistributorStripesConfig(65));
}
@Test
void testDedicatedClusterControllers() {
VespaModel noContentModel = createEnd2EndOneNode(new TestProperties().setHostedVespa(true)
.setMultitenant(true),
"" +
"" +
" " +
" ");
assertEquals(Map.of(), noContentModel.getContentClusters());
assertNull(noContentModel.getAdmin().getClusterControllers(), "No cluster controller without content");
VespaModel oneContentModel = createEnd2EndOneNode(new TestProperties().setHostedVespa(true)
.setMultitenant(true),
"" +
"" +
" " +
" " +
" 1" +
" " +
" " +
" " +
" " +
" ");
assertNotNull(oneContentModel.getAdmin().getClusterControllers(), "Shared cluster controller with content");
String twoContentServices = "" +
"" +
" " +
" " +
" 1" +
" " +
" " +
" " +
" " +
" " +
" 0.618" +
" " +
" " +
" " +
" " +
" 1" +
" " +
" " +
" " +
" " +
" " +
" 0.418" +
" " +
" " +
" " +
" ";
VespaModel twoContentModel = createEnd2EndOneNode(new TestProperties().setHostedVespa(true)
.setMultitenant(true),
twoContentServices);
assertNotNull(twoContentModel.getAdmin().getClusterControllers(), "Shared cluster controller with content");
ClusterControllerContainerCluster clusterControllers = twoContentModel.getAdmin().getClusterControllers();
assertEquals(1, clusterControllers.reindexingContext().documentTypesForCluster("storage").size());
assertEquals(1, clusterControllers.reindexingContext().documentTypesForCluster("dev-null").size());
var storageBuilder = new FleetcontrollerConfig.Builder();
var devNullBuilder = new FleetcontrollerConfig.Builder();
twoContentModel.getConfig(storageBuilder, "admin/standalone/cluster-controllers/0/components/clustercontroller-storage-configurer");
twoContentModel.getConfig(devNullBuilder, "admin/standalone/cluster-controllers/0/components/clustercontroller-dev-null-configurer");
assertEquals(0.618, storageBuilder.build().min_distributor_up_ratio(), 1e-9);
assertEquals(0.418, devNullBuilder.build().min_distributor_up_ratio(), 1e-9);
assertZookeeperServerImplementation("com.yahoo.vespa.zookeeper.ReconfigurableVespaZooKeeperServer",
clusterControllers);
assertZookeeperServerImplementation("com.yahoo.vespa.zookeeper.Reconfigurer",
clusterControllers);
assertZookeeperServerImplementation("com.yahoo.vespa.zookeeper.VespaZooKeeperAdminImpl",
clusterControllers);
}
@Test
void testGroupsAllowedToBeDown() {
assertGroupsAllowedDown(1, 0.5, 1);
assertGroupsAllowedDown(2, 0.5, 1);
assertGroupsAllowedDown(3, 0.5, 1);
assertGroupsAllowedDown(4, 0.5, 2);
assertGroupsAllowedDown(5, 0.5, 2);
assertGroupsAllowedDown(6, 0.5, 3);
assertGroupsAllowedDown(1, 0.33, 1);
assertGroupsAllowedDown(2, 0.33, 1);
assertGroupsAllowedDown(3, 0.33, 1);
assertGroupsAllowedDown(4, 0.33, 1);
assertGroupsAllowedDown(5, 0.33, 1);
assertGroupsAllowedDown(6, 0.33, 1);
assertGroupsAllowedDown(1, 0.67, 1);
assertGroupsAllowedDown(2, 0.67, 1);
assertGroupsAllowedDown(3, 0.67, 2);
assertGroupsAllowedDown(4, 0.67, 2);
assertGroupsAllowedDown(5, 0.67, 3);
assertGroupsAllowedDown(6, 0.67, 4);
assertGroupsAllowedDown(1, 0, 1);
assertGroupsAllowedDown(2, 0, 1);
assertGroupsAllowedDown(1, 1, 1);
assertGroupsAllowedDown(2, 1, 2);
}
private void assertIndexingDocprocEnabled(boolean indexed, boolean force, boolean expEnabled) {
String services = "" +
"" +
" " +
" " +
" " +
" " +
" 1" +
" " +
" " +
" " +
" " +
" " +
"";
VespaModel model = createEnd2EndOneNode(new TestProperties(), services);
var searchCluster = model.getContentClusters().get("search").getSearch();
assertEquals(expEnabled, searchCluster.getIndexingDocproc().isPresent());
}
@Test
void testIndexingDocprocEnabledWhenIndexMode()
{
assertIndexingDocprocEnabled(true, false, true);
}
@Test
void testIndexingDocprocNotEnabledWhenStreamingMode()
{
assertIndexingDocprocEnabled(false, false, false);
}
@Test
void testIndexingDocprocEnabledWhenStreamingModeAndForced()
{
assertIndexingDocprocEnabled(false, true, true);
}
private void assertGroupsAllowedDown(int groupCount, double groupsAllowedDown, int expectedGroupsAllowedDown) {
var services = servicesWithGroups(groupCount, groupsAllowedDown);
var model = createEnd2EndOneNode(new TestProperties(), services);
var fleetControllerConfigBuilder = new FleetcontrollerConfig.Builder();
model.getConfig(fleetControllerConfigBuilder, "admin/cluster-controllers/0/components/clustercontroller-storage-configurer");
var config = fleetControllerConfigBuilder.build();
assertEquals(expectedGroupsAllowedDown, config.max_number_of_groups_allowed_to_be_down());
}
private boolean resolveDistributorOperationCancellationConfig(Integer featureLevel) throws Exception {
var properties = new TestProperties();
if (featureLevel != null) {
properties.setContentLayerMetadataFeatureLevel(featureLevel);
}
var cfg = resolveStorDistributormanagerConfig(properties);
return cfg.enable_operation_cancellation();
}
@Test
void distributor_operation_cancelling_config_controlled_by_properties() throws Exception {
assertFalse(resolveDistributorOperationCancellationConfig(null)); // defaults to false
assertFalse(resolveDistributorOperationCancellationConfig(0));
assertTrue(resolveDistributorOperationCancellationConfig(1));
assertTrue(resolveDistributorOperationCancellationConfig(2));
}
private String servicesWithGroups(int groupCount, double minGroupUpRatio) {
String services = String.format("" +
"" +
" " +
" " +
" %d" +
" " +
" " +
" " +
" ", groupCount);
String distribution = switch (groupCount) {
case 1, 2 -> " ";
case 3 -> " ";
case 4 -> " ";
case 5 -> " ";
case 6 -> " ";
default -> throw new IllegalArgumentException("Does not support groupCount > 6");
};
services += distribution;
for (int i = 0; i < groupCount; i++) {
services += String.format(" " +
" " +
" ",
i, i, i);
}
return services +
String.format(Locale.US, " " +
" " +
" " +
" %f" +
" " +
" " +
" " +
" " +
" %d" +
" " +
" " +
" " +
" ", minGroupUpRatio, groupCount);
}
}