// 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); } }