]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-5681 drop ES indices on DB vendor/schema change
authorSimon Brandhof <simon.brandhof@sonarsource.com>
Fri, 8 Sep 2017 15:04:22 +0000 (17:04 +0200)
committerSimon Brandhof <simon.brandhof@sonarsource.com>
Mon, 11 Sep 2017 19:03:33 +0000 (21:03 +0200)
server/sonar-server/src/main/java/org/sonar/server/es/IndexCreator.java
server/sonar-server/src/main/java/org/sonar/server/es/metadata/EsDbCompatibility.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/es/metadata/EsDbCompatibilityImpl.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/es/metadata/MetadataIndex.java
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
server/sonar-server/src/test/java/org/sonar/server/es/EsTester.java
server/sonar-server/src/test/java/org/sonar/server/es/IndexCreatorTest.java
server/sonar-server/src/test/java/org/sonar/server/es/TestEsDbCompatibility.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/es/metadata/EsDbCompatibilityImplTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/es/metadata/MetadataIndexTest.java

index a2dcfb4895b335c93cac5ff69aec1d9f067f3a43..bd4f9da1dec982262a3270e698c179495720283e 100644 (file)
  */
 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 (file)
index 0000000..3eaec22
--- /dev/null
@@ -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 (file)
index 0000000..fe785a9
--- /dev/null
@@ -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();
+  }
+}
index 4b0d70f68c5667768b8622722ae4552f9bd4e9c1..19a5fb90a4dc7be6c9cf52d4856f896c89e14d39 100644 (file)
@@ -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();
   }
index 2f6b37b75e0b6d37d87630a66c21b9a87176ffb0..a7683cebc35fa1382f0676ee8b8f7157e28d91f8 100644 (file)
@@ -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,
index 5a0bf45b3f0f928ba5cdf34b12828f29d6b81f33..77e46240cfa009373609c27ffda32cdbc1799537 100644 (file)
@@ -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));
   }
index b96d86437e85417635d59f940e8c5758c1731355..cc81c15ac35fa3fb337f07b7a6965cf9fb975526 100644 (file)
  */
 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 (file)
index 0000000..3098cb3
--- /dev/null
@@ -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 (file)
index 0000000..66a15ae
--- /dev/null
@@ -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);
+  }
+}
index 13cb0dc089ac67f49202469eb3e0c09f0cc48bdd..57bd67dbc94fd1f4516ef5629d8df55f14175593 100644 (file)
@@ -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);
+  }
+}