]> source.dussan.org Git - sonarqube.git/commitdiff
Synchronize database and ES with a journal approach
authorSimon Brandhof <simon.brandhof@sonarsource.com>
Wed, 19 Nov 2014 07:26:55 +0000 (08:26 +0100)
committerSimon Brandhof <simon.brandhof@sonarsource.com>
Wed, 19 Nov 2014 07:27:08 +0000 (08:27 +0100)
56 files changed:
server/sonar-search/src/main/java/org/sonar/search/SearchServer.java
server/sonar-search/src/test/java/org/sonar/search/SearchServerTest.java
server/sonar-server/src/main/java/org/sonar/server/db/DbClient.java
server/sonar-server/src/main/java/org/sonar/server/db/ResultSetIterator.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/db/migrations/SelectImpl.java
server/sonar-server/src/main/java/org/sonar/server/db/migrations/SqlUtil.java
server/sonar-server/src/main/java/org/sonar/server/es/BulkIndexRequestIterator.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/es/BulkIndexer.java
server/sonar-server/src/main/java/org/sonar/server/es/DefaultIndexSettings.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/es/DefaultMappingSettings.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/es/EsClient.java
server/sonar-server/src/main/java/org/sonar/server/es/IndexCreator.java
server/sonar-server/src/main/java/org/sonar/server/es/IndexDefinition.java
server/sonar-server/src/main/java/org/sonar/server/es/IndexHash.java
server/sonar-server/src/main/java/org/sonar/server/es/IndexRegistry.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/es/IssueIndexDefinition.java
server/sonar-server/src/main/java/org/sonar/server/es/IssueIndexer.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/es/IssueResultSetIterator.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/es/NewIndex.java
server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueDoc.java
server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java
server/sonar-server/src/main/java/org/sonar/server/search/BaseDoc.java
server/sonar-server/src/test/java/org/sonar/server/db/EmbeddedDatabaseTest.java
server/sonar-server/src/test/java/org/sonar/server/db/ResultSetIteratorTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/db/migrations/SqlUtilTest.java
server/sonar-server/src/test/java/org/sonar/server/es/BulkIndexRequestIteratorTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/es/BulkIndexerTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/es/DefaultIndexSettingsTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/es/DefaultMappingSettingsTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/es/EsTester.java
server/sonar-server/src/test/java/org/sonar/server/es/FakeIndexDefinition.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/es/IndexCreatorTest.java
server/sonar-server/src/test/java/org/sonar/server/es/IndexHashTest.java
server/sonar-server/src/test/java/org/sonar/server/es/IssueIndexDefinitionTest.java
server/sonar-server/src/test/java/org/sonar/server/es/NewIndexTest.java
server/sonar-server/src/test/java/org/sonar/server/search/BaseIndexTest.java
server/sonar-server/src/test/java/org/sonar/server/search/request/ProxyBulkRequestBuilderTest.java
server/sonar-server/src/test/java/org/sonar/server/search/request/ProxyClusterHealthRequestBuilderTest.java
server/sonar-server/src/test/java/org/sonar/server/search/request/ProxyClusterStateRequestBuilderText.java
server/sonar-server/src/test/java/org/sonar/server/search/request/ProxyClusterStatsRequestBuilderText.java
server/sonar-server/src/test/java/org/sonar/server/search/request/ProxyCountRequestBuilderTest.java
server/sonar-server/src/test/java/org/sonar/server/search/request/ProxyCreateIndexRequestBuilderText.java
server/sonar-server/src/test/java/org/sonar/server/search/request/ProxyDeleteByQueryRequestBuilderTest.java
server/sonar-server/src/test/java/org/sonar/server/search/request/ProxyFlushRequestBuilderTest.java
server/sonar-server/src/test/java/org/sonar/server/search/request/ProxyGetRequestBuilderTest.java
server/sonar-server/src/test/java/org/sonar/server/search/request/ProxyIndicesExistsRequestBuilderTest.java
server/sonar-server/src/test/java/org/sonar/server/search/request/ProxyIndicesStatsRequestBuilderTest.java
server/sonar-server/src/test/java/org/sonar/server/search/request/ProxyMultiGetRequestBuilderTest.java
server/sonar-server/src/test/java/org/sonar/server/search/request/ProxyNodesStatsRequestBuilderTest.java
server/sonar-server/src/test/java/org/sonar/server/search/request/ProxyPutMappingRequestBuilderTest.java
server/sonar-server/src/test/java/org/sonar/server/search/request/ProxyRefreshRequestBuilderTest.java
server/sonar-server/src/test/java/org/sonar/server/search/request/ProxySearchRequestBuilderTest.java
server/sonar-server/src/test/java/org/sonar/server/search/request/ProxySearchScrollRequestBuilderTest.java
server/sonar-server/src/test/java/org/sonar/server/tester/ServerTester.java
server/sonar-server/src/test/resources/org/sonar/server/db/ResultSetIteratorTest/feed.xml [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/db/ResultSetIteratorTest/schema.sql [new file with mode: 0644]

index c1f8a1e329f1f84c8798088e89814220b05826e5..18909b942ae06138600f028af3dbadc1c5b52136 100644 (file)
 package org.sonar.search;
 
 import org.elasticsearch.action.admin.cluster.health.ClusterHealthStatus;
-import org.elasticsearch.cluster.metadata.IndexMetaData;
-import org.elasticsearch.common.hppc.cursors.ObjectCursor;
-import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.node.internal.InternalNode;
 import org.slf4j.LoggerFactory;
-import org.sonar.process.MessageException;
 import org.sonar.process.MinimumViableSystem;
 import org.sonar.process.Monitored;
 import org.sonar.process.ProcessEntryPoint;
@@ -49,20 +45,6 @@ public class SearchServer implements Monitored {
 
     node = new InternalNode(settings.build(), true);
     node.start();
-
-    // When joining a cluster, make sur the master(s) have a
-    // replication factor on all indices > 0
-    if (settings.inCluster() && !settings.isMaster()) {
-      for (ObjectCursor<Settings> settingCursor : node.client().admin().indices()
-        .prepareGetSettings().get().getIndexToSettings().values()) {
-        Settings settings = settingCursor.value;
-        String clusterReplicationFactor = settings.get(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, "-1");
-        if (Integer.parseInt(clusterReplicationFactor) <= 0) {
-          node.stop();
-          throw new MessageException("Invalid number of Elasticsearch replicas: " + clusterReplicationFactor);
-        }
-      }
-    }
   }
 
   @Override
index 62953cb7726067860709562a29dbba0cd376e616..122ff0431d01c40ff2dabe092cf588ef862d576e 100644 (file)
@@ -38,7 +38,6 @@ import org.sonar.process.Props;
 import java.util.Properties;
 
 import static org.fest.assertions.Assertions.assertThat;
-import static org.fest.assertions.Fail.fail;
 
 public class SearchServerTest {
 
@@ -130,49 +129,6 @@ public class SearchServerTest {
     slaveServer.awaitStop();
   }
 
-  @Test
-  public void slave_failed_replication() throws Exception {
-    Props props = new Props(new Properties());
-    props.set(ProcessConstants.SEARCH_PORT, String.valueOf(port));
-    props.set(ProcessConstants.CLUSTER_ACTIVATE, "false");
-    props.set(ProcessConstants.CLUSTER_NAME, CLUSTER_NAME);
-    props.set(ProcessConstants.CLUSTER_NODE_NAME, "NOT_MASTER");
-    props.set(ProcessConstants.PATH_HOME, temp.newFolder().getAbsolutePath());
-    searchServer = new SearchServer(props);
-    assertThat(searchServer).isNotNull();
-
-    searchServer.start();
-    assertThat(searchServer.isReady()).isTrue();
-
-    client = getSearchClient();
-    client.admin().indices().prepareCreate("test").get();
-
-    // start a slave
-    props = new Props(new Properties());
-    props.set(ProcessConstants.CLUSTER_ACTIVATE, "true");
-    props.set(ProcessConstants.CLUSTER_MASTER, "false");
-    props.set(ProcessConstants.CLUSTER_MASTER_HOST, "localhost:" + port);
-    props.set(ProcessConstants.CLUSTER_NAME, CLUSTER_NAME);
-    props.set(ProcessConstants.CLUSTER_NODE_NAME, "SLAVE");
-    props.set(ProcessConstants.SEARCH_PORT, String.valueOf(NetworkUtils.freePort()));
-    props.set(ProcessConstants.PATH_HOME, temp.newFolder().getAbsolutePath());
-    SearchServer slaveServer = new SearchServer(props);
-    assertThat(slaveServer).isNotNull();
-
-    try {
-      slaveServer.start();
-      fail();
-    } catch (Exception e) {
-      assertThat(e).hasMessage("Invalid number of Elasticsearch replicas: 0");
-    }
-
-    assertThat(client.admin().cluster().prepareClusterStats().get()
-      .getNodesStats().getCounts().getTotal()).isEqualTo(1);
-
-    slaveServer.stop();
-    slaveServer.awaitStop();
-  }
-
   private Client getSearchClient() {
     Settings settings = ImmutableSettings.settingsBuilder()
       .put("cluster.name", CLUSTER_NAME).build();
index 256d76ab6ff45c3c2ab008c59a52e4346f96e672..bfb143566b9cdcb2ff7d5136f82f2251024ab1f8 100644 (file)
@@ -47,6 +47,10 @@ import org.sonar.server.qualityprofile.db.ActiveRuleDao;
 import org.sonar.server.rule.db.RuleDao;
 import org.sonar.server.user.db.GroupDao;
 
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
 import java.util.IdentityHashMap;
 import java.util.Map;
 
@@ -211,4 +215,17 @@ public class DbClient implements ServerComponent {
   private <K> K getDao(Map<Class, DaoComponent> map, Class<K> clazz) {
     return (K) map.get(clazz);
   }
+
+  /**
+   * Create a PreparedStatement for SELECT requests with scrolling of results
+   */
+  public final PreparedStatement newScrollingSelectStatement(Connection connection, String sql) {
+    try {
+      PreparedStatement stmt = connection.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
+      stmt.setFetchSize(database().getDialect().getScrollDefaultFetchSize());
+      return stmt;
+    } catch (SQLException e) {
+      throw new IllegalStateException("Fail to create SQL statement: " + sql, e);
+    }
+  }
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/db/ResultSetIterator.java b/server/sonar-server/src/main/java/org/sonar/server/db/ResultSetIterator.java
new file mode 100644 (file)
index 0000000..a8f882e
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.db;
+
+import org.apache.commons.dbutils.DbUtils;
+
+import javax.annotation.CheckForNull;
+
+import java.io.Closeable;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Iterator;
+
+/**
+ * {@link java.util.Iterator} applied to {@link java.sql.ResultSet}
+ */
+public abstract class ResultSetIterator<E> implements Iterator<E>, Closeable {
+
+  private final ResultSet rs;
+  private final PreparedStatement stmt;
+
+  // TODO can be simpler by using rs.isLast(). See ResultSetIterator from commons-dbutils
+  private boolean didNext = false;
+  private boolean hasNext = false;
+
+  public ResultSetIterator(PreparedStatement stmt) throws SQLException {
+    this.stmt = stmt;
+    this.rs = stmt.executeQuery();
+  }
+
+  protected ResultSetIterator(ResultSet rs) {
+    this.stmt = null;
+    this.rs = rs;
+  }
+
+  @Override
+  public boolean hasNext() {
+    if (!didNext) {
+      hasNext = doNextQuietly();
+      didNext = true;
+    }
+    return hasNext;
+  }
+
+  @Override
+  @CheckForNull
+  public E next() {
+    if (!didNext) {
+      doNextQuietly();
+    }
+    didNext = false;
+    try {
+      return read(rs);
+    } catch (SQLException e) {
+      // TODO add SQL request to context
+      throw new IllegalStateException("Fail to read result set row", e);
+    }
+  }
+
+  @Override
+  public void remove() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void close() {
+    DbUtils.closeQuietly(rs);
+    DbUtils.closeQuietly(stmt);
+  }
+
+  protected abstract E read(ResultSet rs) throws SQLException;
+
+  private boolean doNextQuietly() {
+    try {
+      return rs.next();
+    } catch (SQLException e) {
+      throw new IllegalStateException("Fail to read row of JDBC result set", e);
+    }
+  }
+}
index 11243866258fb84a61d1604f7694609928a621ed..81a8ac9103e77ecd20c464c56247d4ae8b353af0 100644 (file)
@@ -81,6 +81,7 @@ class SelectImpl extends BaseSqlStatement<Select> implements Select {
   }
 
   static SelectImpl create(Database db, Connection connection, String sql) throws SQLException {
+    // TODO use DbClient#newScrollingSelectStatement()
     PreparedStatement pstmt = connection.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
     pstmt.setFetchSize(db.getDialect().getScrollDefaultFetchSize());
     return new SelectImpl(pstmt);
index 920465cd6660cee65afa0dbb854619722a55e26f..1dcf7b0339a286c7968d2e81eb8989cc747fabb6 100644 (file)
@@ -25,6 +25,8 @@ import javax.annotation.CheckForNull;
 
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.util.Date;
 
 public class SqlUtil {
 
@@ -67,4 +69,33 @@ public class SqlUtil {
     String s = rs.getString(columnName);
     return rs.wasNull() ? null : s;
   }
+
+  @CheckForNull
+  public static Long getLong(ResultSet rs, int columnIndex) throws SQLException {
+    long l = rs.getLong(columnIndex);
+    return rs.wasNull() ? null : l;
+  }
+
+  @CheckForNull
+  public static Double getDouble(ResultSet rs, int columnIndex) throws SQLException {
+    double d = rs.getDouble(columnIndex);
+    return rs.wasNull() ? null : d;
+  }
+
+  @CheckForNull
+  public static Integer getInt(ResultSet rs, int columnIndex) throws SQLException {
+    int i = rs.getInt(columnIndex);
+    return rs.wasNull() ? null : i;
+  }
+
+  @CheckForNull
+  public static String getString(ResultSet rs, int columnIndex) throws SQLException {
+    String s = rs.getString(columnIndex);
+    return rs.wasNull() ? null : s;
+  }
+
+  public static Date getDate(ResultSet rs, int columnIndex) throws SQLException {
+    Timestamp t = rs.getTimestamp(columnIndex);
+    return rs.wasNull() ? null : new Date(t.getTime());
+  }
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/BulkIndexRequestIterator.java b/server/sonar-server/src/main/java/org/sonar/server/es/BulkIndexRequestIterator.java
deleted file mode 100644 (file)
index 966f868..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * SonarQube, open source software quality management tool.
- * Copyright (C) 2008-2014 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * SonarQube 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.
- *
- * SonarQube 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.elasticsearch.action.ActionRequest;
-
-import java.util.Iterator;
-import java.util.List;
-import java.util.NoSuchElementException;
-
-public class BulkIndexRequestIterator<INPUT> implements Iterator<ActionRequest> {
-
-  public static interface InputConverter<INPUT> {
-    List<ActionRequest> convert(INPUT input);
-  }
-
-  private final Iterator<INPUT> input;
-  private final InputConverter<INPUT> converter;
-  private Iterator<ActionRequest> currents = null;
-
-  public BulkIndexRequestIterator(Iterable<INPUT> input, InputConverter<INPUT> converter) {
-    this.input = input.iterator();
-    this.converter = converter;
-    if (this.input.hasNext()) {
-      this.currents = converter.convert(this.input.next()).iterator();
-    }
-  }
-
-  @Override
-  public boolean hasNext() {
-    return currents != null && currents.hasNext();
-  }
-
-  @Override
-  public ActionRequest next() {
-    if (currents == null) {
-      throw new NoSuchElementException();
-    }
-    ActionRequest request = currents.next();
-    peekNext();
-    return request;
-  }
-
-  @Override
-  public void remove() {
-    throw new UnsupportedOperationException();
-  }
-
-  private void peekNext() {
-    if (!currents.hasNext()) {
-      if (input.hasNext()) {
-        currents = converter.convert(input.next()).iterator();
-      } else {
-        currents = null;
-      }
-    }
-  }
-}
index 895e7d74b3c429e9a8ed9e91d882f21299ecd393..07eb4bb8a824c8b9aa8c92ef2f8dc17b3ce989a0 100644 (file)
  */
 package org.sonar.server.es;
 
-import com.google.common.collect.ImmutableMap;
-import org.apache.commons.lang.StringUtils;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Maps;
 import org.elasticsearch.action.ActionRequest;
-import org.elasticsearch.action.admin.indices.settings.get.GetSettingsRequestBuilder;
+import org.elasticsearch.action.admin.indices.settings.get.GetSettingsResponse;
 import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsRequestBuilder;
-import org.elasticsearch.action.bulk.BulkRequest;
-import org.elasticsearch.action.bulk.BulkResponse;
+import org.elasticsearch.action.bulk.BulkRequestBuilder;
 import org.elasticsearch.cluster.metadata.IndexMetaData;
 import org.elasticsearch.common.unit.ByteSizeUnit;
 import org.elasticsearch.common.unit.ByteSizeValue;
-import org.sonar.server.search.SearchClient;
+import org.picocontainer.Startable;
 
-import java.util.Iterator;
+import java.util.Map;
 
 /**
- *
+ * Helper to bulk requests in an efficient way :
+ * <ul>
+ *   <li>bulk request is sent on the wire when its size is higher than 5Mb</li>
+ *   <li>on large table indexing, replicas and automatic refresh can be temporarily disabled</li>
+ *   <li>index refresh is optional (enabled by default)</li>
+ * </ul>
  */
-public class BulkIndexer {
+public class BulkIndexer implements Startable {
+
+  public static final long FLUSH_BYTE_SIZE = new ByteSizeValue(5, ByteSizeUnit.MB).bytes();
 
-  private static final long FLUSH_BYTE_SIZE = new ByteSizeValue(5, ByteSizeUnit.MB).bytes();
-  
-  private final SearchClient client;
+  private final EsClient client;
+  private final String indexName;
+  private boolean large = false;
+  private boolean refresh = true;
+  private long flushByteSize = FLUSH_BYTE_SIZE;
+  private BulkRequestBuilder bulkRequest = null;
+  private Map<String, Object> largeInitialSettings = null;
 
-  public BulkIndexer(SearchClient client) {
+  public BulkIndexer(EsClient client, String indexName) {
     this.client = client;
+    this.indexName = indexName;
   }
 
   /**
-   * Heavy operation that populates an index from scratch. Replicas are disabled during
-   * the bulk indexation and lucene segments are optimized at the end. No need
-   * to call {@link #refresh(String)} after this method.
-   * 
-   * @see BulkIndexRequestIterator
+   * Large indexing is an heavy operation that populates an index generally from scratch. Replicas and
+   * automatic refresh are disabled during bulk indexing and lucene segments are optimized at the end.
    */
-  public void fullIndex(String index, Iterator<ActionRequest> requests) {
-    // deactivate replicas
-    GetSettingsRequestBuilder replicaRequest = client.admin().indices().prepareGetSettings(index);
-    String initialRequestSetting = replicaRequest.get().getSetting(index, IndexMetaData.SETTING_NUMBER_OF_REPLICAS);
-    int initialReplicas = Integer.parseInt(StringUtils.defaultIfEmpty(initialRequestSetting, "0"));
-    if (initialReplicas > 0) {
-      setNumberOfReplicas(index, 0);
-    }
 
-    index(requests);
-    refresh(index);
-    optimize(index);
-
-    if (initialReplicas > 0) {
-      // re-enable replicas
-      setNumberOfReplicas(index, initialReplicas);
-    }
+  public BulkIndexer setLarge(boolean b) {
+    Preconditions.checkState(bulkRequest == null, "Bulk indexing is already started");
+    this.large = b;
+    return this;
   }
 
-  private void setNumberOfReplicas(String index, int replicas) {
-    UpdateSettingsRequestBuilder req = client.admin().indices().prepareUpdateSettings(index);
-    req.setSettings(ImmutableMap.<String, Object>of(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, String.valueOf(replicas)));
-    req.get();
+  public BulkIndexer setRefresh(boolean b) {
+    Preconditions.checkState(bulkRequest == null, "Bulk indexing is already started");
+    this.refresh = b;
+    return this;
   }
 
   /**
-   * @see BulkIndexRequestIterator
+   * Default value is {@link org.sonar.server.es.BulkIndexer#FLUSH_BYTE_SIZE}
+   * @see org.elasticsearch.common.unit.ByteSizeValue
    */
-  public void index(Iterator<ActionRequest> requests) {
-    BulkRequest bulkRequest = client.prepareBulk().request();
-    while (requests.hasNext()) {
-      ActionRequest request = requests.next();
-      bulkRequest.add(request);
-      if (bulkRequest.estimatedSizeInBytes() >= FLUSH_BYTE_SIZE) {
-        executeBulk(bulkRequest);
-        bulkRequest = client.prepareBulk().request();
+  public BulkIndexer setFlushByteSize(long l) {
+    this.flushByteSize = l;
+    return this;
+  }
+
+  @Override
+  public void start() {
+    Preconditions.checkState(bulkRequest == null, "Bulk indexing is already started");
+    if (large) {
+      largeInitialSettings = Maps.newHashMap();
+      Map<String, Object> bulkSettings = Maps.newHashMap();
+      GetSettingsResponse settingsResp = client.nativeClient().admin().indices().prepareGetSettings(indexName).get();
+
+      // deactivate replicas
+      int initialReplicas = Integer.parseInt(settingsResp.getSetting(indexName, IndexMetaData.SETTING_NUMBER_OF_REPLICAS));
+      if (initialReplicas > 0) {
+        largeInitialSettings.put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, initialReplicas);
+        bulkSettings.put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0);
       }
+
+      // deactivate periodical refresh
+      String refreshInterval = settingsResp.getSetting(indexName, "index.refresh_interval");
+      largeInitialSettings.put("index.refresh_interval", refreshInterval);
+      bulkSettings.put("index.refresh_interval", "-1");
+
+      updateSettings(bulkSettings);
     }
-    if (bulkRequest.numberOfActions() > 0) {
+    bulkRequest = client.prepareBulk();
+  }
+
+  public void add(ActionRequest request) {
+    bulkRequest.request().add(request);
+    if (bulkRequest.request().estimatedSizeInBytes() >= flushByteSize) {
       executeBulk(bulkRequest);
     }
   }
 
-  private void executeBulk(BulkRequest bulkRequest) {
-    try {
-      BulkResponse response = client.bulk(bulkRequest).get();
+  @Override
+  public void stop() {
+    if (bulkRequest.numberOfActions() > 0) {
+      executeBulk(bulkRequest);
+    }
+    if (refresh) {
+      client.prepareRefresh(indexName).get();
+    }
+    if (large) {
+      // optimize lucene segments and revert index settings
+      // Optimization must be done before re-applying replicas:
+      // http://www.elasticsearch.org/blog/performance-considerations-elasticsearch-indexing/
+      // TODO do not use nativeClient, else request is not profiled
+      client.nativeClient().admin().indices().prepareOptimize(indexName)
+        .setMaxNumSegments(1)
+        .setWaitForMerge(true)
+        .get();
 
-      // TODO check failures
-      // WARNING - complexity of response#hasFailures() and #buildFailureMessages() is O(n)
-    } catch (Exception e) {
-      throw new IllegalStateException("TODO", e);
+      updateSettings(largeInitialSettings);
     }
+    bulkRequest = null;
   }
 
-  public void refresh(String index) {
-    client.prepareRefresh(index).get();
+  private void updateSettings(Map<String, Object> settings) {
+    UpdateSettingsRequestBuilder req = client.nativeClient().admin().indices().prepareUpdateSettings(indexName);
+    req.setSettings(settings);
+    req.get();
   }
 
-  private void optimize(String index) {
-    client.admin().indices().prepareOptimize(index)
-      .setMaxNumSegments(1)
-      .setWaitForMerge(true)
-      .get();
-  }
+  private void executeBulk(BulkRequestBuilder bulkRequest) {
+    bulkRequest.get();
 
+    // TODO check failures
+    // WARNING - complexity of response#hasFailures() and #buildFailureMessages() is O(n)
+  }
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/DefaultIndexSettings.java b/server/sonar-server/src/main/java/org/sonar/server/es/DefaultIndexSettings.java
new file mode 100644 (file)
index 0000000..328ec30
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.elasticsearch.cluster.metadata.IndexMetaData;
+import org.elasticsearch.common.settings.ImmutableSettings;
+
+class DefaultIndexSettings {
+
+  private DefaultIndexSettings() {
+    // only static stuff
+  }
+
+  static ImmutableSettings.Builder defaults() {
+    return ImmutableSettings.builder()
+      .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1)
+      .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0)
+      .put("index.refresh_interval", "30s")
+      .put("index.mapper.dynamic", false)
+
+      // Sortable text analyzer
+      .put("index.analysis.analyzer.sortable.type", "custom")
+      .put("index.analysis.analyzer.sortable.tokenizer", "keyword")
+      .putArray("index.analysis.analyzer.sortable.filter", "trim", "lowercase")
+
+      // Edge NGram index-analyzer
+      .put("index.analysis.analyzer.index_grams.type", "custom")
+      .put("index.analysis.analyzer.index_grams.tokenizer", "whitespace")
+      .putArray("index.analysis.analyzer.index_grams.filter", "trim", "lowercase", "gram_filter")
+
+      // Edge NGram search-analyzer
+      .put("index.analysis.analyzer.search_grams.type", "custom")
+      .put("index.analysis.analyzer.search_grams.tokenizer", "whitespace")
+      .putArray("index.analysis.analyzer.search_grams.filter", "trim", "lowercase")
+
+      // Word index-analyzer
+      .put("index.analysis.analyzer.index_words.type", "custom")
+      .put("index.analysis.analyzer.index_words.tokenizer", "standard")
+      .putArray("index.analysis.analyzer.index_words.filter",
+        "standard", "word_filter", "lowercase", "stop", "asciifolding", "porter_stem")
+
+      // Word search-analyzer
+      .put("index.analysis.analyzer.search_words.type", "custom")
+      .put("index.analysis.analyzer.search_words.tokenizer", "standard")
+      .putArray("index.analysis.analyzer.search_words.filter",
+        "standard", "lowercase", "stop", "asciifolding", "porter_stem")
+
+      // Edge NGram filter
+      .put("index.analysis.filter.gram_filter.type", "edgeNGram")
+      .put("index.analysis.filter.gram_filter.min_gram", 2)
+      .put("index.analysis.filter.gram_filter.max_gram", 15)
+      .putArray("index.analysis.filter.gram_filter.token_chars", "letter", "digit", "punctuation", "symbol")
+
+      // Word filter
+      .put("index.analysis.filter.word_filter.type", "word_delimiter")
+      .put("index.analysis.filter.word_filter.generate_word_parts", true)
+      .put("index.analysis.filter.word_filter.catenate_words", true)
+      .put("index.analysis.filter.word_filter.catenate_numbers", true)
+      .put("index.analysis.filter.word_filter.catenate_all", true)
+      .put("index.analysis.filter.word_filter.split_on_case_change", true)
+      .put("index.analysis.filter.word_filter.preserve_original", true)
+      .put("index.analysis.filter.word_filter.split_on_numerics", true)
+      .put("index.analysis.filter.word_filter.stem_english_possessive", true)
+
+      // Path Analyzer
+      .put("index.analysis.analyzer.path_analyzer.type", "custom")
+      .put("index.analysis.analyzer.path_analyzer.tokenizer", "path_hierarchy")
+
+      // UUID Module analyzer
+      .put("index.analysis.tokenizer.dot_tokenizer.type", "pattern")
+      .put("index.analysis.tokenizer.dot_tokenizer.pattern", "\\.")
+      .put("index.analysis.analyzer.uuid_analyzer.type", "custom")
+      .putArray("index.analysis.analyzer.uuid_analyzer.filter", "trim", "lowercase")
+      .put("index.analysis.analyzer.uuid_analyzer.tokenizer", "dot_tokenizer");
+
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/DefaultMappingSettings.java b/server/sonar-server/src/main/java/org/sonar/server/es/DefaultMappingSettings.java
deleted file mode 100644 (file)
index be5c670..0000000
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * SonarQube, open source software quality management tool.
- * Copyright (C) 2008-2014 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * SonarQube 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.
- *
- * SonarQube 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.elasticsearch.cluster.metadata.IndexMetaData;
-import org.elasticsearch.common.settings.ImmutableSettings;
-
-class DefaultMappingSettings {
-
-  private DefaultMappingSettings() {
-    // only static stuff
-  }
-
-  static ImmutableSettings.Builder defaults() {
-    return ImmutableSettings.builder()
-      .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1)
-      .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0)
-      .put("index.refresh_interval", "30s")
-      .put("index.mapper.dynamic", false)
-
-      // Sortable text analyzer
-      .put("index.analysis.analyzer.sortable.type", "custom")
-      .put("index.analysis.analyzer.sortable.tokenizer", "keyword")
-      .putArray("index.analysis.analyzer.sortable.filter", "trim", "lowercase")
-
-      // Edge NGram index-analyzer
-      .put("index.analysis.analyzer.index_grams.type", "custom")
-      .put("index.analysis.analyzer.index_grams.tokenizer", "whitespace")
-      .putArray("index.analysis.analyzer.index_grams.filter", "trim", "lowercase", "gram_filter")
-
-      // Edge NGram search-analyzer
-      .put("index.analysis.analyzer.search_grams.type", "custom")
-      .put("index.analysis.analyzer.search_grams.tokenizer", "whitespace")
-      .putArray("index.analysis.analyzer.search_grams.filter", "trim", "lowercase")
-
-      // Word index-analyzer
-      .put("index.analysis.analyzer.index_words.type", "custom")
-      .put("index.analysis.analyzer.index_words.tokenizer", "standard")
-      .putArray("index.analysis.analyzer.index_words.filter",
-        "standard", "word_filter", "lowercase", "stop", "asciifolding", "porter_stem")
-
-      // Word search-analyzer
-      .put("index.analysis.analyzer.search_words.type", "custom")
-      .put("index.analysis.analyzer.search_words.tokenizer", "standard")
-      .putArray("index.analysis.analyzer.search_words.filter",
-        "standard", "lowercase", "stop", "asciifolding", "porter_stem")
-
-      // Edge NGram filter
-      .put("index.analysis.filter.gram_filter.type", "edgeNGram")
-      .put("index.analysis.filter.gram_filter.min_gram", 2)
-      .put("index.analysis.filter.gram_filter.max_gram", 15)
-      .putArray("index.analysis.filter.gram_filter.token_chars", "letter", "digit", "punctuation", "symbol")
-
-      // Word filter
-      .put("index.analysis.filter.word_filter.type", "word_delimiter")
-      .put("index.analysis.filter.word_filter.generate_word_parts", true)
-      .put("index.analysis.filter.word_filter.catenate_words", true)
-      .put("index.analysis.filter.word_filter.catenate_numbers", true)
-      .put("index.analysis.filter.word_filter.catenate_all", true)
-      .put("index.analysis.filter.word_filter.split_on_case_change", true)
-      .put("index.analysis.filter.word_filter.preserve_original", true)
-      .put("index.analysis.filter.word_filter.split_on_numerics", true)
-      .put("index.analysis.filter.word_filter.stem_english_possessive", true)
-
-      // Path Analyzer
-      .put("index.analysis.analyzer.path_analyzer.type", "custom")
-      .put("index.analysis.analyzer.path_analyzer.tokenizer", "path_hierarchy")
-
-      // UUID Module analyzer
-      .put("index.analysis.tokenizer.dot_tokenizer.type", "pattern")
-      .put("index.analysis.tokenizer.dot_tokenizer.pattern", "\\.")
-      .put("index.analysis.analyzer.uuid_analyzer.type", "custom")
-      .putArray("index.analysis.analyzer.uuid_analyzer.filter", "trim", "lowercase")
-      .put("index.analysis.analyzer.uuid_analyzer.tokenizer", "dot_tokenizer");
-
-  }
-}
index 3227328f0a3f6901d3326341857a3d219da954ac..35cb739359af3c2e8929db364110428b19f505bb 100644 (file)
@@ -39,6 +39,9 @@ import org.elasticsearch.action.get.MultiGetRequestBuilder;
 import org.elasticsearch.action.search.SearchRequestBuilder;
 import org.elasticsearch.action.search.SearchScrollRequestBuilder;
 import org.elasticsearch.client.Client;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.elasticsearch.search.aggregations.AggregationBuilders;
+import org.elasticsearch.search.aggregations.metrics.max.Max;
 import org.picocontainer.Startable;
 import org.sonar.core.profiling.Profiling;
 import org.sonar.server.search.ClusterHealth;
@@ -75,7 +78,7 @@ public class EsClient implements Startable {
     this.client = deprecatedClient;
   }
 
-  public EsClient(Profiling profiling, Client client) {
+  EsClient(Profiling profiling, Client client) {
     this.profiling = profiling;
     this.client = client;
   }
@@ -166,6 +169,17 @@ public class EsClient implements Startable {
     return new ProxyDeleteByQueryRequestBuilder(client, profiling).setIndices(indices);
   }
 
+  public long getLastUpdatedAt(String indexName, String typeName) {
+    SearchRequestBuilder request = prepareSearch(indexName)
+      .setTypes(typeName)
+      .setQuery(QueryBuilders.matchAllQuery())
+      .setSize(0)
+      .addAggregation(AggregationBuilders.max("latest").field("updatedAt"));
+
+    Max max = request.get().getAggregations().get("latest");
+    return (long) max.getValue();
+  }
+
   @Override
   public void start() {
     // nothing to do
@@ -173,7 +187,8 @@ public class EsClient implements Startable {
 
   @Override
   public void stop() {
-    client.close();
+    // TODO re-enable when SearchClient is dropped
+    //client.close();
   }
 
   protected Client nativeClient() {
index 01008a4cffd6eb8cf88ec993d1c789cdd7ee54da..d088c7a9718eeb8764a3b9efb2983119f5a950ee 100644 (file)
@@ -24,13 +24,14 @@ import org.elasticsearch.common.settings.ImmutableSettings;
 import org.picocontainer.Startable;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.sonar.api.ServerComponent;
 
 import java.util.Map;
 
 /**
  * Create registered indices in Elasticsearch.
  */
-public class IndexCreator implements Startable {
+public class IndexCreator implements ServerComponent, Startable {
 
   private static final Logger LOGGER = LoggerFactory.getLogger(IndexCreator.class);
 
@@ -41,59 +42,50 @@ public class IndexCreator implements Startable {
   private static final String SETTING_HASH = "sonar_hash";
 
   private final EsClient client;
-  private final IndexDefinition[] definitions;
+  private final IndexRegistry registry;
 
-  public IndexCreator(EsClient client, IndexDefinition[] definitions) {
+  public IndexCreator(EsClient client, IndexRegistry registry) {
     this.client = client;
-    this.definitions = definitions;
+    this.registry = registry;
   }
 
   @Override
   public void start() {
-    create();
-  }
-
-  @Override
-  public void stop() {
-    // nothing to do
-  }
-
-  public void create() {
-    // collect definitions
-    IndexDefinition.IndexDefinitionContext context = new IndexDefinition.IndexDefinitionContext();
-    for (IndexDefinition definition : definitions) {
-      definition.define(context);
-    }
-
     // create indices that do not exist or that have a new definition (different mapping, cluster enabled, ...)
-    for (NewIndex newIndex : context.getIndices().values()) {
-      boolean exists = client.prepareExists(newIndex.getName()).get().isExists();
+    for (IndexRegistry.Index index : registry.getIndices().values()) {
+      boolean exists = client.prepareExists(index.getName()).get().isExists();
       if (exists) {
-        if (needsToDeleteIndex(newIndex)) {
-          LOGGER.info(String.format("Delete index %s (settings changed)", newIndex.getName()));
-          deleteIndex(newIndex.getName());
+        if (needsToDeleteIndex(index)) {
+          LOGGER.info(String.format("Delete index %s (settings changed)", index.getName()));
+          deleteIndex(index.getName());
           exists = false;
         }
       }
       if (!exists) {
-        createIndex(newIndex);
+        createIndex(index);
       }
     }
   }
 
-  private void createIndex(NewIndex newIndex) {
-    LOGGER.info(String.format("Create index %s", newIndex.getName()));
-    ImmutableSettings.Builder settings = newIndex.getSettings();
-    settings.put(SETTING_HASH, new IndexHash().of(newIndex));
+  @Override
+  public void stop() {
+    // nothing to do
+  }
+
+  private void createIndex(IndexRegistry.Index index) {
+    LOGGER.info(String.format("Create index %s", index.getName()));
+    ImmutableSettings.Builder settings = ImmutableSettings.builder();
+    settings.put(index.getSettings());
+    settings.put(SETTING_HASH, new IndexHash().of(index));
     client
-      .prepareCreate(newIndex.getName())
+      .prepareCreate(index.getName())
       .setSettings(settings)
       .get();
 
     // create types
-    for (Map.Entry<String, NewIndex.NewMapping> entry : newIndex.getMappings().entrySet()) {
-      LOGGER.info(String.format("Create type %s/%s", newIndex.getName(), entry.getKey()));
-      client.preparePutMapping(newIndex.getName())
+    for (Map.Entry<String, IndexRegistry.IndexType> entry : index.getTypes().entrySet()) {
+      LOGGER.info(String.format("Create type %s/%s", index.getName(), entry.getKey()));
+      client.preparePutMapping(index.getName())
         .setType(entry.getKey())
         .setIgnoreConflicts(false)
         .setSource(entry.getValue().getAttributes())
@@ -105,7 +97,7 @@ public class IndexCreator implements Startable {
     client.nativeClient().admin().indices().prepareDelete(indexName).get();
   }
 
-  private boolean needsToDeleteIndex(NewIndex index) {
+  private boolean needsToDeleteIndex(IndexRegistry.Index index) {
     boolean toBeDeleted = false;
     String hash = client.nativeClient().admin().indices().prepareGetSettings(index.getName()).get().getSetting(index.getName(), "index." + SETTING_HASH);
     if (hash != null) {
index 93b03587ed213cf3c2299c5a613fc5ab4be9399a..edf29d61b105b03cfb02c88aba99602c10413c4e 100644 (file)
  */
 package org.sonar.server.es;
 
-import java.util.HashMap;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Maps;
+import org.sonar.api.ServerComponent;
+
 import java.util.Map;
 
-public interface IndexDefinition {
+public interface IndexDefinition extends ServerComponent {
 
   public static class IndexDefinitionContext {
-    private final Map<String, NewIndex> byKey = new HashMap<String, NewIndex>();
+    private final Map<String, NewIndex> byKey = Maps.newHashMap();
 
     public NewIndex create(String key) {
-      NewIndex index = byKey.get(key);
-      if (index == null) {
-        index = new NewIndex(key);
-        byKey.put(key, index);
-      }
+      Preconditions.checkArgument(!byKey.containsKey(key), String.format("Index already exists: %s", key));
+      NewIndex index = new NewIndex(key);
+      byKey.put(key, index);
       return index;
     }
 
@@ -41,5 +42,7 @@ public interface IndexDefinition {
     }
   }
 
+
   void define(IndexDefinitionContext context);
+
 }
index 42be03b148b7fcae2c75020101eddd0ad7234d5a..259e62431eb41a8972702f1202c636045383142b 100644 (file)
@@ -23,8 +23,6 @@ import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.Lists;
 import org.apache.commons.codec.digest.DigestUtils;
 
-import javax.annotation.Nullable;
-
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -34,8 +32,8 @@ class IndexHash {
 
   private static final char DELIMITER = ',';
 
-  String of(NewIndex index) {
-    return of(index.getSettings().internalMap(), index.getMappings());
+  String of(IndexRegistry.Index index) {
+    return of(index.getSettings().getAsMap(), index.getTypes());
   }
 
   String of(Map... maps) {
@@ -47,8 +45,8 @@ class IndexHash {
   }
 
   private void appendObject(StringBuilder sb, Object value) {
-    if (value instanceof NewIndex.NewMapping) {
-      appendMapping(sb, (NewIndex.NewMapping) value);
+    if (value instanceof IndexRegistry.IndexType) {
+      appendIndexType(sb, (IndexRegistry.IndexType) value);
     } else if (value instanceof Map) {
       appendMap(sb, (Map) value);
     } else if (value instanceof Iterable) {
@@ -58,8 +56,8 @@ class IndexHash {
     }
   }
 
-  private void appendMapping(StringBuilder sb, NewIndex.NewMapping mapping) {
-    appendMap(sb, mapping.getAttributes());
+  private void appendIndexType(StringBuilder sb, IndexRegistry.IndexType type) {
+    appendMap(sb, type.getAttributes());
   }
 
   private void appendMap(StringBuilder sb, Map attributes) {
diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/IndexRegistry.java b/server/sonar-server/src/main/java/org/sonar/server/es/IndexRegistry.java
new file mode 100644 (file)
index 0000000..867cfc3
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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 com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import org.elasticsearch.common.settings.Settings;
+import org.picocontainer.Startable;
+import org.sonar.api.ServerComponent;
+
+import java.util.Map;
+
+public class IndexRegistry implements ServerComponent, Startable {
+
+  /**
+   * Immutable copy of {@link org.sonar.server.es.NewIndex}
+   */
+  public static class Index {
+    private final String name;
+    private final Settings settings;
+    private final Map<String, IndexType> types;
+
+    Index(NewIndex newIndex) {
+      this.name = newIndex.getName();
+      this.settings = newIndex.getSettings().build();
+      ImmutableMap.Builder<String, IndexType> builder = ImmutableMap.builder();
+      for (NewIndex.NewIndexType newIndexType : newIndex.getTypes().values()) {
+        IndexType type = new IndexType(newIndexType);
+        builder.put(type.getName(), type);
+      }
+      this.types = builder.build();
+    }
+
+    public String getName() {
+      return name;
+    }
+
+    public Settings getSettings() {
+      return settings;
+    }
+
+    public Map<String, IndexType> getTypes() {
+      return types;
+    }
+  }
+
+  /**
+   * Immutable copy of {@link org.sonar.server.es.NewIndex.NewIndexType}
+   */
+  public static class IndexType {
+    private final String name;
+    private final Map<String, Object> attributes;
+
+    private IndexType(NewIndex.NewIndexType newType) {
+      this.name = newType.getName();
+      this.attributes = ImmutableMap.copyOf(newType.getAttributes());
+    }
+
+    public String getName() {
+      return name;
+    }
+
+    public Map<String, Object> getAttributes() {
+      return attributes;
+    }
+  }
+
+  private final Map<String, Index> byKey = Maps.newHashMap();
+  private final IndexDefinition[] defs;
+
+  public IndexRegistry(IndexDefinition[] defs) {
+    this.defs = defs;
+  }
+
+  public Map<String, Index> getIndices() {
+    return byKey;
+  }
+
+  @Override
+  public void start() {
+    // collect definitions
+    IndexDefinition.IndexDefinitionContext context = new IndexDefinition.IndexDefinitionContext();
+    for (IndexDefinition definition : defs) {
+      definition.define(context);
+    }
+
+    for (Map.Entry<String, NewIndex> entry : context.getIndices().entrySet()) {
+      byKey.put(entry.getKey(), new Index(entry.getValue()));
+    }
+  }
+
+  @Override
+  public void stop() {
+    // nothing to do
+  }
+}
index 63f16081a9ce24d9741936c155294c8f13ae6108..894bb11c7bf6eac60dcaf3d5ec81848ddd1300ad 100644 (file)
@@ -33,9 +33,37 @@ import org.sonar.server.search.BaseNormalizer;
 public class IssueIndexDefinition implements IndexDefinition {
 
   public static final String INDEX_ISSUES = "issues";
+
   public static final String TYPE_ISSUE_AUTHORIZATION = "issueAuthorization";
   public static final String TYPE_ISSUE = "issue";
 
+  public static final String FIELD_ISSUE_ACTION_PLAN = "actionPlan";
+  public static final String FIELD_ISSUE_ASSIGNEE = "assignee";
+  public static final String FIELD_ISSUE_ATTRIBUTES = "attributes";
+  public static final String FIELD_ISSUE_AUTHOR_LOGIN = "authorLogin";
+  public static final String FIELD_ISSUE_COMPONENT_UUID = "component";
+  public static final String FIELD_ISSUE_CREATED_AT = "createdAt";
+  public static final String FIELD_ISSUE_DEBT = "debt";
+  public static final String FIELD_ISSUE_EFFORT = "effort";
+  public static final String FIELD_ISSUE_FILE_PATH = "filePath";
+  public static final String FIELD_ISSUE_FUNC_CREATED_AT = "issueCreatedAt";
+  public static final String FIELD_ISSUE_FUNC_UPDATED_AT = "issueUpdatedAt";
+  public static final String FIELD_ISSUE_FUNC_CLOSED_AT = "issueClosedAt";
+  public static final String FIELD_ISSUE_KEY = "key";
+  public static final String FIELD_ISSUE_LANGUAGE = "language";
+  public static final String FIELD_ISSUE_LINE = "line";
+  public static final String FIELD_ISSUE_MESSAGE = "message";
+  public static final String FIELD_ISSUE_MODULE_UUID = "module";
+  public static final String FIELD_ISSUE_MODULE_PATH = "modulePath";
+  public static final String FIELD_ISSUE_PROJECT_UUID = "project";
+  public static final String FIELD_ISSUE_REPORTER = "reporter";
+  public static final String FIELD_ISSUE_RESOLUTION = "resolution";
+  public static final String FIELD_ISSUE_RULE_KEY = "ruleKey";
+  public static final String FIELD_ISSUE_SEVERITY = "severity";
+  public static final String FIELD_ISSUE_SEVERITY_VALUE = "severityValue";
+  public static final String FIELD_ISSUE_STATUS = "status";
+  public static final String FIELD_ISSUE_UPDATED_AT = "updatedAt";
+
   private final Settings settings;
 
   public IssueIndexDefinition(Settings settings) {
@@ -55,7 +83,7 @@ public class IssueIndexDefinition implements IndexDefinition {
     }
 
     // type "issueAuthorization"
-    NewIndex.NewMapping authorizationMapping = index.createMapping(TYPE_ISSUE_AUTHORIZATION);
+    NewIndex.NewIndexType authorizationMapping = index.createType(TYPE_ISSUE_AUTHORIZATION);
     authorizationMapping.setAttribute("_id", ImmutableMap.of("path", IssueAuthorizationNormalizer.IssueAuthorizationField.PROJECT.field()));
     authorizationMapping.createDateTimeField(BaseNormalizer.UPDATED_AT_FIELD);
     authorizationMapping.stringFieldBuilder("project").build();
@@ -63,39 +91,42 @@ public class IssueIndexDefinition implements IndexDefinition {
     authorizationMapping.stringFieldBuilder("users").build();
 
     // type "issue"
-    NewIndex.NewMapping issueMapping = index.createMapping(TYPE_ISSUE);
+    NewIndex.NewIndexType issueMapping = index.createType(TYPE_ISSUE);
     issueMapping.setAttribute("_id", ImmutableMap.of("path", IssueNormalizer.IssueField.KEY.field()));
     issueMapping.setAttribute("_parent", ImmutableMap.of("type", TYPE_ISSUE_AUTHORIZATION));
     issueMapping.setAttribute("_routing", ImmutableMap.of("required", true, "path", IssueNormalizer.IssueField.PROJECT.field()));
-    issueMapping.stringFieldBuilder("component").build();
-    issueMapping.stringFieldBuilder("actionPlan").build();
+    issueMapping.stringFieldBuilder(FIELD_ISSUE_ACTION_PLAN).build();
     // TODO do we really sort by assignee ?
-    issueMapping.stringFieldBuilder("assignee").enableSorting().build();
-    issueMapping.stringFieldBuilder("attributes").build();
-    issueMapping.stringFieldBuilder("authorLogin").build();
-    issueMapping.createDateTimeField("createdAt");
-    issueMapping.createDoubleField("debt");
-    issueMapping.createDoubleField("effort");
-    issueMapping.stringFieldBuilder("filePath").enableSorting().build();
-    issueMapping.createDateTimeField("issueCreatedAt");
-    issueMapping.createDateTimeField("issueUpdatedAt");
-    issueMapping.createDateTimeField("issueClosedAt");
-    issueMapping.stringFieldBuilder("key").enableSorting().build();
-    issueMapping.stringFieldBuilder("language").build();
-    issueMapping.createIntegerField("line");
-    issueMapping.stringFieldBuilder("message").build();
-    issueMapping.stringFieldBuilder("module").build();
-    issueMapping.createUuidPathField("modulePath");
+    issueMapping.stringFieldBuilder(FIELD_ISSUE_ASSIGNEE).enableSorting().build();
+    issueMapping.stringFieldBuilder(FIELD_ISSUE_ATTRIBUTES).build();
+    issueMapping.stringFieldBuilder(FIELD_ISSUE_AUTHOR_LOGIN).build();
+    // TODO rename into componentUuid ? or restrict to fileUuid ?
+    issueMapping.stringFieldBuilder(FIELD_ISSUE_COMPONENT_UUID).build();
+    issueMapping.createDoubleField(FIELD_ISSUE_DEBT);
+    issueMapping.createDoubleField(FIELD_ISSUE_EFFORT);
+    issueMapping.stringFieldBuilder(FIELD_ISSUE_FILE_PATH).enableSorting().build();
+    issueMapping.createDateTimeField(FIELD_ISSUE_FUNC_CREATED_AT);
+    issueMapping.createDateTimeField(FIELD_ISSUE_FUNC_UPDATED_AT);
+    issueMapping.createDateTimeField(FIELD_ISSUE_FUNC_CLOSED_AT);
+    issueMapping.stringFieldBuilder(FIELD_ISSUE_KEY).enableSorting().build();
+    issueMapping.stringFieldBuilder(FIELD_ISSUE_LANGUAGE).build();
+    issueMapping.createIntegerField(FIELD_ISSUE_LINE);
+    issueMapping.stringFieldBuilder(FIELD_ISSUE_MESSAGE).build();
+    issueMapping.stringFieldBuilder(FIELD_ISSUE_MODULE_UUID).build();
+    issueMapping.createUuidPathField(FIELD_ISSUE_MODULE_PATH);
     // TODO do we need to sort by project ?
-    issueMapping.stringFieldBuilder("project").enableSorting().build();
-    issueMapping.stringFieldBuilder("reporter").build();
-    issueMapping.stringFieldBuilder("resolution").build();
-    issueMapping.stringFieldBuilder("ruleKey").build();
-    issueMapping.stringFieldBuilder("severity").build();
+    // TODO rename into projectUuid ?
+    issueMapping.stringFieldBuilder(FIELD_ISSUE_PROJECT_UUID).enableSorting().build();
+    issueMapping.stringFieldBuilder(FIELD_ISSUE_REPORTER).build();
+    issueMapping.stringFieldBuilder(FIELD_ISSUE_RESOLUTION).build();
+    issueMapping.stringFieldBuilder(FIELD_ISSUE_RULE_KEY).build();
+    issueMapping.stringFieldBuilder(FIELD_ISSUE_SEVERITY).build();
     // TODO do we need to sort by severity ?
-    issueMapping.createByteField("severityValue");
+    issueMapping.createByteField(FIELD_ISSUE_SEVERITY_VALUE);
     // TODO do we really sort by status ? If yes, then we should sort by "int value", but not by string key
-    issueMapping.stringFieldBuilder("status").enableSorting().build();
-    issueMapping.createDateTimeField("updatedAt");
+    issueMapping.stringFieldBuilder(FIELD_ISSUE_STATUS).enableSorting().build();
+    // TODO is createdAt required ?
+    issueMapping.createDateTimeField(FIELD_ISSUE_CREATED_AT);
+    issueMapping.createDateTimeField(FIELD_ISSUE_UPDATED_AT);
   }
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/IssueIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/es/IssueIndexer.java
new file mode 100644 (file)
index 0000000..dcd3277
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.elasticsearch.action.update.UpdateRequest;
+import org.sonar.api.ServerComponent;
+import org.sonar.core.persistence.DbSession;
+import org.sonar.server.db.DbClient;
+import org.sonar.server.issue.index.IssueDoc;
+
+import java.sql.Connection;
+import java.util.Iterator;
+
+/**
+ * Not thread-safe
+ */
+public class IssueIndexer implements ServerComponent {
+
+  private final DbClient dbClient;
+  private final EsClient esClient;
+  private long lastUpdatedAt = 0L;
+
+  public IssueIndexer(DbClient dbClient, EsClient esClient) {
+    this.dbClient = dbClient;
+    this.esClient = esClient;
+  }
+
+  public void indexProjectPermissions() {
+    final BulkIndexer bulk = new BulkIndexer(esClient, IssueIndexDefinition.INDEX_ISSUES);
+
+    DbSession dbSession = dbClient.openSession(false);
+    Connection dbConnection = dbSession.getConnection();
+    try {
+      IssueResultSetIterator rowIt = IssueResultSetIterator.create(dbClient, dbConnection, getLastUpdatedAt());
+      indexIssues(bulk, rowIt);
+      rowIt.close();
+
+    } finally {
+      dbSession.close();
+    }
+  }
+
+  public void indexIssues(boolean large) {
+    // TODO support timezones
+    final BulkIndexer bulk = new BulkIndexer(esClient, IssueIndexDefinition.INDEX_ISSUES);
+    bulk.setLarge(large);
+
+    DbSession dbSession = dbClient.openSession(false);
+    Connection dbConnection = dbSession.getConnection();
+    try {
+      IssueResultSetIterator rowIt = IssueResultSetIterator.create(dbClient, dbConnection, getLastUpdatedAt());
+      indexIssues(bulk, rowIt);
+      rowIt.close();
+
+    } finally {
+      dbSession.close();
+    }
+  }
+
+  public void indexIssues(BulkIndexer bulk, Iterator<IssueDoc> issues) {
+    bulk.start();
+    while (issues.hasNext()) {
+      IssueDoc issue = issues.next();
+      bulk.add(newUpsertRequest(issue));
+
+      // it's more efficient to sort programmatically than in SQL on some databases (MySQL for instance)
+      long dtoUpdatedAt = issue.updateDate().getTime();
+      if (lastUpdatedAt < dtoUpdatedAt) {
+        lastUpdatedAt = dtoUpdatedAt;
+      }
+    }
+    bulk.stop();
+  }
+
+  private long getLastUpdatedAt() {
+    long result;
+    if (lastUpdatedAt <= 0L) {
+      // request ES to get the max(updatedAt)
+      result = esClient.getLastUpdatedAt(IssueIndexDefinition.INDEX_ISSUES, IssueIndexDefinition.TYPE_ISSUE);
+    } else {
+      // use cache. Will not work with Tomcat cluster.
+      result = lastUpdatedAt;
+    }
+    return result;
+  }
+
+  private UpdateRequest newUpsertRequest(IssueDoc issue) {
+    String projectUuid = issue.projectUuid();
+    issue.setField("_parent", projectUuid);
+    return new UpdateRequest(IssueIndexDefinition.INDEX_ISSUES, IssueIndexDefinition.TYPE_ISSUE, issue.key())
+      .routing(projectUuid)
+      .parent(projectUuid)
+      .doc(issue.getFields())
+      .upsert(issue.getFields());
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/IssueResultSetIterator.java b/server/sonar-server/src/main/java/org/sonar/server/es/IssueResultSetIterator.java
new file mode 100644 (file)
index 0000000..f71ff23
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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 com.google.common.collect.Maps;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.server.db.DbClient;
+import org.sonar.server.db.ResultSetIterator;
+import org.sonar.server.db.migrations.SqlUtil;
+import org.sonar.server.issue.index.IssueDoc;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.util.Date;
+
+/**
+ * Scroll over table ISSUES and directly read the maps required to
+ * post index requests
+ */
+class IssueResultSetIterator extends ResultSetIterator<IssueDoc> {
+
+  private static final String[] FIELDS = {
+    // column 1
+    "i.kee",
+    "root.uuid",
+    "i.updated_at",
+    "i.created_at",
+    "i.action_plan_key",
+    "i.assignee",
+    "i.effort_to_fix",
+    "i.issue_attributes",
+    "i.line",
+    "i.message",
+
+    // column 11
+    "i.resolution",
+    "i.severity",
+    "i.status",
+    "i.technical_debt",
+    "i.reporter",
+    "i.author_login",
+    "i.issue_close_date",
+    "i.issue_creation_date",
+    "i.issue_update_date",
+    "r.plugin_rule_key",
+
+    // column 21
+    "r.language",
+    "p.uuid",
+    "p.module_uuid",
+    "p.module_uuid_path",
+    "p.path"
+  };
+
+  private static final String SQL_ALL = "select " + StringUtils.join(FIELDS, ",") + " from issues i " +
+    "inner join rules r on r.id=i.rule_id " +
+    "inner join projects p on p.id=i.component_id " +
+    "inner join projects root on root.id=i.root_component_id";
+
+  private static final String SQL_AFTER_DATE = SQL_ALL + " where i.updated_at>=?";
+
+  static IssueResultSetIterator create(DbClient dbClient, Connection connection, long afterDate) {
+    try {
+      String sql = afterDate > 0L ? SQL_AFTER_DATE : SQL_ALL;
+      PreparedStatement stmt = dbClient.newScrollingSelectStatement(connection, sql);
+      if (afterDate > 0L) {
+        stmt.setTimestamp(0, new Timestamp(afterDate));
+      }
+      return new IssueResultSetIterator(stmt);
+    } catch (SQLException e) {
+      throw new IllegalStateException("Fail to prepare SQL request to select all issues", e);
+    }
+  }
+
+  private IssueResultSetIterator(PreparedStatement stmt) throws SQLException {
+    super(stmt);
+  }
+
+  @Override
+  protected IssueDoc read(ResultSet rs) throws SQLException {
+    IssueDoc doc = new IssueDoc(Maps.<String, Object>newHashMapWithExpectedSize(30));
+
+    String key = rs.getString(1);
+    String projectUuid = rs.getString(2);
+
+    // all the keys must be present, even if value is null
+    doc.setKey(key);
+    doc.setProjectUuid(projectUuid);
+    doc.setUpdateDate(SqlUtil.getDate(rs, 3));
+    doc.setCreationDate(new Date(rs.getTimestamp(4).getTime()));
+    doc.setActionPlanKey(rs.getString(5));
+    doc.setAssignee(rs.getString(6));
+    doc.setEffortToFix(SqlUtil.getDouble(rs, 7));
+    doc.setAttributes(rs.getString(8));
+    doc.setLine(SqlUtil.getInt(rs, 9));
+    doc.setMessage(rs.getString(10));
+    doc.setResolution(rs.getString(11));
+    doc.setSeverity(rs.getString(12));
+    doc.setStatus(rs.getString(13));
+    doc.setDebt(SqlUtil.getLong(rs, 14));
+    doc.setReporter(rs.getString(15));
+    doc.setAuthorLogin(rs.getString(16));
+    doc.setFuncCloseDate(SqlUtil.getDate(rs, 17));
+    doc.setFuncCreationDate(SqlUtil.getDate(rs, 18));
+    doc.setFuncUpdateDate(SqlUtil.getDate(rs, 19));
+    doc.setRuleKey(rs.getString(20));
+    doc.setLanguage(rs.getString(21));
+    doc.setComponentUuid(rs.getString(22));
+    doc.setModuleUuid(rs.getString(23));
+    doc.setModuleUuidPath(rs.getString(24));
+    doc.setFilePath(rs.getString(25));
+    return doc;
+  }
+}
index c236153fcc800991fc87420e307798b161c91e0c..fb2f531099b60d4ac50c88f6dde90a4aae9d8373 100644 (file)
@@ -34,11 +34,13 @@ import java.util.TreeMap;
 
 public class NewIndex {
 
-  public static class NewMapping {
+  public static class NewIndexType {
+    private final String name;
     private final Map<String, Object> attributes = new TreeMap<String, Object>();
     private final Map<String, Object> properties = new TreeMap<String, Object>();
 
-    private NewMapping() {
+    private NewIndexType(String typeName) {
+      this.name = typeName;
       // defaults
       attributes.put("dynamic", false);
       attributes.put("_all", ImmutableSortedMap.of("enabled", false));
@@ -46,10 +48,14 @@ public class NewIndex {
       attributes.put("properties", properties);
     }
 
+    public String getName() {
+      return name;
+    }
+
     /**
      * Complete the root json hash of mapping type, for example to set the attribute "_id"
      */
-    public NewMapping setAttribute(String key, Object value) {
+    public NewIndexType setAttribute(String key, Object value) {
       attributes.put(key, value);
       return this;
     }
@@ -57,7 +63,7 @@ public class NewIndex {
     /**
      * Complete the json hash named "properties" in mapping type, usually to declare fields
      */
-    public NewMapping setProperty(String key, Object value) {
+    public NewIndexType setProperty(String key, Object value) {
       properties.put(key, value);
       return this;
     }
@@ -66,35 +72,35 @@ public class NewIndex {
       return new StringFieldBuilder(this, fieldName);
     }
 
-    public NewMapping createBooleanField(String fieldName) {
+    public NewIndexType createBooleanField(String fieldName) {
       return setProperty(fieldName, ImmutableMap.of("type", "boolean"));
     }
 
-    public NewMapping createByteField(String fieldName) {
+    public NewIndexType createByteField(String fieldName) {
       return setProperty(fieldName, ImmutableMap.of("type", "byte"));
     }
 
-    public NewMapping createDateTimeField(String fieldName) {
+    public NewIndexType createDateTimeField(String fieldName) {
       return setProperty(fieldName, ImmutableMap.of("type", "date", "format", "date_time"));
     }
 
-    public NewMapping createDoubleField(String fieldName) {
+    public NewIndexType createDoubleField(String fieldName) {
       return setProperty(fieldName, ImmutableMap.of("type", "double"));
     }
 
-    public NewMapping createIntegerField(String fieldName) {
+    public NewIndexType createIntegerField(String fieldName) {
       return setProperty(fieldName, ImmutableMap.of("type", "integer"));
     }
 
-    public NewMapping createLongField(String fieldName) {
+    public NewIndexType createLongField(String fieldName) {
       return setProperty(fieldName, ImmutableMap.of("type", "long"));
     }
 
-    public NewMapping createShortField(String fieldName) {
+    public NewIndexType createShortField(String fieldName) {
       return setProperty(fieldName, ImmutableMap.of("type", "short"));
     }
 
-    public NewMapping createUuidPathField(String fieldName) {
+    public NewIndexType createUuidPathField(String fieldName) {
       return setProperty(fieldName, ImmutableSortedMap.of(
         "type", "string",
         "index", "analyzed",
@@ -119,12 +125,12 @@ public class NewIndex {
       "type", "string",
       "index", "not_analyzed");
 
-    private final NewMapping newMapping;
+    private final NewIndexType indexType;
     private final String fieldName;
     private boolean sortable = false, wordSearch = false, gramSearch = false;
 
-    private StringFieldBuilder(NewMapping newMapping, String fieldName) {
-      this.newMapping = newMapping;
+    private StringFieldBuilder(NewIndexType indexType, String fieldName) {
+      this.indexType = indexType;
       this.fieldName = fieldName;
     }
 
@@ -178,13 +184,13 @@ public class NewIndex {
         hash.putAll(NOT_ANALYZED);
       }
 
-      newMapping.setProperty(fieldName, hash);
+      indexType.setProperty(fieldName, hash);
     }
   }
 
   private final String indexName;
-  private final ImmutableSettings.Builder settings = DefaultMappingSettings.defaults();
-  private final SortedMap<String, NewMapping> mappings = new TreeMap<String, NewMapping>();
+  private final ImmutableSettings.Builder settings = DefaultIndexSettings.defaults();
+  private final SortedMap<String, NewIndexType> types = new TreeMap<String, NewIndexType>();
 
   NewIndex(String indexName) {
     Preconditions.checkArgument(StringUtils.isAllLowerCase(indexName), "Index name must be lower-case: " + indexName);
@@ -199,13 +205,13 @@ public class NewIndex {
     return settings;
   }
 
-  public NewMapping createMapping(String typeName) {
-    NewMapping type = new NewMapping();
-    mappings.put(typeName, type);
+  public NewIndexType createType(String typeName) {
+    NewIndexType type = new NewIndexType(typeName);
+    types.put(typeName, type);
     return type;
   }
 
-  public SortedMap<String, NewMapping> getMappings() {
-    return mappings;
+  public SortedMap<String, NewIndexType> getTypes() {
+    return types;
   }
 }
index 5ae2f503320f44a73a33bd165d675a46bbcba70b..d4e690fef246403ca19cc472ca6abe18196f5d7b 100644 (file)
@@ -22,12 +22,15 @@ package org.sonar.server.issue.index;
 import org.sonar.api.issue.Issue;
 import org.sonar.api.issue.IssueComment;
 import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rule.Severity;
 import org.sonar.api.utils.Duration;
 import org.sonar.api.utils.KeyValueFormat;
+import org.sonar.server.es.IssueIndexDefinition;
 import org.sonar.server.search.BaseDoc;
 import org.sonar.server.search.IndexUtils;
 
 import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
 
 import java.util.Collections;
 import java.util.Date;
@@ -42,7 +45,7 @@ public class IssueDoc extends BaseDoc implements Issue {
 
   @Override
   public String key() {
-    return getField(IssueNormalizer.IssueField.KEY.field());
+    return getField(IssueIndexDefinition.FIELD_ISSUE_KEY);
   }
 
   @Override
@@ -192,4 +195,105 @@ public class IssueDoc extends BaseDoc implements Issue {
   public String filePath() {
     return getNullableField(IssueNormalizer.IssueField.FILE_PATH.field());
   }
+
+  public void setKey(String s) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_KEY, s);
+  }
+
+  public void setComponentUuid(String s) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_COMPONENT_UUID, s);
+  }
+
+  public void setModuleUuid(String s) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_MODULE_UUID, s);
+  }
+
+  public void setProjectUuid(String s) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID, s);
+  }
+
+  public void setRuleKey(String s) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_RULE_KEY, s);
+  }
+
+  public void setLanguage(String s) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_LANGUAGE, s);
+  }
+
+  public void setSeverity(String s) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_SEVERITY, s);
+    setField(IssueIndexDefinition.FIELD_ISSUE_SEVERITY_VALUE, Severity.ALL.indexOf(s));
+  }
+
+  public void setMessage(String s) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_MESSAGE, s);
+  }
+
+  public void setLine(@Nullable Integer i) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_LINE, i);
+  }
+
+  public void setEffortToFix(@Nullable Double d) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_EFFORT, d);
+  }
+
+  public void setStatus(String s) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_STATUS, s);
+  }
+
+  public void setResolution(String s) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION, s);
+  }
+
+  public void setReporter(String s) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_REPORTER, s);
+  }
+
+  public void setAssignee(String s) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE, s);
+  }
+
+  public void setCreationDate(Date d) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_CREATED_AT, d);
+  }
+
+  public void setUpdateDate(Date d) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_UPDATED_AT, d);
+  }
+
+  public void setFuncCreationDate(Date d) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT, d);
+  }
+
+  public void setFuncUpdateDate(Date d) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_FUNC_UPDATED_AT, d);
+  }
+
+  public void setFuncCloseDate(Date d) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_FUNC_CLOSED_AT, d);
+  }
+
+  public void setAttributes(String s) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_ATTRIBUTES, s);
+  }
+
+  public void setAuthorLogin(String s) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_AUTHOR_LOGIN, s);
+  }
+
+  public void setActionPlanKey(String s) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_ACTION_PLAN, s);
+  }
+
+  public void setDebt(Long l) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_DEBT, l);
+  }
+
+  public void setFilePath(String s) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_FILE_PATH, s);
+  }
+
+  public void setModuleUuidPath(String s) {
+    setField(IssueIndexDefinition.FIELD_ISSUE_MODULE_PATH, s);
+  }
 }
index 2e01a36ca47b5c64ed84d03a891c7650ceee7344..3bbd0c35dc73cb9d6ae6549a68cb63c2b23aaef1 100644 (file)
@@ -143,7 +143,9 @@ import org.sonar.server.duplication.ws.DuplicationsParser;
 import org.sonar.server.duplication.ws.DuplicationsWs;
 import org.sonar.server.es.EsClient;
 import org.sonar.server.es.IndexCreator;
+import org.sonar.server.es.IndexRegistry;
 import org.sonar.server.es.IssueIndexDefinition;
+import org.sonar.server.es.IssueIndexer;
 import org.sonar.server.issue.ActionService;
 import org.sonar.server.issue.AssignAction;
 import org.sonar.server.issue.CommentAction;
@@ -479,6 +481,7 @@ class ServerComponents {
     pico.addSingleton(Periods.class);
     pico.addSingleton(ServerWs.class);
     pico.addSingleton(BackendCleanup.class);
+    pico.addSingleton(IndexRegistry.class);
     pico.addSingleton(IndexCreator.class);
 
     // batch
@@ -628,6 +631,7 @@ class ServerComponents {
 
     // issues
     pico.addSingleton(IssueIndexDefinition.class);
+    pico.addSingleton(IssueIndexer.class);
     pico.addSingleton(ServerIssueStorage.class);
     pico.addSingleton(IssueUpdater.class);
     pico.addSingleton(FunctionExecutor.class);
index 9b1b8cd6329ebf04ebae3763e21b477f815d4593..194a5eda5eab5b4212abb154afbb40a1a67a68eb 100644 (file)
@@ -20,6 +20,7 @@
 package org.sonar.server.search;
 
 import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
 import java.util.Map;
 
 /**
@@ -58,4 +59,12 @@ public abstract class BaseDoc {
     }
     return value;
   }
+
+  public void setField(String key, @Nullable Object value) {
+    fields.put(key, value);
+  }
+
+  public Map<String, Object> getFields() {
+    return fields;
+  }
 }
index 548340194d5856483637e6382a88f6528690fa90..055d9223acbe6425b63361f4d6749200fa5f01cb 100644 (file)
@@ -40,7 +40,7 @@ public class EmbeddedDatabaseTest {
   @Rule
   public ExpectedException throwable = ExpectedException.none();
 
-  @Test(timeout = 10000)
+  @Test(timeout = 5000)
   public void should_start_and_stop() throws IOException {
     int port = NetworkUtils.freePort();
 
@@ -58,28 +58,8 @@ public class EmbeddedDatabaseTest {
     database.stop();
   }
 
-  @Test(timeout = 10000)
-  public void should_support_memory_database() throws IOException {
-    int port = NetworkUtils.freePort();
-
-    EmbeddedDatabase database = new EmbeddedDatabase(testSettings(port)
-      .setProperty(DatabaseProperties.PROP_URL, "jdbc:h2:tcp://localhost:" + port + "/mem:sonarIT;USER=sonar;PASSWORD=sonar"));
-    database.start();
-
-    try {
-      String driverUrl = String.format("jdbc:h2:tcp://localhost:%d/mem:sonarIT;USER=sonar;PASSWORD=sonar", port);
-      DriverManager.registerDriver(new Driver());
-      DriverManager.getConnection(driverUrl).close();
-    } catch (Exception ex) {
-      fail("Unable to connect after start");
-    }
-
-    database.stop();
-  }
-
   @Test
   public void should_return_embedded_data_directory() throws Exception {
-
     Settings settings = testSettings(0);
     EmbeddedDatabase database = new EmbeddedDatabase(settings);
 
diff --git a/server/sonar-server/src/test/java/org/sonar/server/db/ResultSetIteratorTest.java b/server/sonar-server/src/test/java/org/sonar/server/db/ResultSetIteratorTest.java
new file mode 100644 (file)
index 0000000..0e1ba81
--- /dev/null
@@ -0,0 +1,155 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.db;
+
+import org.apache.commons.dbutils.DbUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.core.persistence.TestDatabase;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Fail.fail;
+
+public class ResultSetIteratorTest {
+
+  @Rule
+  public TestDatabase dbTester = new TestDatabase().schema(ResultSetIteratorTest.class, "schema.sql");
+
+  Connection connection = null;
+
+  @Before
+  public void setUp() throws Exception {
+    connection = dbTester.openConnection();
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    DbUtils.closeQuietly(connection);
+  }
+
+  @Test
+  public void iterate_through_statement() throws Exception {
+    dbTester.prepareDbUnit(getClass(), "feed.xml");
+
+    PreparedStatement stmt = connection.prepareStatement("select * from fake order by id");
+    FirstIntColumnIterator iterator = new FirstIntColumnIterator(stmt);
+
+    assertThat(iterator.hasNext()).isTrue();
+
+    // calling multiple times hasNext() is ok
+    assertThat(iterator.hasNext()).isTrue();
+
+    assertThat(iterator.next()).isEqualTo(10);
+    assertThat(iterator.hasNext()).isTrue();
+    assertThat(iterator.next()).isEqualTo(20);
+
+    // call next() without calling hasNext()
+    assertThat(iterator.next()).isEqualTo(30);
+    assertThat(iterator.hasNext()).isFalse();
+
+    iterator.close();
+    // statement is closed by ResultSetIterator
+    assertThat(stmt.isClosed()).isTrue();
+  }
+
+  @Test
+  public void iterate_through_rs() throws Exception {
+    dbTester.prepareDbUnit(getClass(), "feed.xml");
+
+    PreparedStatement stmt = connection.prepareStatement("select * from fake order by id");
+    ResultSet rs = stmt.executeQuery();
+    FirstIntColumnIterator iterator = new FirstIntColumnIterator(rs);
+
+    assertThat(iterator.next()).isEqualTo(10);
+    assertThat(iterator.next()).isEqualTo(20);
+    assertThat(iterator.next()).isEqualTo(30);
+
+    iterator.close();
+    assertThat(rs.isClosed()).isTrue();
+    stmt.close();
+  }
+
+  @Test
+  public void remove_row_is_not_supported() throws Exception {
+    PreparedStatement stmt = connection.prepareStatement("select * from fake order by id");
+    FirstIntColumnIterator iterator = new FirstIntColumnIterator(stmt);
+
+    try {
+      iterator.remove();
+      fail();
+    } catch (UnsupportedOperationException ok) {
+    }
+
+    iterator.close();
+  }
+
+  @Test
+  public void fail_to_read_row() throws Exception {
+    dbTester.prepareDbUnit(getClass(), "feed.xml");
+
+    PreparedStatement stmt = connection.prepareStatement("select * from fake order by id");
+    FailIterator iterator = new FailIterator(stmt);
+
+    assertThat(iterator.hasNext()).isTrue();
+    try {
+      iterator.next();
+      fail();
+    } catch (IllegalStateException e) {
+      assertThat(e.getCause()).isInstanceOf(SQLException.class);
+    }
+    iterator.close();
+  }
+
+  private static class FirstIntColumnIterator extends ResultSetIterator<Integer> {
+
+    public FirstIntColumnIterator(PreparedStatement stmt) throws SQLException {
+      super(stmt);
+    }
+
+    public FirstIntColumnIterator(ResultSet rs) {
+      super(rs);
+    }
+
+    @Override
+    protected Integer read(ResultSet rs) throws SQLException {
+      return rs.getInt(1);
+    }
+  }
+
+  private static class FailIterator extends ResultSetIterator<Integer> {
+
+    public FailIterator(PreparedStatement stmt) throws SQLException {
+      super(stmt);
+    }
+
+    @Override
+    protected Integer read(ResultSet rs) throws SQLException {
+      // column does not exist
+      return rs.getInt(1234);
+    }
+  }
+}
index 6594dc4ff254b89d15a9ecba3aa8282799b704a0..b3bcc72ead86a3f2fd9b1ff2f5028b2144392a66 100644 (file)
@@ -21,7 +21,6 @@ package org.sonar.server.db.migrations;
 
 import org.junit.Test;
 import org.slf4j.Logger;
-import org.sonar.server.db.migrations.SqlUtil;
 
 import java.sql.SQLException;
 
diff --git a/server/sonar-server/src/test/java/org/sonar/server/es/BulkIndexRequestIteratorTest.java b/server/sonar-server/src/test/java/org/sonar/server/es/BulkIndexRequestIteratorTest.java
deleted file mode 100644 (file)
index 73c6de6..0000000
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * SonarQube, open source software quality management tool.
- * Copyright (C) 2008-2014 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * SonarQube 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.
- *
- * SonarQube 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.elasticsearch.action.ActionRequest;
-import org.elasticsearch.action.index.IndexRequest;
-import org.junit.Test;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.NoSuchElementException;
-
-import static org.fest.assertions.Assertions.assertThat;
-import static org.fest.assertions.Fail.fail;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verifyZeroInteractions;
-
-public class BulkIndexRequestIteratorTest {
-
-  @Test
-  public void iterate_over_requests() throws Exception {
-    List<String> input = Arrays.asList("foo", "bar", "3requests", "baz");
-    BulkIndexRequestIterator.InputConverter converter = new BulkIndexRequestIterator.InputConverter<String>() {
-      @Override
-      public List<ActionRequest> convert(String input) {
-        if ("3requests".equals(input)) {
-          return Collections.nCopies(3, (ActionRequest) new IndexRequest(input));
-        }
-        return Arrays.asList((ActionRequest) new IndexRequest(input));
-      }
-    };
-
-    BulkIndexRequestIterator<String> it = new BulkIndexRequestIterator<String>(input, converter);
-
-    assertThat(it.hasNext()).isTrue();
-    assertIndex(it.next(), "foo");
-
-    assertThat(it.hasNext()).isTrue();
-    assertIndex(it.next(), "bar");
-
-    assertThat(it.hasNext()).isTrue();
-    assertIndex(it.next(), "3requests");
-    assertThat(it.hasNext()).isTrue();
-    assertIndex(it.next(), "3requests");
-    assertThat(it.hasNext()).isTrue();
-    assertIndex(it.next(), "3requests");
-
-    assertThat(it.hasNext()).isTrue();
-    assertIndex(it.next(), "baz");
-
-    assertThat(it.hasNext()).isFalse();
-    try {
-      it.next();
-      fail();
-    } catch (NoSuchElementException e) {
-      // expected
-    }
-  }
-
-  @Test
-  public void empty() throws Exception {
-    List<String> input = Collections.emptyList();
-    BulkIndexRequestIterator.InputConverter converter = mock(BulkIndexRequestIterator.InputConverter.class);
-
-    BulkIndexRequestIterator<String> it = new BulkIndexRequestIterator<String>(input, converter);
-
-    assertThat(it.hasNext()).isFalse();
-    verifyZeroInteractions(converter);
-  }
-
-  @Test
-  public void removal_is_not_supported() throws Exception {
-    List<String> input = Arrays.asList("foo");
-    BulkIndexRequestIterator.InputConverter converter = mock(BulkIndexRequestIterator.InputConverter.class);
-
-    BulkIndexRequestIterator<String> it = new BulkIndexRequestIterator<String>(input, converter);
-
-    try {
-      it.remove();
-      fail();
-    } catch (UnsupportedOperationException e) {
-      // expected
-    }
-  }
-
-  private void assertIndex(ActionRequest req, String indexName) {
-    assertThat(req).isNotNull();
-    assertThat(req).isInstanceOf(IndexRequest.class);
-    assertThat(((IndexRequest) req).index()).isEqualTo(indexName);
-  }
-}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/es/BulkIndexerTest.java b/server/sonar-server/src/test/java/org/sonar/server/es/BulkIndexerTest.java
new file mode 100644 (file)
index 0000000..7dfa031
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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 com.google.common.collect.ImmutableMap;
+import org.elasticsearch.action.admin.indices.settings.get.GetSettingsResponse;
+import org.elasticsearch.action.index.IndexRequest;
+import org.elasticsearch.cluster.metadata.IndexMetaData;
+import org.elasticsearch.common.unit.ByteSizeUnit;
+import org.elasticsearch.common.unit.ByteSizeValue;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+public class BulkIndexerTest {
+
+  @Rule
+  public EsTester esTester = new EsTester().addDefinitions(new FakeIndexDefinition().setReplicas(1));
+
+  @Test
+  public void index_nothing() throws Exception {
+    BulkIndexer indexer = new BulkIndexer(esTester.client(), FakeIndexDefinition.INDEX);
+    indexer.start();
+    indexer.stop();
+
+    assertThat(count()).isEqualTo(0);
+  }
+
+  @Test
+  public void index_documents() throws Exception {
+    BulkIndexer indexer = new BulkIndexer(esTester.client(), FakeIndexDefinition.INDEX)
+      .setRefresh(true);
+    indexer.start();
+    indexer.add(newIndexRequest(42));
+    indexer.add(newIndexRequest(78));
+
+    // request is not sent yet
+    assertThat(count()).isEqualTo(0);
+
+    // send remaining requests
+    indexer.stop();
+    assertThat(count()).isEqualTo(2);
+  }
+
+  @Test
+  public void large_indexing() throws Exception {
+    // index has one replica
+    assertThat(replicas()).isEqualTo(1);
+
+    BulkIndexer indexer = new BulkIndexer(esTester.client(), FakeIndexDefinition.INDEX)
+      .setLarge(true)
+      .setFlushByteSize(new ByteSizeValue(1, ByteSizeUnit.BYTES).bytes());
+    indexer.start();
+
+    // replicas are temporarily disabled
+    assertThat(replicas()).isEqualTo(0);
+
+    for (int i = 0; i < 10; i++) {
+      indexer.add(newIndexRequest(i));
+    }
+    indexer.stop();
+
+    assertThat(count()).isEqualTo(10);
+
+    // replicas are re-enabled
+    assertThat(replicas()).isEqualTo(1);
+  }
+
+  private long count() {
+    return esTester.client().prepareCount(FakeIndexDefinition.INDEX).get().getCount();
+  }
+
+  private int replicas() {
+    GetSettingsResponse settingsResp = esTester.client().nativeClient().admin().indices()
+      .prepareGetSettings(FakeIndexDefinition.INDEX).get();
+    return Integer.parseInt(settingsResp.getSetting(FakeIndexDefinition.INDEX, IndexMetaData.SETTING_NUMBER_OF_REPLICAS));
+  }
+
+  private IndexRequest newIndexRequest(int intField) {
+    return new IndexRequest(FakeIndexDefinition.INDEX, FakeIndexDefinition.TYPE)
+      .source(ImmutableMap.of(FakeIndexDefinition.INT_FIELD, intField));
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/es/DefaultIndexSettingsTest.java b/server/sonar-server/src/test/java/org/sonar/server/es/DefaultIndexSettingsTest.java
new file mode 100644 (file)
index 0000000..a6f8eb3
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.elasticsearch.common.collect.ImmutableMap;
+import org.junit.Test;
+import org.sonar.test.TestUtils;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+public class DefaultIndexSettingsTest {
+
+  @Test
+  public void defaults() throws Exception {
+    ImmutableMap<String, String> map = DefaultIndexSettings.defaults().build().getAsMap();
+    assertThat(map).isNotEmpty();
+
+    // test some values
+    assertThat(map.get("index.number_of_shards")).isEqualTo("1");
+    assertThat(map.get("index.number_of_replicas")).isEqualTo("0");
+    assertThat(map.get("index.analysis.analyzer.sortable.type")).isEqualTo("custom");
+  }
+
+  @Test
+  public void only_statics() throws Exception {
+    TestUtils.hasOnlyPrivateConstructors(DefaultIndexSettings.class);
+
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/es/DefaultMappingSettingsTest.java b/server/sonar-server/src/test/java/org/sonar/server/es/DefaultMappingSettingsTest.java
deleted file mode 100644 (file)
index 3e2a130..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * SonarQube, open source software quality management tool.
- * Copyright (C) 2008-2014 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * SonarQube 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.
- *
- * SonarQube 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.elasticsearch.common.collect.ImmutableMap;
-import org.junit.Test;
-import org.sonar.test.TestUtils;
-
-import static org.fest.assertions.Assertions.assertThat;
-
-public class DefaultMappingSettingsTest {
-
-  @Test
-  public void defaults() throws Exception {
-    ImmutableMap<String, String> map = DefaultMappingSettings.defaults().build().getAsMap();
-    assertThat(map).isNotEmpty();
-
-    // test some values
-    assertThat(map.get("index.number_of_shards")).isEqualTo("1");
-    assertThat(map.get("index.number_of_replicas")).isEqualTo("0");
-    assertThat(map.get("index.analysis.analyzer.sortable.type")).isEqualTo("custom");
-  }
-
-  @Test
-  public void only_statics() throws Exception {
-    TestUtils.hasOnlyPrivateConstructors(DefaultMappingSettings.class);
-
-  }
-}
index 82837661d53d53a79c4cb5c346b4fa541e300c96..d7e199981f083d124aee9fc9bcd09a84a4be791b 100644 (file)
@@ -19,8 +19,9 @@
  */
 package org.sonar.server.es;
 
+import com.google.common.collect.Lists;
+import org.apache.commons.lang.math.RandomUtils;
 import org.elasticsearch.action.admin.indices.delete.DeleteIndexResponse;
-import org.elasticsearch.cluster.ClusterName;
 import org.elasticsearch.cluster.metadata.IndexMetaData;
 import org.elasticsearch.cluster.node.DiscoveryNode;
 import org.elasticsearch.common.settings.ImmutableSettings;
@@ -29,46 +30,72 @@ import org.elasticsearch.node.Node;
 import org.elasticsearch.node.NodeBuilder;
 import org.junit.rules.ExternalResource;
 import org.sonar.api.config.Settings;
+import org.sonar.api.platform.ComponentContainer;
 import org.sonar.core.profiling.Profiling;
 
-import java.util.Date;
+import java.util.Collections;
+import java.util.List;
 
 import static org.fest.assertions.Assertions.assertThat;
 
 public class EsTester extends ExternalResource {
 
-  /**
-   * This instance is shared for performance reasons. Never stopped.
-   */
-  private static Node sharedNode;
-  private static EsClient client;
+  private static int PROCESS_ID = RandomUtils.nextInt(Integer.MAX_VALUE);
+  private Node node;
+  private EsClient client;
+  private final List<IndexDefinition> definitions = Lists.newArrayList();
+
+  public EsTester addDefinitions(IndexDefinition... defs) {
+    Collections.addAll(definitions, defs);
+    return this;
+  }
 
-  @Override
   protected void before() throws Throwable {
-    if (sharedNode == null) {
-      String nodeName = EsTester.class.getName();
-      sharedNode = NodeBuilder.nodeBuilder().local(true).data(true).settings(ImmutableSettings.builder()
-        .put(ClusterName.SETTING, nodeName)
-        .put("node.name", nodeName)
-        // the two following properties are probably not used because they are
-        // declared on indices too
-        .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1)
-        .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0)
-        // limit the number of threads created (see org.elasticsearch.common.util.concurrent.EsExecutors)
-        .put("processors", 1)
-        .put("http.enabled", false)
-        .put("index.store.type", "ram")
-        .put("config.ignore_system_properties", true)
-        .put("gateway.type", "none"))
-        .build();
-      sharedNode.start();
-      assertThat(DiscoveryNode.localNode(sharedNode.settings())).isTrue();
-      client = new EsClient(new Profiling(new Settings()), sharedNode.client());
+    String nodeName = "es-ram-" + PROCESS_ID;
+    node = NodeBuilder.nodeBuilder().local(true).data(true).settings(ImmutableSettings.builder()
+      .put("cluster.name", nodeName)
+      .put("node.name", nodeName)
+      // the two following properties are probably not used because they are
+      // declared on indices too
+      .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1)
+      .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0)
+      // limit the number of threads created (see org.elasticsearch.common.util.concurrent.EsExecutors)
+      .put("processors", 1)
+      .put("http.enabled", false)
+      .put("index.store.type", "memory")
+      .put("config.ignore_system_properties", true)
+      // reuse the same directory than other tests for faster initialization
+      .put("path.home", "target/es-" + PROCESS_ID)
+      .put("gateway.type", "none"))
+      .build();
+    node.start();
+    assertThat(DiscoveryNode.localNode(node.settings())).isTrue();
+
+    // delete the indices created by previous tests
+    DeleteIndexResponse response = node.client().admin().indices().prepareDelete("_all").get();
+    assertThat(response.isAcknowledged()).isTrue();
+
+    client = new EsClient(new Profiling(new Settings()), node.client());
+    client.start();
 
-    } else {
-      // delete the indices created by previous tests
-      DeleteIndexResponse response = sharedNode.client().admin().indices().prepareDelete("_all").get();
-      assertThat(response.isAcknowledged()).isTrue();
+    if (!definitions.isEmpty()) {
+      ComponentContainer container = new ComponentContainer();
+      container.addSingletons(definitions);
+      container.addSingleton(client);
+      container.addSingleton(IndexRegistry.class);
+      container.addSingleton(IndexCreator.class);
+      container.startComponents();
+    }
+  }
+
+  @Override
+  protected void after() {
+    if (client != null) {
+      client.stop();
+    }
+    if (node != null) {
+      node.stop();
+      node.close();
     }
   }
 
@@ -87,7 +114,7 @@ public class EsTester extends ExternalResource {
   }
 
   public Node node() {
-    return sharedNode;
+    return node;
   }
 
   public EsClient client() {
diff --git a/server/sonar-server/src/test/java/org/sonar/server/es/FakeIndexDefinition.java b/server/sonar-server/src/test/java/org/sonar/server/es/FakeIndexDefinition.java
new file mode 100644 (file)
index 0000000..d0e9e56
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.elasticsearch.cluster.metadata.IndexMetaData;
+
+public class FakeIndexDefinition implements IndexDefinition {
+
+  public static final String INDEX = "fakes";
+  public static final String TYPE = "fake";
+  public static final String INT_FIELD = "intField";
+
+  private int replicas = 0;
+
+  public FakeIndexDefinition setReplicas(int replicas) {
+    this.replicas = replicas;
+    return this;
+  }
+
+  @Override
+  public void define(IndexDefinitionContext context) {
+    NewIndex index = context.create(INDEX);
+    index.getSettings().put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, replicas);
+    NewIndex.NewIndexType type = index.createType(TYPE);
+    type.createIntegerField(INT_FIELD);
+  }
+}
index 25917025cacaa3c13c76b05ccedd803168eb6a21..8f46e89cc6b79d56a260f34befa67d4c7294d3c7 100644 (file)
@@ -26,6 +26,7 @@ import org.junit.Rule;
 import org.junit.Test;
 
 import javax.annotation.CheckForNull;
+
 import java.io.IOException;
 import java.util.Map;
 
@@ -40,7 +41,9 @@ public class IndexCreatorTest {
   public void create_index() throws Exception {
     assertThat(mappings()).isEmpty();
 
-    IndexCreator creator = new IndexCreator(es.client(), new IndexDefinition[]{new FakeIndexDefinition()});
+    IndexRegistry registry = new IndexRegistry(new IndexDefinition[] {new FakeIndexDefinition()});
+    registry.start();
+    IndexCreator creator = new IndexCreator(es.client(), registry);
     creator.start();
 
     // check that index is created with related mapping
@@ -58,24 +61,23 @@ public class IndexCreatorTest {
     assertThat(mappings()).isNotEmpty();
   }
 
-  private String setting(String indexName, String settingKey) {
-    GetSettingsResponse indexSettings = es.client().nativeClient().admin().indices().prepareGetSettings(indexName).get();
-    return indexSettings.getSetting(indexName, settingKey);
-  }
-
   @Test
   public void recreate_index_on_definition_changes() throws Exception {
     assertThat(mappings()).isEmpty();
 
     // v1
-    IndexCreator creator = new IndexCreator(es.client(), new IndexDefinition[]{new FakeIndexDefinition()});
+    IndexRegistry registry = new IndexRegistry(new IndexDefinition[] {new FakeIndexDefinition()});
+    registry.start();
+    IndexCreator creator = new IndexCreator(es.client(), registry);
     creator.start();
     creator.stop();
     String hashV1 = setting("fakes", "index.sonar_hash");
     assertThat(hashV1).isNotEmpty();
 
     // v2
-    creator = new IndexCreator(es.client(), new IndexDefinition[]{new FakeIndexDefinitionV2()});
+    registry = new IndexRegistry(new IndexDefinition[] {new FakeIndexDefinitionV2()});
+    registry.start();
+    creator = new IndexCreator(es.client(), registry);
     creator.start();
     ImmutableOpenMap<String, ImmutableOpenMap<String, MappingMetaData>> mappings = mappings();
     MappingMetaData mapping = mappings.get("fakes").get("fake");
@@ -87,6 +89,11 @@ public class IndexCreatorTest {
     creator.stop();
   }
 
+  private String setting(String indexName, String settingKey) {
+    GetSettingsResponse indexSettings = es.client().nativeClient().admin().indices().prepareGetSettings(indexName).get();
+    return indexSettings.getSetting(indexName, settingKey);
+  }
+
   private ImmutableOpenMap<String, ImmutableOpenMap<String, MappingMetaData>> mappings() {
     return es.client().nativeClient().admin().indices().prepareGetMappings().get().mappings();
   }
@@ -105,7 +112,7 @@ public class IndexCreatorTest {
     @Override
     public void define(IndexDefinitionContext context) {
       NewIndex index = context.create("fakes");
-      NewIndex.NewMapping mapping = index.createMapping("fake");
+      NewIndex.NewIndexType mapping = index.createType("fake");
       mapping.stringFieldBuilder("key").build();
       mapping.createDateTimeField("updatedAt");
     }
@@ -115,7 +122,7 @@ public class IndexCreatorTest {
     @Override
     public void define(IndexDefinitionContext context) {
       NewIndex index = context.create("fakes");
-      NewIndex.NewMapping mapping = index.createMapping("fake");
+      NewIndex.NewIndexType mapping = index.createType("fake");
       mapping.stringFieldBuilder("key").build();
       mapping.createDateTimeField("updatedAt");
       mapping.createIntegerField("newField");
index 8f537276c577d2dce2a11901077369733026b25f..7856f9852c229459d7a72466a3f3349242250cbe 100644 (file)
@@ -29,25 +29,25 @@ public class IndexHashTest {
 
   @Test
   public void of() throws Exception {
-    NewIndex indexV1 = createIndex();
+    IndexRegistry.Index indexV1 = new IndexRegistry.Index(createIndex());
     String hashV1 = new IndexHash().of(indexV1);
     assertThat(hashV1).isNotEmpty();
     // always the same
     assertThat(hashV1).isEqualTo(new IndexHash().of(indexV1));
 
-    NewIndex indexV2 = createIndex();
-    indexV2.getMappings().get("fake").createIntegerField("max");
-    String hashV2 = new IndexHash().of(indexV2);
+    NewIndex newIndexV2 = createIndex();
+    newIndexV2.getTypes().get("fake").createIntegerField("max");
+    String hashV2 = new IndexHash().of(new IndexRegistry.Index(newIndexV2));
     assertThat(hashV2).isNotEmpty().isNotEqualTo(hashV1);
   }
 
   private NewIndex createIndex() {
-    NewIndex index = new NewIndex("fakes");
-    NewIndex.NewMapping mapping = index.createMapping("fake");
+    NewIndex newIndex = new NewIndex("fakes");
+    NewIndex.NewIndexType mapping = newIndex.createType("fake");
     mapping.setAttribute("list_attr", Arrays.asList("foo", "bar"));
     mapping.stringFieldBuilder("key").build();
     mapping.createDateTimeField("updatedAt");
-    return index;
+    return newIndex;
   }
 
 }
index 1a5d442f0ad78a26a816f0a06fb56ac8d5d771f1..893d9f563fe4e8650b0897025042cb3389b1f297 100644 (file)
@@ -37,7 +37,7 @@ public class IssueIndexDefinitionTest {
     assertThat(context.getIndices()).hasSize(1);
     NewIndex issuesIndex = context.getIndices().get("issues");
     assertThat(issuesIndex).isNotNull();
-    assertThat(issuesIndex.getMappings().keySet()).containsOnly("issue", "issueAuthorization");
+    assertThat(issuesIndex.getTypes().keySet()).containsOnly("issue", "issueAuthorization");
 
     // no cluster by default
     assertThat(issuesIndex.getSettings().get("index.number_of_shards")).isEqualTo("1");
index 80df925250db316137d7f832ad159cc0696312f2..eac18f6c939d044de16895d5f5651ee18e9bb0bc 100644 (file)
@@ -35,7 +35,7 @@ public class NewIndexTest {
   public void most_basic_index() throws Exception {
     NewIndex index = new NewIndex("issues");
     assertThat(index.getName()).isEqualTo("issues");
-    assertThat(index.getMappings()).isEmpty();
+    assertThat(index.getTypes()).isEmpty();
     Settings settings = index.getSettings().build();
     // test some basic settings
     assertThat(settings.get("index.number_of_shards")).isNotEmpty();
@@ -54,7 +54,7 @@ public class NewIndexTest {
   @Test
   public void define_fields() throws Exception {
     NewIndex index = new NewIndex("issues");
-    NewIndex.NewMapping mapping = index.createMapping("issue");
+    NewIndex.NewIndexType mapping = index.createType("issue");
     mapping.setAttribute("dynamic", "true");
     mapping.setProperty("foo_field", ImmutableMap.of("type", "string"));
     mapping.createBooleanField("boolean_field");
@@ -66,7 +66,7 @@ public class NewIndexTest {
     mapping.createShortField("short_field");
     mapping.createUuidPathField("uuid_path_field");
 
-    mapping = index.getMappings().get("issue");
+    mapping = index.getTypes().get("issue");
     assertThat(mapping.getAttributes().get("dynamic")).isEqualTo("true");
     assertThat(mapping).isNotNull();
     assertThat(mapping.getProperty("foo_field")).isInstanceOf(Map.class);
@@ -84,7 +84,7 @@ public class NewIndexTest {
   @Test
   public void define_string_field() throws Exception {
     NewIndex index = new NewIndex("issues");
-    NewIndex.NewMapping mapping = index.createMapping("issue");
+    NewIndex.NewIndexType mapping = index.createType("issue");
     mapping.stringFieldBuilder("basic_field").build();
     mapping.stringFieldBuilder("all_capabilities_field")
       .enableGramSearch()
index 74073a450295ea0deed88e69bbb9cd7298168b13..f0a2d2c9a575c8845779f198cd0b9796a8430268 100644 (file)
@@ -21,6 +21,7 @@ package org.sonar.server.search;
 
 import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsResponse;
 import org.elasticsearch.common.settings.ImmutableSettings;
+import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
@@ -86,6 +87,13 @@ public class BaseIndexTest {
     searchClient = new SearchClient(settings);
   }
 
+  @After
+  public void tearDown() throws Exception {
+    if (searchClient != null) {
+      searchClient.stop();
+    }
+  }
+
   @Test
   public void can_load() {
     BaseIndex index = getIndex(this.searchClient);
index febf81402ee7e1e9ce4e16631b40e68b25c46822..b31c40f3fcf4838d80f4b0d8d597af64fc8d19a6 100644 (file)
@@ -25,6 +25,7 @@ import org.elasticsearch.action.delete.DeleteRequest;
 import org.elasticsearch.action.index.IndexRequest;
 import org.elasticsearch.action.update.UpdateRequest;
 import org.elasticsearch.common.unit.TimeValue;
+import org.junit.After;
 import org.junit.Test;
 import org.sonar.api.config.Settings;
 import org.sonar.core.profiling.Profiling;
@@ -41,6 +42,11 @@ public class ProxyBulkRequestBuilderTest {
   Profiling profiling = new Profiling(new Settings().setProperty(Profiling.CONFIG_PROFILING_LEVEL, Profiling.Level.FULL.name()));
   SearchClient searchClient = new SearchClient(new Settings(), profiling);
 
+  @After
+  public void tearDown() throws Exception {
+    searchClient.stop();
+  }
+
   @Test
   public void bulk() {
     BulkRequestBuilder bulkRequestBuilder = searchClient.prepareBulk();
@@ -96,6 +102,7 @@ public class ProxyBulkRequestBuilderTest {
     }
 
     // TODO assert profiling
+    searchClient.stop();
   }
 
   @Test(expected = UnsupportedOperationException.class)
index c180ca3ef85bbc26a070e48589633762b7bb7c29..60fb4459b9b4a49e49dcedec4a09335fc9c2429c 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.server.search.request;
 
 import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequestBuilder;
 import org.elasticsearch.common.unit.TimeValue;
+import org.junit.After;
 import org.junit.Test;
 import org.sonar.api.config.Settings;
 import org.sonar.core.profiling.Profiling;
@@ -35,6 +36,11 @@ public class ProxyClusterHealthRequestBuilderTest {
   Profiling profiling = new Profiling(new Settings().setProperty(Profiling.CONFIG_PROFILING_LEVEL, Profiling.Level.FULL.name()));
   SearchClient searchClient = new SearchClient(new Settings(), profiling);
 
+  @After
+  public void tearDown() throws Exception {
+    searchClient.stop();
+  }
+
   @Test
   public void state() {
     try {
@@ -70,6 +76,7 @@ public class ProxyClusterHealthRequestBuilderTest {
     }
 
     // TODO assert profiling
+    searchClient.stop();
   }
 
   @Test
index 1450e3debeb53504cf33586a211889c59d9022f0..b6b4b7025b17da806dab55e1dc69263e23cd6449 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.server.search.request;
 
 import org.elasticsearch.action.admin.cluster.state.ClusterStateRequestBuilder;
 import org.elasticsearch.common.unit.TimeValue;
+import org.junit.After;
 import org.junit.Test;
 import org.sonar.api.config.Settings;
 import org.sonar.core.profiling.Profiling;
@@ -35,6 +36,11 @@ public class ProxyClusterStateRequestBuilderText {
   Profiling profiling = new Profiling(new Settings().setProperty(Profiling.CONFIG_PROFILING_LEVEL, Profiling.Level.FULL.name()));
   SearchClient searchClient = new SearchClient(new Settings(), profiling);
 
+  @After
+  public void tearDown() throws Exception {
+    searchClient.stop();
+  }
+
   @Test
   public void state() {
     try {
@@ -56,9 +62,9 @@ public class ProxyClusterStateRequestBuilderText {
 
   @Test
   public void with_profiling_basic() {
+    Profiling profiling = new Profiling(new Settings().setProperty(Profiling.CONFIG_PROFILING_LEVEL, Profiling.Level.BASIC.name()));
+    SearchClient searchClient = new SearchClient(new Settings(), profiling);
     try {
-      Profiling profiling = new Profiling(new Settings().setProperty(Profiling.CONFIG_PROFILING_LEVEL, Profiling.Level.BASIC.name()));
-      SearchClient searchClient = new SearchClient(new Settings(), profiling);
 
       ClusterStateRequestBuilder requestBuilder = searchClient.prepareState();
       requestBuilder.get();
@@ -70,6 +76,7 @@ public class ProxyClusterStateRequestBuilderText {
     }
 
     // TODO assert profiling
+    searchClient.stop();
   }
 
   @Test
index af0d1737ae35901e6bfdb51f882b1d75d3135d20..f130a7fa80db31a5b4c2816ad49094e4846f7772 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.server.search.request;
 
 import org.elasticsearch.action.admin.cluster.stats.ClusterStatsRequestBuilder;
 import org.elasticsearch.common.unit.TimeValue;
+import org.junit.After;
 import org.junit.Test;
 import org.sonar.api.config.Settings;
 import org.sonar.core.profiling.Profiling;
@@ -35,6 +36,11 @@ public class ProxyClusterStatsRequestBuilderText {
   Profiling profiling = new Profiling(new Settings().setProperty(Profiling.CONFIG_PROFILING_LEVEL, Profiling.Level.FULL.name()));
   SearchClient searchClient = new SearchClient(new Settings(), profiling);
 
+  @After
+  public void tearDown() throws Exception {
+    searchClient.stop();
+  }
+
   @Test
   public void stats() {
     try {
@@ -70,6 +76,7 @@ public class ProxyClusterStatsRequestBuilderText {
     }
 
     // TODO assert profiling
+    searchClient.stop();
   }
 
   @Test
index 016e2bd3f866877a9c5ed6b4700f9b2305646697..e927fefe5b226f86fd82ee2d0c34778d58f15a3a 100644 (file)
@@ -21,6 +21,7 @@
 package org.sonar.server.search.request;
 
 import org.elasticsearch.common.unit.TimeValue;
+import org.junit.After;
 import org.junit.Test;
 import org.sonar.api.config.Settings;
 import org.sonar.core.profiling.Profiling;
@@ -35,6 +36,11 @@ public class ProxyCountRequestBuilderTest {
   Profiling profiling = new Profiling(new Settings().setProperty(Profiling.CONFIG_PROFILING_LEVEL, Profiling.Level.FULL.name()));
   SearchClient searchClient = new SearchClient(new Settings(), profiling);
 
+  @After
+  public void tearDown() throws Exception {
+    searchClient.stop();
+  }
+
   @Test
   public void count() {
     try {
@@ -69,6 +75,7 @@ public class ProxyCountRequestBuilderTest {
     }
 
     // TODO assert profiling
+    searchClient.stop();
   }
 
   @Test
index ea715872792f213f2e69b7b67bdbc6f434ad154f..14260a0a7f6907961131ff9a6d8bc2a49c35c9d5 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.server.search.request;
 
 import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder;
 import org.elasticsearch.common.unit.TimeValue;
+import org.junit.After;
 import org.junit.Test;
 import org.sonar.api.config.Settings;
 import org.sonar.core.profiling.Profiling;
@@ -35,6 +36,11 @@ public class ProxyCreateIndexRequestBuilderText {
   Profiling profiling = new Profiling(new Settings().setProperty(Profiling.CONFIG_PROFILING_LEVEL, Profiling.Level.FULL.name()));
   SearchClient searchClient = new SearchClient(new Settings(), profiling);
 
+  @After
+  public void tearDown() throws Exception {
+    searchClient.stop();
+  }
+
   @Test
   public void create_index() {
     try {
@@ -69,6 +75,7 @@ public class ProxyCreateIndexRequestBuilderText {
     }
 
     // TODO assert profiling
+    searchClient.stop();
   }
 
   @Test
index e939c5285531183320d8e0b0a804b71585fb5d6a..39ed9a9a5f663637777683cad433ce06d7a38f2a 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.server.search.request;
 
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.index.query.QueryBuilders;
+import org.junit.After;
 import org.junit.Test;
 import org.sonar.api.config.Settings;
 import org.sonar.core.profiling.Profiling;
@@ -36,6 +37,11 @@ public class ProxyDeleteByQueryRequestBuilderTest {
   Profiling profiling = new Profiling(new Settings().setProperty(Profiling.CONFIG_PROFILING_LEVEL, Profiling.Level.FULL.name()));
   SearchClient searchClient = new SearchClient(new Settings(), profiling);
 
+  @After
+  public void tearDown() throws Exception {
+    searchClient.stop();
+  }
+
   @Test
   public void bulk() {
     try {
@@ -69,6 +75,7 @@ public class ProxyDeleteByQueryRequestBuilderTest {
     }
 
     // TODO assert profiling
+    searchClient.stop();
   }
 
   @Test
index 5639c9ed0bf2310fb0a35f7a7a8e18ebc416b5c4..354f333b3cc29edadf16efdd387a6dd65cf72312 100644 (file)
@@ -21,6 +21,7 @@
 package org.sonar.server.search.request;
 
 import org.elasticsearch.common.unit.TimeValue;
+import org.junit.After;
 import org.junit.Test;
 import org.sonar.api.config.Settings;
 import org.sonar.core.profiling.Profiling;
@@ -35,6 +36,11 @@ public class ProxyFlushRequestBuilderTest {
   Profiling profiling = new Profiling(new Settings().setProperty(Profiling.CONFIG_PROFILING_LEVEL, Profiling.Level.FULL.name()));
   SearchClient searchClient = new SearchClient(new Settings(), profiling);
 
+  @After
+  public void tearDown() throws Exception {
+    searchClient.stop();
+  }
+
   @Test
   public void flush() {
     try {
@@ -68,6 +74,7 @@ public class ProxyFlushRequestBuilderTest {
     }
 
     // TODO assert profiling
+    searchClient.stop();
   }
 
   @Test
index 27d371db52f76362dbd7f136fcb09567fbee85d4..0ca52fe95f5ac2f7f3c502ac717a85b8ed822eb7 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.server.search.request;
 
 import org.elasticsearch.action.get.GetRequestBuilder;
 import org.elasticsearch.common.unit.TimeValue;
+import org.junit.After;
 import org.junit.Test;
 import org.sonar.api.config.Settings;
 import org.sonar.core.profiling.Profiling;
@@ -36,6 +37,11 @@ public class ProxyGetRequestBuilderTest {
   Profiling profiling = new Profiling(new Settings().setProperty(Profiling.CONFIG_PROFILING_LEVEL, Profiling.Level.FULL.name()));
   SearchClient searchClient = new SearchClient(new Settings(), profiling);
 
+  @After
+  public void tearDown() throws Exception {
+    searchClient.stop();
+  }
+
   @Test
   public void get() {
     try {
@@ -83,6 +89,7 @@ public class ProxyGetRequestBuilderTest {
     }
 
     // TODO assert profiling
+    searchClient.stop();
   }
 
   @Test
index c528dd17c1b03821978e5534c2a94e219cf5b3d8..881bf7ff65cba459ec1426132dffbbd8e2e41701 100644 (file)
@@ -21,6 +21,7 @@
 package org.sonar.server.search.request;
 
 import org.elasticsearch.common.unit.TimeValue;
+import org.junit.After;
 import org.junit.Test;
 import org.sonar.api.config.Settings;
 import org.sonar.core.profiling.Profiling;
@@ -35,6 +36,11 @@ public class ProxyIndicesExistsRequestBuilderTest {
   Profiling profiling = new Profiling(new Settings().setProperty(Profiling.CONFIG_PROFILING_LEVEL, Profiling.Level.FULL.name()));
   SearchClient searchClient = new SearchClient(new Settings(), profiling);
 
+  @After
+  public void tearDown() throws Exception {
+    searchClient.stop();
+  }
+
   @Test
   public void exists() {
     try {
@@ -62,6 +68,7 @@ public class ProxyIndicesExistsRequestBuilderTest {
     }
 
     // TODO assert profiling
+    searchClient.stop();
   }
 
   @Test
index 0886a0fb3d98cde2ad2a1a88570f6b2f96b730b6..55b80f340c55153b3cb90edc5520d4a8ddbfb119 100644 (file)
@@ -21,6 +21,7 @@
 package org.sonar.server.search.request;
 
 import org.elasticsearch.common.unit.TimeValue;
+import org.junit.After;
 import org.junit.Test;
 import org.sonar.api.config.Settings;
 import org.sonar.core.profiling.Profiling;
@@ -35,6 +36,11 @@ public class ProxyIndicesStatsRequestBuilderTest {
   Profiling profiling = new Profiling(new Settings().setProperty(Profiling.CONFIG_PROFILING_LEVEL, Profiling.Level.FULL.name()));
   SearchClient searchClient = new SearchClient(new Settings(), profiling);
 
+  @After
+  public void tearDown() throws Exception {
+    searchClient.stop();
+  }
+
   @Test
   public void stats() {
     try {
@@ -68,6 +74,7 @@ public class ProxyIndicesStatsRequestBuilderTest {
     }
 
     // TODO assert profiling
+    searchClient.stop();
   }
 
   @Test
index e53ce97fdfc10409f85ab602d024d20c41d0190a..eac43b06bb854b28300c2fe5fbd68002fe0cdb9e 100644 (file)
@@ -24,6 +24,7 @@ import org.elasticsearch.action.get.MultiGetRequest;
 import org.elasticsearch.action.get.MultiGetRequestBuilder;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.search.fetch.source.FetchSourceContext;
+import org.junit.After;
 import org.junit.Test;
 import org.sonar.api.config.Settings;
 import org.sonar.core.profiling.Profiling;
@@ -38,6 +39,11 @@ public class ProxyMultiGetRequestBuilderTest {
   Profiling profiling = new Profiling(new Settings().setProperty(Profiling.CONFIG_PROFILING_LEVEL, Profiling.Level.FULL.name()));
   SearchClient searchClient = new SearchClient(new Settings(), profiling);
 
+  @After
+  public void tearDown() throws Exception {
+    searchClient.stop();
+  }
+
   @Test
   public void multi_get() {
     try {
@@ -82,6 +88,7 @@ public class ProxyMultiGetRequestBuilderTest {
     }
 
     // TODO assert profiling
+    searchClient.stop();
   }
 
   @Test
index ab14b04ac84ab759d8f5af7908aa23edd04b1d7f..0536a91cd1e4ad2dc646e3f994bedb98c4c72ecb 100644 (file)
@@ -21,6 +21,7 @@
 package org.sonar.server.search.request;
 
 import org.elasticsearch.common.unit.TimeValue;
+import org.junit.After;
 import org.junit.Test;
 import org.sonar.api.config.Settings;
 import org.sonar.core.profiling.Profiling;
@@ -35,6 +36,11 @@ public class ProxyNodesStatsRequestBuilderTest {
   Profiling profiling = new Profiling(new Settings().setProperty(Profiling.CONFIG_PROFILING_LEVEL, Profiling.Level.FULL.name()));
   SearchClient searchClient = new SearchClient(new Settings(), profiling);
 
+  @After
+  public void tearDown() throws Exception {
+    searchClient.stop();
+  }
+
   @Test
   public void stats() {
     try {
@@ -70,6 +76,7 @@ public class ProxyNodesStatsRequestBuilderTest {
     }
 
     // TODO assert profiling
+    searchClient.stop();
   }
 
   @Test
index 1093fc565075b4264d96b8d90d96a0f6bfef5f7b..c129c2bf663a3241b2c1d96c673642bd9ccd8c3d 100644 (file)
@@ -23,6 +23,7 @@ package org.sonar.server.search.request;
 import com.google.common.collect.ImmutableMap;
 import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequestBuilder;
 import org.elasticsearch.common.unit.TimeValue;
+import org.junit.After;
 import org.junit.Test;
 import org.sonar.api.config.Settings;
 import org.sonar.core.profiling.Profiling;
@@ -40,6 +41,11 @@ public class ProxyPutMappingRequestBuilderTest {
   Profiling profiling = new Profiling(new Settings().setProperty(Profiling.CONFIG_PROFILING_LEVEL, Profiling.Level.FULL.name()));
   SearchClient searchClient = new SearchClient(new Settings(), profiling);
 
+  @After
+  public void tearDown() throws Exception {
+    searchClient.stop();
+  }
+
   @Test
   public void put_mapping() {
     try {
@@ -85,6 +91,7 @@ public class ProxyPutMappingRequestBuilderTest {
     }
 
     // TODO assert profiling
+    searchClient.stop();
   }
 
   @Test
index 4c786a5cc93d3a0c52d8a50ced9187e61ec9bf87..3d3efb956b3a410c7d5fae8ffc35cee90b6a40e4 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.server.search.request;
 
 import org.elasticsearch.action.admin.indices.refresh.RefreshRequestBuilder;
 import org.elasticsearch.common.unit.TimeValue;
+import org.junit.After;
 import org.junit.Test;
 import org.sonar.api.config.Settings;
 import org.sonar.core.profiling.Profiling;
@@ -36,6 +37,11 @@ public class ProxyRefreshRequestBuilderTest {
   Profiling profiling = new Profiling(new Settings().setProperty(Profiling.CONFIG_PROFILING_LEVEL, Profiling.Level.FULL.name()));
   SearchClient searchClient = new SearchClient(new Settings(), profiling);
 
+  @After
+  public void tearDown() throws Exception {
+    searchClient.stop();
+  }
+
   @Test
   public void refresh() {
     try {
@@ -73,6 +79,7 @@ public class ProxyRefreshRequestBuilderTest {
     }
 
     // TODO assert profiling
+    searchClient.stop();
   }
 
   @Test
index e9dd3820ce17b4f3acc424d5e7a886a6fcb2dac4..682d6a68f810deb9299b222769b2a9c568c99f09 100644 (file)
@@ -21,6 +21,7 @@
 package org.sonar.server.search.request;
 
 import org.elasticsearch.common.unit.TimeValue;
+import org.junit.After;
 import org.junit.Test;
 import org.sonar.api.config.Settings;
 import org.sonar.core.profiling.Profiling;
@@ -35,6 +36,11 @@ public class ProxySearchRequestBuilderTest {
   Profiling profiling = new Profiling(new Settings().setProperty(Profiling.CONFIG_PROFILING_LEVEL, Profiling.Level.FULL.name()));
   SearchClient searchClient = new SearchClient(new Settings(), profiling);
 
+  @After
+  public void tearDown() throws Exception {
+    searchClient.stop();
+  }
+
   @Test
   public void search() {
     try {
@@ -72,6 +78,7 @@ public class ProxySearchRequestBuilderTest {
     }
 
     // TODO assert profiling
+    searchClient.stop();
   }
 
   @Test
index c7dfbe6b786147a806a16a62424120dd443437cf..34f5db920c0702119a2e804993922dd4050815cb 100644 (file)
@@ -23,6 +23,7 @@ package org.sonar.server.search.request;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.action.search.SearchType;
 import org.elasticsearch.common.unit.TimeValue;
+import org.junit.After;
 import org.junit.Test;
 import org.sonar.api.config.Settings;
 import org.sonar.core.profiling.Profiling;
@@ -37,6 +38,11 @@ public class ProxySearchScrollRequestBuilderTest {
   Profiling profiling = new Profiling(new Settings().setProperty(Profiling.CONFIG_PROFILING_LEVEL, Profiling.Level.FULL.name()));
   SearchClient searchClient = new SearchClient(new Settings(), profiling);
 
+  @After
+  public void tearDown() throws Exception {
+    searchClient.stop();
+  }
+
   @Test
   public void search_scroll() {
     try {
@@ -56,9 +62,9 @@ public class ProxySearchScrollRequestBuilderTest {
 
   @Test
   public void with_profiling_basic() {
+    Profiling profiling = new Profiling(new Settings().setProperty(Profiling.CONFIG_PROFILING_LEVEL, Profiling.Level.BASIC.name()));
+    SearchClient searchClient = new SearchClient(new Settings(), profiling);
     try {
-      Profiling profiling = new Profiling(new Settings().setProperty(Profiling.CONFIG_PROFILING_LEVEL, Profiling.Level.BASIC.name()));
-      SearchClient searchClient = new SearchClient(new Settings(), profiling);
       SearchResponse search = searchClient.prepareSearch(IndexDefinition.RULE.getIndexName())
         .setSearchType(SearchType.SCAN)
         .setScroll(TimeValue.timeValueSeconds(3L))
@@ -71,6 +77,7 @@ public class ProxySearchScrollRequestBuilderTest {
       assertThat(e).isInstanceOf(IllegalStateException.class);
       assertThat(e.getMessage()).contains("Fail to execute ES search request '{}' on indices '[rules]'");
     }
+    searchClient.stop();
   }
 
   @Test
index dd4b6ce5162719e541727d3e329000fb2321de94..a543d47403cb6c98dce1125780bcf971bfe6446d 100644 (file)
@@ -105,10 +105,15 @@ public class ServerTester extends ExternalResource {
       }
     }
 
-    searchServer.start();
-    platform.init(properties);
-    platform.addComponents(components);
-    platform.doStart();
+    try {
+      searchServer.start();
+      platform.init(properties);
+      platform.addComponents(components);
+      platform.doStart();
+    } catch (RuntimeException e) {
+      stop();
+      throw e;
+    }
     if (!platform.isStarted()) {
       throw new IllegalStateException("Server not started. You should check that db migrations " +
         "are correctly declared, for example in schema-h2.sql or DatabaseVersion");
@@ -118,7 +123,7 @@ public class ServerTester extends ExternalResource {
   private File createTempDir() {
     try {
       // Technique to create a temp directory from a temp file
-      File f = File.createTempFile("SonarQube", "");
+      File f = File.createTempFile("tmp-sq", "");
       f.delete();
       f.mkdir();
       return f;
@@ -139,8 +144,20 @@ public class ServerTester extends ExternalResource {
    * This method should not be called by test when ServerTester is annotated with {@link org.junit.Rule}
    */
   public void stop() {
-    platform.doStop();
-    searchServer.stop();
+    try {
+      if (platform != null) {
+        platform.doStop();
+      }
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
+    try {
+      if (searchServer != null) {
+        searchServer.stop();
+      }
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
     FileUtils.deleteQuietly(homeDir);
   }
 
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/db/ResultSetIteratorTest/feed.xml b/server/sonar-server/src/test/resources/org/sonar/server/db/ResultSetIteratorTest/feed.xml
new file mode 100644 (file)
index 0000000..f072b93
--- /dev/null
@@ -0,0 +1,5 @@
+<dataset>
+  <fake id="10" kee="AB" />
+  <fake id="20" kee="CD" />
+  <fake id="30" kee="EF" />
+</dataset>
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/db/ResultSetIteratorTest/schema.sql b/server/sonar-server/src/test/resources/org/sonar/server/db/ResultSetIteratorTest/schema.sql
new file mode 100644 (file)
index 0000000..09a2922
--- /dev/null
@@ -0,0 +1,4 @@
+CREATE TABLE "FAKE" (
+  "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
+  "KEE" VARCHAR(200) NOT NULL
+);