diff options
author | Simon Brandhof <simon.brandhof@sonarsource.com> | 2017-09-08 17:04:22 +0200 |
---|---|---|
committer | Simon Brandhof <simon.brandhof@sonarsource.com> | 2017-09-11 21:03:33 +0200 |
commit | abe8771346151c14f983abf1a618744b5b750fef (patch) | |
tree | f037943c00e207ae671a33ad52e6fcbf484ae997 | |
parent | 9aa886bd35904977b6fa15eaf647a3001893fead (diff) | |
download | sonarqube-abe8771346151c14f983abf1a618744b5b750fef.tar.gz sonarqube-abe8771346151c14f983abf1a618744b5b750fef.zip |
SONAR-5681 drop ES indices on DB vendor/schema change
10 files changed, 541 insertions, 71 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/IndexCreator.java b/server/sonar-server/src/main/java/org/sonar/server/es/IndexCreator.java index a2dcfb4895b..bd4f9da1dec 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/es/IndexCreator.java +++ b/server/sonar-server/src/main/java/org/sonar/server/es/IndexCreator.java @@ -19,17 +19,22 @@ */ package org.sonar.server.es; +import java.util.Arrays; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import org.apache.commons.lang.StringUtils; import org.elasticsearch.action.admin.indices.create.CreateIndexResponse; import org.elasticsearch.action.admin.indices.mapping.put.PutMappingResponse; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.common.settings.Settings; import org.picocontainer.Startable; +import org.sonar.api.config.Configuration; import org.sonar.api.server.ServerSide; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; import org.sonar.server.es.IndexDefinitions.Index; +import org.sonar.server.es.metadata.EsDbCompatibility; import org.sonar.server.es.metadata.MetadataIndex; import org.sonar.server.es.metadata.MetadataIndexDefinition; @@ -40,22 +45,27 @@ import org.sonar.server.es.metadata.MetadataIndexDefinition; public class IndexCreator implements Startable { private static final Logger LOGGER = Loggers.get(IndexCreator.class); + private static final String PROPERY_DISABLE_CHECK = "sonar.search.disableDropOnDbMigration"; private final MetadataIndexDefinition metadataIndexDefinition; private final MetadataIndex metadataIndex; private final EsClient client; private final IndexDefinitions definitions; + private final EsDbCompatibility esDbCompatibility; + private final Configuration configuration; - public IndexCreator(EsClient client, IndexDefinitions definitions, MetadataIndexDefinition metadataIndexDefinition, MetadataIndex metadataIndex) { + public IndexCreator(EsClient client, IndexDefinitions definitions, MetadataIndexDefinition metadataIndexDefinition, + MetadataIndex metadataIndex, EsDbCompatibility esDbCompatibility, Configuration configuration) { this.client = client; this.definitions = definitions; this.metadataIndexDefinition = metadataIndexDefinition; this.metadataIndex = metadataIndex; + this.esDbCompatibility = esDbCompatibility; + this.configuration = configuration; } @Override public void start() { - // create the "metadata" index first if (!client.prepareIndicesExist(MetadataIndexDefinition.INDEX_TYPE_METADATA.getIndex()).get().isExists()) { IndexDefinition.IndexDefinitionContext context = new IndexDefinition.IndexDefinitionContext(); @@ -64,11 +74,13 @@ public class IndexCreator implements Startable { createIndex(new Index(index), false); } + checkDbCompatibility(); + // create indices that do not exist or that have a new definition (different mapping, cluster enabled, ...) for (Index index : definitions.getIndices().values()) { boolean exists = client.prepareIndicesExist(index.getName()).get().isExists(); - if (exists && !index.getName().equals(MetadataIndexDefinition.INDEX_TYPE_METADATA.getIndex()) && needsToDeleteIndex(index)) { - LOGGER.info(String.format("Delete index %s (settings changed)", index.getName())); + if (exists && !index.getName().equals(MetadataIndexDefinition.INDEX_TYPE_METADATA.getIndex()) && hasDefinitionChange(index)) { + LOGGER.info("Delete Elasticsearch index {} (structure changed)", index.getName()); deleteIndex(index.getName()); exists = false; } @@ -120,11 +132,40 @@ public class IndexCreator implements Startable { client.nativeClient().admin().indices().prepareDelete(indexName).get(); } - private boolean needsToDeleteIndex(Index index) { + private boolean hasDefinitionChange(Index index) { return metadataIndex.getHash(index.getName()) .map(hash -> { String defHash = IndexDefinitionHash.of(index); return !StringUtils.equals(hash, defHash); }).orElse(true); } + + private void checkDbCompatibility() { + boolean disabledCheck = configuration.getBoolean(PROPERY_DISABLE_CHECK).orElse(false); + if (disabledCheck) { + LOGGER.warn("Automatic drop of search indices in turned off (see property " + PROPERY_DISABLE_CHECK + ")"); + } + + List<String> existingIndices = loadExistingIndicesExceptMetadata(); + if (!disabledCheck && !existingIndices.isEmpty()) { + boolean delete = false; + if (!esDbCompatibility.hasSameDbVendor()) { + LOGGER.info("Delete Elasticsearch indices (DB vendor changed)"); + delete = true; + } else if (!esDbCompatibility.hasSameDbSchemaVersion()) { + LOGGER.info("Delete Elasticsearch indices (DB schema changed)"); + delete = true; + } + if (delete) { + existingIndices.forEach(this::deleteIndex); + } + } + esDbCompatibility.markAsCompatible(); + } + + private List<String> loadExistingIndicesExceptMetadata() { + return Arrays.stream(client.nativeClient().admin().indices().prepareGetIndex().get().getIndices()) + .filter(index -> !MetadataIndexDefinition.INDEX_TYPE_METADATA.getIndex().equals(index)) + .collect(Collectors.toList()); + } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/metadata/EsDbCompatibility.java b/server/sonar-server/src/main/java/org/sonar/server/es/metadata/EsDbCompatibility.java new file mode 100644 index 00000000000..3eaec225498 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/es/metadata/EsDbCompatibility.java @@ -0,0 +1,46 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.es.metadata; + +/** + * Checks when Elasticsearch indices must be dropped because + * of changes in database + */ +public interface EsDbCompatibility { + + /** + * Whether the effective DB vendor equals the vendor + * registered in Elasticsearch metadata. + * Return {@code false} if at least one of the values is absent + */ + boolean hasSameDbVendor(); + + /** + * Whether the effective DB schema version equals the version + * registered in Elasticsearch metadata. + * Return {@code false} if at least one of the values is absent + */ + boolean hasSameDbSchemaVersion(); + + /** + * Stores in Elasticsearch the metadata about database + */ + void markAsCompatible(); +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/metadata/EsDbCompatibilityImpl.java b/server/sonar-server/src/main/java/org/sonar/server/es/metadata/EsDbCompatibilityImpl.java new file mode 100644 index 00000000000..fe785a95a4c --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/es/metadata/EsDbCompatibilityImpl.java @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.es.metadata; + +import java.util.Objects; +import java.util.Optional; +import org.sonar.db.DbClient; +import org.sonar.server.platform.db.migration.history.MigrationHistory; + +public class EsDbCompatibilityImpl implements EsDbCompatibility { + + private final DbClient dbClient; + private final MetadataIndex metadataIndex; + private final MigrationHistory dbMigrationHistory; + + public EsDbCompatibilityImpl(DbClient dbClient, MetadataIndex metadataIndex, MigrationHistory dbMigrationHistory) { + this.dbClient = dbClient; + this.metadataIndex = metadataIndex; + this.dbMigrationHistory = dbMigrationHistory; + } + + @Override + public boolean hasSameDbVendor() { + Optional<String> registeredDbVendor = metadataIndex.getDbVendor(); + return registeredDbVendor.isPresent() && registeredDbVendor.get().equals(getDbVendor()); + } + + @Override + public boolean hasSameDbSchemaVersion() { + Optional<Long> registeredVersion = metadataIndex.getDbSchemaVersion(); + if (!registeredVersion.isPresent()) { + return false; + } + return getDbSchemaVersion() + .filter(effectiveVersion -> Objects.equals(registeredVersion.get(), effectiveVersion)) + .isPresent(); + } + + @Override + public void markAsCompatible() { + metadataIndex.setDbMetadata(getDbVendor(), getDbSchemaVersion() + .orElseThrow(() -> new IllegalStateException("DB schema version is not present in database"))); + } + + private String getDbVendor() { + return dbClient.getDatabase().getDialect().getId(); + } + + private Optional<Long> getDbSchemaVersion() { + return dbMigrationHistory.getLastMigrationNumber(); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/metadata/MetadataIndex.java b/server/sonar-server/src/main/java/org/sonar/server/es/metadata/MetadataIndex.java index 4b0d70f68c5..19a5fb90a4d 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/es/metadata/MetadataIndex.java +++ b/server/sonar-server/src/main/java/org/sonar/server/es/metadata/MetadataIndex.java @@ -30,6 +30,9 @@ import static org.sonar.server.es.DefaultIndexSettings.REFRESH_IMMEDIATE; public class MetadataIndex { + private static final String DB_VENDOR_KEY = "dbVendor"; + private static final String DB_SCHEMA_VERSION_KEY = "dbSchemaVersion"; + private final EsClient esClient; public MetadataIndex(EsClient esClient) { @@ -41,7 +44,7 @@ public class MetadataIndex { } public void setHash(String index, String hash) { - setMetadata(hash, hashId(index)); + setMetadata(hashId(index), hash); } private static String hashId(String index) { @@ -53,13 +56,26 @@ public class MetadataIndex { } public void setInitialized(IndexType indexType, boolean initialized) { - setMetadata(String.valueOf(initialized), initializedId(indexType)); + setMetadata(initializedId(indexType), String.valueOf(initialized)); } private static String initializedId(IndexType indexType) { return indexType.getIndex() + "." + indexType.getType() + ".initialized"; } + public Optional<String> getDbVendor() { + return getMetadata(DB_VENDOR_KEY); + } + + public Optional<Long> getDbSchemaVersion() { + return getMetadata(DB_SCHEMA_VERSION_KEY).map(Long::parseLong); + } + + public void setDbMetadata(String vendor, long schemaVersion) { + setMetadata(DB_VENDOR_KEY, vendor); + setMetadata(DB_SCHEMA_VERSION_KEY, String.valueOf(schemaVersion)); + } + private Optional<String> getMetadata(String id) { GetRequestBuilder request = esClient.prepareGet(MetadataIndexDefinition.INDEX_TYPE_METADATA, id) .setStoredFields(MetadataIndexDefinition.FIELD_VALUE); @@ -72,10 +88,10 @@ public class MetadataIndex { return Optional.empty(); } - private void setMetadata(String hash, String id) { + private void setMetadata(String id, String value) { esClient.prepareIndex(MetadataIndexDefinition.INDEX_TYPE_METADATA) .setId(id) - .setSource(MetadataIndexDefinition.FIELD_VALUE, hash) + .setSource(MetadataIndexDefinition.FIELD_VALUE, value) .setRefreshPolicy(REFRESH_IMMEDIATE) .get(); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index 2f6b37b75e0..a7683cebc35 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -55,6 +55,7 @@ import org.sonar.server.es.IndexCreator; import org.sonar.server.es.IndexDefinitions; import org.sonar.server.es.ProjectIndexersImpl; import org.sonar.server.es.RecoveryIndexer; +import org.sonar.server.es.metadata.EsDbCompatibilityImpl; import org.sonar.server.es.metadata.MetadataIndex; import org.sonar.server.es.metadata.MetadataIndexDefinition; import org.sonar.server.event.NewAlerts; @@ -236,7 +237,8 @@ public class PlatformLevel4 extends PlatformLevel { addIfStartupLeader( IndexCreator.class, MetadataIndexDefinition.class, - MetadataIndex.class); + MetadataIndex.class, + EsDbCompatibilityImpl.class); add( PluginDownloader.class, diff --git a/server/sonar-server/src/test/java/org/sonar/server/es/EsTester.java b/server/sonar-server/src/test/java/org/sonar/server/es/EsTester.java index 5a0bf45b3f0..77e46240cfa 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/es/EsTester.java +++ b/server/sonar-server/src/test/java/org/sonar/server/es/EsTester.java @@ -159,6 +159,7 @@ public class EsTester extends ExternalResource { container.addSingleton(IndexCreator.class); container.addSingleton(MetadataIndex.class); container.addSingleton(MetadataIndexDefinition.class); + container.addSingleton(TestEsDbCompatibility.class); container.startComponents(); container.stopComponents(); client().close(); @@ -188,10 +189,11 @@ public class EsTester extends ExternalResource { private void afterTest() throws Exception { if (cluster != null) { MetaData metaData = cluster.client().admin().cluster().prepareState().execute().actionGet().getState().getMetaData(); - assertEquals("test leaves persistent cluster metadata behind: " + metaData.persistentSettings().getAsMap(), metaData - .persistentSettings().getAsMap().size(), 0); - assertEquals("test leaves transient cluster metadata behind: " + metaData.transientSettings().getAsMap(), metaData - .transientSettings().getAsMap().size(), 0); + assertEquals("test leaves persistent cluster metadata behind: " + metaData.persistentSettings().getAsMap(), + 0, + metaData.persistentSettings().getAsMap().size()); + assertEquals("test leaves transient cluster metadata behind: " + metaData.transientSettings().getAsMap(), 0, metaData + .transientSettings().getAsMap().size()); ensureClusterSizeConsistency(); ensureClusterStateConsistency(); cluster.beforeIndexDeletion(); @@ -267,6 +269,23 @@ public class EsTester extends ExternalResource { } } + public void putDocuments(IndexType indexType, Map<String,Object>... docs) { + try { + BulkRequestBuilder bulk = cluster.client().prepareBulk() + .setRefreshPolicy(REFRESH_IMMEDIATE); + for (Map<String,Object> doc : docs) { + bulk.add(new IndexRequest(indexType.getIndex(), indexType.getType()) + .source(doc)); + } + BulkResponse bulkResponse = bulk.get(); + if (bulkResponse.hasFailures()) { + throw new IllegalStateException(bulkResponse.buildFailureMessage()); + } + } catch (Exception e) { + throw Throwables.propagate(e); + } + } + public long countDocuments(String index, String type) { return countDocuments(new IndexType(index, type)); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/es/IndexCreatorTest.java b/server/sonar-server/src/test/java/org/sonar/server/es/IndexCreatorTest.java index b96d86437e8..cc81c15ac35 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/es/IndexCreatorTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/es/IndexCreatorTest.java @@ -19,8 +19,10 @@ */ package org.sonar.server.es; +import com.google.common.collect.ImmutableMap; import java.io.IOException; import java.util.Map; +import java.util.function.Consumer; import javax.annotation.CheckForNull; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.cluster.metadata.MappingMetaData; @@ -28,36 +30,37 @@ import org.elasticsearch.common.collect.ImmutableOpenMap; import org.junit.Rule; import org.junit.Test; import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; import org.sonar.server.es.metadata.MetadataIndex; import org.sonar.server.es.metadata.MetadataIndexDefinition; import static org.assertj.core.api.Assertions.assertThat; import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.sonar.server.es.NewIndex.SettingsConfiguration.newBuilder; public class IndexCreatorTest { - private static final NewIndex.SettingsConfiguration settingsConfiguration = newBuilder(new MapSettings().asConfig()).build(); + private static final NewIndex.SettingsConfiguration SETTINGS_CONFIGURATION = newBuilder(new MapSettings().asConfig()).build(); + private static final String LOG_DB_VENDOR_CHANGED = "Delete Elasticsearch indices (DB vendor changed)"; + private static final String LOG_DB_SCHEMA_CHANGED = "Delete Elasticsearch indices (DB schema changed)"; + + @Rule + public LogTester logTester = new LogTester(); @Rule public EsTester es = new EsTester(); private MetadataIndexDefinition metadataIndexDefinition = new MetadataIndexDefinition(new MapSettings().asConfig()); private MetadataIndex metadataIndex = new MetadataIndex(es.client()); + private TestEsDbCompatibility esDbCompatibility = new TestEsDbCompatibility(); + private MapSettings settings = new MapSettings(); @Test public void create_index() throws Exception { assertThat(mappings()).isEmpty(); - IndexDefinitions registry = new IndexDefinitions(new IndexDefinition[] {new FakeIndexDefinition()}, new MapSettings().asConfig()); - registry.start(); - IndexCreator creator = new IndexCreator(es.client(), registry, metadataIndexDefinition, metadataIndex); - creator.start(); + IndexCreator underTest = startNewCreator(new FakeIndexDefinition()); // check that index is created with related mapping ImmutableOpenMap<String, ImmutableOpenMap<String, MappingMetaData>> mappings = mappings(); @@ -68,39 +71,27 @@ public class IndexCreatorTest { assertThat(field(mapping, "updatedAt").get("type")).isEqualTo("date"); // of course do not delete indices on stop - creator.stop(); + underTest.stop(); assertThat(mappings()).isNotEmpty(); } @Test public void mark_all_non_existing_index_types_as_uninitialized() throws Exception { - MetadataIndex metadataIndexMock = mock(MetadataIndex.class); - IndexDefinitions registry = new IndexDefinitions(new IndexDefinition[] {context -> { - - NewIndex i = context.create("i", settingsConfiguration); + startNewCreator(context -> { + NewIndex i = context.create("i", SETTINGS_CONFIGURATION); i.createType("t1"); i.createType("t2"); - }}, new MapSettings().asConfig()); - registry.start(); - IndexCreator creator = new IndexCreator(es.client(), registry, metadataIndexDefinition, metadataIndexMock); - creator.start(); + }); - verify(metadataIndexMock).setHash(eq("i"), anyString()); - verify(metadataIndexMock).setInitialized(eq(new IndexType("i", "t1")), eq(false)); - verify(metadataIndexMock).setInitialized(eq(new IndexType("i", "t2")), eq(false)); - verifyNoMoreInteractions(metadataIndexMock); + assertThat(metadataIndex.getHash("i")).isNotEmpty(); + assertThat(metadataIndex.getInitialized(new IndexType("i", "t1"))).isFalse(); + assertThat(metadataIndex.getInitialized(new IndexType("i", "t2"))).isFalse(); } @Test public void recreate_index_on_definition_changes() throws Exception { - assertThat(mappings()).isEmpty(); - // v1 - IndexDefinitions registry = new IndexDefinitions(new IndexDefinition[] {new FakeIndexDefinition()}, new MapSettings().asConfig()); - registry.start(); - IndexCreator creator = new IndexCreator(es.client(), registry, metadataIndexDefinition, metadataIndex); - creator.start(); - creator.stop(); + startNewCreator(new FakeIndexDefinition()); IndexType fakeIndexType = new IndexType("fakes", "fake"); String id = "1"; @@ -108,53 +99,110 @@ public class IndexCreatorTest { assertThat(es.client().prepareGet(fakeIndexType, id).get().isExists()).isTrue(); // v2 - registry = new IndexDefinitions(new IndexDefinition[] {new FakeIndexDefinitionV2()}, new MapSettings().asConfig()); - registry.start(); - creator = new IndexCreator(es.client(), registry, metadataIndexDefinition, metadataIndex); - creator.start(); + startNewCreator(new FakeIndexDefinitionV2()); + ImmutableOpenMap<String, ImmutableOpenMap<String, MappingMetaData>> mappings = mappings(); MappingMetaData mapping = mappings.get("fakes").get("fake"); assertThat(countMappingFields(mapping)).isEqualTo(3); assertThat(field(mapping, "updatedAt").get("type")).isEqualTo("date"); assertThat(field(mapping, "newField").get("type")).isEqualTo("integer"); - creator.stop(); assertThat(es.client().prepareGet(fakeIndexType, id).get().isExists()).isFalse(); } @Test public void do_not_recreate_index_on_unchanged_definition() throws Exception { - assertThat(mappings()).isEmpty(); - // v1 - IndexDefinitions registry = new IndexDefinitions(new IndexDefinition[] {new FakeIndexDefinition()}, new MapSettings().asConfig()); - registry.start(); - IndexCreator creator = new IndexCreator(es.client(), registry, metadataIndexDefinition, metadataIndex); - creator.start(); - creator.stop(); - + startNewCreator(new FakeIndexDefinition()); IndexType fakeIndexType = new IndexType("fakes", "fake"); String id = "1"; es.client().prepareIndex(fakeIndexType).setId(id).setSource(new FakeDoc().getFields()).setRefreshPolicy(IMMEDIATE).get(); assertThat(es.client().prepareGet(fakeIndexType, id).get().isExists()).isTrue(); // v1 - registry = new IndexDefinitions(new IndexDefinition[] {new FakeIndexDefinition()}, new MapSettings().asConfig()); - registry.start(); - creator = new IndexCreator(es.client(), registry, metadataIndexDefinition, metadataIndex); - creator.start(); - creator.stop(); - + startNewCreator(new FakeIndexDefinition()); assertThat(es.client().prepareGet(fakeIndexType, id).get().isExists()).isTrue(); } + @Test + public void delete_existing_indices_if_db_vendor_changed() { + testDeleteOnDbChange(LOG_DB_VENDOR_CHANGED, + c -> c.setHasSameDbVendor(false)); + } + + @Test + public void delete_existing_indices_if_db_schema_changed() { + testDeleteOnDbChange(LOG_DB_SCHEMA_CHANGED, + c -> c.setHasSameDbSchemaVersion(false)); + } + + @Test + public void do_not_check_db_compatibility_on_fresh_es() { + // supposed to be ignored + esDbCompatibility.setHasSameDbVendor(false); + esDbCompatibility.setHasSameDbSchemaVersion(false); + + startNewCreator(new FakeIndexDefinition()); + + assertThat(logTester.logs(LoggerLevel.INFO)) + .doesNotContain(LOG_DB_VENDOR_CHANGED) + .doesNotContain(LOG_DB_SCHEMA_CHANGED) + .contains("Create type fakes/fake") + .contains("Create type metadatas/metadata"); + } + + @Test + public void do_not_check_db_compatibility_if_disabled_by_configuration() { + settings.setProperty("sonar.search.disableDropOnDbMigration", true); + + // initial startup, automatic drop may be ignored because indices do not exist + startNewCreator(new FakeIndexDefinition()); + logTester.clear(); + + // second startup, automatic drop can be disabled only by configuration + + // supposed to be ignored + esDbCompatibility.setHasSameDbVendor(false); + esDbCompatibility.setHasSameDbSchemaVersion(false); + + startNewCreator(new FakeIndexDefinition()); + + assertThat(logTester.logs(LoggerLevel.INFO)) + .doesNotContain(LOG_DB_VENDOR_CHANGED) + .doesNotContain(LOG_DB_SCHEMA_CHANGED); + assertThat(logTester.logs(LoggerLevel.WARN)).contains("Automatic drop of search indices in turned off (see property sonar.search.disableDropOnDbMigration)"); + } + + private void testDeleteOnDbChange(String expectedLog, Consumer<TestEsDbCompatibility> afterFirstStart) { + startNewCreator(new FakeIndexDefinition()); + assertThat(logTester.logs(LoggerLevel.INFO)) + .doesNotContain(expectedLog) + .contains("Create type fakes/fake") + .contains("Create type metadatas/metadata"); + putFakeDocument(); + assertThat(es.countDocuments(FakeIndexDefinition.INDEX_TYPE)).isEqualTo(1); + + afterFirstStart.accept(esDbCompatibility); + logTester.clear(); + startNewCreator(new FakeIndexDefinition()); + + assertThat(logTester.logs(LoggerLevel.INFO)) + .contains(expectedLog) + .contains("Create type fakes/fake") + // keep existing metadata + .doesNotContain("Create type metadatas/metadata"); + // index has been dropped and re-created + assertThat(es.countDocuments(FakeIndexDefinition.INDEX_TYPE)).isEqualTo(0); + } + private ImmutableOpenMap<String, ImmutableOpenMap<String, MappingMetaData>> mappings() { return es.client().nativeClient().admin().indices().prepareGetMappings().get().mappings(); } @CheckForNull + @SuppressWarnings("unchecked") private Map<String, Object> field(MappingMetaData mapping, String field) throws IOException { - Map<String, Object> props = (Map) mapping.getSourceAsMap().get("properties"); + Map<String, Object> props = (Map<String, Object>) mapping.getSourceAsMap().get("properties"); return (Map<String, Object>) props.get(field); } @@ -162,20 +210,34 @@ public class IndexCreatorTest { return ((Map) mapping.getSourceAsMap().get("properties")).size(); } - public static class FakeIndexDefinition implements IndexDefinition { + private IndexCreator startNewCreator(IndexDefinition... definitions) { + IndexDefinitions defs = new IndexDefinitions(definitions, new MapSettings().asConfig()); + defs.start(); + IndexCreator creator = new IndexCreator(es.client(), defs, metadataIndexDefinition, metadataIndex, esDbCompatibility, settings.asConfig()); + creator.start(); + return creator; + } + + private void putFakeDocument() { + es.putDocuments(FakeIndexDefinition.INDEX_TYPE, ImmutableMap.of("key", "foo")); + } + + private static class FakeIndexDefinition implements IndexDefinition { + private static final IndexType INDEX_TYPE = new IndexType("fakes", "fake"); + @Override public void define(IndexDefinitionContext context) { - NewIndex index = context.create("fakes", settingsConfiguration); + NewIndex index = context.create("fakes", SETTINGS_CONFIGURATION); NewIndex.NewIndexType mapping = index.createType("fake"); mapping.keywordFieldBuilder("key").build(); mapping.createDateTimeField("updatedAt"); } } - public static class FakeIndexDefinitionV2 implements IndexDefinition { + private static class FakeIndexDefinitionV2 implements IndexDefinition { @Override public void define(IndexDefinitionContext context) { - NewIndex index = context.create("fakes", settingsConfiguration); + NewIndex index = context.create("fakes", SETTINGS_CONFIGURATION); NewIndex.NewIndexType mapping = index.createType("fake"); mapping.keywordFieldBuilder("key").build(); mapping.createDateTimeField("updatedAt"); diff --git a/server/sonar-server/src/test/java/org/sonar/server/es/TestEsDbCompatibility.java b/server/sonar-server/src/test/java/org/sonar/server/es/TestEsDbCompatibility.java new file mode 100644 index 00000000000..3098cb3870b --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/es/TestEsDbCompatibility.java @@ -0,0 +1,53 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.es; + +import org.sonar.server.es.metadata.EsDbCompatibility; + +public class TestEsDbCompatibility implements EsDbCompatibility { + + private boolean hasSameDbVendor = true; + private boolean hasSameDbSchemaVersion = true; + + public TestEsDbCompatibility setHasSameDbVendor(boolean b) { + this.hasSameDbVendor = b; + return this; + } + + public TestEsDbCompatibility setHasSameDbSchemaVersion(boolean b) { + this.hasSameDbSchemaVersion = b; + return this; + } + + @Override + public boolean hasSameDbVendor() { + return hasSameDbVendor; + } + + @Override + public boolean hasSameDbSchemaVersion() { + return hasSameDbSchemaVersion; + } + + @Override + public void markAsCompatible() { + + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/es/metadata/EsDbCompatibilityImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/es/metadata/EsDbCompatibilityImplTest.java new file mode 100644 index 00000000000..66a15ae4a6a --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/es/metadata/EsDbCompatibilityImplTest.java @@ -0,0 +1,150 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.es.metadata; + +import java.util.Optional; +import javax.annotation.Nullable; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.Mockito; +import org.sonar.db.DbClient; +import org.sonar.server.es.EsTester; +import org.sonar.server.es.FakeIndexDefinition; +import org.sonar.server.platform.db.migration.history.MigrationHistory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class EsDbCompatibilityImplTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public EsTester es = new EsTester(new FakeIndexDefinition()); + private DbClient dbClient = mock(DbClient.class, Mockito.RETURNS_DEEP_STUBS); + private MigrationHistory migrationHistory = mock(MigrationHistory.class); + private MetadataIndex metadataIndex = new MetadataIndex(es.client()); + private EsDbCompatibilityImpl underTest = new EsDbCompatibilityImpl(dbClient, metadataIndex, migrationHistory); + + @Test + public void hasSameDbVendor_is_true_if_values_match() { + prepareDb("mysql", 1_800L); + prepareEs("mysql", 2_000L); + + assertThat(underTest.hasSameDbVendor()).isTrue(); + } + + @Test + public void hasSameDbVendor_is_false_if_values_dont_match() { + prepareDb("mysql", 1_800L); + prepareEs("postgres", 1_800L); + + assertThat(underTest.hasSameDbVendor()).isFalse(); + } + + @Test + public void hasSameDbVendor_is_false_if_value_is_absent_from_es() { + prepareDb("mysql", 1_800L); + + assertThat(underTest.hasSameDbVendor()).isFalse(); + } + + @Test + public void hasSameDbSchemaVersion_is_true_if_values_match() { + prepareDb("mysql", 1_800L); + prepareEs("postgres", 1_800L); + + assertThat(underTest.hasSameDbSchemaVersion()).isTrue(); + } + + @Test + public void hasSameDbSchemaVersion_is_false_if_values_dont_match() { + prepareDb("mysql", 1_800L); + prepareEs("postgres", 2_000L); + + assertThat(underTest.hasSameDbSchemaVersion()).isFalse(); + } + + @Test + public void hasSameDbSchemaVersion_is_false_if_value_is_absent_from_db() { + prepareDb("mysql", null); + prepareEs("postgres", 1_800L); + + assertThat(underTest.hasSameDbSchemaVersion()).isFalse(); + } + + @Test + public void hasSameDbSchemaVersion_is_false_if_value_is_absent_from_es() { + prepareDb("mysql", 1_800L); + + assertThat(underTest.hasSameDbSchemaVersion()).isFalse(); + } + + @Test + public void store_db_metadata_in_es() { + prepareDb("mysql", 1_800L); + + underTest.markAsCompatible(); + + assertThat(metadataIndex.getDbVendor()).hasValue("mysql"); + assertThat(metadataIndex.getDbSchemaVersion()).hasValue(1_800L); + } + + @Test + public void store_updates_db_metadata_in_es() { + prepareEs("mysql", 1_800L); + prepareDb("postgres", 2_000L); + + underTest.markAsCompatible(); + + assertThat(metadataIndex.getDbVendor()).hasValue("postgres"); + assertThat(metadataIndex.getDbSchemaVersion()).hasValue(2_000L); + } + + @Test + public void store_throws_ISE_if_metadata_cant_be_loaded_from_db() { + prepareDb("postgres", null); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("DB schema version is not present in database"); + + underTest.markAsCompatible(); + } + + @Test + public void store_marks_es_as_compatible_with_db() { + prepareDb("postgres", 1_800L); + + underTest.markAsCompatible(); + + assertThat(underTest.hasSameDbSchemaVersion()).isTrue(); + assertThat(underTest.hasSameDbVendor()).isTrue(); + } + + private void prepareDb(String dbVendor, @Nullable Long dbSchemaVersion) { + when(dbClient.getDatabase().getDialect().getId()).thenReturn(dbVendor); + when(migrationHistory.getLastMigrationNumber()).thenReturn(Optional.ofNullable(dbSchemaVersion)); + } + + private void prepareEs(String dbVendor, long dbSchemaVersion) { + metadataIndex.setDbMetadata(dbVendor, dbSchemaVersion); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/es/metadata/MetadataIndexTest.java b/server/sonar-server/src/test/java/org/sonar/server/es/metadata/MetadataIndexTest.java index 13cb0dc089a..57bd67dbc94 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/es/metadata/MetadataIndexTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/es/metadata/MetadataIndexTest.java @@ -34,8 +34,6 @@ public class MetadataIndexTest { public EsTester es = new EsTester(new FakeIndexDefinition()); private final MetadataIndex underTest = new MetadataIndex(es.client()); private final String index = randomAlphanumeric(20); - private final String type = randomAlphanumeric(20); - @Test public void type_should_be_not_initialized_by_default() throws Exception { @@ -61,4 +59,18 @@ public class MetadataIndexTest { underTest.setHash(index, hash); assertThat(underTest.getHash(index)).hasValue(hash); } -}
\ No newline at end of file + + @Test + public void database_metadata_are_empty_if_absent_from_index() { + assertThat(underTest.getDbVendor()).isNotPresent(); + assertThat(underTest.getDbSchemaVersion()).isNotPresent(); + } + + @Test + public void database_metadata_are_present_from_index() { + underTest.setDbMetadata("postgres", 1_800); + + assertThat(underTest.getDbVendor()).hasValue("postgres"); + assertThat(underTest.getDbSchemaVersion()).hasValue(1_800L); + } +} |