// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.model.application.validation.change; import com.yahoo.config.application.api.ValidationId; import com.yahoo.config.model.api.ConfigChangeAction; import com.yahoo.config.model.api.ServiceInfo; import com.yahoo.config.model.deploy.DeployState; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.vespa.model.VespaModel; import com.yahoo.vespa.model.application.validation.ValidationTester; import com.yahoo.vespa.model.content.utils.ApplicationPackageBuilder; import com.yahoo.vespa.model.content.utils.ContentClusterBuilder; import com.yahoo.vespa.model.content.utils.DocType; import com.yahoo.vespa.model.content.utils.SchemaBuilder; import org.junit.jupiter.api.Test; import java.util.List; import static com.yahoo.vespa.model.application.validation.change.ConfigChangeTestUtils.assertEqualActions; import static com.yahoo.vespa.model.application.validation.change.ConfigChangeTestUtils.normalizeServicesInActions; import static org.junit.jupiter.api.Assertions.assertTrue; public class StreamingSchemaClusterChangeValidatorTest { private static class Fixture { VespaModel currentModel; VespaModel nextModel; StreamingSearchClusterChangeValidator validator; public Fixture(VespaModel currentModel, VespaModel nextModel) { this.currentModel = currentModel; this.nextModel = nextModel; validator = new StreamingSearchClusterChangeValidator(); } public static Fixture withOneDocType(String currentSd, String nextSd) { return new Fixture(createOneDocModel(currentSd), createOneDocModel(nextSd)); } public static VespaModel createOneDocModel(String sdContent) { return new ApplicationPackageBuilder() .addCluster(new ContentClusterBuilder().name("foo").docTypes(List.of(DocType.streaming("d1")))) .addSchemas(new SchemaBuilder().name("d1").content(sdContent).build()) .buildCreator().create(); } public static Fixture withTwoDocTypes(String currentSd, String nextSd) { return new Fixture(createTwoDocModel(currentSd, currentSd), createTwoDocModel(nextSd, nextSd)); } public static VespaModel createTwoDocModel(String d1Content, String d2Content) { return new ApplicationPackageBuilder() .addCluster(new ContentClusterBuilder().name("foo").docTypes(List.of(DocType.streaming("d1"), DocType.streaming("d2")))) .addSchemas(new SchemaBuilder().name("d1").content(d1Content).build()) .addSchemas(new SchemaBuilder().name("d2").content(d2Content).build()) .buildCreator().create(); } public static Fixture withTwoClusters(String currentSd, String nextSd) { return new Fixture(createTwoClusterModel(currentSd, currentSd), createTwoClusterModel(nextSd, nextSd)); } public static VespaModel createTwoClusterModel(String d1Content, String d2Content) { return new ApplicationPackageBuilder() .addCluster(new ContentClusterBuilder().name("foo").docTypes(List.of(DocType.streaming("d1")))) .addCluster(new ContentClusterBuilder().name("bar").docTypes(List.of(DocType.streaming("d2")))) .addSchemas(new SchemaBuilder().name("d1").content(d1Content).build()) .addSchemas(new SchemaBuilder().name("d2").content(d2Content).build()) .buildCreator().create(); } public List validate() { return normalizeServicesInActions(ValidationTester.validateChanges(validator, nextModel, new DeployState.Builder().previousModel(currentModel).build())); } public void assertValidation() { assertTrue(validate().isEmpty()); } public void assertValidation(ConfigChangeAction exp) { assertValidation(List.of(exp)); } public void assertValidation(List exp) { assertEqualActions(exp, validate()); } } private static final String STRING_FIELD = "field f1 type string { indexing: summary }"; private static final String INT_FIELD = "field f1 type int { indexing: summary }"; private static final String ATTRIBUTE_INT_FIELD = "field f1 type int { indexing: attribute | summary }"; private static final String ATTRIBUTE_FAST_ACCESS_INT_FIELD = "field f1 type int { indexing: attribute | summary \n attribute: fast-access }"; private static final List FOO_SERVICE = List.of(new ServiceInfo("searchnode", "null", null, null, "foo/search/0", "null")); private static final List BAR_SERVICE = List.of(new ServiceInfo("searchnode2", "null", null, null, "bar/search/0", "null")); @Test void changing_field_type_requires_refeed() { Fixture.withOneDocType(STRING_FIELD, INT_FIELD) .assertValidation(createFieldTypeChangeRefeedAction("d1", FOO_SERVICE)); } @Test void changes_in_multiple_streaming_clusters_are_discovered() { Fixture.withTwoClusters(STRING_FIELD, INT_FIELD) .assertValidation(List.of(createFieldTypeChangeRefeedAction("d1", FOO_SERVICE), createFieldTypeChangeRefeedAction("d2", BAR_SERVICE))); } @Test void changes_in_multiple_document_types_are_discovered() { Fixture.withTwoDocTypes(STRING_FIELD, INT_FIELD) .assertValidation(List.of(createFieldTypeChangeRefeedAction("d1", FOO_SERVICE), createFieldTypeChangeRefeedAction("d2", FOO_SERVICE))); } @Test void adding_fast_access_to_an_attribute_requires_restart() { Fixture.withOneDocType(INT_FIELD, ATTRIBUTE_FAST_ACCESS_INT_FIELD) .assertValidation(createAddFastAccessRestartAction()); Fixture.withOneDocType(ATTRIBUTE_INT_FIELD, ATTRIBUTE_FAST_ACCESS_INT_FIELD) .assertValidation(createAddFastAccessRestartAction()); } @Test void removing_fast_access_from_an_attribute_requires_restart() { Fixture.withOneDocType(ATTRIBUTE_FAST_ACCESS_INT_FIELD, INT_FIELD) .assertValidation(createRemoveFastAccessRestartAction()); Fixture.withOneDocType(ATTRIBUTE_FAST_ACCESS_INT_FIELD, ATTRIBUTE_INT_FIELD) .assertValidation(createRemoveFastAccessRestartAction()); } @Test void adding_attribute_field_is_ok() { Fixture.withOneDocType(INT_FIELD, ATTRIBUTE_INT_FIELD).assertValidation(); } @Test void removing_attribute_field_is_ok() { Fixture.withOneDocType(ATTRIBUTE_INT_FIELD, INT_FIELD).assertValidation(); } @Test void unchanged_fast_access_attribute_field_is_ok() { Fixture.withOneDocType(ATTRIBUTE_FAST_ACCESS_INT_FIELD, ATTRIBUTE_FAST_ACCESS_INT_FIELD).assertValidation(); } @Test void adding_streaming_cluster_is_ok() { new Fixture(Fixture.createOneDocModel(STRING_FIELD), Fixture.createTwoClusterModel(STRING_FIELD, STRING_FIELD)).assertValidation(); } @Test void removing_streaming_cluster_is_ok() { new Fixture(Fixture.createTwoClusterModel(STRING_FIELD, STRING_FIELD), Fixture.createOneDocModel(STRING_FIELD)).assertValidation(); } private static VespaConfigChangeAction createFieldTypeChangeRefeedAction(String docType, List service) { return ConfigChangeTestUtils.newRefeedAction(ClusterSpec.Id.from("test"), ValidationId.fieldTypeChange, "Document type '" + docType + "': Field 'f1' changed: data type: 'string' -> 'int'", service, docType); } private static VespaConfigChangeAction createAddFastAccessRestartAction() { return ConfigChangeTestUtils.newRestartAction(ClusterSpec.Id.from("test"), "Document type 'd1': Field 'f1' changed: add fast-access attribute", FOO_SERVICE); } private static VespaConfigChangeAction createRemoveFastAccessRestartAction() { return ConfigChangeTestUtils.newRestartAction(ClusterSpec.Id.from("test"), "Document type 'd1': Field 'f1' changed: remove fast-access attribute", FOO_SERVICE); } }