]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9514 SONAR-9516 SONAR-9517 ES resilience from POST WS
authorEric Hartmann <hartmann.eric@gmail.com>
Wed, 12 Jul 2017 04:52:41 +0000 (06:52 +0200)
committerSimon Brandhof <simon.brandhof@sonarsource.com>
Fri, 21 Jul 2017 22:31:15 +0000 (00:31 +0200)
118 files changed:
server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java
server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentMapper.java
server/sonar-db-dao/src/main/java/org/sonar/db/es/EsQueueDto.java
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java
server/sonar-db-dao/src/main/resources/org/sonar/db/component/ComponentMapper.xml
server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml
server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDaoTest.java
server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentTesting.java
server/sonar-db-dao/src/test/java/org/sonar/db/es/EsQueueDaoTest.java
server/sonar-server/src/main/java/org/sonar/server/component/ComponentCleanerService.java
server/sonar-server/src/main/java/org/sonar/server/component/ComponentService.java
server/sonar-server/src/main/java/org/sonar/server/component/ComponentUpdater.java
server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexer.java
server/sonar-server/src/main/java/org/sonar/server/component/ws/SuggestionsAction.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/BaseIssuesLoader.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/IndexAnalysisStep.java
server/sonar-server/src/main/java/org/sonar/server/es/BulkIndexer.java
server/sonar-server/src/main/java/org/sonar/server/es/DocId.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/es/EsUtils.java
server/sonar-server/src/main/java/org/sonar/server/es/IndexType.java
server/sonar-server/src/main/java/org/sonar/server/es/IndexerStartupTask.java
server/sonar-server/src/main/java/org/sonar/server/es/IndexingListener.java
server/sonar-server/src/main/java/org/sonar/server/es/IndexingResult.java
server/sonar-server/src/main/java/org/sonar/server/es/OneToManyResilientIndexingListener.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/es/OneToOneResilientIndexingListener.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/es/ProjectIndexer.java
server/sonar-server/src/main/java/org/sonar/server/es/ProjectIndexers.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/es/ProjectIndexersImpl.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/es/RecoveryIndexer.java
server/sonar-server/src/main/java/org/sonar/server/es/ResiliencyIndexingListener.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/es/ResilientIndexer.java
server/sonar-server/src/main/java/org/sonar/server/issue/IssueStorage.java
server/sonar-server/src/main/java/org/sonar/server/issue/ServerIssueStorage.java
server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueDoc.java
server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndexer.java
server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexer.java
server/sonar-server/src/main/java/org/sonar/server/permission/PermissionTemplateService.java
server/sonar-server/src/main/java/org/sonar/server/permission/PermissionUpdater.java
server/sonar-server/src/main/java/org/sonar/server/permission/index/AuthorizationTypeSupport.java
server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java
server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexerDao.java
server/sonar-server/src/main/java/org/sonar/server/permission/ws/template/ApplyTemplateAction.java
server/sonar-server/src/main/java/org/sonar/server/permission/ws/template/BulkApplyTemplateAction.java
server/sonar-server/src/main/java/org/sonar/server/platform/BackendCleanup.java
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
server/sonar-server/src/main/java/org/sonar/server/project/ws/UpdateVisibilityAction.java
server/sonar-server/src/main/java/org/sonar/server/projecttag/ws/SetAction.java
server/sonar-server/src/main/java/org/sonar/server/qualityprofile/index/ActiveRuleIndexer.java
server/sonar-server/src/main/java/org/sonar/server/rule/index/RuleIndexer.java
server/sonar-server/src/main/java/org/sonar/server/test/index/TestIndexer.java
server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndexer.java
server/sonar-server/src/main/java/org/sonar/server/view/index/ViewIndex.java
server/sonar-server/src/main/java/org/sonar/server/view/index/ViewIndexer.java
server/sonar-server/src/test/java/org/sonar/server/component/ComponentCleanerServiceTest.java
server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceTest.java
server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceUpdateKeyTest.java
server/sonar-server/src/test/java/org/sonar/server/component/ComponentUpdaterTest.java
server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexerTest.java
server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java
server/sonar-server/src/test/java/org/sonar/server/component/ws/SuggestionsActionTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/IndexAnalysisStepTest.java
server/sonar-server/src/test/java/org/sonar/server/es/BulkIndexerTest.java
server/sonar-server/src/test/java/org/sonar/server/es/EsTester.java
server/sonar-server/src/test/java/org/sonar/server/es/IndexTypeTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/es/IndexingResultTest.java
server/sonar-server/src/test/java/org/sonar/server/es/OneToManyResilientIndexingListenerTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/es/OneToOneResilientIndexingListenerTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/es/ProjectIndexersImplTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/es/RecoveryIndexerTest.java
server/sonar-server/src/test/java/org/sonar/server/es/TestProjectIndexers.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/issue/IssueServiceMediumTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/IssueUpdaterTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexDebtTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexerTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/AddCommentActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/AssignActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/AuthorsActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsMediumTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetSeverityActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTagsActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/TagsActionTest.java
server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexerTest.java
server/sonar-server/src/test/java/org/sonar/server/permission/PermissionTemplateServiceTest.java
server/sonar-server/src/test/java/org/sonar/server/permission/index/FooIndexer.java
server/sonar-server/src/test/java/org/sonar/server/permission/index/PermissionIndexerDaoTest.java
server/sonar-server/src/test/java/org/sonar/server/permission/index/PermissionIndexerTest.java
server/sonar-server/src/test/java/org/sonar/server/permission/ws/BasePermissionWsTest.java
server/sonar-server/src/test/java/org/sonar/server/permission/ws/template/ApplyTemplateActionTest.java
server/sonar-server/src/test/java/org/sonar/server/permission/ws/template/BulkApplyTemplateActionTest.java
server/sonar-server/src/test/java/org/sonar/server/project/ws/CreateActionTest.java
server/sonar-server/src/test/java/org/sonar/server/project/ws/UpdateVisibilityActionTest.java
server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/SetActionTest.java
server/sonar-server/src/test/java/org/sonar/server/qualityprofile/index/ActiveRuleIndexerTest.java
server/sonar-server/src/test/java/org/sonar/server/rule/ws/SearchActionMediumTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/test/index/TestIndexerTest.java
server/sonar-server/src/test/java/org/sonar/server/view/index/ViewIndexerTest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/project/BulkDeleteRequest.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/client/project/DeleteRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsService.java
sonar-ws/src/test/java/org/sonarqube/ws/client/project/ProjectsServiceTest.java
tests/src/test/java/org/sonarqube/tests/Category1Suite.java
tests/src/test/java/org/sonarqube/tests/Category6Suite.java
tests/src/test/java/org/sonarqube/tests/Elasticsearch.java [new file with mode: 0644]
tests/src/test/java/org/sonarqube/tests/Tester.java
tests/src/test/java/org/sonarqube/tests/projectAdministration/BulkDeletionTest.java [deleted file]
tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectBulkDeletionPageTest.java [new file with mode: 0644]
tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectDeletionTest.java [new file with mode: 0644]
tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectProvisioningTest.java [new file with mode: 0644]
tests/src/test/resources/projectAdministration/BulkDeletionTest/bulk-delete-filter-projects.html [deleted file]
tests/src/test/resources/projectAdministration/ProjectBulkDeletionPageTest/bulk-delete-filter-projects.html [new file with mode: 0644]

index 59e18e5f04937cd257576fa9532dda2cd7f5735e..eae580c3a3ea8d2fe061597b9237356deeaea49f 100644 (file)
@@ -22,7 +22,6 @@ package org.sonar.ce.container;
 import com.google.common.annotations.VisibleForTesting;
 import java.util.List;
 import javax.annotation.CheckForNull;
-import org.sonar.db.DBSessionsImpl;
 import org.sonar.api.SonarQubeSide;
 import org.sonar.api.SonarQubeVersion;
 import org.sonar.api.config.EmailSettings;
@@ -67,6 +66,7 @@ import org.sonar.core.platform.PluginClassloaderFactory;
 import org.sonar.core.platform.PluginLoader;
 import org.sonar.core.timemachine.Periods;
 import org.sonar.core.util.UuidFactoryImpl;
+import org.sonar.db.DBSessionsImpl;
 import org.sonar.db.DaoModule;
 import org.sonar.db.DatabaseChecker;
 import org.sonar.db.DbClient;
@@ -74,7 +74,6 @@ import org.sonar.db.DefaultDatabase;
 import org.sonar.db.purge.PurgeProfiler;
 import org.sonar.process.Props;
 import org.sonar.process.logging.LogbackHelper;
-import org.sonar.server.component.ComponentCleanerService;
 import org.sonar.server.component.ComponentFinder;
 import org.sonar.server.component.index.ComponentIndexer;
 import org.sonar.server.computation.task.projectanalysis.ProjectAnalysisTaskModule;
@@ -350,7 +349,6 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer {
       ComponentFinder.class, // used in ComponentService
       NewAlerts.class,
       NewAlerts.newMetadata(),
-      ComponentCleanerService.class,
       ProjectMeasuresIndexer.class,
       ComponentIndexer.class,
 
index ad62ace71c2f988a6355cbce482aa227f7787894..17eee92bbe02526eff429951a08c270d79f67955 100644 (file)
@@ -113,7 +113,7 @@ public class ComputeEngineContainerImplTest {
     assertThat(picoContainer.getComponentAdapters())
       .hasSize(
         CONTAINER_ITSELF
-          + 73 // level 4
+          + 72 // level 4
           + 4 // content of CeConfigurationModule
           + 4 // content of CeQueueModule
           + 4 // content of CeHttpModule
index f23f1fe91a8f7686dcd13006dde0514a2c8c5130..7caca7706ba9eff76790ad72a49d0c0d19f052b6 100644 (file)
@@ -279,9 +279,8 @@ public class ComponentDao implements Dao {
    * @param projectUuid the project uuid, which is selected with all of its children
    * @param handler the action to be applied to every result
    */
-  public void selectForIndexing(DbSession session, @Nullable String projectUuid, ResultHandler<ComponentDto> handler) {
-    requireNonNull(handler);
-    mapper(session).selectForIndexing(projectUuid, handler);
+  public void scrollForIndexing(DbSession session, @Nullable String projectUuid, ResultHandler<ComponentDto> handler) {
+    mapper(session).scrollForIndexing(projectUuid, handler);
   }
 
   /**
index 0cfc494d2ddff52df56de231c81c926399b0c514..c476910b2ed44d9c5d068149200acb17c79e2b26 100644 (file)
@@ -130,7 +130,7 @@ public interface ComponentMapper {
 
   List<ComponentDto> selectProjectsByNameQuery(@Param("nameQuery") @Nullable String nameQuery, @Param("includeModules") boolean includeModules);
 
-  void selectForIndexing(@Param("projectUuid") @Nullable String projectUuid, ResultHandler<ComponentDto> handler);
+  void scrollForIndexing(@Param("projectUuid") @Nullable String projectUuid, ResultHandler<ComponentDto> handler);
 
   void insert(ComponentDto componentDto);
 
index 63751bf5de55d91f59e0233ac6c1cfa3b4b603c1..b0449159aefd4570e21b1027e20d42608a62b819 100644 (file)
@@ -24,12 +24,8 @@ import javax.annotation.Nullable;
 
 public final class EsQueueDto {
 
-  public enum Type {
-    USER, RULE, RULE_EXTENSION, ACTIVE_RULE
-  }
-
   private String uuid;
-  private Type docType;
+  private String docType;
   private String docId;
   private String docIdType;
   private String docRouting;
@@ -43,11 +39,11 @@ public final class EsQueueDto {
     return this;
   }
 
-  public Type getDocType() {
+  public String getDocType() {
     return docType;
   }
 
-  private EsQueueDto setDocType(Type t) {
+  private EsQueueDto setDocType(String t) {
     this.docType = t;
     return this;
   }
@@ -93,11 +89,11 @@ public final class EsQueueDto {
     return sb.toString();
   }
 
-  public static EsQueueDto create(Type docType, String docUuid) {
+  public static EsQueueDto create(String docType, String docUuid) {
     return new EsQueueDto().setDocType(docType).setDocId(docUuid);
   }
 
-  public static EsQueueDto create(Type docType, String docId, @Nullable String docIdType, @Nullable String docRouting) {
+  public static EsQueueDto create(String docType, String docId, @Nullable String docIdType, @Nullable String docRouting) {
     return new EsQueueDto().setDocType(docType)
       .setDocId(docId).setDocIdType(docIdType).setDocRouting(docRouting);
   }
index 94c1c6b072bf6cb3c806850792c01ff355533949..40d148ec7412864c054a49b9bb658d0490502128 100644 (file)
@@ -95,7 +95,7 @@ public class IssueDao implements Dao {
   }
 
   public void scrollNonClosedByComponentUuid(DbSession dbSession, String componentUuid, ResultHandler<IssueDto> handler) {
-    mapper(dbSession).selectNonClosedByComponentUuid(componentUuid, handler);
+    mapper(dbSession).scrollNonClosedByComponentUuid(componentUuid, handler);
   }
 
   public void scrollNonClosedByModuleOrProject(DbSession dbSession, ComponentDto module, ResultHandler<IssueDto> handler) {
index 77355540661f173e74a4b1c0e03fdbb017713506..d08d71999ab7fd3d52f4de10463e1e91c3eab133 100644 (file)
@@ -38,7 +38,7 @@ public interface IssueMapper {
 
   int updateIfBeforeSelectedDate(IssueDto issue);
 
-  void selectNonClosedByComponentUuid(@Param("componentUuid") String componentUuid, ResultHandler<IssueDto> handler);
+  void scrollNonClosedByComponentUuid(@Param("componentUuid") String componentUuid, ResultHandler<IssueDto> handler);
 
   void scrollNonClosedByModuleOrProject(
     @Param("projectUuid") String projectUuid,
index eb361e3a45d5400b43be006227d205a0c546fce0..f9a99818714983dd3bcf28d05c238c9c38bd736f 100644 (file)
     </if>
   </sql>
 
-  <select id="selectForIndexing" parameterType="map" resultType="Component" fetchSize="${_scrollFetchSize}" resultSetType="FORWARD_ONLY">
+  <select id="scrollForIndexing" parameterType="map" resultType="Component" fetchSize="${_scrollFetchSize}" resultSetType="FORWARD_ONLY">
     select
       <include refid="componentColumns"/>
     from projects p
index 5211809d2b856bb38870e74fddfdbee556a6713a..744482c6cb1c75f8af52da6b8a9eeea03b04aa74 100644 (file)
     </if>
   </sql>
 
+  <sql id="issueForIndexingColumns">
+    i.kee as "key",
+    root.uuid as "projectUuid",
+    i.updated_at as "updatedAt",
+    i.assignee,
+    i.gap,
+    i.issue_attributes as "attributes",
+    i.line,
+    i.message,
+    i.resolution,
+    i.severity,
+    i.manual_severity as "manualSeverity",
+    i.checksum,
+    i.status,
+    i.effort,
+    i.author_login as "authorLogin",
+    i.issue_close_date as "issueCloseDate",
+    i.issue_creation_date as "issueCreationDate",
+    i.issue_update_date as "issueUpdateDate",
+    r.plugin_name as "pluginName",
+    r.plugin_rule_key as "pluginRuleKey",
+    r.language,
+    p.uuid as "projectUuid",
+    p.module_uuid_path as "moduleUuidPath",
+    p.path,
+    p.scope,
+    p.organization_uuid as "organizationUuid",
+    i.tags,
+    i.issue_type as "issueType"
+  </sql>
+
+
   <insert id="insert" parameterType="Issue" useGeneratedKeys="false" keyProperty="id">
     INSERT INTO issues (kee, rule_id, severity, manual_severity,
     message, line, locations, gap, effort, status, tags,
     where i.kee=#{kee,jdbcType=VARCHAR}
   </select>
 
-  <select id="selectNonClosedByComponentUuid" parameterType="String" resultType="Issue" fetchSize="${_scrollFetchSize}" resultSetType="FORWARD_ONLY">
+  <select id="scrollNonClosedByComponentUuid" parameterType="String" resultType="Issue" fetchSize="${_scrollFetchSize}" resultSetType="FORWARD_ONLY">
     select
     <include refid="issueColumns"/>
     from issues i
index ee6b738ce57e09ca77f5f12b8d22623e33549ff7..c9c83436a8e38b1833f5062df0f7a23e05c9f7b2 100644 (file)
@@ -668,7 +668,7 @@ public class ComponentDaoTest {
     db.prepareDbUnit(getClass(), "selectForIndexing.xml");
 
     List<ComponentDto> components = new ArrayList<>();
-    underTest.selectForIndexing(dbSession, projectUuid, context -> components.add(context.getResultObject()));
+    underTest.scrollForIndexing(dbSession, projectUuid, context -> components.add(context.getResultObject()));
     return assertThat(components).extracting(ComponentDto::uuid);
   }
 
index f15288642bc0c1464f6bff74341e43a74484117e..3445b4778b4d1c89fd0c41738daee9bdfe031c9b 100644 (file)
@@ -41,10 +41,11 @@ public class ComponentTesting {
   }
 
   public static ComponentDto newFileDto(ComponentDto module, @Nullable ComponentDto directory, String fileUuid) {
-    String path = "src/main/xoo/org/sonar/samples/File.xoo";
+    String filename = "NAME_" + fileUuid;
+    String path = directory != null ? directory.path() + "/" + filename : module.path() + "/" + filename;
     return newChildComponent(fileUuid, module, directory == null ? module : directory)
       .setKey("KEY_" + fileUuid)
-      .setName("NAME_" + fileUuid)
+      .setName(filename)
       .setLongName(path)
       .setScope(Scopes.FILE)
       .setQualifier(Qualifiers.FILE)
index 373a88b201394de0413bb5e66e7cacb558620eb5..6a5cf1531f65a2697c7e9257535c1d91958fec65 100644 (file)
@@ -48,7 +48,7 @@ public class EsQueueDaoTest {
     int nbOfInsert = 10 + new Random().nextInt(20);
     List<EsQueueDto> esQueueDtos = new ArrayList<>();
     IntStream.rangeClosed(1, nbOfInsert).forEach(
-      i -> esQueueDtos.add(EsQueueDto.create(EsQueueDto.Type.USER, UuidFactoryFast.getInstance().create()))
+      i -> esQueueDtos.add(EsQueueDto.create("foo", UuidFactoryFast.getInstance().create()))
     );
     underTest.insert(dbSession, esQueueDtos);
 
@@ -60,11 +60,11 @@ public class EsQueueDaoTest {
     int nbOfInsert = 10 + new Random().nextInt(20);
     List<EsQueueDto> esQueueDtos = new ArrayList<>();
     IntStream.rangeClosed(1, nbOfInsert).forEach(
-      i -> esQueueDtos.add(EsQueueDto.create(EsQueueDto.Type.USER, UuidFactoryFast.getInstance().create()))
+      i -> esQueueDtos.add(EsQueueDto.create("foo", UuidFactoryFast.getInstance().create()))
     );
     underTest.insert(dbSession, esQueueDtos);
 
-    underTest.delete(dbSession, EsQueueDto.create(EsQueueDto.Type.USER, UuidFactoryFast.getInstance().create()));
+    underTest.delete(dbSession, EsQueueDto.create("foo", UuidFactoryFast.getInstance().create()));
 
     assertThat(dbTester.countSql(dbSession, "select count(*) from es_queue")).isEqualTo(nbOfInsert);
   }
@@ -74,7 +74,7 @@ public class EsQueueDaoTest {
     int nbOfInsert = 10 + new Random().nextInt(20);
     List<EsQueueDto> esQueueDtos = new ArrayList<>();
     IntStream.rangeClosed(1, nbOfInsert).forEach(
-      i -> esQueueDtos.add(EsQueueDto.create(EsQueueDto.Type.USER, UuidFactoryFast.getInstance().create()))
+      i -> esQueueDtos.add(EsQueueDto.create("foo", UuidFactoryFast.getInstance().create()))
     );
     underTest.insert(dbSession, esQueueDtos);
     assertThat(dbTester.countSql(dbSession, "select count(*) from es_queue")).isEqualTo(nbOfInsert);
@@ -87,11 +87,11 @@ public class EsQueueDaoTest {
   @Test
   public void selectForRecovery_must_return_limit_when_there_are_more_rows()  {
     system2.setNow(1_000L);
-    EsQueueDto i1 = underTest.insert(dbSession, EsQueueDto.create(EsQueueDto.Type.USER, UuidFactoryFast.getInstance().create()));
+    EsQueueDto i1 = underTest.insert(dbSession, EsQueueDto.create("foo", UuidFactoryFast.getInstance().create()));
     system2.setNow(1_001L);
-    EsQueueDto i2 = underTest.insert(dbSession, EsQueueDto.create(EsQueueDto.Type.USER, UuidFactoryFast.getInstance().create()));
+    EsQueueDto i2 = underTest.insert(dbSession, EsQueueDto.create("foo", UuidFactoryFast.getInstance().create()));
     system2.setNow(1_002L);
-    EsQueueDto i3 = underTest.insert(dbSession, EsQueueDto.create(EsQueueDto.Type.USER, UuidFactoryFast.getInstance().create()));
+    EsQueueDto i3 = underTest.insert(dbSession, EsQueueDto.create("foo", UuidFactoryFast.getInstance().create()));
 
     assertThat(underTest.selectForRecovery(dbSession, 2_000, 1))
       .extracting(EsQueueDto::getUuid)
@@ -109,11 +109,11 @@ public class EsQueueDaoTest {
   @Test
   public void selectForRecovery_returns_ordered_rows_created_before_date()  {
     system2.setNow(1_000L);
-    EsQueueDto i1 = underTest.insert(dbSession, EsQueueDto.create(EsQueueDto.Type.USER, UuidFactoryFast.getInstance().create()));
+    EsQueueDto i1 = underTest.insert(dbSession, EsQueueDto.create("foo", UuidFactoryFast.getInstance().create()));
     system2.setNow(1_001L);
-    EsQueueDto i2 = underTest.insert(dbSession, EsQueueDto.create(EsQueueDto.Type.USER, UuidFactoryFast.getInstance().create()));
+    EsQueueDto i2 = underTest.insert(dbSession, EsQueueDto.create("foo", UuidFactoryFast.getInstance().create()));
     system2.setNow(1_002L);
-    EsQueueDto i3 = underTest.insert(dbSession, EsQueueDto.create(EsQueueDto.Type.USER, UuidFactoryFast.getInstance().create()));
+    EsQueueDto i3 = underTest.insert(dbSession, EsQueueDto.create("foo", UuidFactoryFast.getInstance().create()));
 
     assertThat(underTest.selectForRecovery(dbSession, 999, LIMIT)).hasSize(0);
     assertThat(underTest.selectForRecovery(dbSession, 1_000, LIMIT))
index 7dd68335f9a1d30935c5bdf185f15e8b78acc4eb..33d357e3a75378df5fafb0eca3ae42a66829778c 100644 (file)
@@ -19,9 +19,7 @@
  */
 package org.sonar.server.component;
 
-import java.util.Collection;
 import java.util.List;
-import org.sonar.api.ce.ComputeEngineSide;
 import org.sonar.api.resources.ResourceType;
 import org.sonar.api.resources.ResourceTypes;
 import org.sonar.api.resources.Scopes;
@@ -30,21 +28,21 @@ import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.server.es.ProjectIndexer;
+import org.sonar.server.es.ProjectIndexers;
 
-import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
 
 @ServerSide
-@ComputeEngineSide
 public class ComponentCleanerService {
 
   private final DbClient dbClient;
   private final ResourceTypes resourceTypes;
-  private final Collection<ProjectIndexer> projectIndexers;
+  private final ProjectIndexers projectIndexers;
 
-  public ComponentCleanerService(DbClient dbClient, ResourceTypes resourceTypes, ProjectIndexer... projectIndexers) {
+  public ComponentCleanerService(DbClient dbClient, ResourceTypes resourceTypes, ProjectIndexers projectIndexers) {
     this.dbClient = dbClient;
     this.resourceTypes = resourceTypes;
-    this.projectIndexers = asList(projectIndexers);
+    this.projectIndexers = projectIndexers;
   }
 
   public void delete(DbSession dbSession, List<ComponentDto> projects) {
@@ -58,13 +56,7 @@ public class ComponentCleanerService {
       throw new IllegalArgumentException("Only projects can be deleted");
     }
     dbClient.purgeDao().deleteRootComponent(dbSession, project.uuid());
-    dbSession.commit();
-
-    deleteFromIndices(project.uuid());
-  }
-
-  private void deleteFromIndices(String projectUuid) {
-    projectIndexers.forEach(i -> i.deleteProject(projectUuid));
+    projectIndexers.commitAndIndex(dbSession, singletonList(project.uuid()), ProjectIndexer.Cause.PROJECT_DELETION);
   }
 
   private static boolean hasNotProjectScope(ComponentDto project) {
index a3ad4ebd2133df8ee624b1cd3754e40e4ab48aa3..1cf8b58feffc9b91decbddad123488eb92d8ec08 100644 (file)
@@ -25,8 +25,10 @@ import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.server.es.ProjectIndexer;
+import org.sonar.server.es.ProjectIndexers;
 import org.sonar.server.user.UserSession;
 
+import static java.util.Collections.singletonList;
 import static org.sonar.core.component.ComponentKeys.isValidModuleKey;
 import static org.sonar.db.component.ComponentKeyUpdaterDao.checkIsProjectOrModule;
 import static org.sonar.server.ws.WsUtils.checkRequest;
@@ -35,35 +37,27 @@ import static org.sonar.server.ws.WsUtils.checkRequest;
 public class ComponentService {
   private final DbClient dbClient;
   private final UserSession userSession;
-  private final ProjectIndexer[] projectIndexers;
+  private final ProjectIndexers projectIndexers;
 
-  public ComponentService(DbClient dbClient, UserSession userSession, ProjectIndexer... projectIndexers) {
+  public ComponentService(DbClient dbClient, UserSession userSession, ProjectIndexers projectIndexers) {
     this.dbClient = dbClient;
     this.userSession = userSession;
     this.projectIndexers = projectIndexers;
   }
 
-  // TODO should be moved to ComponentUpdater
+  // TODO should be moved to UpdateKeyAction
   public void updateKey(DbSession dbSession, ComponentDto component, String newKey) {
     userSession.checkComponentPermission(UserRole.ADMIN, component);
     checkIsProjectOrModule(component);
     checkProjectOrModuleKeyFormat(newKey);
     dbClient.componentKeyUpdaterDao().updateKey(dbSession, component.uuid(), newKey);
-    dbSession.commit();
-    index(component.uuid());
+    projectIndexers.commitAndIndex(dbSession, singletonList(component.uuid()), ProjectIndexer.Cause.PROJECT_KEY_UPDATE);
   }
 
-  // TODO should be moved to ComponentUpdater
+  // TODO should be moved to BulkUpdateKeyAction
   public void bulkUpdateKey(DbSession dbSession, String projectUuid, String stringToReplace, String replacementString) {
     dbClient.componentKeyUpdaterDao().bulkUpdateKey(dbSession, projectUuid, stringToReplace, replacementString);
-    dbSession.commit();
-    index(projectUuid);
-  }
-
-  private void index(String projectUuid) {
-    for (ProjectIndexer projectIndexer : projectIndexers) {
-      projectIndexer.indexProject(projectUuid, ProjectIndexer.Cause.PROJECT_KEY_UPDATE);
-    }
+    projectIndexers.commitAndIndex(dbSession, singletonList(projectUuid), ProjectIndexer.Cause.PROJECT_KEY_UPDATE);
   }
 
   private static void checkProjectOrModuleKeyFormat(String key) {
index 759033e7f204957c5379baceb38d55f5e7f517fa..115a7e28f4a4d1bdfba7cd26a5afb7ae345489dd 100644 (file)
@@ -19,7 +19,6 @@
  */
 package org.sonar.server.component;
 
-import java.util.Collection;
 import java.util.Date;
 import java.util.List;
 import java.util.Locale;
@@ -32,12 +31,12 @@ import org.sonar.core.util.Uuids;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.ComponentDto;
-import org.sonar.server.es.ProjectIndexer;
 import org.sonar.server.es.ProjectIndexer.Cause;
+import org.sonar.server.es.ProjectIndexers;
 import org.sonar.server.favorite.FavoriteUpdater;
 import org.sonar.server.permission.PermissionTemplateService;
 
-import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
 import static org.sonar.api.resources.Qualifiers.PROJECT;
 import static org.sonar.core.component.ComponentKeys.isValidModuleKey;
 import static org.sonar.server.ws.WsUtils.checkRequest;
@@ -49,17 +48,17 @@ public class ComponentUpdater {
   private final System2 system2;
   private final PermissionTemplateService permissionTemplateService;
   private final FavoriteUpdater favoriteUpdater;
-  private final Collection<ProjectIndexer> projectIndexers;
+  private final ProjectIndexers projectIndexers;
 
   public ComponentUpdater(DbClient dbClient, I18n i18n, System2 system2,
     PermissionTemplateService permissionTemplateService, FavoriteUpdater favoriteUpdater,
-    ProjectIndexer... projectIndexers) {
+    ProjectIndexers projectIndexers) {
     this.dbClient = dbClient;
     this.i18n = i18n;
     this.system2 = system2;
     this.permissionTemplateService = permissionTemplateService;
     this.favoriteUpdater = favoriteUpdater;
-    this.projectIndexers = asList(projectIndexers);
+    this.projectIndexers = projectIndexers;
   }
 
   /**
@@ -73,8 +72,7 @@ public class ComponentUpdater {
     ComponentDto componentDto = createRootComponent(dbSession, newComponent);
     removeDuplicatedProjects(dbSession, componentDto.getKey());
     handlePermissionTemplate(dbSession, componentDto, newComponent.getOrganizationUuid(), userId);
-    dbSession.commit();
-    index(componentDto);
+    projectIndexers.commitAndIndex(dbSession, singletonList(componentDto.uuid()), Cause.PROJECT_CREATION);
     return componentDto;
   }
 
@@ -140,7 +138,4 @@ public class ComponentUpdater {
     return i18n.message(Locale.getDefault(), "qualifier." + qualifier, "Project");
   }
 
-  private void index(ComponentDto project) {
-    projectIndexers.forEach(i -> i.indexProject(project.uuid(), Cause.PROJECT_CREATION));
-  }
 }
index 2f02c9871fc73c0d0410b7d19e889b1d36562afd..cce63e7e2c704c63ef1b204e56bf018ce6ad1fca 100644 (file)
  */
 package org.sonar.server.component.index;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableSet;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 import javax.annotation.Nullable;
 import org.elasticsearch.action.index.IndexRequest;
+import org.elasticsearch.action.search.SearchRequestBuilder;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.ComponentDto;
+import org.sonar.db.es.EsQueueDto;
 import org.sonar.server.es.BulkIndexer;
 import org.sonar.server.es.BulkIndexer.Size;
 import org.sonar.server.es.EsClient;
 import org.sonar.server.es.IndexType;
+import org.sonar.server.es.IndexingResult;
+import org.sonar.server.es.OneToManyResilientIndexingListener;
 import org.sonar.server.es.ProjectIndexer;
-import org.sonar.server.es.StartupIndexer;
 import org.sonar.server.permission.index.AuthorizationScope;
 import org.sonar.server.permission.index.NeedAuthorizationIndexer;
 
-import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
-import static org.elasticsearch.index.query.QueryBuilders.termQuery;
+import static java.util.Collections.emptyList;
 import static org.sonar.server.component.index.ComponentIndexDefinition.INDEX_TYPE_COMPONENT;
 
-public class ComponentIndexer implements ProjectIndexer, NeedAuthorizationIndexer, StartupIndexer {
+public class ComponentIndexer implements ProjectIndexer, NeedAuthorizationIndexer {
 
   private static final AuthorizationScope AUTHORIZATION_SCOPE = new AuthorizationScope(INDEX_TYPE_COMPONENT, project -> true);
+  private static final ImmutableSet<IndexType> INDEX_TYPES = ImmutableSet.of(INDEX_TYPE_COMPONENT);
 
   private final DbClient dbClient;
   private final EsClient esClient;
@@ -55,7 +63,7 @@ public class ComponentIndexer implements ProjectIndexer, NeedAuthorizationIndexe
 
   @Override
   public Set<IndexType> getIndexTypes() {
-    return ImmutableSet.of(ComponentIndexDefinition.INDEX_TYPE_COMPONENT);
+    return INDEX_TYPES;
   }
 
   @Override
@@ -64,15 +72,31 @@ public class ComponentIndexer implements ProjectIndexer, NeedAuthorizationIndexe
   }
 
   @Override
-  public void indexProject(String projectUuid, Cause cause) {
+  public void indexOnAnalysis(String projectUuid) {
+    doIndexByProjectUuid(projectUuid, Size.REGULAR);
+  }
+
+  @Override
+  public AuthorizationScope getAuthorizationScope() {
+    return AUTHORIZATION_SCOPE;
+  }
+
+  @Override
+  public Collection<EsQueueDto> prepareForRecovery(DbSession dbSession, Collection<String> projectUuids, Cause cause) {
     switch (cause) {
       case PROJECT_TAGS_UPDATE:
-        break;
+      case PERMISSION_CHANGE:
+        // tags and permissions are not part of type components/component
+        return emptyList();
+
       case PROJECT_CREATION:
+      case PROJECT_DELETION:
       case PROJECT_KEY_UPDATE:
-      case NEW_ANALYSIS:
-        doIndexByProjectUuid(projectUuid, Size.REGULAR);
-        break;
+        List<EsQueueDto> items = projectUuids.stream()
+          .map(projectUuid -> EsQueueDto.create(INDEX_TYPE_COMPONENT.format(), projectUuid, null, projectUuid))
+          .collect(MoreCollectors.toArrayList(projectUuids.size()));
+        return dbClient.esQueueDao().insert(dbSession, items);
+
       default:
         // defensive case
         throw new IllegalStateException("Unsupported cause: " + cause);
@@ -80,8 +104,31 @@ public class ComponentIndexer implements ProjectIndexer, NeedAuthorizationIndexe
   }
 
   @Override
-  public AuthorizationScope getAuthorizationScope() {
-    return AUTHORIZATION_SCOPE;
+  public IndexingResult index(DbSession dbSession, Collection<EsQueueDto> items) {
+    if (items.isEmpty()) {
+      return new IndexingResult();
+    }
+
+    OneToManyResilientIndexingListener listener = new OneToManyResilientIndexingListener(dbClient, dbSession, items);
+    BulkIndexer bulkIndexer = new BulkIndexer(esClient, INDEX_TYPE_COMPONENT, Size.REGULAR, listener);
+    bulkIndexer.start();
+    Set<String> projectUuids = items.stream().map(EsQueueDto::getDocId).collect(MoreCollectors.toHashSet(items.size()));
+    Set<String> remaining = new HashSet<>(projectUuids);
+
+    for (String projectUuid : projectUuids) {
+      // TODO allow scrolling multiple projects at the same time
+      dbClient.componentDao().scrollForIndexing(dbSession, projectUuid, context -> {
+        ComponentDto dto = context.getResultObject();
+        bulkIndexer.add(newIndexRequest(toDocument(dto)));
+        remaining.remove(dto.projectUuid());
+      });
+    }
+
+    // the remaining uuids reference projects that don't exist in db. They must
+    // be deleted from index.
+    remaining.forEach(projectUuid -> addProjectDeletionToBulkIndexer(bulkIndexer, projectUuid));
+
+    return bulkIndexer.stop();
   }
 
   /**
@@ -89,12 +136,12 @@ public class ComponentIndexer implements ProjectIndexer, NeedAuthorizationIndexe
    * <b>Warning:</b> only use {@code null} during startup.
    */
   private void doIndexByProjectUuid(@Nullable String projectUuid, Size bulkSize) {
-    BulkIndexer bulk = new BulkIndexer(esClient, INDEX_TYPE_COMPONENT.getIndex(), bulkSize);
+    BulkIndexer bulk = new BulkIndexer(esClient, INDEX_TYPE_COMPONENT, bulkSize);
 
     bulk.start();
     try (DbSession dbSession = dbClient.openSession(false)) {
       dbClient.componentDao()
-        .selectForIndexing(dbSession, projectUuid, context -> {
+        .scrollForIndexing(dbSession, projectUuid, context -> {
           ComponentDto dto = context.getResultObject();
           bulk.add(newIndexRequest(toDocument(dto)));
         });
@@ -102,23 +149,23 @@ public class ComponentIndexer implements ProjectIndexer, NeedAuthorizationIndexe
     bulk.stop();
   }
 
-  @Override
-  public void deleteProject(String projectUuid) {
-    BulkIndexer.delete(esClient, INDEX_TYPE_COMPONENT.getIndex(), esClient.prepareSearch(INDEX_TYPE_COMPONENT)
-      .setQuery(boolQuery()
-        .filter(
-          termQuery(ComponentIndexDefinition.FIELD_PROJECT_UUID, projectUuid))));
+  private void addProjectDeletionToBulkIndexer(BulkIndexer bulkIndexer, String projectUuid) {
+    SearchRequestBuilder searchRequest = esClient.prepareSearch(INDEX_TYPE_COMPONENT)
+      .setQuery(QueryBuilders.termQuery(ComponentIndexDefinition.FIELD_PROJECT_UUID, projectUuid))
+      .setRouting(projectUuid);
+    bulkIndexer.addDeletion(searchRequest);
   }
 
   public void delete(String projectUuid, Collection<String> disabledComponentUuids) {
-    BulkIndexer bulk = new BulkIndexer(esClient, INDEX_TYPE_COMPONENT.getIndex(), Size.REGULAR);
+    BulkIndexer bulk = new BulkIndexer(esClient, INDEX_TYPE_COMPONENT, Size.REGULAR);
     bulk.start();
     disabledComponentUuids.forEach(uuid -> bulk.addDeletion(INDEX_TYPE_COMPONENT, uuid, projectUuid));
     bulk.stop();
   }
 
+  @VisibleForTesting
   void index(ComponentDto... docs) {
-    BulkIndexer bulk = new BulkIndexer(esClient, INDEX_TYPE_COMPONENT.getIndex(), Size.REGULAR);
+    BulkIndexer bulk = new BulkIndexer(esClient, INDEX_TYPE_COMPONENT, Size.REGULAR);
     bulk.start();
     Arrays.stream(docs)
       .map(ComponentIndexer::toDocument)
index ced1b25dda16bf4a05dfe6e3a7cbe44549769e72..00df270a2750c9738a79c998f9c03eefc6f9bcd3 100644 (file)
@@ -28,10 +28,12 @@ import java.util.Comparator;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
+import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 import org.sonar.api.resources.ResourceType;
 import org.sonar.api.resources.ResourceTypes;
@@ -315,6 +317,7 @@ public class SuggestionsAction implements ComponentsWsAction {
 
       List<Suggestion> suggestions = qualifier.getHits().stream()
         .map(hit -> toSuggestion(hit, recentlyBrowsedKeys, favoriteUuids, componentsByUuids, organizationByUuids, projectsByUuids))
+        .filter(Objects::nonNull)
         .collect(toList());
 
       return Category.newBuilder()
@@ -325,9 +328,17 @@ public class SuggestionsAction implements ComponentsWsAction {
     }).collect(toList());
   }
 
+  /**
+   * @return null when the component exists in Elasticsearch but not in database. That
+   * occurs when failed indexing requests are been recovering.
+   */
+  @CheckForNull
   private static Suggestion toSuggestion(ComponentHit hit, Set<String> recentlyBrowsedKeys, Set<String> favoriteUuids, Map<String, ComponentDto> componentsByUuids,
     Map<String, OrganizationDto> organizationByUuids, Map<String, ComponentDto> projectsByUuids) {
     ComponentDto result = componentsByUuids.get(hit.getUuid());
+    if (result == null) {
+      return null;
+    }
     String organizationKey = organizationByUuids.get(result.getOrganizationUuid()).getKey();
     checkState(organizationKey != null, "Organization with uuid '%s' not found", result.getOrganizationUuid());
     Suggestion.Builder builder = Suggestion.newBuilder()
@@ -340,8 +351,7 @@ public class SuggestionsAction implements ComponentsWsAction {
     if (QUALIFIERS_FOR_WHICH_TO_RETURN_PROJECT.contains(result.qualifier())) {
       builder.setProject(projectsByUuids.get(result.projectUuid()).getKey());
     }
-    return builder
-      .build();
+    return builder.build();
   }
 
   private static List<Organization> toOrganizations(Map<String, OrganizationDto> organizationByUuids) {
index 93404c0b81e6be0d6fc6a6cec0009fa0a39c954d..eef3eea4d0b87caa5b6e0cd4ab55694ae7ca526e 100644 (file)
@@ -53,7 +53,7 @@ public class BaseIssuesLoader {
   public List<DefaultIssue> loadForComponentUuid(String componentUuid) {
     try (DbSession dbSession = dbClient.openSession(false)) {
       List<DefaultIssue> result = new ArrayList<>();
-      dbSession.getMapper(IssueMapper.class).selectNonClosedByComponentUuid(componentUuid, resultContext -> {
+      dbSession.getMapper(IssueMapper.class).scrollNonClosedByComponentUuid(componentUuid, resultContext -> {
         DefaultIssue issue = (resultContext.getResultObject()).toDefaultIssue();
 
         // TODO this field should be set outside this class
index 39f9631c3b78608ad7980cc959bac6e3b1dd74a9..bc2ec1621cd0fd05ac4babb452462b7d0686c308 100644 (file)
@@ -42,7 +42,7 @@ public class IndexAnalysisStep implements ComputationStep {
     String projectUuid = treeRootHolder.getRoot().getUuid();
     for (ProjectIndexer indexer : indexers) {
       LOGGER.debug("Call {}", indexer);
-      indexer.indexProject(projectUuid, ProjectIndexer.Cause.NEW_ANALYSIS);
+      indexer.indexOnAnalysis(projectUuid);
     }
   }
 
index ee167116e172913878d730b7efeda6ab0f096ea4..a844a5f1d5908c845f9794b76d01b88fe8948966 100644 (file)
@@ -67,19 +67,19 @@ public class BulkIndexer {
   private static final int DEFAULT_NUMBER_OF_SHARDS = 5;
 
   private final EsClient client;
-  private final String indexName;
+  private final IndexType indexType;
   private final BulkProcessor bulkProcessor;
   private final IndexingResult result = new IndexingResult();
   private final IndexingListener indexingListener;
   private final SizeHandler sizeHandler;
 
-  public BulkIndexer(EsClient client, String indexName, Size size) {
-    this(client, indexName, size, IndexingListener.noop());
+  public BulkIndexer(EsClient client, IndexType indexType, Size size) {
+    this(client, indexType, size, IndexingListener.NOOP);
   }
 
-  public BulkIndexer(EsClient client, String indexName, Size size, IndexingListener indexingListener) {
+  public BulkIndexer(EsClient client, IndexType indexType, Size size, IndexingListener indexingListener) {
     this.client = client;
-    this.indexName = indexName;
+    this.indexType = indexType;
     this.sizeHandler = size.createHandler(Runtime2.INSTANCE);
     this.indexingListener = indexingListener;
     BulkProcessorListener bulkProcessorListener = new BulkProcessorListener();
@@ -91,6 +91,10 @@ public class BulkIndexer {
       .build();
   }
 
+  public IndexType getIndexType() {
+    return indexType;
+  }
+
   public void start() {
     result.clear();
     sizeHandler.beforeStart(this);
@@ -106,12 +110,13 @@ public class BulkIndexer {
       Thread.currentThread().interrupt();
       throw new IllegalStateException("Elasticsearch bulk requests still being executed after 1 minute", e);
     }
-    client.prepareRefresh(indexName).get();
+    client.prepareRefresh(indexType.getIndex()).get();
     sizeHandler.afterStop(this);
+    indexingListener.onFinish(result);
     return result;
   }
 
-  public void add(ActionRequest<?> request) {
+  public void add(ActionRequest request) {
     result.incrementRequests();
     bulkProcessor.add(request);
   }
@@ -163,10 +168,10 @@ public class BulkIndexer {
    * Delete all the documents matching the given search request. This method is blocking.
    * Index is refreshed, so docs are not searchable as soon as method is executed.
    *
-   * Note that the parameter indexName could be removed if progress logs are not needed.
+   * Note that the parameter indexType could be removed if progress logs are not needed.
    */
-  public static IndexingResult delete(EsClient client, String indexName, SearchRequestBuilder searchRequest) {
-    BulkIndexer bulk = new BulkIndexer(client, indexName, Size.REGULAR);
+  public static IndexingResult delete(EsClient client, IndexType indexType, SearchRequestBuilder searchRequest) {
+    BulkIndexer bulk = new BulkIndexer(client, indexType, Size.REGULAR);
     bulk.start();
     bulk.addDeletion(searchRequest);
     return bulk.stop();
@@ -180,16 +185,15 @@ public class BulkIndexer {
 
     @Override
     public void afterBulk(long executionId, BulkRequest request, BulkResponse response) {
-      List<String> successDocIds = new ArrayList<>();
+      List<DocId> successDocIds = new ArrayList<>();
       for (BulkItemResponse item : response.getItems()) {
         if (item.isFailed()) {
           LOGGER.error("index [{}], type [{}], id [{}], message [{}]", item.getIndex(), item.getType(), item.getId(), item.getFailureMessage());
         } else {
           result.incrementSuccess();
-          successDocIds.add(item.getId());
+          successDocIds.add(new DocId(item.getIndex(), item.getType(), item.getId()));
         }
       }
-
       indexingListener.onSuccess(successDocIds);
     }
 
@@ -270,21 +274,21 @@ public class BulkIndexer {
 
     @Override
     void beforeStart(BulkIndexer bulkIndexer) {
-      this.progress = new ProgressLogger(format("Progress[BulkIndexer[%s]]", bulkIndexer.indexName), bulkIndexer.result.total, LOGGER)
+      this.progress = new ProgressLogger(format("Progress[BulkIndexer[%s]]", bulkIndexer.indexType.getIndex()), bulkIndexer.result.total, LOGGER)
         .setPluralLabel("requests");
       this.progress.start();
       Map<String, Object> temporarySettings = new HashMap<>();
-      GetSettingsResponse settingsResp = bulkIndexer.client.nativeClient().admin().indices().prepareGetSettings(bulkIndexer.indexName).get();
+      GetSettingsResponse settingsResp = bulkIndexer.client.nativeClient().admin().indices().prepareGetSettings(bulkIndexer.indexType.getIndex()).get();
 
       // deactivate replicas
-      int initialReplicas = Integer.parseInt(settingsResp.getSetting(bulkIndexer.indexName, IndexMetaData.SETTING_NUMBER_OF_REPLICAS));
+      int initialReplicas = Integer.parseInt(settingsResp.getSetting(bulkIndexer.indexType.getIndex(), IndexMetaData.SETTING_NUMBER_OF_REPLICAS));
       if (initialReplicas > 0) {
         initialSettings.put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, initialReplicas);
         temporarySettings.put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0);
       }
 
       // deactivate periodical refresh
-      String refreshInterval = settingsResp.getSetting(bulkIndexer.indexName, REFRESH_INTERVAL_SETTING);
+      String refreshInterval = settingsResp.getSetting(bulkIndexer.indexType.getIndex(), REFRESH_INTERVAL_SETTING);
       initialSettings.put(REFRESH_INTERVAL_SETTING, refreshInterval);
       temporarySettings.put(REFRESH_INTERVAL_SETTING, "-1");
 
@@ -296,14 +300,14 @@ public class BulkIndexer {
       // optimize lucene segments and revert index settings
       // Optimization must be done before re-applying replicas:
       // http://www.elasticsearch.org/blog/performance-considerations-elasticsearch-indexing/
-      bulkIndexer.client.prepareForceMerge(bulkIndexer.indexName).get();
+      bulkIndexer.client.prepareForceMerge(bulkIndexer.indexType.getIndex()).get();
 
       updateSettings(bulkIndexer, initialSettings);
       this.progress.stop();
     }
 
     private static void updateSettings(BulkIndexer bulkIndexer, Map<String, Object> settings) {
-      UpdateSettingsRequestBuilder req = bulkIndexer.client.nativeClient().admin().indices().prepareUpdateSettings(bulkIndexer.indexName);
+      UpdateSettingsRequestBuilder req = bulkIndexer.client.nativeClient().admin().indices().prepareUpdateSettings(bulkIndexer.indexType.getIndex());
       req.setSettings(settings);
       req.get();
     }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/DocId.java b/server/sonar-server/src/main/java/org/sonar/server/es/DocId.java
new file mode 100644 (file)
index 0000000..98365a1
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.es;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+class DocId {
+
+  private final String index;
+  private final String indexType;
+  private final String id;
+
+  DocId(IndexType indexType, String id) {
+    this(indexType.getIndex(), indexType.getType(), id);
+  }
+
+  DocId(String index, String indexType, String id) {
+    this.index = index;
+    this.indexType = indexType;
+    this.id = id;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    DocId docId = (DocId) o;
+
+    if (!index.equals(docId.index)) {
+      return false;
+    }
+    if (!indexType.equals(docId.indexType)) {
+      return false;
+    }
+    return id.equals(docId.id);
+  }
+
+  @Override
+  public int hashCode() {
+    int result = index.hashCode();
+    result = 31 * result + indexType.hashCode();
+    result = 31 * result + id.hashCode();
+    return result;
+  }
+}
index 529329e74e4b542e1baeaa40ce5e2aea9bd71d39..7fe8e4205bf9e912e36bb8e1f1766d0120c0efe2 100644 (file)
@@ -33,8 +33,6 @@ import java.util.function.Function;
 import java.util.regex.Pattern;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
-import org.elasticsearch.action.bulk.BulkRequestBuilder;
-import org.elasticsearch.action.bulk.BulkResponse;
 import org.elasticsearch.action.search.SearchScrollRequestBuilder;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.search.SearchHit;
@@ -43,8 +41,6 @@ import org.elasticsearch.search.aggregations.bucket.terms.Terms;
 import org.joda.time.format.ISODateTimeFormat;
 import org.sonar.core.util.stream.MoreCollectors;
 
-import static java.lang.String.format;
-
 public class EsUtils {
 
   public static final int SCROLL_TIME_IN_MINUTES = 3;
@@ -99,15 +95,6 @@ public class EsUtils {
     return null;
   }
 
-  public static BulkResponse executeBulkRequest(BulkRequestBuilder builder, String errorMessage, Object... errorMessageArgs) {
-    BulkResponse bulkResponse = builder.get();
-    if (bulkResponse.hasFailures()) {
-      // do not use Preconditions as the message is expensive to generate (see buildFailureMessage())
-      throw new IllegalStateException(format(errorMessage, errorMessageArgs) + ": " + bulkResponse.buildFailureMessage());
-    }
-    return bulkResponse;
-  }
-
   public static <D extends BaseDoc> Iterator<D> scroll(EsClient esClient, String scrollId, Function<Map<String, Object>, D> docConverter) {
     return new DocScrollIterator<>(esClient, scrollId, docConverter);
   }
index 4ead86465ef7fddfc87cd8e36da70aa770297b4d..0180521625044d1d6b085ce2a03b62b7545e26cd 100644 (file)
@@ -19,7 +19,9 @@
  */
 package org.sonar.server.es;
 
+import com.google.common.base.Splitter;
 import java.util.Arrays;
+import java.util.List;
 import java.util.function.Function;
 import org.sonar.core.util.stream.MoreCollectors;
 
@@ -27,12 +29,17 @@ import static java.util.Objects.requireNonNull;
 
 public class IndexType {
 
+  private static final String SEPARATOR = "/";
+  private static final Splitter SEPARATOR_SPLITTER = Splitter.on(SEPARATOR);
+
   private final String index;
   private final String type;
+  private final String key;
 
   public IndexType(String index, String type) {
     this.index = requireNonNull(index);
     this.type = requireNonNull(type);
+    this.key = index + SEPARATOR + type;
   }
 
   public String getIndex() {
@@ -55,6 +62,21 @@ public class IndexType {
     return Arrays.stream(indexTypes).map(function).collect(MoreCollectors.toSet(indexTypes.length)).toArray(new String[0]);
   }
 
+  public String format() {
+    return key;
+  }
+
+  /**
+   * Parse a String generated by {@link #format()}
+   */
+  public static IndexType parse(String s) {
+    List<String> split = SEPARATOR_SPLITTER.splitToList(s);
+    if (split.size() != 2) {
+      throw new IllegalArgumentException("Unsupported IndexType value: " + s);
+    }
+    return new IndexType(split.get(0), split.get(1));
+  }
+
   @Override
   public boolean equals(Object o) {
     if (this == o) {
@@ -63,18 +85,14 @@ public class IndexType {
     if (o == null || getClass() != o.getClass()) {
       return false;
     }
-    IndexType that = (IndexType) o;
-    if (!index.equals(that.index)) {
-      return false;
-    }
-    return type.equals(that.type);
+
+    IndexType indexType = (IndexType) o;
+    return key.equals(indexType.key);
   }
 
   @Override
   public int hashCode() {
-    int result = index.hashCode();
-    result = 31 * result + type.hashCode();
-    return result;
+    return key.hashCode();
   }
 
   @Override
index 717f290d8be0ce17499cffc636700df506d458e2..c30695280bcfb061faa3fa7ede27907f71f26558 100644 (file)
@@ -53,7 +53,7 @@ public class IndexerStartupTask {
   public void execute() {
     if (indexesAreEnabled()) {
       stream(indexers)
-        .forEach(this::indexEmptyTypes);
+        .forEach(this::indexUninitializedTypes);
     }
   }
 
@@ -61,7 +61,7 @@ public class IndexerStartupTask {
     return !config.getBoolean("sonar.internal.es.disableIndexes").orElse(false);
   }
 
-  private void indexEmptyTypes(StartupIndexer indexer) {
+  private void indexUninitializedTypes(StartupIndexer indexer) {
     Set<IndexType> uninizializedTypes = getUninitializedTypes(indexer);
     if (!uninizializedTypes.isEmpty()) {
       Profiler profiler = Profiler.create(LOG);
@@ -80,7 +80,7 @@ public class IndexerStartupTask {
     return isUninitialized(indexType, esClient);
   }
 
-  public static boolean isUninitialized(IndexType indexType, EsClient esClient) {
+  private static boolean isUninitialized(IndexType indexType, EsClient esClient) {
     String setting = esClient.nativeClient().admin().indices().prepareGetSettings(indexType.getIndex()).get().getSetting(indexType.getIndex(),
       getInitializedSettingName(indexType));
     return !"true".equals(setting);
index 639931780bda96d04d2916c3df9e449b746d7d57..ce74bd9cddef0fcb4cf0c2ff8624d2fbdc794589 100644 (file)
  */
 package org.sonar.server.es;
 
-import java.util.Collection;
+import java.util.List;
 
 public interface IndexingListener {
 
-  void onSuccess(Collection<String> docIds);
+  void onSuccess(List<DocId> docIds);
 
-  static IndexingListener noop() {
-    return docIds -> {};
-  }
+  void onFinish(IndexingResult result);
+
+  IndexingListener NOOP = new IndexingListener() {
+    @Override
+    public void onSuccess(List<DocId> docIds) {
+      // nothing to do
+    }
+
+    @Override
+    public void onFinish(IndexingResult result) {
+      // nothing to do
+    }
+  };
 }
index e66d842ed4bde2127df1752a6c699606d2d96ccd..b178ddc50777eff3fababa30dc6c26cfee0e2977 100644 (file)
@@ -34,11 +34,11 @@ public class IndexingResult {
     return this;
   }
 
-  void incrementRequests() {
+  public void incrementRequests() {
     total.incrementAndGet();
   }
 
-  IndexingResult incrementSuccess() {
+  public IndexingResult incrementSuccess() {
     successes += 1;
     return this;
   }
@@ -60,13 +60,8 @@ public class IndexingResult {
     return successes;
   }
 
-  /**
-   * Get the failure ratio,
-   * if the total is 0, we always return 1 in order to break loop
-   * @see {@link RecoveryIndexer#recover()}
-   */
-  public double getFailureRatio() {
-    return total.get() == 0 ? 1 : ((1.0d * getFailures()) / total.get());
+  public double getSuccessRatio() {
+    return total.get() == 0 ? 1.0 : ((1.0 * successes) / total.get());
   }
 
   public boolean isSuccess() {
diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/OneToManyResilientIndexingListener.java b/server/sonar-server/src/main/java/org/sonar/server/es/OneToManyResilientIndexingListener.java
new file mode 100644 (file)
index 0000000..f777222
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.es;
+
+import java.util.Collection;
+import java.util.List;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.es.EsQueueDto;
+
+/**
+ * Clean-up the db table "es_queue" when documents
+ * are successfully indexed so that the recovery
+ * daemon does not re-index them.
+ *
+ * This implementation assumes that one row in table es_queue
+ * is associated to multiple index documents. The column
+ * es_queue.doc_id is not equal to ids of documents.
+ *
+ * Important. All the provided EsQueueDto instances must
+ * reference documents involved in the BulkIndexer call, otherwise
+ * some items will be marked as successfully processed, even
+ * if not processed at all.
+ */
+public class OneToManyResilientIndexingListener implements IndexingListener {
+
+  private final DbClient dbClient;
+  private final DbSession dbSession;
+  private final Collection<EsQueueDto> items;
+
+  public OneToManyResilientIndexingListener(DbClient dbClient, DbSession dbSession, Collection<EsQueueDto> items) {
+    this.dbClient = dbClient;
+    this.dbSession = dbSession;
+    this.items = items;
+  }
+
+  @Override
+  public void onSuccess(List<DocId> successDocIds) {
+    // it's not possible to deduce which ES_QUEUE row
+    // must be deleted. For example:
+    // items: project P1
+    // successDocIds: issue 1 and issue 2
+    // --> no relationship between items and successDocIds
+  }
+
+  @Override
+  public void onFinish(IndexingResult result) {
+    if (result.isSuccess()) {
+      dbClient.esQueueDao().delete(dbSession, items);
+      dbSession.commit();
+    }
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/OneToOneResilientIndexingListener.java b/server/sonar-server/src/main/java/org/sonar/server/es/OneToOneResilientIndexingListener.java
new file mode 100644 (file)
index 0000000..be7ffdd
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.es;
+
+import com.google.common.collect.Multimap;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.es.EsQueueDto;
+
+/**
+ * Clean-up the db table "es_queue" when documents
+ * are successfully indexed so that the recovery
+ * daemon does not re-index them.
+ *
+ * This implementation assumes that one row in table es_queue
+ * is associated to one index document and that es_queue.doc_id
+ * equals document id.
+ */
+public class OneToOneResilientIndexingListener implements IndexingListener {
+
+  private final DbClient dbClient;
+  private final DbSession dbSession;
+  private final Multimap<DocId, EsQueueDto> itemsById;
+
+  public OneToOneResilientIndexingListener(DbClient dbClient, DbSession dbSession, Collection<EsQueueDto> items) {
+    this.dbClient = dbClient;
+    this.dbSession = dbSession;
+    this.itemsById = items.stream()
+      .collect(MoreCollectors.index(i -> new DocId(IndexType.parse(i.getDocType()), i.getDocId()), Function.identity()));
+  }
+
+  @Override
+  public void onSuccess(List<DocId> successDocIds) {
+    if (!successDocIds.isEmpty()) {
+      Collection<EsQueueDto> itemsToDelete = successDocIds.stream()
+        .map(itemsById::get)
+        .flatMap(Collection::stream)
+        .filter(Objects::nonNull)
+        .collect(MoreCollectors.toArrayList());
+      dbClient.esQueueDao().delete(dbSession, itemsToDelete);
+      dbSession.commit();
+    }
+  }
+
+  @Override
+  public void onFinish(IndexingResult result) {
+    // nothing to do, items that have been successfully indexed
+    // are already deleted from db (see method onSuccess())
+  }
+}
index 0c4a11fd9fc00b1aade2ed480b17a02f6f7e3e38..c36c7a5076e1500e4639f08b311c84af3ae141b2 100644 (file)
  */
 package org.sonar.server.es;
 
+import java.util.Collection;
+import org.sonar.db.DbSession;
+import org.sonar.db.es.EsQueueDto;
+
 /**
  * A {@link ProjectIndexer} populates an Elasticsearch index
  * containing project-related documents, for instance issues
@@ -29,10 +33,14 @@ package org.sonar.server.es;
  * then the implementation of {@link ProjectIndexer} must
  * also implement {@link org.sonar.server.permission.index.NeedAuthorizationIndexer}
  */
-public interface ProjectIndexer {
+public interface ProjectIndexer extends ResilientIndexer {
 
   enum Cause {
-    PROJECT_CREATION, PROJECT_KEY_UPDATE, NEW_ANALYSIS, PROJECT_TAGS_UPDATE
+    PROJECT_CREATION,
+    PROJECT_DELETION,
+    PROJECT_KEY_UPDATE,
+    PROJECT_TAGS_UPDATE,
+    PERMISSION_CHANGE
   }
 
   /**
@@ -40,19 +48,8 @@ public interface ProjectIndexer {
    * for example when project is created or when a new analysis
    * is being processed.
    * @param projectUuid non-null UUID of project
-   * @param cause the reason why indexing is triggered. That
-   *              allows some implementations to ignore
-   *              re-indexing in some cases. For example
-   *              there is no need to index measures when
-   *              a project is being created because they
-   *              are not computed yet.
-   */
-  void indexProject(String projectUuid, Cause cause);
-
-  /**
-   * This method is called when a project is deleted.
-   * @param projectUuid non-null UUID of project
    */
-  void deleteProject(String projectUuid);
+  void indexOnAnalysis(String projectUuid);
 
+  Collection<EsQueueDto> prepareForRecovery(DbSession dbSession, Collection<String> projectUuids, ProjectIndexer.Cause cause);
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/ProjectIndexers.java b/server/sonar-server/src/main/java/org/sonar/server/es/ProjectIndexers.java
new file mode 100644 (file)
index 0000000..51a9b34
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.es;
+
+import java.util.Collection;
+import org.sonar.db.DbSession;
+
+public interface ProjectIndexers {
+
+  /**
+   * Commits the DB transaction and indexes the specified projects, if needed (according to
+   * "cause" parameter).
+   */
+  void commitAndIndex(DbSession dbSession, Collection<String> projectUuid, ProjectIndexer.Cause cause);
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/ProjectIndexersImpl.java b/server/sonar-server/src/main/java/org/sonar/server/es/ProjectIndexersImpl.java
new file mode 100644 (file)
index 0000000..d26ba07
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.es;
+
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import org.sonar.db.DbSession;
+import org.sonar.db.es.EsQueueDto;
+
+import static java.util.Arrays.asList;
+
+public class ProjectIndexersImpl implements ProjectIndexers {
+
+  private final List<ProjectIndexer> indexers;
+
+  public ProjectIndexersImpl(ProjectIndexer... indexers) {
+    this.indexers = asList(indexers);
+  }
+
+  @Override
+  public void commitAndIndex(DbSession dbSession, Collection<String> projectUuids, ProjectIndexer.Cause cause) {
+    Map<ProjectIndexer, Collection<EsQueueDto>> itemsByIndexer = new IdentityHashMap<>();
+    indexers.forEach(i -> itemsByIndexer.put(i, i.prepareForRecovery(dbSession, projectUuids, cause)));
+    dbSession.commit();
+
+    // ensure that indexer#index() is called only with the item type that it supports
+    itemsByIndexer.forEach((indexer, items) -> indexer.index(dbSession, items));
+  }
+}
index 709f652e88c24925685062c10feaeb7fb824bbca..3cb2731267943628b3e2c7d6c192d9997c2b6716 100644 (file)
 
 package org.sonar.server.es;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ListMultimap;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
@@ -38,9 +41,6 @@ import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.es.EsQueueDto;
-import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
-import org.sonar.server.rule.index.RuleIndexer;
-import org.sonar.server.user.index.UserIndexer;
 
 import static java.lang.String.format;
 
@@ -55,7 +55,7 @@ public class RecoveryIndexer implements Startable {
   private static final long DEFAULT_DELAY_IN_MS = 5L * 60 * 1000;
   private static final long DEFAULT_MIN_AGE_IN_MS = 5L * 60 * 1000;
   private static final int DEFAULT_LOOP_LIMIT = 10_000;
-  private static final double CIRCUIT_BREAKER_IN_PERCENT = 0.3;
+  private static final double CIRCUIT_BREAKER_IN_PERCENT = 0.7;
 
   private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1,
     new ThreadFactoryBuilder()
@@ -65,20 +65,16 @@ public class RecoveryIndexer implements Startable {
   private final System2 system2;
   private final Configuration config;
   private final DbClient dbClient;
-  private final UserIndexer userIndexer;
-  private final RuleIndexer ruleIndexer;
-  private final ActiveRuleIndexer activeRuleIndexer;
+  private final Map<IndexType, ResilientIndexer> indexersByType;
   private final long minAgeInMs;
   private final long loopLimit;
 
-  public RecoveryIndexer(System2 system2, Configuration config, DbClient dbClient,
-    UserIndexer userIndexer, RuleIndexer ruleIndexer, ActiveRuleIndexer activeRuleIndexer) {
+  public RecoveryIndexer(System2 system2, Configuration config, DbClient dbClient, ResilientIndexer... indexers) {
     this.system2 = system2;
     this.config = config;
     this.dbClient = dbClient;
-    this.userIndexer = userIndexer;
-    this.ruleIndexer = ruleIndexer;
-    this.activeRuleIndexer = activeRuleIndexer;
+    this.indexersByType = new HashMap<>();
+    Arrays.stream(indexers).forEach(i -> i.getIndexTypes().forEach(indexType -> indexersByType.put(indexType, i)));
     this.minAgeInMs = getSetting(PROPERTY_MIN_AGE, DEFAULT_MIN_AGE_IN_MS);
     this.loopLimit = getSetting(PROPERTY_LOOP_LIMIT, DEFAULT_LOOP_LIMIT);
   }
@@ -110,6 +106,7 @@ public class RecoveryIndexer implements Startable {
     }
   }
 
+  @VisibleForTesting
   void recover() {
     try (DbSession dbSession = dbClient.openSession(false)) {
       Profiler profiler = Profiler.create(LOGGER).start();
@@ -120,16 +117,18 @@ public class RecoveryIndexer implements Startable {
       while (!items.isEmpty()) {
         IndexingResult loopResult = new IndexingResult();
 
-        ListMultimap<EsQueueDto.Type, EsQueueDto> itemsByType = groupItemsByType(items);
-        for (Map.Entry<EsQueueDto.Type, Collection<EsQueueDto>> entry : itemsByType.asMap().entrySet()) {
-          loopResult.add(doIndex(dbSession, entry.getKey(), entry.getValue()));
-        }
-
+        groupItemsByType(items).asMap().forEach((type, typeItems) -> loopResult.add(doIndex(dbSession, type, typeItems)));
         result.add(loopResult);
-        if (loopResult.getFailureRatio() >= CIRCUIT_BREAKER_IN_PERCENT) {
+
+        if (loopResult.getSuccessRatio() <= CIRCUIT_BREAKER_IN_PERCENT) {
           LOGGER.error(LOG_PREFIX + "too many failures [{}/{} documents], waiting for next run", loopResult.getFailures(), loopResult.getTotal());
           break;
         }
+
+        if (loopResult.getTotal() == 0L) {
+          break;
+        }
+
         items = dbClient.esQueueDao().selectForRecovery(dbSession, beforeDate, loopLimit);
       }
       if (result.getTotal() > 0L) {
@@ -140,24 +139,19 @@ public class RecoveryIndexer implements Startable {
     }
   }
 
-  private IndexingResult doIndex(DbSession dbSession, EsQueueDto.Type type, Collection<EsQueueDto> typeItems) {
+  private IndexingResult doIndex(DbSession dbSession, IndexType type, Collection<EsQueueDto> typeItems) {
     LOGGER.trace(LOG_PREFIX + "processing {} {}", typeItems.size(), type);
-    switch (type) {
-      case USER:
-        return userIndexer.index(dbSession, typeItems);
-      case RULE_EXTENSION:
-      case RULE:
-        return ruleIndexer.index(dbSession, typeItems);
-      case ACTIVE_RULE:
-        return activeRuleIndexer.index(dbSession, typeItems);
-      default:
-        LOGGER.error(LOG_PREFIX + "ignore {} documents with unsupported type {}", typeItems.size(), type);
-        return new IndexingResult();
+
+    ResilientIndexer indexer = indexersByType.get(type);
+    if (indexer == null) {
+      LOGGER.error(LOG_PREFIX + "ignore {} items with unsupported type {}", typeItems.size(), type);
+      return new IndexingResult();
     }
+    return indexer.index(dbSession, typeItems);
   }
 
-  private static ListMultimap<EsQueueDto.Type, EsQueueDto> groupItemsByType(Collection<EsQueueDto> items) {
-    return items.stream().collect(MoreCollectors.index(EsQueueDto::getDocType));
+  private static ListMultimap<IndexType, EsQueueDto> groupItemsByType(Collection<EsQueueDto> items) {
+    return items.stream().collect(MoreCollectors.index(i -> IndexType.parse(i.getDocType())));
   }
 
   private long getSetting(String key, long defaultValue) {
diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/ResiliencyIndexingListener.java b/server/sonar-server/src/main/java/org/sonar/server/es/ResiliencyIndexingListener.java
deleted file mode 100644 (file)
index cc29e14..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.server.es;
-
-import com.google.common.collect.Multimap;
-import java.util.Collection;
-import java.util.Objects;
-import java.util.function.Function;
-import org.sonar.core.util.stream.MoreCollectors;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.es.EsQueueDto;
-
-/**
- * Clean-up the db table es_queue when documents
- * are successfully indexed so that the recovery
- * daemon does not re-index them.
- */
-public class ResiliencyIndexingListener implements IndexingListener {
-
-  private final DbClient dbClient;
-  private final DbSession dbSession;
-  private final Collection<EsQueueDto> items;
-
-  public ResiliencyIndexingListener(DbClient dbClient, DbSession dbSession, Collection<EsQueueDto> items) {
-    this.dbClient = dbClient;
-    this.dbSession = dbSession;
-    this.items = items;
-  }
-
-  @Override
-  public void onSuccess(Collection<String> docIds) {
-    if (!docIds.isEmpty()) {
-      Multimap<String, EsQueueDto> itemsById = items.stream().collect(MoreCollectors.index(EsQueueDto::getDocId, Function.identity()));
-
-      Collection<EsQueueDto> itemsToDelete = docIds
-        .stream()
-        .map(itemsById::get)
-        .flatMap(Collection::stream)
-        .filter(Objects::nonNull)
-        .collect(MoreCollectors.toArrayList(docIds.size()));
-      dbClient.esQueueDao().delete(dbSession, itemsToDelete);
-      dbSession.commit();
-    }
-  }
-}
index b717c831e09c1ede44315cf2d6611f664c680ca2..a54e4317f9476b68866a5f305a0a13885d818c57 100644 (file)
@@ -27,7 +27,7 @@ import org.sonar.db.es.EsQueueDto;
 /**
  * This kind of indexers that are resilient
  */
-public interface ResilientIndexer {
+public interface ResilientIndexer extends StartupIndexer {
 
   /**
    * Index the items and delete them from es_queue table when the indexation
index 2d039842c3cfd13ea9470f3a0453e195e6acdf65..3097b673276c824ebf90d8ef0452d90468cd6588 100644 (file)
@@ -85,7 +85,7 @@ public abstract class IssueStorage {
     }
   }
 
-  private Collection<IssueDto> doSave(DbSession session, Iterable<DefaultIssue> issues) {
+  private Collection<IssueDto> doSave(DbSession dbSession, Iterable<DefaultIssue> issues) {
     // Batch session can not be used for updates. It does not return the number of updated rows,
     // required for detecting conflicts.
     long now = system2.now();
@@ -94,18 +94,17 @@ public abstract class IssueStorage {
     List<DefaultIssue> issuesToInsert = firstNonNull(issuesNewOrUpdated.get(true), emptyList());
     List<DefaultIssue> issuesToUpdate = firstNonNull(issuesNewOrUpdated.get(false), emptyList());
 
-    Collection<IssueDto> inserted = insert(session, issuesToInsert, now);
+    Collection<IssueDto> inserted = insert(dbSession, issuesToInsert, now);
     Collection<IssueDto> updated = update(issuesToUpdate, now);
 
-    doAfterSave(Stream.concat(inserted.stream(), updated.stream())
-      .map(IssueDto::getKey)
+    doAfterSave(dbSession, Stream.concat(inserted.stream(), updated.stream())
       .collect(toSet(issuesToInsert.size() + issuesToUpdate.size())));
 
     return Stream.concat(inserted.stream(), updated.stream())
       .collect(toSet(issuesToInsert.size() + issuesToUpdate.size()));
   }
 
-  protected void doAfterSave(Collection<String> issues) {
+  protected void doAfterSave(DbSession dbSession, Collection<IssueDto> issues) {
     // overridden on server-side to index ES
   }
 
index 6e0fe687338492fe84e08cda154b00da4366bfb4..74ca0c4448fed316a333514d6ff5995c63b29a32 100644 (file)
@@ -62,8 +62,8 @@ public class ServerIssueStorage extends IssueStorage {
   }
 
   @Override
-  protected void doAfterSave(Collection<String> issueKeys) {
-    indexer.index(issueKeys);
+  protected void doAfterSave(DbSession dbSession, Collection<IssueDto> issues) {
+    indexer.commitAndIndexIssues(dbSession, issues);
   }
 
   protected ComponentDto component(DbSession session, DefaultIssue issue) {
index 3a93ccd41abd4e0ba3c1758a9f85537c429f74a0..eef37179e5b1492e86fd79f7e804a4c9a68c5d30 100644 (file)
@@ -272,4 +272,5 @@ public class IssueDoc extends BaseDoc {
     setField(IssueIndexDefinition.FIELD_ISSUE_ORGANIZATION_UUID, s);
     return this;
   }
+
 }
index 5dbf88356bb69ea57412f2c512bc9e5aa847c519..45d2c4708ba3f7a23f2ef557404622aab6bf21c9 100644 (file)
  */
 package org.sonar.server.issue.index;
 
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
-import javax.annotation.Nullable;
-import org.elasticsearch.action.bulk.BulkRequestBuilder;
 import org.elasticsearch.action.index.IndexRequest;
 import org.elasticsearch.action.search.SearchRequestBuilder;
 import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.es.EsQueueDto;
+import org.sonar.db.issue.IssueDto;
 import org.sonar.server.es.BulkIndexer;
 import org.sonar.server.es.BulkIndexer.Size;
 import org.sonar.server.es.EsClient;
-import org.sonar.server.es.EsUtils;
 import org.sonar.server.es.IndexType;
+import org.sonar.server.es.IndexingListener;
+import org.sonar.server.es.IndexingResult;
+import org.sonar.server.es.OneToManyResilientIndexingListener;
+import org.sonar.server.es.OneToOneResilientIndexingListener;
 import org.sonar.server.es.ProjectIndexer;
-import org.sonar.server.es.StartupIndexer;
 import org.sonar.server.permission.index.AuthorizationScope;
 import org.sonar.server.permission.index.NeedAuthorizationIndexer;
 
+import static java.util.Collections.emptyList;
 import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
 import static org.elasticsearch.index.query.QueryBuilders.termQuery;
 import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID;
 import static org.sonar.server.issue.index.IssueIndexDefinition.INDEX_TYPE_ISSUE;
 
-public class IssueIndexer implements ProjectIndexer, NeedAuthorizationIndexer, StartupIndexer {
+public class IssueIndexer implements ProjectIndexer, NeedAuthorizationIndexer {
 
-  private static final String DELETE_ERROR_MESSAGE = "Fail to delete some issues of project [%s]";
-  private static final int MAX_BATCH_SIZE = 1000;
+  /**
+   * Indicates that es_queue.doc_id references an issue. Only this issue must be indexed.
+   */
+  private static final String ID_TYPE_ISSUE_KEY = "issueKey";
+  /**
+   * Indicates that es_queue.doc_id references a project. All the issues of the project must be indexed.
+   */
+  private static final String ID_TYPE_PROJECT_UUID = "projectUuid";
+  private static final Logger LOGGER = Loggers.get(IssueIndexer.class);
   private static final AuthorizationScope AUTHORIZATION_SCOPE = new AuthorizationScope(INDEX_TYPE_ISSUE, project -> Qualifiers.PROJECT.equals(project.getQualifier()));
+  private static final ImmutableSet<IndexType> INDEX_TYPES = ImmutableSet.of(INDEX_TYPE_ISSUE);
 
   private final EsClient esClient;
+  private final DbClient dbClient;
   private final IssueIteratorFactory issueIteratorFactory;
 
-  public IssueIndexer(EsClient esClient, IssueIteratorFactory issueIteratorFactory) {
+  public IssueIndexer(EsClient esClient, DbClient dbClient, IssueIteratorFactory issueIteratorFactory) {
     this.esClient = esClient;
+    this.dbClient = dbClient;
     this.issueIteratorFactory = issueIteratorFactory;
   }
 
@@ -65,26 +86,40 @@ public class IssueIndexer implements ProjectIndexer, NeedAuthorizationIndexer, S
 
   @Override
   public Set<IndexType> getIndexTypes() {
-    return ImmutableSet.of(INDEX_TYPE_ISSUE);
+    return INDEX_TYPES;
   }
 
   @Override
   public void indexOnStartup(Set<IndexType> uninitializedIndexTypes) {
-    doIndex(createBulkIndexer(Size.LARGE), (String) null);
+    try (IssueIterator issues = issueIteratorFactory.createForAll()) {
+      doIndex(issues, Size.LARGE, IndexingListener.NOOP);
+    }
   }
 
   @Override
-  public void indexProject(String projectUuid, Cause cause) {
+  public void indexOnAnalysis(String projectUuid) {
+    try (IssueIterator issues = issueIteratorFactory.createForProject(projectUuid)) {
+      doIndex(issues, Size.REGULAR, IndexingListener.NOOP);
+    }
+  }
+
+  @Override
+  public Collection<EsQueueDto> prepareForRecovery(DbSession dbSession, Collection<String> projectUuids, ProjectIndexer.Cause cause) {
     switch (cause) {
       case PROJECT_CREATION:
         // nothing to do, issues do not exist at project creation
       case PROJECT_KEY_UPDATE:
       case PROJECT_TAGS_UPDATE:
-        // nothing to do, project key and tags are not used in this index
-        break;
-      case NEW_ANALYSIS:
-        doIndex(createBulkIndexer(Size.REGULAR), projectUuid);
-        break;
+      case PERMISSION_CHANGE:
+        // nothing to do, permissions, project key and tags are not used in type issues/issue
+        return emptyList();
+
+      case PROJECT_DELETION:
+        List<EsQueueDto> items = projectUuids.stream()
+          .map(projectUuid -> createQueueDto(projectUuid, ID_TYPE_PROJECT_UUID, projectUuid))
+          .collect(MoreCollectors.toArrayList(projectUuids.size()));
+        return dbClient.esQueueDao().insert(dbSession, items);
+
       default:
         // defensive case
         throw new IllegalStateException("Unsupported cause: " + cause);
@@ -92,29 +127,118 @@ public class IssueIndexer implements ProjectIndexer, NeedAuthorizationIndexer, S
   }
 
   /**
-   * For benchmarks
+   * Commits the DB transaction and adds the issues to Elasticsearch index.
+   * <p>
+   * If indexing fails, then the recovery daemon will retry later and this
+   * method successfully returns. Meanwhile these issues will be "eventually
+   * consistent" when requesting the index.
    */
-  public void index(Iterator<IssueDoc> issues) {
-    doIndex(createBulkIndexer(Size.REGULAR), issues);
+  public void commitAndIndexIssues(DbSession dbSession, Collection<IssueDto> issues) {
+    ListMultimap<String, EsQueueDto> itemsByIssueKey = ArrayListMultimap.create();
+    issues.stream()
+      .map(issue -> createQueueDto(issue.getKey(), ID_TYPE_ISSUE_KEY, issue.getProjectUuid()))
+      // a mutable ListMultimap is needed for doIndexIssueItems, so MoreCollectors.index() is
+      // not used
+      .forEach(i -> itemsByIssueKey.put(i.getDocId(), i));
+    dbClient.esQueueDao().insert(dbSession, itemsByIssueKey.values());
+
+    dbSession.commit();
+
+    doIndexIssueItems(dbSession, itemsByIssueKey);
   }
 
-  public void index(Collection<String> issueKeys) {
-    doIndex(createBulkIndexer(Size.REGULAR), issueKeys);
+  @Override
+  public IndexingResult index(DbSession dbSession, Collection<EsQueueDto> items) {
+    ListMultimap<String, EsQueueDto> itemsByIssueKey = ArrayListMultimap.create();
+    ListMultimap<String, EsQueueDto> itemsByProjectKey = ArrayListMultimap.create();
+    items.forEach(i -> {
+      if (ID_TYPE_ISSUE_KEY.equals(i.getDocIdType())) {
+        itemsByIssueKey.put(i.getDocId(), i);
+      } else if (ID_TYPE_PROJECT_UUID.equals(i.getDocIdType())) {
+        itemsByProjectKey.put(i.getDocId(), i);
+      } else {
+        LOGGER.error("Unsupported es_queue.doc_id_type for issues. Manual fix is required: " + i);
+      }
+    });
+
+    IndexingResult result = new IndexingResult();
+    result.add(doIndexIssueItems(dbSession, itemsByIssueKey));
+    result.add(doIndexProjectItems(dbSession, itemsByProjectKey));
+    return result;
   }
 
-  private void doIndex(BulkIndexer bulk, Collection<String> issueKeys) {
-    try (IssueIterator issues = issueIteratorFactory.createForIssueKeys(issueKeys)) {
-      doIndex(bulk, issues);
+  private IndexingResult doIndexIssueItems(DbSession dbSession, ListMultimap<String, EsQueueDto> itemsByIssueKey) {
+    if (itemsByIssueKey.isEmpty()) {
+      return new IndexingResult();
+    }
+    IndexingListener listener = new OneToOneResilientIndexingListener(dbClient, dbSession, itemsByIssueKey.values());
+    BulkIndexer bulkIndexer = createBulkIndexer(Size.REGULAR, listener);
+    bulkIndexer.start();
+
+    try (IssueIterator issues = issueIteratorFactory.createForIssueKeys(itemsByIssueKey.keySet())) {
+      while (issues.hasNext()) {
+        IssueDoc issue = issues.next();
+        bulkIndexer.add(newIndexRequest(issue));
+        itemsByIssueKey.removeAll(issue.getId());
+      }
     }
+
+    // the remaining uuids reference issues that don't exist in db. They must
+    // be deleted from index.
+    itemsByIssueKey.values().forEach(
+      item -> bulkIndexer.addDeletion(INDEX_TYPE_ISSUE, item.getDocId(), item.getDocRouting()));
+
+    return bulkIndexer.stop();
   }
 
-  private void doIndex(BulkIndexer bulk, @Nullable String projectUuid) {
-    try (IssueIterator issues = issueIteratorFactory.createForProject(projectUuid)) {
-      doIndex(bulk, issues);
+  private IndexingResult doIndexProjectItems(DbSession dbSession, ListMultimap<String, EsQueueDto> itemsByProjectUuid) {
+    if (itemsByProjectUuid.isEmpty()) {
+      return new IndexingResult();
+    }
+
+    // one project, referenced by es_queue.doc_id = many issues
+    IndexingListener listener = new OneToManyResilientIndexingListener(dbClient, dbSession, itemsByProjectUuid.values());
+    BulkIndexer bulkIndexer = createBulkIndexer(Size.REGULAR, listener);
+    bulkIndexer.start();
+
+    for (String projectUuid : itemsByProjectUuid.keySet()) {
+      // TODO support loading of multiple projects in a single SQL request
+      try (IssueIterator issues = issueIteratorFactory.createForProject(projectUuid)) {
+        if (issues.hasNext()) {
+          do {
+            IssueDoc doc = issues.next();
+            bulkIndexer.add(newIndexRequest(doc));
+          } while (issues.hasNext());
+        } else {
+          // project does not exist or has no issues. In both case
+          // all the documents related to this project are deleted.
+          addProjectDeletionToBulkIndexer(bulkIndexer, projectUuid);
+        }
+      }
     }
+
+    return bulkIndexer.stop();
   }
 
-  private static void doIndex(BulkIndexer bulk, Iterator<IssueDoc> issues) {
+  // Used by Compute Engine, no need to recovery on errors
+  public void deleteByKeys(String projectUuid, Collection<String> issueKeys) {
+    if (issueKeys.isEmpty()) {
+      return;
+    }
+
+    BulkIndexer bulkIndexer = createBulkIndexer(Size.REGULAR, IndexingListener.NOOP);
+    bulkIndexer.start();
+    issueKeys.forEach(issueKey -> bulkIndexer.addDeletion(INDEX_TYPE_ISSUE, issueKey, projectUuid));
+    bulkIndexer.stop();
+  }
+
+  @VisibleForTesting
+  protected void index(Iterator<IssueDoc> issues) {
+    doIndex(issues, Size.LARGE, IndexingListener.NOOP);
+  }
+
+  private void doIndex(Iterator<IssueDoc> issues, Size size, IndexingListener listener) {
+    BulkIndexer bulk = createBulkIndexer(size, listener);
     bulk.start();
     while (issues.hasNext()) {
       IssueDoc issue = issues.next();
@@ -123,49 +247,28 @@ public class IssueIndexer implements ProjectIndexer, NeedAuthorizationIndexer, S
     bulk.stop();
   }
 
-  @Override
-  public void deleteProject(String uuid) {
-    BulkIndexer bulk = new BulkIndexer(esClient, INDEX_TYPE_ISSUE.getIndex(), Size.REGULAR);
-    bulk.start();
-    SearchRequestBuilder search = esClient.prepareSearch(INDEX_TYPE_ISSUE)
-      .setRouting(uuid)
-      .setQuery(boolQuery().must(termQuery(FIELD_ISSUE_PROJECT_UUID, uuid)));
-    bulk.addDeletion(search);
-    bulk.stop();
+  private IndexRequest newIndexRequest(IssueDoc issue) {
+    String projectUuid = issue.projectUuid();
+    return esClient.prepareIndex(INDEX_TYPE_ISSUE)
+      .setId(issue.key())
+      .setRouting(projectUuid)
+      .setParent(projectUuid)
+      .setSource(issue.getFields())
+      .request();
   }
 
-  public void deleteByKeys(String projectUuid, List<String> issueKeys) {
-    if (issueKeys.isEmpty()) {
-      return;
-    }
-
-    int count = 0;
-    BulkRequestBuilder builder = esClient.prepareBulk();
-    for (String issueKey : issueKeys) {
-      builder.add(esClient.prepareDelete(INDEX_TYPE_ISSUE, issueKey)
-        .setRefresh(false)
-        .setRouting(projectUuid));
-      count++;
-      if (count >= MAX_BATCH_SIZE) {
-        EsUtils.executeBulkRequest(builder, DELETE_ERROR_MESSAGE, projectUuid);
-        builder = esClient.prepareBulk();
-        count = 0;
-      }
-    }
-    EsUtils.executeBulkRequest(builder, DELETE_ERROR_MESSAGE, projectUuid);
-    esClient.prepareRefresh(INDEX_TYPE_ISSUE.getIndex()).get();
+  private void addProjectDeletionToBulkIndexer(BulkIndexer bulkIndexer, String projectUuid) {
+    SearchRequestBuilder search = esClient.prepareSearch(INDEX_TYPE_ISSUE)
+      .setRouting(projectUuid)
+      .setQuery(boolQuery().must(termQuery(FIELD_ISSUE_PROJECT_UUID, projectUuid)));
+    bulkIndexer.addDeletion(search);
   }
 
-  private BulkIndexer createBulkIndexer(Size bulkSize) {
-    return new BulkIndexer(esClient, INDEX_TYPE_ISSUE.getIndex(), bulkSize);
+  private static EsQueueDto createQueueDto(String docId, String docIdType, String projectUuid) {
+    return EsQueueDto.create(INDEX_TYPE_ISSUE.format(), docId, docIdType, projectUuid);
   }
 
-  private static IndexRequest newIndexRequest(IssueDoc issue) {
-    String projectUuid = issue.projectUuid();
-
-    return new IndexRequest(INDEX_TYPE_ISSUE.getIndex(), INDEX_TYPE_ISSUE.getType(), issue.key())
-      .routing(projectUuid)
-      .parent(projectUuid)
-      .source(issue.getFields());
+  private BulkIndexer createBulkIndexer(Size size, IndexingListener listener) {
+    return new BulkIndexer(esClient, INDEX_TYPE_ISSUE, size, listener);
   }
 }
index 84d52030769f7b2441ceeb082aaad22025335f9a..fbb1cbb6822ada86627bf59a1e943ab96e684a1a 100644 (file)
 package org.sonar.server.measure.index;
 
 import com.google.common.collect.ImmutableSet;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.Date;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Set;
 import javax.annotation.Nullable;
 import org.elasticsearch.action.index.IndexRequest;
 import org.sonar.api.resources.Qualifiers;
+import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
+import org.sonar.db.es.EsQueueDto;
 import org.sonar.db.measure.ProjectMeasuresIndexerIterator;
 import org.sonar.db.measure.ProjectMeasuresIndexerIterator.ProjectMeasures;
 import org.sonar.server.es.BulkIndexer;
 import org.sonar.server.es.BulkIndexer.Size;
 import org.sonar.server.es.EsClient;
 import org.sonar.server.es.IndexType;
+import org.sonar.server.es.IndexingListener;
+import org.sonar.server.es.IndexingResult;
+import org.sonar.server.es.OneToOneResilientIndexingListener;
 import org.sonar.server.es.ProjectIndexer;
-import org.sonar.server.es.StartupIndexer;
 import org.sonar.server.permission.index.AuthorizationScope;
 import org.sonar.server.permission.index.NeedAuthorizationIndexer;
 
 import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.INDEX_TYPE_PROJECT_MEASURES;
 
-public class ProjectMeasuresIndexer implements ProjectIndexer, NeedAuthorizationIndexer, StartupIndexer {
+public class ProjectMeasuresIndexer implements ProjectIndexer, NeedAuthorizationIndexer {
 
   private static final AuthorizationScope AUTHORIZATION_SCOPE = new AuthorizationScope(INDEX_TYPE_PROJECT_MEASURES, project -> Qualifiers.PROJECT.equals(project.getQualifier()));
+  private static final ImmutableSet<IndexType> INDEX_TYPES = ImmutableSet.of(INDEX_TYPE_PROJECT_MEASURES);
 
   private final DbClient dbClient;
   private final EsClient esClient;
@@ -55,12 +63,12 @@ public class ProjectMeasuresIndexer implements ProjectIndexer, NeedAuthorization
 
   @Override
   public Set<IndexType> getIndexTypes() {
-    return ImmutableSet.of(INDEX_TYPE_PROJECT_MEASURES);
+    return INDEX_TYPES;
   }
 
   @Override
   public void indexOnStartup(Set<IndexType> uninitializedIndexTypes) {
-    doIndex(createBulkIndexer(Size.LARGE), (String) null);
+    doIndex(Size.LARGE, null);
   }
 
   @Override
@@ -69,16 +77,28 @@ public class ProjectMeasuresIndexer implements ProjectIndexer, NeedAuthorization
   }
 
   @Override
-  public void indexProject(String projectUuid, Cause cause) {
+  public void indexOnAnalysis(String projectUuid) {
+    doIndex(Size.REGULAR, projectUuid);
+  }
+
+  @Override
+  public Collection<EsQueueDto> prepareForRecovery(DbSession dbSession, Collection<String> projectUuids, ProjectIndexer.Cause cause) {
     switch (cause) {
+      case PERMISSION_CHANGE:
+        // nothing to do, permissions are not used in type projectmeasures/projectmeasure
+        return Collections.emptyList();
+
       case PROJECT_KEY_UPDATE:
         // project must be re-indexed because key is used in this index
       case PROJECT_CREATION:
         // provisioned projects are supported by WS api/components/search_projects
-      case NEW_ANALYSIS:
       case PROJECT_TAGS_UPDATE:
-        doIndex(createBulkIndexer(Size.REGULAR), projectUuid);
-        break;
+      case PROJECT_DELETION:
+        List<EsQueueDto> items = projectUuids.stream()
+          .map(projectUuid -> EsQueueDto.create(INDEX_TYPE_PROJECT_MEASURES.format(), projectUuid, null, projectUuid))
+          .collect(MoreCollectors.toArrayList(projectUuids.size()));
+        return dbClient.esQueueDao().insert(dbSession, items);
+
       default:
         // defensive case
         throw new IllegalStateException("Unsupported cause: " + cause);
@@ -86,32 +106,49 @@ public class ProjectMeasuresIndexer implements ProjectIndexer, NeedAuthorization
   }
 
   @Override
-  public void deleteProject(String uuid) {
-    esClient
-      .prepareDelete(INDEX_TYPE_PROJECT_MEASURES, uuid)
-      .setRouting(uuid)
-      .setRefresh(true)
-      .get();
-  }
-
-  private void doIndex(BulkIndexer bulk, @Nullable String projectUuid) {
-    try (DbSession dbSession = dbClient.openSession(false);
-      ProjectMeasuresIndexerIterator rowIt = ProjectMeasuresIndexerIterator.create(dbSession, projectUuid)) {
-      doIndex(bulk, rowIt);
+  public IndexingResult index(DbSession dbSession, Collection<EsQueueDto> items) {
+    if (items.isEmpty()) {
+      return new IndexingResult();
+    }
+    OneToOneResilientIndexingListener listener = new OneToOneResilientIndexingListener(dbClient, dbSession, items);
+    BulkIndexer bulkIndexer = createBulkIndexer(Size.REGULAR, listener);
+    bulkIndexer.start();
+
+    List<String> projectUuids = items.stream().map(EsQueueDto::getDocId).collect(MoreCollectors.toArrayList(items.size()));
+    Iterator<String> it = projectUuids.iterator();
+    while (it.hasNext()) {
+      String projectUuid = it.next();
+      try (ProjectMeasuresIndexerIterator rowIt = ProjectMeasuresIndexerIterator.create(dbSession, projectUuid)) {
+        while (rowIt.hasNext()) {
+          bulkIndexer.add(newIndexRequest(toProjectMeasuresDoc(rowIt.next())));
+          it.remove();
+        }
+      }
     }
+
+    // the remaining uuids reference issues that don't exist in db. They must
+    // be deleted from index.
+    projectUuids.forEach(projectUuid -> bulkIndexer.addDeletion(INDEX_TYPE_PROJECT_MEASURES, projectUuid, projectUuid));
+
+    return bulkIndexer.stop();
   }
 
-  private static void doIndex(BulkIndexer bulk, Iterator<ProjectMeasures> docs) {
-    bulk.start();
-    while (docs.hasNext()) {
-      ProjectMeasures doc = docs.next();
-      bulk.add(newIndexRequest(toProjectMeasuresDoc(doc)));
+  private void doIndex(Size size, @Nullable String projectUuid) {
+    try (DbSession dbSession = dbClient.openSession(false);
+         ProjectMeasuresIndexerIterator rowIt = ProjectMeasuresIndexerIterator.create(dbSession, projectUuid)) {
+
+      BulkIndexer bulkIndexer = createBulkIndexer(size, IndexingListener.NOOP);
+      bulkIndexer.start();
+      while (rowIt.hasNext()) {
+        ProjectMeasures doc = rowIt.next();
+        bulkIndexer.add(newIndexRequest(toProjectMeasuresDoc(doc)));
+      }
+      bulkIndexer.stop();
     }
-    bulk.stop();
   }
 
-  private BulkIndexer createBulkIndexer(Size bulkSize) {
-    return new BulkIndexer(esClient, INDEX_TYPE_PROJECT_MEASURES.getIndex(), bulkSize);
+  private BulkIndexer createBulkIndexer(Size bulkSize, IndexingListener listener) {
+    return new BulkIndexer(esClient, INDEX_TYPE_PROJECT_MEASURES, bulkSize, listener);
   }
 
   private static IndexRequest newIndexRequest(ProjectMeasuresDoc doc) {
index 875d1b0a93a85e4a98cdf2c232efffacc657397f..82735ce115f6dee7912ce299ec4ab10573e46ccb 100644 (file)
@@ -44,7 +44,8 @@ import org.sonar.db.permission.template.PermissionTemplateCharacteristicDto;
 import org.sonar.db.permission.template.PermissionTemplateDto;
 import org.sonar.db.permission.template.PermissionTemplateGroupDto;
 import org.sonar.db.permission.template.PermissionTemplateUserDto;
-import org.sonar.server.permission.index.PermissionIndexer;
+import org.sonar.server.es.ProjectIndexer;
+import org.sonar.server.es.ProjectIndexers;
 import org.sonar.server.permission.ws.template.DefaultTemplatesResolver;
 import org.sonar.server.permission.ws.template.DefaultTemplatesResolverImpl;
 import org.sonar.server.user.UserSession;
@@ -59,14 +60,14 @@ import static org.sonar.api.security.DefaultGroups.isAnyone;
 public class PermissionTemplateService {
 
   private final DbClient dbClient;
-  private final PermissionIndexer permissionIndexer;
+  private final ProjectIndexers projectIndexers;
   private final UserSession userSession;
   private final DefaultTemplatesResolver defaultTemplatesResolver;
 
-  public PermissionTemplateService(DbClient dbClient, PermissionIndexer permissionIndexer, UserSession userSession,
+  public PermissionTemplateService(DbClient dbClient, ProjectIndexers projectIndexers, UserSession userSession,
     DefaultTemplatesResolver defaultTemplatesResolver) {
     this.dbClient = dbClient;
-    this.permissionIndexer = permissionIndexer;
+    this.projectIndexers = projectIndexers;
     this.userSession = userSession;
     this.defaultTemplatesResolver = defaultTemplatesResolver;
   }
@@ -95,7 +96,7 @@ public class PermissionTemplateService {
    * is not verified. The projects must exist, so the "project creator" permissions defined in the
    * template are ignored.
    */
-  public void apply(DbSession dbSession, PermissionTemplateDto template, Collection<ComponentDto> projects) {
+  public void applyAndCommit(DbSession dbSession, PermissionTemplateDto template, Collection<ComponentDto> projects) {
     if (projects.isEmpty()) {
       return;
     }
@@ -103,8 +104,7 @@ public class PermissionTemplateService {
     for (ComponentDto project : projects) {
       copyPermissions(dbSession, template, project, null);
     }
-    dbSession.commit();
-    indexProjectPermissions(dbSession, projects.stream().map(ComponentDto::uuid).collect(MoreCollectors.toList()));
+    projectIndexers.commitAndIndex(dbSession, projects.stream().map(ComponentDto::uuid).collect(MoreCollectors.toList()), ProjectIndexer.Cause.PERMISSION_CHANGE);
   }
 
   /**
@@ -116,8 +116,6 @@ public class PermissionTemplateService {
     PermissionTemplateDto template = findTemplate(dbSession, organizationUuid, component);
     checkArgument(template != null, "Cannot retrieve default permission template");
     copyPermissions(dbSession, template, component, projectCreatorUserId);
-    dbSession.commit();
-    indexProjectPermissions(dbSession, asList(component.uuid()));
   }
 
   public boolean hasDefaultTemplateWithPermissionOnProjectCreator(DbSession dbSession, String organizationUuid, ComponentDto component) {
@@ -130,10 +128,6 @@ public class PermissionTemplateService {
       .anyMatch(PermissionTemplateCharacteristicDto::getWithProjectCreator);
   }
 
-  private void indexProjectPermissions(DbSession dbSession, List<String> projectOrViewUuids) {
-    permissionIndexer.indexProjectsByUuids(dbSession, projectOrViewUuids);
-  }
-
   private void copyPermissions(DbSession dbSession, PermissionTemplateDto template, ComponentDto project, @Nullable Integer projectCreatorUserId) {
     dbClient.resourceDao().updateAuthorizationDate(project.getId(), dbSession);
     dbClient.groupPermissionDao().deleteByRootComponentId(dbSession, project.getId());
index 52d76459fecb9dc48949b3565b795b82c55ec7f1..9238fd5e36e7e10e6674654de65178bb248241dc 100644 (file)
@@ -27,7 +27,8 @@ import java.util.Optional;
 import java.util.Set;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
-import org.sonar.server.permission.index.PermissionIndexer;
+import org.sonar.server.es.ProjectIndexer;
+import org.sonar.server.es.ProjectIndexers;
 
 /**
  * Add or remove global/project permissions to a group. This class
@@ -37,14 +38,14 @@ import org.sonar.server.permission.index.PermissionIndexer;
 public class PermissionUpdater {
 
   private final DbClient dbClient;
-  private final PermissionIndexer permissionIndexer;
+  private final ProjectIndexers projectIndexers;
   private final UserPermissionChanger userPermissionChanger;
   private final GroupPermissionChanger groupPermissionChanger;
 
-  public PermissionUpdater(DbClient dbClient, PermissionIndexer permissionIndexer,
+  public PermissionUpdater(DbClient dbClient, ProjectIndexers projectIndexers,
                            UserPermissionChanger userPermissionChanger, GroupPermissionChanger groupPermissionChanger) {
     this.dbClient = dbClient;
-    this.permissionIndexer = permissionIndexer;
+    this.projectIndexers = projectIndexers;
     this.userPermissionChanger = userPermissionChanger;
     this.groupPermissionChanger = groupPermissionChanger;
   }
@@ -63,11 +64,8 @@ public class PermissionUpdater {
     for (Long projectId : projectIds) {
       dbClient.resourceDao().updateAuthorizationDate(projectId, dbSession);
     }
-    dbSession.commit();
 
-    if (!projectIds.isEmpty()) {
-      permissionIndexer.indexProjectsByUuids(dbSession, projectOrViewUuids);
-    }
+    projectIndexers.commitAndIndex(dbSession, projectOrViewUuids, ProjectIndexer.Cause.PERMISSION_CHANGE);
   }
 
   private boolean doApply(DbSession dbSession, PermissionChange change) {
index 749bd51ba09af73415e5b3c30fdc4215fce20ad9..90f7289ab37c2488db2a3b6c0e43a9a869fde5ca 100644 (file)
@@ -40,7 +40,7 @@ import static org.elasticsearch.index.query.QueryBuilders.termQuery;
 @ComputeEngineSide
 public class AuthorizationTypeSupport {
 
-  private static final String TYPE_AUTHORIZATION = "authorization";
+  public static final String TYPE_AUTHORIZATION = "authorization";
   public static final String FIELD_GROUP_IDS = "groupIds";
   public static final String FIELD_USER_IDS = "userIds";
   public static final String FIELD_UPDATED_AT = "updatedAt";
index 8c7ad3e6693eba5e0c42236e058e84caa6052922..078fe2f10914970b57bc5a6ecfc963b1334112b0 100644 (file)
@@ -26,38 +26,37 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.elasticsearch.action.index.IndexRequest;
 import org.sonar.api.utils.DateUtils;
 import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
+import org.sonar.db.es.EsQueueDto;
 import org.sonar.server.es.BulkIndexer;
 import org.sonar.server.es.BulkIndexer.Size;
 import org.sonar.server.es.EsClient;
 import org.sonar.server.es.IndexType;
+import org.sonar.server.es.IndexingResult;
+import org.sonar.server.es.OneToOneResilientIndexingListener;
 import org.sonar.server.es.ProjectIndexer;
-import org.sonar.server.es.StartupIndexer;
 import org.sonar.server.permission.index.PermissionIndexerDao.Dto;
 
-import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Collections.emptyList;
+import static org.sonar.core.util.stream.MoreCollectors.toArrayList;
 import static org.sonar.core.util.stream.MoreCollectors.toSet;
 
 /**
- * Manages the synchronization of indexes with authorization settings defined in database:
- * <ul>
- *   <li>index the projects with recent permission changes</li>
- *   <li>delete project orphans from index</li>
- * </ul>
+ * Populates the types "authorization" of each index requiring project
+ * authorization.
  */
-public class PermissionIndexer implements ProjectIndexer, StartupIndexer {
-
-  @VisibleForTesting
-  static final int MAX_BATCH_SIZE = 1000;
+public class PermissionIndexer implements ProjectIndexer {
 
   private final DbClient dbClient;
   private final EsClient esClient;
   private final Collection<AuthorizationScope> authorizationScopes;
+  private final Set<IndexType> indexTypes;
 
   public PermissionIndexer(DbClient dbClient, EsClient esClient, NeedAuthorizationIndexer... needAuthorizationIndexers) {
     this(dbClient, esClient, Arrays.stream(needAuthorizationIndexers)
@@ -70,70 +69,60 @@ public class PermissionIndexer implements ProjectIndexer, StartupIndexer {
     this.dbClient = dbClient;
     this.esClient = esClient;
     this.authorizationScopes = authorizationScopes;
+    this.indexTypes = authorizationScopes.stream()
+      .map(AuthorizationScope::getIndexType)
+      .collect(toSet(authorizationScopes.size()));
   }
 
   @Override
   public Set<IndexType> getIndexTypes() {
-    return authorizationScopes.stream()
-      .map(AuthorizationScope::getIndexType)
-      .collect(toSet(authorizationScopes.size()));
+    return indexTypes;
   }
 
   @Override
   public void indexOnStartup(Set<IndexType> uninitializedIndexTypes) {
+    // TODO do not load everything in memory. Db rows should be scrolled.
     List<Dto> authorizations = getAllAuthorizations();
     Stream<AuthorizationScope> scopes = getScopes(uninitializedIndexTypes);
     index(authorizations, scopes, Size.LARGE);
   }
 
-  private List<Dto> getAllAuthorizations() {
-    try (DbSession dbSession = dbClient.openSession(false)) {
-      return new PermissionIndexerDao().selectAll(dbClient, dbSession);
-    }
-  }
-
-  public void indexProjectsByUuids(DbSession dbSession, List<String> viewOrProjectUuids) {
-    checkArgument(!viewOrProjectUuids.isEmpty(), "viewOrProjectUuids cannot be empty");
-    PermissionIndexerDao dao = new PermissionIndexerDao();
-    List<Dto> authorizations = dao.selectByUuids(dbClient, dbSession, viewOrProjectUuids);
-    index(authorizations);
-  }
-
   @VisibleForTesting
   void index(List<Dto> authorizations) {
     index(authorizations, authorizationScopes.stream(), Size.REGULAR);
   }
 
   @Override
-  public void indexProject(String projectUuid, Cause cause) {
+  public void indexOnAnalysis(String projectUuid) {
+    // nothing to do, permissions don't change during an analysis
+  }
+
+  @Override
+  public Collection<EsQueueDto> prepareForRecovery(DbSession dbSession, Collection<String> projectUuids, ProjectIndexer.Cause cause) {
     switch (cause) {
-      case PROJECT_CREATION:
-        // nothing to do, permissions are indexed explicitly
-        // when permission template is applied after project creation
-      case NEW_ANALYSIS:
-        // nothing to do, permissions don't change during an analysis
       case PROJECT_KEY_UPDATE:
       case PROJECT_TAGS_UPDATE:
-        // nothing to do, key and tags are not used in this index
-        break;
+        // nothing to change, project key and tags are not part of this index
+        return emptyList();
+
+      case PROJECT_CREATION:
+      case PROJECT_DELETION:
+      case PERMISSION_CHANGE:
+        return insertIntoEsQueue(dbSession, projectUuids);
+
       default:
         // defensive case
         throw new IllegalStateException("Unsupported cause: " + cause);
     }
   }
 
-  @Override
-  public void deleteProject(String projectUuid) {
-    authorizationScopes.forEach(scope -> esClient
-      .prepareDelete(scope.getIndexType(), projectUuid)
-      .setRouting(projectUuid)
-      .setRefresh(true)
-      .get());
-  }
+  private Collection<EsQueueDto> insertIntoEsQueue(DbSession dbSession, Collection<String> projectUuids) {
+    List<EsQueueDto> items = indexTypes.stream()
+      .flatMap(indexType -> projectUuids.stream().map(projectUuid -> EsQueueDto.create(indexType.format(), projectUuid, null, projectUuid)))
+      .collect(toArrayList());
 
-  private Stream<AuthorizationScope> getScopes(Set<IndexType> indexTypes) {
-    return authorizationScopes.stream()
-      .filter(scope -> indexTypes.contains(scope.getIndexType()));
+    dbClient.esQueueDao().insert(dbSession, items);
+    return items;
   }
 
   private void index(Collection<PermissionIndexerDao.Dto> authorizations, Stream<AuthorizationScope> scopes, Size bulkSize) {
@@ -142,21 +131,53 @@ public class PermissionIndexer implements ProjectIndexer, StartupIndexer {
     }
 
     // index each authorization in each scope
-    scopes.forEach(scope -> index(authorizations, scope, bulkSize));
+    scopes.forEach(scope -> {
+      IndexType indexType = scope.getIndexType();
+
+      BulkIndexer bulkIndexer = new BulkIndexer(esClient, indexType, bulkSize);
+      bulkIndexer.start();
+
+      authorizations.stream()
+        .filter(scope.getProjectPredicate())
+        .map(dto -> newIndexRequest(dto, indexType))
+        .forEach(bulkIndexer::add);
+
+      bulkIndexer.stop();
+    });
   }
 
-  private void index(Collection<PermissionIndexerDao.Dto> authorizations, AuthorizationScope scope, Size bulkSize) {
-    IndexType indexType = scope.getIndexType();
+  @Override
+  public IndexingResult index(DbSession dbSession, Collection<EsQueueDto> items) {
+    IndexingResult result = new IndexingResult();
+
+    List<BulkIndexer> bulkIndexers = items.stream()
+      .map(EsQueueDto::getDocType)
+      .distinct()
+      .map(IndexType::parse)
+      .filter(indexTypes::contains)
+      .map(indexType -> new BulkIndexer(esClient, indexType, Size.REGULAR, new OneToOneResilientIndexingListener(dbClient, dbSession, items)))
+      .collect(Collectors.toList());
+
+    if (bulkIndexers.isEmpty()) {
+      return result;
+    }
+
+    bulkIndexers.forEach(BulkIndexer::start);
+
+    PermissionIndexerDao permissionIndexerDao = new PermissionIndexerDao();
+    Set<String> remainingProjectUuids = items.stream().map(EsQueueDto::getDocId).collect(MoreCollectors.toHashSet());
+    permissionIndexerDao.selectByUuids(dbClient, dbSession, remainingProjectUuids).forEach(p -> {
+      remainingProjectUuids.remove(p.getProjectUuid());
+      bulkIndexers.forEach(bi -> bi.add(newIndexRequest(p, bi.getIndexType())));
+    });
 
-    BulkIndexer bulkIndexer = new BulkIndexer(esClient, indexType.getIndex(), bulkSize);
-    bulkIndexer.start();
+    // the remaining references on projects that don't exist in db. They must
+    // be deleted from index.
+    remainingProjectUuids.forEach(projectUuid -> bulkIndexers.forEach(bi -> bi.addDeletion(bi.getIndexType(), projectUuid, projectUuid)));
 
-    authorizations.stream()
-      .filter(scope.getProjectPredicate())
-      .map(dto -> newIndexRequest(dto, indexType))
-      .forEach(bulkIndexer::add);
+    bulkIndexers.forEach(b -> result.add(b.stop()));
 
-    bulkIndexer.stop();
+    return result;
   }
 
   private static IndexRequest newIndexRequest(PermissionIndexerDao.Dto dto, IndexType indexType) {
@@ -170,8 +191,20 @@ public class PermissionIndexer implements ProjectIndexer, StartupIndexer {
       doc.put(AuthorizationTypeSupport.FIELD_GROUP_IDS, dto.getGroupIds());
       doc.put(AuthorizationTypeSupport.FIELD_USER_IDS, dto.getUserIds());
     }
-    return new IndexRequest(indexType.getIndex(), indexType.getType(), dto.getProjectUuid())
+    return new IndexRequest(indexType.getIndex(), indexType.getType())
+      .id(dto.getProjectUuid())
       .routing(dto.getProjectUuid())
       .source(doc);
   }
+
+  private Stream<AuthorizationScope> getScopes(Set<IndexType> indexTypes) {
+    return authorizationScopes.stream()
+      .filter(scope -> indexTypes.contains(scope.getIndexType()));
+  }
+
+  private List<Dto> getAllAuthorizations() {
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      return new PermissionIndexerDao().selectAll(dbClient, dbSession);
+    }
+  }
 }
index 55d575a8269f9de6d7422b934896b500f7fbbbff..a854f9caaca652dcaab5ba68d06e81805786477b 100644 (file)
@@ -24,6 +24,7 @@ import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -176,7 +177,7 @@ public class PermissionIndexerDao {
     return doSelectByProjects(dbClient, session, Collections.emptyList());
   }
 
-  List<Dto> selectByUuids(DbClient dbClient, DbSession session, List<String> projectOrViewUuids) {
+  List<Dto> selectByUuids(DbClient dbClient, DbSession session, Collection<String> projectOrViewUuids) {
     return executeLargeInputs(projectOrViewUuids, subProjectOrViewUuids -> doSelectByProjects(dbClient, session, subProjectOrViewUuids));
   }
 
index 747a427312fbeb623f329ef94853400205c062c8..65f57f7ccaf972a2ecefbfffd65fb7f7ad8d9163 100644 (file)
@@ -94,7 +94,7 @@ public class ApplyTemplateAction implements PermissionsWsAction {
       ComponentDto project = wsSupport.getRootComponentOrModule(dbSession, newWsProjectRef(request.getProjectId(), request.getProjectKey()));
       checkGlobalAdmin(userSession, template.getOrganizationUuid());
 
-      permissionTemplateService.apply(dbSession, template, Collections.singletonList(project));
+      permissionTemplateService.applyAndCommit(dbSession, template, Collections.singletonList(project));
     }
   }
 }
index 27fbaf744d96fda0249f9891665aab2152bdfecb..e56b103db7bfaf46df96fd2fbc7d2c016e4c4047 100644 (file)
@@ -116,7 +116,7 @@ public class BulkApplyTemplateAction implements PermissionsWsAction {
         .build();
       List<ComponentDto> projects = dbClient.componentDao().selectByQuery(dbSession, template.getOrganizationUuid(), componentQuery, 0, Integer.MAX_VALUE);
 
-      permissionTemplateService.apply(dbSession, template, projects);
+      permissionTemplateService.applyAndCommit(dbSession, template, projects);
     }
   }
 
index d465a9a8ba5b64870989f5aada972f246c91b80a..a508d4de517dda5c01ac907ec150def7384df7c3 100644 (file)
@@ -36,6 +36,7 @@ import org.sonar.db.version.SqTables;
 import org.sonar.server.component.index.ComponentIndexDefinition;
 import org.sonar.server.es.BulkIndexer;
 import org.sonar.server.es.EsClient;
+import org.sonar.server.es.IndexType;
 import org.sonar.server.issue.index.IssueIndexDefinition;
 import org.sonar.server.measure.index.ProjectMeasuresIndexDefinition;
 import org.sonar.server.property.InternalProperties;
@@ -95,7 +96,7 @@ public class BackendCleanup {
       esClient.prepareClearCache().get();
 
       for (String index : esClient.prepareState().get().getState().getMetaData().concreteAllIndices()) {
-        clearIndex(index);
+        clearIndex(new IndexType(index, index));
       }
     } catch (Exception e) {
       throw new IllegalStateException("Unable to clear indexes", e);
@@ -120,10 +121,10 @@ public class BackendCleanup {
       throw new IllegalStateException("Fail to reset data", e);
     }
 
-    clearIndex(IssueIndexDefinition.INDEX_TYPE_ISSUE.getIndex());
-    clearIndex(ViewIndexDefinition.INDEX_TYPE_VIEW.getIndex());
-    clearIndex(ProjectMeasuresIndexDefinition.INDEX_TYPE_PROJECT_MEASURES.getIndex());
-    clearIndex(ComponentIndexDefinition.INDEX_TYPE_COMPONENT.getIndex());
+    clearIndex(IssueIndexDefinition.INDEX_TYPE_ISSUE);
+    clearIndex(ViewIndexDefinition.INDEX_TYPE_VIEW);
+    clearIndex(ProjectMeasuresIndexDefinition.INDEX_TYPE_PROJECT_MEASURES);
+    clearIndex(ComponentIndexDefinition.INDEX_TYPE_COMPONENT);
   }
 
   private void truncateAnalysisTables(Connection connection) throws SQLException {
@@ -165,8 +166,8 @@ public class BackendCleanup {
   /**
    * Completely remove a index with all types
    */
-  public void clearIndex(String indexName) {
-    BulkIndexer.delete(esClient, indexName, esClient.prepareSearch(indexName).setQuery(matchAllQuery()));
+  public void clearIndex(IndexType indexType) {
+    BulkIndexer.delete(esClient, indexType, esClient.prepareSearch(indexType.getIndex()).setQuery(matchAllQuery()));
   }
 
   @FunctionalInterface
index 8145619988e91466e0f843a004ddfd434833f272..5a8c8ca0a44d3a407ac12e0fbc8926429b208834 100644 (file)
@@ -53,6 +53,7 @@ import org.sonar.server.duplication.ws.ShowResponseBuilder;
 import org.sonar.server.email.ws.EmailsWsModule;
 import org.sonar.server.es.IndexCreator;
 import org.sonar.server.es.IndexDefinitions;
+import org.sonar.server.es.ProjectIndexersImpl;
 import org.sonar.server.es.RecoveryIndexer;
 import org.sonar.server.event.NewAlerts;
 import org.sonar.server.favorite.FavoriteModule;
@@ -521,7 +522,8 @@ public class PlatformLevel4 extends PlatformLevel {
       // Http Request ID
       HttpRequestIdModule.class,
 
-      RecoveryIndexer.class);
+      RecoveryIndexer.class,
+      ProjectIndexersImpl.class);
     addAll(level4AddedComponents);
   }
 
index 6e11774162253e692f0550cbb02bd9681857eb1c..990839e226cff20bdf79f3fd924f63f68ed496c6 100644 (file)
@@ -34,7 +34,8 @@ import org.sonar.db.organization.OrganizationDto;
 import org.sonar.db.permission.GroupPermissionDto;
 import org.sonar.db.permission.UserPermissionDto;
 import org.sonar.server.component.ComponentFinder;
-import org.sonar.server.permission.index.PermissionIndexer;
+import org.sonar.server.es.ProjectIndexer;
+import org.sonar.server.es.ProjectIndexers;
 import org.sonar.server.project.Visibility;
 import org.sonar.server.user.UserSession;
 import org.sonarqube.ws.client.project.ProjectsWsParameters;
@@ -53,15 +54,15 @@ public class UpdateVisibilityAction implements ProjectsWsAction {
   private final DbClient dbClient;
   private final ComponentFinder componentFinder;
   private final UserSession userSession;
-  private final PermissionIndexer permissionIndexer;
+  private final ProjectIndexers projectIndexers;
   private final ProjectsWsSupport projectsWsSupport;
 
   public UpdateVisibilityAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession,
-    PermissionIndexer permissionIndexer, ProjectsWsSupport projectsWsSupport) {
+                                ProjectIndexers projectIndexers, ProjectsWsSupport projectsWsSupport) {
     this.dbClient = dbClient;
     this.componentFinder = componentFinder;
     this.userSession = userSession;
-    this.permissionIndexer = permissionIndexer;
+    this.projectIndexers = projectIndexers;
     this.projectsWsSupport = projectsWsSupport;
   }
 
@@ -108,8 +109,7 @@ public class UpdateVisibilityAction implements ProjectsWsAction {
         } else {
           updatePermissionsToPublic(dbSession, component);
         }
-        dbSession.commit();
-        permissionIndexer.indexProjectsByUuids(dbSession, singletonList(component.uuid()));
+        projectIndexers.commitAndIndex(dbSession, singletonList(component.uuid()), ProjectIndexer.Cause.PERMISSION_CHANGE);
       }
     }
   }
index 8fdffb9b20b925a2670b5e7242e7763987d80637..71fab7c61143223334fe5e6c166d218ca23432db 100644 (file)
@@ -32,9 +32,10 @@ import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.server.component.ComponentFinder;
-import org.sonar.server.es.ProjectIndexer;
+import org.sonar.server.es.ProjectIndexers;
 import org.sonar.server.user.UserSession;
 
+import static java.util.Collections.singletonList;
 import static org.sonar.api.resources.Qualifiers.PROJECT;
 import static org.sonar.server.es.ProjectIndexer.Cause.PROJECT_TAGS_UPDATE;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
@@ -52,13 +53,13 @@ public class SetAction implements ProjectTagsWsAction {
   private final DbClient dbClient;
   private final ComponentFinder componentFinder;
   private final UserSession userSession;
-  private final List<ProjectIndexer> indexers;
+  private final ProjectIndexers projectIndexers;
 
-  public SetAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession, List<ProjectIndexer> indexers) {
+  public SetAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession, ProjectIndexers projectIndexers) {
     this.dbClient = dbClient;
     this.componentFinder = componentFinder;
     this.userSession = userSession;
-    this.indexers = indexers;
+    this.projectIndexers = projectIndexers;
   }
 
   @Override
@@ -94,12 +95,11 @@ public class SetAction implements ProjectTagsWsAction {
     try (DbSession dbSession = dbClient.openSession(false)) {
       ComponentDto project = componentFinder.getByKey(dbSession, projectKey);
       checkRequest(PROJECT.equals(project.qualifier()), "Component '%s' is not a project", project.key());
-      userSession.checkComponentUuidPermission(UserRole.ADMIN, project.uuid());
+      userSession.checkComponentPermission(UserRole.ADMIN, project);
 
       project.setTags(tags);
       dbClient.componentDao().updateTags(dbSession, project);
-      dbSession.commit();
-      indexers.forEach(i -> i.indexProject(project.uuid(), PROJECT_TAGS_UPDATE));
+      projectIndexers.commitAndIndex(dbSession, singletonList(project.uuid()), PROJECT_TAGS_UPDATE);
     }
 
     response.noContent();
index 5f768e0f88be04ba78e635846966c60129a68b44..1a4610573a525b9b878d749a63bbe182cafada27 100644 (file)
@@ -45,9 +45,8 @@ import org.sonar.server.es.EsClient;
 import org.sonar.server.es.IndexType;
 import org.sonar.server.es.IndexingListener;
 import org.sonar.server.es.IndexingResult;
-import org.sonar.server.es.ResiliencyIndexingListener;
+import org.sonar.server.es.OneToOneResilientIndexingListener;
 import org.sonar.server.es.ResilientIndexer;
-import org.sonar.server.es.StartupIndexer;
 import org.sonar.server.qualityprofile.ActiveRule;
 import org.sonar.server.qualityprofile.ActiveRuleChange;
 import org.sonar.server.rule.index.RuleIndexDefinition;
@@ -57,7 +56,7 @@ import static org.sonar.core.util.stream.MoreCollectors.toArrayList;
 import static org.sonar.server.rule.index.RuleIndexDefinition.FIELD_ACTIVE_RULE_PROFILE_UUID;
 import static org.sonar.server.rule.index.RuleIndexDefinition.INDEX_TYPE_ACTIVE_RULE;
 
-public class ActiveRuleIndexer implements StartupIndexer, ResilientIndexer {
+public class ActiveRuleIndexer implements ResilientIndexer {
 
   private static final Logger LOGGER = Loggers.get(ActiveRuleIndexer.class);
   private static final String ID_TYPE_ACTIVE_RULE_ID = "activeRuleId";
@@ -74,7 +73,7 @@ public class ActiveRuleIndexer implements StartupIndexer, ResilientIndexer {
   @Override
   public void indexOnStartup(Set<IndexType> uninitializedIndexTypes) {
     try (DbSession dbSession = dbClient.openSession(false)) {
-      BulkIndexer bulkIndexer = createBulkIndexer(Size.LARGE, IndexingListener.noop());
+      BulkIndexer bulkIndexer = createBulkIndexer(Size.LARGE, IndexingListener.NOOP);
       bulkIndexer.start();
       dbClient.activeRuleDao().scrollAllForIndexing(dbSession, ar -> bulkIndexer.add(newIndexRequest(ar)));
       bulkIndexer.stop();
@@ -83,7 +82,7 @@ public class ActiveRuleIndexer implements StartupIndexer, ResilientIndexer {
 
   @Override
   public Set<IndexType> getIndexTypes() {
-    return ImmutableSet.of(RuleIndexDefinition.INDEX_TYPE_ACTIVE_RULE);
+    return ImmutableSet.of(INDEX_TYPE_ACTIVE_RULE);
   }
 
   public void commitAndIndex(DbSession dbSession, Collection<ActiveRuleChange> changes) {
@@ -150,12 +149,11 @@ public class ActiveRuleIndexer implements StartupIndexer, ResilientIndexer {
   }
 
   private IndexingResult doIndexActiveRules(DbSession dbSession, Map<Long, EsQueueDto> activeRuleItems) {
-    BulkIndexer bulkIndexer = createBulkIndexer(Size.REGULAR, new ResiliencyIndexingListener(dbClient, dbSession, activeRuleItems.values()));
+    OneToOneResilientIndexingListener listener = new OneToOneResilientIndexingListener(dbClient, dbSession, activeRuleItems.values());
+    BulkIndexer bulkIndexer = createBulkIndexer(Size.REGULAR, listener);
     bulkIndexer.start();
     Map<Long, EsQueueDto> remaining = new HashMap<>(activeRuleItems);
     dbClient.activeRuleDao().scrollByIdsForIndexing(dbSession, activeRuleItems.keySet(),
-      // only index requests, no deletion requests.
-      // Deactivated users are not deleted but updated.
       i -> {
         remaining.remove(i.getId());
         bulkIndexer.add(newIndexRequest(i));
@@ -181,10 +179,10 @@ public class ActiveRuleIndexer implements StartupIndexer, ResilientIndexer {
         // profile does not exist anymore in db --> related documents must be deleted from index rules/activeRule
         SearchRequestBuilder search = esClient.prepareSearch(INDEX_TYPE_ACTIVE_RULE)
           .setQuery(QueryBuilders.boolQuery().must(termQuery(FIELD_ACTIVE_RULE_PROFILE_UUID, ruleProfileUUid)));
-        profileResult = BulkIndexer.delete(esClient, INDEX_TYPE_ACTIVE_RULE.getIndex(), search);
+        profileResult = BulkIndexer.delete(esClient, INDEX_TYPE_ACTIVE_RULE, search);
 
       } else {
-        BulkIndexer bulkIndexer = createBulkIndexer(Size.REGULAR, IndexingListener.noop());
+        BulkIndexer bulkIndexer = createBulkIndexer(Size.REGULAR, IndexingListener.NOOP);
         bulkIndexer.start();
         dbClient.activeRuleDao().scrollByRuleProfileForIndexing(dbSession, ruleProfileUUid, i -> bulkIndexer.add(newIndexRequest(i)));
         profileResult = bulkIndexer.stop();
@@ -205,7 +203,7 @@ public class ActiveRuleIndexer implements StartupIndexer, ResilientIndexer {
   }
 
   private BulkIndexer createBulkIndexer(Size size, IndexingListener listener) {
-    return new BulkIndexer(esClient, INDEX_TYPE_ACTIVE_RULE.getIndex(), size, listener);
+    return new BulkIndexer(esClient, INDEX_TYPE_ACTIVE_RULE, size, listener);
   }
 
   private static IndexRequest newIndexRequest(IndexedActiveRuleDto dto) {
@@ -224,6 +222,6 @@ public class ActiveRuleIndexer implements StartupIndexer, ResilientIndexer {
   }
 
   private static EsQueueDto newQueueDto(String docId, String docIdType, @Nullable String routing) {
-    return EsQueueDto.create(EsQueueDto.Type.ACTIVE_RULE, docId, docIdType, routing);
+    return EsQueueDto.create(INDEX_TYPE_ACTIVE_RULE.format(), docId, docIdType, routing);
   }
 }
index edb2402016350ad73c6b8d40d36de29f4b7bec6e..c6ceb47cb8decfff51e08010e6bb2c3c66835884 100644 (file)
@@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ListMultimap;
 import java.util.Collection;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 import org.elasticsearch.action.index.IndexRequest;
 import org.sonar.api.rule.RuleKey;
@@ -40,19 +41,17 @@ import org.sonar.server.es.EsClient;
 import org.sonar.server.es.IndexType;
 import org.sonar.server.es.IndexingListener;
 import org.sonar.server.es.IndexingResult;
-import org.sonar.server.es.ResiliencyIndexingListener;
+import org.sonar.server.es.OneToOneResilientIndexingListener;
 import org.sonar.server.es.ResilientIndexer;
-import org.sonar.server.es.StartupIndexer;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static java.util.Arrays.asList;
 import static java.util.Collections.singletonList;
-import static java.util.Objects.requireNonNull;
 import static org.sonar.core.util.stream.MoreCollectors.toHashSet;
 import static org.sonar.server.rule.index.RuleIndexDefinition.INDEX_TYPE_RULE;
 import static org.sonar.server.rule.index.RuleIndexDefinition.INDEX_TYPE_RULE_EXTENSION;
 
-public class RuleIndexer implements StartupIndexer, ResilientIndexer {
+public class RuleIndexer implements ResilientIndexer {
 
   private final EsClient esClient;
   private final DbClient dbClient;
@@ -70,7 +69,7 @@ public class RuleIndexer implements StartupIndexer, ResilientIndexer {
   @Override
   public void indexOnStartup(Set<IndexType> uninitializedIndexTypes) {
     try (DbSession dbSession = dbClient.openSession(false)) {
-      BulkIndexer bulk = createBulkIndexer(Size.LARGE, IndexingListener.noop());
+      BulkIndexer bulk = createBulkIndexer(Size.LARGE, IndexingListener.NOOP);
       bulk.start();
 
       // index all definitions and system extensions
@@ -127,29 +126,23 @@ public class RuleIndexer implements StartupIndexer, ResilientIndexer {
   public IndexingResult index(DbSession dbSession, Collection<EsQueueDto> items) {
     IndexingResult result = new IndexingResult();
     if (!items.isEmpty()) {
-      ListMultimap<EsQueueDto.Type, EsQueueDto> itemsByType = groupItemsByType(items);
-      result.add(doIndexRules(dbSession, itemsByType.get(EsQueueDto.Type.RULE)));
-      result.add(doIndexRuleExtensions(dbSession, itemsByType.get(EsQueueDto.Type.RULE_EXTENSION)));
+      ListMultimap<IndexType, EsQueueDto> itemsByType = groupItemsByType(items);
+      result.add(doIndexRules(dbSession, itemsByType.get(INDEX_TYPE_RULE)));
+      result.add(doIndexRuleExtensions(dbSession, itemsByType.get(INDEX_TYPE_RULE_EXTENSION)));
     }
     return result;
   }
 
   private IndexingResult doIndexRules(DbSession dbSession, List<EsQueueDto> items) {
-    BulkIndexer bulkIndexer = createBulkIndexer(Size.REGULAR, new ResiliencyIndexingListener(dbClient, dbSession, items));
+    BulkIndexer bulkIndexer = createBulkIndexer(Size.REGULAR, new OneToOneResilientIndexingListener(dbClient, dbSession, items));
     bulkIndexer.start();
 
     Set<RuleKey> ruleKeys = items
       .stream()
-      .filter(i -> {
-        requireNonNull(i.getDocId(), () -> "BUG - " + i + " has not been persisted before indexing");
-        return i.getDocType() == EsQueueDto.Type.RULE;
-      })
       .map(i -> RuleKey.parse(i.getDocId()))
       .collect(toHashSet(items.size()));
 
     dbClient.ruleDao().scrollIndexingRulesByKeys(dbSession, ruleKeys,
-      // only index requests, no deletion requests.
-      // Deactivated users are not deleted but updated.
       r -> {
         bulkIndexer.add(newRuleDocIndexRequest(r));
         bulkIndexer.add(newRuleExtensionDocIndexRequest(r));
@@ -158,24 +151,20 @@ public class RuleIndexer implements StartupIndexer, ResilientIndexer {
 
     // the remaining items reference rows that don't exist in db. They must
     // be deleted from index.
-    ruleKeys.forEach(r -> {
-      bulkIndexer.addDeletion(RuleIndexDefinition.INDEX_TYPE_RULE, r.toString(), r.toString());
-      bulkIndexer.addDeletion(RuleIndexDefinition.INDEX_TYPE_RULE_EXTENSION, RuleExtensionDoc.idOf(r, RuleExtensionScope.system()), r.toString());
+    ruleKeys.forEach(ruleKey -> {
+      bulkIndexer.addDeletion(INDEX_TYPE_RULE, ruleKey.toString(), ruleKey.toString());
+      bulkIndexer.addDeletion(INDEX_TYPE_RULE_EXTENSION, RuleExtensionDoc.idOf(ruleKey, RuleExtensionScope.system()), ruleKey.toString());
     });
 
     return bulkIndexer.stop();
   }
 
   private IndexingResult doIndexRuleExtensions(DbSession dbSession, List<EsQueueDto> items) {
-    BulkIndexer bulkIndexer = createBulkIndexer(Size.REGULAR, new ResiliencyIndexingListener(dbClient, dbSession, items));
+    BulkIndexer bulkIndexer = createBulkIndexer(Size.REGULAR, new OneToOneResilientIndexingListener(dbClient, dbSession, items));
     bulkIndexer.start();
 
     Set<RuleExtensionId> docIds = items
       .stream()
-      .filter(i -> {
-        requireNonNull(i.getDocId(), () -> "BUG - " + i + " has not been persisted before indexing");
-        return i.getDocType() == EsQueueDto.Type.RULE_EXTENSION;
-      })
       .map(RuleIndexer::explodeRuleExtensionDocId)
       .collect(toHashSet(items.size()));
 
@@ -192,7 +181,7 @@ public class RuleIndexer implements StartupIndexer, ResilientIndexer {
     // be deleted from index.
     docIds.forEach(docId -> {
       RuleKey ruleKey = RuleKey.of(docId.getRepositoryName(), docId.getRuleKey());
-      bulkIndexer.addDeletion(RuleIndexDefinition.INDEX_TYPE_RULE_EXTENSION, docId.getId(), ruleKey.toString());
+      bulkIndexer.addDeletion(INDEX_TYPE_RULE_EXTENSION, docId.getId(), ruleKey.toString());
     });
 
     return bulkIndexer.stop();
@@ -227,25 +216,25 @@ public class RuleIndexer implements StartupIndexer, ResilientIndexer {
   }
 
   private BulkIndexer createBulkIndexer(Size bulkSize, IndexingListener listener) {
-    return new BulkIndexer(esClient, INDEX_TYPE_RULE.getIndex(), bulkSize, listener);
+    return new BulkIndexer(esClient, INDEX_TYPE_RULE, bulkSize, listener);
   }
 
-  private static ListMultimap<EsQueueDto.Type, EsQueueDto> groupItemsByType(Collection<EsQueueDto> items) {
-    return items.stream().collect(MoreCollectors.index(EsQueueDto::getDocType));
+  private static ListMultimap<IndexType, EsQueueDto> groupItemsByType(Collection<EsQueueDto> items) {
+    return items.stream().collect(MoreCollectors.index(i -> IndexType.parse(i.getDocType())));
   }
 
   private static RuleExtensionId explodeRuleExtensionDocId(EsQueueDto esQueueDto) {
-    checkArgument(esQueueDto.getDocType() == EsQueueDto.Type.RULE_EXTENSION);
+    checkArgument(Objects.equals(esQueueDto.getDocType(), "rules/ruleExtension"));
     return new RuleExtensionId(esQueueDto.getDocId());
   }
 
   private static EsQueueDto createQueueDtoForRule(RuleKey ruleKey) {
-    return EsQueueDto.create(EsQueueDto.Type.RULE, ruleKey.toString(), null, ruleKey.toString());
+    return EsQueueDto.create("rules/rule", ruleKey.toString(), null, ruleKey.toString());
   }
 
   private static EsQueueDto createQueueDtoForRuleExtension(RuleKey ruleKey, OrganizationDto organization) {
     String docId = RuleExtensionDoc.idOf(ruleKey, RuleExtensionScope.organization(organization));
-    return EsQueueDto.create(EsQueueDto.Type.RULE_EXTENSION, docId, null, ruleKey.toString());
+    return EsQueueDto.create("rules/ruleExtension", docId, null, ruleKey.toString());
   }
 
 }
index 30919221b0a8f4e7675b0946545b7d2e238d315a..ebd02c87a239c104816b8df58db4fe4a56d891fa 100644 (file)
  */
 package org.sonar.server.test.index;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableSet;
+import java.util.Collection;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Set;
-import javax.annotation.Nullable;
 import org.elasticsearch.action.search.SearchRequestBuilder;
 import org.elasticsearch.index.query.QueryBuilders;
+import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
+import org.sonar.db.es.EsQueueDto;
 import org.sonar.server.es.BulkIndexer;
 import org.sonar.server.es.BulkIndexer.Size;
 import org.sonar.server.es.EsClient;
 import org.sonar.server.es.IndexType;
+import org.sonar.server.es.IndexingListener;
+import org.sonar.server.es.IndexingResult;
+import org.sonar.server.es.OneToManyResilientIndexingListener;
 import org.sonar.server.es.ProjectIndexer;
-import org.sonar.server.es.StartupIndexer;
 import org.sonar.server.source.index.FileSourcesUpdaterHelper;
 
+import static java.util.Collections.emptyList;
 import static org.sonar.server.test.index.TestIndexDefinition.FIELD_FILE_UUID;
 import static org.sonar.server.test.index.TestIndexDefinition.INDEX_TYPE_TEST;
 
 /**
  * Add to Elasticsearch index {@link TestIndexDefinition} the rows of
  * db table FILE_SOURCES of type TEST that are not indexed yet
+ * <p>
+ * This indexer is not resilient by itself since it's called by Compute Engine
  */
-public class TestIndexer implements ProjectIndexer, StartupIndexer {
+public class TestIndexer implements ProjectIndexer {
 
   private final DbClient dbClient;
   private final EsClient esClient;
@@ -52,25 +61,6 @@ public class TestIndexer implements ProjectIndexer, StartupIndexer {
     this.esClient = esClient;
   }
 
-  @Override
-  public void indexProject(String projectUuid, Cause cause) {
-    switch (cause) {
-      case PROJECT_CREATION:
-        // no need to index, not tests at that time
-      case PROJECT_KEY_UPDATE:
-      case PROJECT_TAGS_UPDATE:
-        // no need to index, project key and tags are not used
-        break;
-      case NEW_ANALYSIS:
-        deleteProject(projectUuid);
-        doIndex(projectUuid, Size.REGULAR);
-        break;
-      default:
-        // defensive case
-        throw new IllegalStateException("Unsupported cause: " + cause);
-    }
-  }
-
   @Override
   public Set<IndexType> getIndexTypes() {
     return ImmutableSet.of(INDEX_TYPE_TEST);
@@ -78,48 +68,100 @@ public class TestIndexer implements ProjectIndexer, StartupIndexer {
 
   @Override
   public void indexOnStartup(Set<IndexType> uninitializedIndexTypes) {
-    doIndex(null, Size.LARGE);
+    try (DbSession dbSession = dbClient.openSession(false);
+         TestResultSetIterator rowIt = TestResultSetIterator.create(dbClient, dbSession, null)) {
+
+      BulkIndexer bulkIndexer = new BulkIndexer(esClient, TestIndexDefinition.INDEX_TYPE_TEST, Size.LARGE);
+      bulkIndexer.start();
+      addTestsToBulkIndexer(rowIt, bulkIndexer);
+      bulkIndexer.stop();
+    }
   }
 
-  public long index(Iterator<FileSourcesUpdaterHelper.Row> dbRows) {
-    BulkIndexer bulk = new BulkIndexer(esClient, INDEX_TYPE_TEST.getIndex(), Size.REGULAR);
-    return doIndex(bulk, dbRows);
+  @Override
+  public void indexOnAnalysis(String projectUuid) {
+    BulkIndexer bulkIndexer = new BulkIndexer(esClient, TestIndexDefinition.INDEX_TYPE_TEST, Size.REGULAR);
+    bulkIndexer.start();
+    addProjectDeletionToBulkIndexer(bulkIndexer, projectUuid);
+    try (DbSession dbSession = dbClient.openSession(false);
+         TestResultSetIterator rowIt = TestResultSetIterator.create(dbClient, dbSession, projectUuid)) {
+      addTestsToBulkIndexer(rowIt, bulkIndexer);
+    }
+    bulkIndexer.stop();
   }
 
-  private long doIndex(@Nullable String projectUuid, Size bulkSize) {
-    final BulkIndexer bulk = new BulkIndexer(esClient, INDEX_TYPE_TEST.getIndex(), bulkSize);
+  @Override
+  public Collection<EsQueueDto> prepareForRecovery(DbSession dbSession, Collection<String> projectUuids, Cause cause) {
+    switch (cause) {
+      case PROJECT_CREATION:
+        // no tests at that time
+        return emptyList();
+
+      case PROJECT_KEY_UPDATE:
+      case PROJECT_TAGS_UPDATE:
+      case PERMISSION_CHANGE:
+        // project key, tags and permissions are not part of tests/test
+        return emptyList();
 
-    try (DbSession dbSession = dbClient.openSession(false)) {
-      TestResultSetIterator rowIt = TestResultSetIterator.create(dbClient, dbSession, projectUuid);
-      long maxUpdatedAt = doIndex(bulk, rowIt);
-      rowIt.close();
-      return maxUpdatedAt;
+      case PROJECT_DELETION:
+        List<EsQueueDto> items = projectUuids.stream()
+          .map(projectUuid -> EsQueueDto.create(INDEX_TYPE_TEST.format(), projectUuid, null, projectUuid))
+          .collect(MoreCollectors.toArrayList(projectUuids.size()));
+        return dbClient.esQueueDao().insert(dbSession, items);
+
+      default:
+        // defensive case
+        throw new IllegalStateException("Unsupported cause: " + cause);
     }
   }
 
-  private static long doIndex(BulkIndexer bulk, Iterator<FileSourcesUpdaterHelper.Row> dbRows) {
-    long maxUpdatedAt = 0L;
+  @VisibleForTesting
+  protected IndexingResult doIndex(Iterator<FileSourcesUpdaterHelper.Row> dbRows, Size bulkSize, IndexingListener listener) {
+    BulkIndexer bulk = new BulkIndexer(esClient, INDEX_TYPE_TEST, bulkSize, listener);
     bulk.start();
     while (dbRows.hasNext()) {
       FileSourcesUpdaterHelper.Row row = dbRows.next();
       row.getUpdateRequests().forEach(bulk::add);
-      maxUpdatedAt = Math.max(maxUpdatedAt, row.getUpdatedAt());
     }
-    bulk.stop();
-    return maxUpdatedAt;
+    return bulk.stop();
   }
 
   public void deleteByFile(String fileUuid) {
     SearchRequestBuilder searchRequest = esClient.prepareSearch(INDEX_TYPE_TEST)
-      .setQuery(QueryBuilders.termsQuery(FIELD_FILE_UUID, fileUuid));
-    BulkIndexer.delete(esClient, INDEX_TYPE_TEST.getIndex(), searchRequest);
+      .setQuery(QueryBuilders.termQuery(FIELD_FILE_UUID, fileUuid));
+    BulkIndexer.delete(esClient, INDEX_TYPE_TEST, searchRequest);
   }
 
   @Override
-  public void deleteProject(String projectUuid) {
+  public IndexingResult index(DbSession dbSession, Collection<EsQueueDto> items) {
+    if (items.isEmpty()) {
+      return new IndexingResult();
+    }
+
+    IndexingListener listener = new OneToManyResilientIndexingListener(dbClient, dbSession, items);
+    BulkIndexer bulkIndexer = new BulkIndexer(esClient, TestIndexDefinition.INDEX_TYPE_TEST, Size.REGULAR, listener);
+    bulkIndexer.start();
+    items.forEach(i -> {
+      String projectUuid = i.getDocId();
+      addProjectDeletionToBulkIndexer(bulkIndexer, projectUuid);
+      try (TestResultSetIterator rowIt = TestResultSetIterator.create(dbClient, dbSession, projectUuid)) {
+        addTestsToBulkIndexer(rowIt, bulkIndexer);
+      }
+    });
+
+    return bulkIndexer.stop();
+  }
+
+  private void addProjectDeletionToBulkIndexer(BulkIndexer bulkIndexer, String projectUuid) {
     SearchRequestBuilder searchRequest = esClient.prepareSearch(INDEX_TYPE_TEST)
-      .setTypes(INDEX_TYPE_TEST.getType())
       .setQuery(QueryBuilders.termQuery(TestIndexDefinition.FIELD_PROJECT_UUID, projectUuid));
-    BulkIndexer.delete(esClient, INDEX_TYPE_TEST.getIndex(), searchRequest);
+    bulkIndexer.addDeletion(searchRequest);
+  }
+
+  private static void addTestsToBulkIndexer(TestResultSetIterator rowIt, BulkIndexer bulkIndexer) {
+    while (rowIt.hasNext()) {
+      FileSourcesUpdaterHelper.Row row = rowIt.next();
+      row.getUpdateRequests().forEach(bulkIndexer::add);
+    }
   }
 }
index 7cead64cb48f0bf49cfaaa261187c8df9a782ef7..50d43af95afd6c7b9b27bff09b94c3f16ef50dc1 100644 (file)
@@ -39,16 +39,14 @@ import org.sonar.server.es.EsClient;
 import org.sonar.server.es.IndexType;
 import org.sonar.server.es.IndexingListener;
 import org.sonar.server.es.IndexingResult;
-import org.sonar.server.es.ResiliencyIndexingListener;
+import org.sonar.server.es.OneToOneResilientIndexingListener;
 import org.sonar.server.es.ResilientIndexer;
-import org.sonar.server.es.StartupIndexer;
 
 import static java.util.Collections.singletonList;
-import static java.util.Objects.requireNonNull;
 import static org.sonar.core.util.stream.MoreCollectors.toHashSet;
 import static org.sonar.server.user.index.UserIndexDefinition.INDEX_TYPE_USER;
 
-public class UserIndexer implements StartupIndexer, ResilientIndexer {
+public class UserIndexer implements ResilientIndexer {
 
   private final DbClient dbClient;
   private final EsClient esClient;
@@ -69,7 +67,7 @@ public class UserIndexer implements StartupIndexer, ResilientIndexer {
       ListMultimap<String, String> organizationUuidsByLogin = ArrayListMultimap.create();
       dbClient.organizationMemberDao().selectAllForUserIndexing(dbSession, organizationUuidsByLogin::put);
 
-      BulkIndexer bulkIndexer = newBulkIndexer(Size.LARGE, IndexingListener.noop());
+      BulkIndexer bulkIndexer = newBulkIndexer(Size.LARGE, IndexingListener.NOOP);
       bulkIndexer.start();
       dbClient.userDao().scrollAll(dbSession,
         // only index requests, no deletion requests.
@@ -89,7 +87,7 @@ public class UserIndexer implements StartupIndexer, ResilientIndexer {
 
   public void commitAndIndexByLogins(DbSession dbSession, Collection<String> logins) {
     List<EsQueueDto> items = logins.stream()
-      .map(l -> EsQueueDto.create(EsQueueDto.Type.USER, l))
+      .map(l -> EsQueueDto.create(INDEX_TYPE_USER.format(), l))
       .collect(MoreCollectors.toArrayList());
 
     dbClient.esQueueDao().insert(dbSession, items);
@@ -115,17 +113,13 @@ public class UserIndexer implements StartupIndexer, ResilientIndexer {
     }
     Set<String> logins = items
       .stream()
-      .filter(i -> {
-        requireNonNull(i.getDocId(), () -> "BUG - " + i + " has not been persisted before indexing");
-        return i.getDocType() == EsQueueDto.Type.USER;
-      })
       .map(EsQueueDto::getDocId)
       .collect(toHashSet(items.size()));
 
     ListMultimap<String, String> organizationUuidsByLogin = ArrayListMultimap.create();
     dbClient.organizationMemberDao().selectForUserIndexing(dbSession, logins, organizationUuidsByLogin::put);
 
-    BulkIndexer bulkIndexer = newBulkIndexer(Size.REGULAR, new ResiliencyIndexingListener(dbClient, dbSession, items));
+    BulkIndexer bulkIndexer = newBulkIndexer(Size.REGULAR, new OneToOneResilientIndexingListener(dbClient, dbSession, items));
     bulkIndexer.start();
     dbClient.userDao().scrollByLogins(dbSession, logins,
       // only index requests, no deletion requests.
@@ -137,12 +131,12 @@ public class UserIndexer implements StartupIndexer, ResilientIndexer {
 
     // the remaining logins reference rows that don't exist in db. They must
     // be deleted from index.
-    logins.forEach(l -> bulkIndexer.addDeletion(UserIndexDefinition.INDEX_TYPE_USER, l));
+    logins.forEach(l -> bulkIndexer.addDeletion(INDEX_TYPE_USER, l));
     return bulkIndexer.stop();
   }
 
   private BulkIndexer newBulkIndexer(Size bulkSize, IndexingListener listener) {
-    return new BulkIndexer(esClient, UserIndexDefinition.INDEX_TYPE_USER.getIndex(), bulkSize, listener);
+    return new BulkIndexer(esClient, INDEX_TYPE_USER, bulkSize, listener);
   }
 
   private static IndexRequest newIndexRequest(UserDto user, ListMultimap<String, String> organizationUuidsByLogins) {
@@ -155,7 +149,7 @@ public class UserIndexer implements StartupIndexer, ResilientIndexer {
     doc.setScmAccounts(UserDto.decodeScmAccounts(user.getScmAccounts()));
     doc.setOrganizationUuids(organizationUuidsByLogins.get(user.getLogin()));
 
-    return new IndexRequest(UserIndexDefinition.INDEX_TYPE_USER.getIndex(), UserIndexDefinition.INDEX_TYPE_USER.getType())
+    return new IndexRequest(INDEX_TYPE_USER.getIndex(), INDEX_TYPE_USER.getType())
       .id(doc.getId())
       .routing(doc.getRouting())
       .source(doc.getFields());
index de300419b2f98a46561614b0eee37175076f92b9..4b7dc2f49364f59c03112be1c34658c3cae87d98 100644 (file)
@@ -79,6 +79,6 @@ public class ViewIndex {
   public void delete(Collection<String> viewUuids) {
     SearchRequestBuilder searchRequest = esClient.prepareSearch(ViewIndexDefinition.INDEX_TYPE_VIEW)
       .setQuery(boolQuery().must(matchAllQuery()).filter(termsQuery(ViewIndexDefinition.FIELD_UUID, viewUuids)));
-    BulkIndexer.delete(esClient, ViewIndexDefinition.INDEX_TYPE_VIEW.getIndex(), searchRequest);
+    BulkIndexer.delete(esClient, ViewIndexDefinition.INDEX_TYPE_VIEW, searchRequest);
   }
 }
index 2c92637976c5918643d54c2c1a4cf317874d28ec..313599b80eaf63da3a94eb456b3ea62ae41de4e1 100644 (file)
@@ -88,14 +88,14 @@ public class ViewIndexer implements StartupIndexer {
    * The views lookup cache will be cleared
    */
   public void index(ViewDoc viewDoc) {
-    BulkIndexer bulk = new BulkIndexer(esClient, ViewIndexDefinition.INDEX_TYPE_VIEW.getIndex(), Size.REGULAR);
+    BulkIndexer bulk = new BulkIndexer(esClient, ViewIndexDefinition.INDEX_TYPE_VIEW, Size.REGULAR);
     bulk.start();
     doIndex(bulk, viewDoc, true);
     bulk.stop();
   }
 
   private void index(DbSession dbSession, Map<String, String> viewAndProjectViewUuidMap, boolean needClearCache, Size bulkSize) {
-    BulkIndexer bulk = new BulkIndexer(esClient, ViewIndexDefinition.INDEX_TYPE_VIEW.getIndex(), bulkSize);
+    BulkIndexer bulk = new BulkIndexer(esClient, ViewIndexDefinition.INDEX_TYPE_VIEW, bulkSize);
     bulk.start();
     for (Map.Entry<String, String> entry : viewAndProjectViewUuidMap.entrySet()) {
       String viewUuid = entry.getKey();
index a2fb921d485edcad25cda364a82c9ffa69d99a27..1a53c05b7edff923d1833efbc5614009fa7098f8 100644 (file)
@@ -38,17 +38,16 @@ import org.sonar.db.issue.IssueDto;
 import org.sonar.db.issue.IssueTesting;
 import org.sonar.db.rule.RuleDefinitionDto;
 import org.sonar.db.rule.RuleTesting;
-import org.sonar.server.es.ProjectIndexer;
+import org.sonar.server.es.TestProjectIndexers;
 
 import static java.util.Arrays.asList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
 import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto;
+import static org.sonar.server.es.ProjectIndexer.Cause.PROJECT_DELETION;
 
 public class ComponentCleanerServiceTest {
 
@@ -62,9 +61,9 @@ public class ComponentCleanerServiceTest {
 
   private DbClient dbClient = db.getDbClient();
   private DbSession dbSession = db.getSession();
-  private ProjectIndexer projectIndexer = mock(ProjectIndexer.class);
+  private TestProjectIndexers projectIndexers = new TestProjectIndexers();
   private ResourceTypes mockResourceTypes = mock(ResourceTypes.class);
-  private ComponentCleanerService underTest = new ComponentCleanerService(dbClient, mockResourceTypes, projectIndexer);
+  private ComponentCleanerService underTest = new ComponentCleanerService(dbClient, mockResourceTypes, projectIndexers);
 
   @Test
   public void delete_project_from_db_and_index() {
@@ -151,12 +150,13 @@ public class ComponentCleanerServiceTest {
 
   private void assertNotExists(DbData data) {
     assertDataInDb(data, false);
-    verify(projectIndexer).deleteProject(data.project.uuid());
+
+    assertThat(projectIndexers.hasBeenCalled(data.project.uuid(), PROJECT_DELETION)).isTrue();
   }
 
   private void assertExists(DbData data) {
     assertDataInDb(data, true);
-    verify(projectIndexer, never()).deleteProject(data.project.uuid());
+    assertThat(projectIndexers.hasBeenCalled(data.project.uuid(), PROJECT_DELETION)).isFalse();
   }
 
   private void assertDataInDb(DbData data, boolean exists) {
index fd7b5fcabae28abe6f8719d477b299d9cce8168a..b8928a8419fc42f44150ac36739fdce8f9c2b0f5 100644 (file)
@@ -29,11 +29,10 @@ import org.sonar.db.DbTester;
 import org.sonar.db.component.ComponentDbTester;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.component.ComponentTesting;
-import org.sonar.server.es.ProjectIndexer;
+import org.sonar.server.es.TestProjectIndexers;
 import org.sonar.server.tester.UserSessionRule;
 
 import static org.assertj.guava.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
 import static org.sonar.db.component.ComponentTesting.newModuleDto;
 
@@ -49,9 +48,9 @@ public class ComponentServiceTest {
   private ComponentDbTester componentDb = new ComponentDbTester(dbTester);
   private DbClient dbClient = dbTester.getDbClient();
   private DbSession dbSession = dbTester.getSession();
-  private ProjectIndexer projectIndexer = mock(ProjectIndexer.class);
+  private TestProjectIndexers projectIndexers = new TestProjectIndexers();
 
-  private ComponentService underTest = new ComponentService(dbClient, userSession, projectIndexer);
+  private ComponentService underTest = new ComponentService(dbClient, userSession, projectIndexers);
 
   @Test
   public void bulk_update() {
index 6b2e0933d7cacb499c21f72b5c78380231b9c1c2..b42087054889142b1bedb6bde584109865e0a3bf 100644 (file)
@@ -31,13 +31,12 @@ import org.sonar.db.component.ComponentDbTester;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.component.ComponentTesting;
 import org.sonar.server.es.ProjectIndexer;
+import org.sonar.server.es.TestProjectIndexers;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.tester.UserSessionRule;
 
 import static org.assertj.guava.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
 import static org.sonar.db.component.ComponentTesting.newModuleDto;
 
@@ -55,8 +54,8 @@ public class ComponentServiceUpdateKeyTest {
   private ComponentDbTester componentDb = new ComponentDbTester(db);
   private DbClient dbClient = db.getDbClient();
   private DbSession dbSession = db.getSession();
-  private ProjectIndexer projectIndexer = mock(ProjectIndexer.class);
-  private ComponentService underTest = new ComponentService(dbClient, userSession, projectIndexer);
+  private TestProjectIndexers projectIndexers = new TestProjectIndexers();
+  private ComponentService underTest = new ComponentService(dbClient, userSession, projectIndexers);
 
   @Test
   public void update_project_key() {
@@ -80,7 +79,7 @@ public class ComponentServiceUpdateKeyTest {
 
     assertThat(dbClient.componentDao().selectByKey(dbSession, inactiveFile.getKey())).isPresent();
 
-    verify(projectIndexer).indexProject(project.uuid(), ProjectIndexer.Cause.PROJECT_KEY_UPDATE);
+    org.assertj.core.api.Assertions.assertThat(projectIndexers.hasBeenCalled(project.uuid(), ProjectIndexer.Cause.PROJECT_KEY_UPDATE)).isTrue();
   }
 
   @Test
@@ -100,7 +99,7 @@ public class ComponentServiceUpdateKeyTest {
     assertComponentKeyHasBeenUpdated(module.key(), "sample:root2:module");
     assertComponentKeyHasBeenUpdated(file.key(), "sample:root2:module:src/File.xoo");
 
-    verify(projectIndexer).indexProject(module.uuid(), ProjectIndexer.Cause.PROJECT_KEY_UPDATE);
+    org.assertj.core.api.Assertions.assertThat(projectIndexers.hasBeenCalled(module.uuid(), ProjectIndexer.Cause.PROJECT_KEY_UPDATE)).isTrue();
   }
 
   @Test
@@ -114,7 +113,7 @@ public class ComponentServiceUpdateKeyTest {
     dbSession.commit();
 
     assertComponentKeyHasBeenUpdated(provisionedProject.key(), "provisionedProject2");
-    verify(projectIndexer).indexProject(provisionedProject.uuid(), ProjectIndexer.Cause.PROJECT_KEY_UPDATE);
+    org.assertj.core.api.Assertions.assertThat(projectIndexers.hasBeenCalled(provisionedProject.uuid(), ProjectIndexer.Cause.PROJECT_KEY_UPDATE)).isTrue();
   }
 
   @Test
index fae662acb0a7693a78b9676a7c4a0a492196f4df..868e7bf706684373b78fcdae4d346f8b9ec7ff19 100644 (file)
@@ -30,6 +30,7 @@ import org.sonar.db.component.ComponentDto;
 import org.sonar.db.organization.OrganizationDto;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.es.ProjectIndexer;
+import org.sonar.server.es.TestProjectIndexers;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.favorite.FavoriteUpdater;
 import org.sonar.server.i18n.I18nRule;
@@ -57,13 +58,13 @@ public class ComponentUpdaterTest {
   @Rule
   public I18nRule i18n = new I18nRule().put("qualifier.TRK", "Project");
 
-  private ProjectIndexer projectIndexer = mock(ProjectIndexer.class);
+  private TestProjectIndexers projectIndexers = new TestProjectIndexers();
   private PermissionTemplateService permissionTemplateService = mock(PermissionTemplateService.class);
 
   private ComponentUpdater underTest = new ComponentUpdater(db.getDbClient(), i18n, system2,
     permissionTemplateService,
     new FavoriteUpdater(db.getDbClient()),
-    projectIndexer);
+    projectIndexers);
 
   @Test
   public void should_persist_and_index_when_creating_project() throws Exception {
@@ -91,7 +92,7 @@ public class ComponentUpdaterTest {
     assertThat(loaded.getCreatedAt()).isNotNull();
     assertThat(db.getDbClient().componentDao().selectOrFailByKey(db.getSession(), DEFAULT_PROJECT_KEY)).isNotNull();
 
-    verify(projectIndexer).indexProject(loaded.uuid(), ProjectIndexer.Cause.PROJECT_CREATION);
+    assertThat(projectIndexers.hasBeenCalled(loaded.uuid(), ProjectIndexer.Cause.PROJECT_CREATION)).isTrue();
   }
 
   @Test
@@ -283,7 +284,7 @@ public class ComponentUpdaterTest {
     assertThat(loaded.getKey()).isEqualTo("view-key");
     assertThat(loaded.name()).isEqualTo("view-name");
     assertThat(loaded.qualifier()).isEqualTo("VW");
-    verify(projectIndexer).indexProject(loaded.uuid(), ProjectIndexer.Cause.PROJECT_CREATION);
+    assertThat(projectIndexers.hasBeenCalled(loaded.uuid(), ProjectIndexer.Cause.PROJECT_CREATION)).isTrue();
   }
 
 }
index cf69abaaf33f65920827e3b145b693192bba2691..959ec117e9e24ec31b188e53ef2387200fc1d65e 100644 (file)
@@ -19,7 +19,9 @@
  */
 package org.sonar.server.component.index;
 
-import org.junit.Before;
+import java.util.Arrays;
+import java.util.Collection;
+import org.elasticsearch.search.SearchHit;
 import org.junit.Rule;
 import org.junit.Test;
 import org.sonar.api.config.internal.MapSettings;
@@ -28,148 +30,200 @@ import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
 import org.sonar.db.component.ComponentDto;
-import org.sonar.db.component.ComponentTesting;
 import org.sonar.db.component.ComponentUpdateDto;
-import org.sonar.db.organization.OrganizationDto;
-import org.sonar.db.organization.OrganizationTesting;
+import org.sonar.db.es.EsQueueDto;
 import org.sonar.server.es.EsTester;
+import org.sonar.server.es.IndexingResult;
 import org.sonar.server.es.ProjectIndexer;
 
+import static java.util.Collections.emptySet;
+import static java.util.Collections.singletonList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.elasticsearch.index.query.QueryBuilders.termQuery;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
+import static org.sonar.db.component.ComponentTesting.newFileDto;
 import static org.sonar.server.component.index.ComponentIndexDefinition.FIELD_NAME;
 import static org.sonar.server.component.index.ComponentIndexDefinition.INDEX_TYPE_COMPONENT;
+import static org.sonar.server.es.ProjectIndexer.Cause.PROJECT_CREATION;
+import static org.sonar.server.es.ProjectIndexer.Cause.PROJECT_DELETION;
 
 public class ComponentIndexerTest {
 
   private System2 system2 = System2.INSTANCE;
 
   @Rule
-  public EsTester esTester = new EsTester(new ComponentIndexDefinition(new MapSettings().asConfig()));
-
+  public EsTester es = new EsTester(new ComponentIndexDefinition(new MapSettings().asConfig()));
   @Rule
-  public DbTester dbTester = DbTester.create(system2);
+  public DbTester db = DbTester.create(system2);
 
-  private DbClient dbClient = dbTester.getDbClient();
-  private DbSession dbSession = dbTester.getSession();
-  private OrganizationDto organization;
+  private DbClient dbClient = db.getDbClient();
+  private DbSession dbSession = db.getSession();
+  private ComponentIndexer underTest = new ComponentIndexer(db.getDbClient(), es.client());
 
-  @Before
-  public void setUp() {
-    organization = OrganizationTesting.newOrganizationDto();
+  @Test
+  public void test_getIndexTypes() {
+    assertThat(underTest.getIndexTypes()).containsExactly(INDEX_TYPE_COMPONENT);
   }
 
   @Test
-  public void index_on_startup() {
-    ComponentIndexer indexer = spy(createIndexer());
-    doNothing().when(indexer).index();
-    indexer.indexOnStartup(null);
-    verify(indexer).indexOnStartup(null);
+  public void indexOnStartup_does_nothing_if_no_projects() {
+    underTest.indexOnStartup(emptySet());
+
+    assertThatIndexHasSize(0);
   }
 
   @Test
-  public void index_nothing() {
-    index();
-    assertThat(count()).isZero();
+  public void indexOnStartup_indexes_all_components() {
+    ComponentDto project1 = db.components().insertPrivateProject();
+    ComponentDto project2 = db.components().insertPrivateProject();
+
+    underTest.indexOnStartup(emptySet());
+
+    assertThatIndexContainsOnly(project1, project2);
   }
 
+
   @Test
-  public void index_everything() {
-    insert(ComponentTesting.newPrivateProjectDto(organization));
+  public void indexOnAnalysis_indexes_project() {
+    ComponentDto project = db.components().insertPrivateProject();
+    ComponentDto file = db.components().insertComponent(newFileDto(project));
+
+    underTest.indexOnAnalysis(project.uuid());
 
-    index();
-    assertThat(count()).isEqualTo(1);
+    assertThatIndexContainsOnly(project, file);
   }
 
   @Test
-  public void index_unexisting_project_while_database_contains_another() {
-    insert(ComponentTesting.newPrivateProjectDto(organization, "UUID-1"));
-
-    index("UUID-2");
-    assertThat(count()).isEqualTo(0);
+  public void indexOnAnalysis_indexes_new_components() {
+    ComponentDto project = db.components().insertPrivateProject();
+    underTest.indexOnAnalysis(project.uuid());
+    assertThatIndexContainsOnly(project);
+
+    ComponentDto file = db.components().insertComponent(newFileDto(project));
+    underTest.indexOnAnalysis(project.uuid());
+    assertThatIndexContainsOnly(project, file);
   }
 
   @Test
-  public void index_one_project() {
-    ComponentDto project = ComponentTesting.newPrivateProjectDto(organization, "UUID-1");
-    insert(project);
+  public void indexOnAnalysis_updates_index_on_changes() {
+    ComponentDto project = db.components().insertPrivateProject();
+    underTest.indexOnAnalysis(project.uuid());
+    assertThatComponentHasName(project, project.name());
+
+    // modify
+    project.setName("NewName");
+    updateDb(project);
 
-    index(project);
-    assertThat(count()).isEqualTo(1);
+    // verify that index is updated
+    underTest.indexOnAnalysis(project.uuid());
+    assertThatIndexContainsOnly(project);
+    assertThatComponentHasName(project, "NewName");
   }
 
   @Test
-  public void index_one_project_containing_a_file() {
-    ComponentDto projectComponent = ComponentTesting.newPrivateProjectDto(organization, "UUID-PROJECT-1");
-    insert(projectComponent);
-    insert(ComponentTesting.newFileDto(projectComponent));
+  public void do_not_update_index_on_project_tag_update() {
+    ComponentDto project = db.components().insertPrivateProject();
+
+    indexProject(project, ProjectIndexer.Cause.PROJECT_TAGS_UPDATE);
 
-    index(projectComponent);
-    assertThat(count()).isEqualTo(2);
+    assertThatIndexHasSize(0);
   }
 
   @Test
-  public void index_and_update_and_reindex_project() {
+  public void do_not_update_index_on_permission_change() {
+    ComponentDto project = db.components().insertPrivateProject();
 
-    // insert
-    ComponentDto component = ComponentTesting.newPrivateProjectDto(organization, "UUID-1").setName("OldName");
-    insert(component);
+    indexProject(project, ProjectIndexer.Cause.PERMISSION_CHANGE);
 
-    // verify insert
-    index(component);
-    assertMatches("OldName", 1);
+    assertThatIndexHasSize(0);
+  }
 
-    // modify
-    component.setName("NewName");
-    update(component);
+  @Test
+  public void update_index_on_project_creation() {
+    ComponentDto project = db.components().insertPrivateProject();
+    ComponentDto file = db.components().insertComponent(newFileDto(project));
 
-    // verify modification
-    index(component);
-    assertMatches("OldName", 0);
-    assertMatches("NewName", 1);
+    IndexingResult result = indexProject(project, PROJECT_CREATION);
+
+    assertThatIndexContainsOnly(project, file);
+    // two requests (one per component)
+    assertThat(result.getTotal()).isEqualTo(2L);
+    assertThat(result.getSuccess()).isEqualTo(2L);
   }
 
   @Test
-  public void index_and_update_and_reindex_project_with_files() {
+  public void do_not_delete_orphans_when_updating_project() {
+    ComponentDto project = db.components().insertPrivateProject();
+    ComponentDto file = db.components().insertComponent(newFileDto(project));
 
-    // insert
-    ComponentDto project = dbTester.components().insertPrivateProject();
-    ComponentDto file = dbTester.components().insertComponent(ComponentTesting.newFileDto(project).setName("OldFile"));
+    indexProject(project, PROJECT_CREATION);
+    assertThatIndexContainsOnly(project, file);
 
-    // verify insert
-    index(project);
-    assertMatches("OldFile", 1);
+    db.getDbClient().componentDao().delete(db.getSession(), file.getId());
 
-    // modify
-    file.setName("NewFile");
-    update(file);
+    IndexingResult result = indexProject(project, ProjectIndexer.Cause.PROJECT_KEY_UPDATE);
+    assertThatIndexContainsOnly(project, file);
+    // single request for project, no request for file
+    assertThat(result.getTotal()).isEqualTo(1);
+    assertThat(result.getSuccess()).isEqualTo(1);
+  }
 
-    // verify modification
-    index(project);
-    assertMatches("OldFile", 0);
-    assertMatches("NewFile", 1);
+  @Test
+  public void delete_some_components() {
+    ComponentDto project = db.components().insertPrivateProject();
+    ComponentDto file1 = db.components().insertComponent(newFileDto(project));
+    ComponentDto file2 = db.components().insertComponent(newFileDto(project));
+    indexProject(project, PROJECT_CREATION);
+
+    underTest.delete(project.uuid(), singletonList(file1.uuid()));
+
+    assertThatIndexContainsOnly(project, file2);
   }
 
   @Test
-  public void full_reindexing_on_empty_index() {
+  public void delete_project() {
+    ComponentDto project = db.components().insertPrivateProject();
+    ComponentDto file = db.components().insertComponent(newFileDto(project));
+    indexProject(project, PROJECT_CREATION);
+
+    db.getDbClient().componentDao().delete(db.getSession(), project.getId());
+    db.getDbClient().componentDao().delete(db.getSession(), file.getId());
+    indexProject(project, PROJECT_DELETION);
 
-    // insert
-    ComponentDto project = dbTester.components().insertPrivateProject();
-    dbTester.components().insertComponent(ComponentTesting.newFileDto(project).setName("OldFile"));
+    assertThatIndexHasSize(0);
+  }
 
-    // verify insert
-    index();
-    assertMatches("OldFile", 1);
+  @Test
+  public void errors_during_indexing_are_recovered() {
+    ComponentDto project = db.components().insertPrivateProject();
+    ComponentDto file = db.components().insertComponent(newFileDto(project));
+    es.lockWrites(INDEX_TYPE_COMPONENT);
+
+    IndexingResult result = indexProject(project, PROJECT_CREATION);
+    assertThat(result.getTotal()).isEqualTo(2L);
+    assertThat(result.getFailures()).isEqualTo(2L);
+
+    // index is still read-only, fail to recover
+    result = recover();
+    assertThat(result.getTotal()).isEqualTo(2L);
+    assertThat(result.getFailures()).isEqualTo(2L);
+    assertThat(es.countDocuments(INDEX_TYPE_COMPONENT)).isEqualTo(0);
+
+    es.unlockWrites(INDEX_TYPE_COMPONENT);
+
+    result = recover();
+    assertThat(result.getTotal()).isEqualTo(2L);
+    assertThat(result.getFailures()).isEqualTo(0L);
+    assertThatIndexContainsOnly(project, file);
   }
 
-  private void insert(ComponentDto component) {
-    dbTester.components().insertComponent(component);
+  private IndexingResult indexProject(ComponentDto project, ProjectIndexer.Cause cause) {
+    DbSession dbSession = db.getSession();
+    Collection<EsQueueDto> items = underTest.prepareForRecovery(dbSession, singletonList(project.uuid()), cause);
+    dbSession.commit();
+    return underTest.index(dbSession, items);
   }
 
-  private void update(ComponentDto component) {
+  private void updateDb(ComponentDto component) {
     ComponentUpdateDto updateComponent = ComponentUpdateDto.copyFrom(component);
     updateComponent.setBChanged(true);
     dbClient.componentDao().update(dbSession, updateComponent);
@@ -177,34 +231,30 @@ public class ComponentIndexerTest {
     dbSession.commit();
   }
 
-  private void index() {
-    createIndexer().indexOnStartup(null);
+  private IndexingResult recover() {
+    Collection<EsQueueDto> items = db.getDbClient().esQueueDao().selectForRecovery(db.getSession(), System.currentTimeMillis() + 1_000L, 10);
+    return underTest.index(db.getSession(), items);
   }
 
-  private void index(ComponentDto component) {
-    index(component.uuid());
-  }
 
-  private void index(String uuid) {
-    createIndexer().indexProject(uuid, ProjectIndexer.Cause.PROJECT_CREATION);
+  private void assertThatIndexHasSize(int expectedSize) {
+    assertThat(es.countDocuments(INDEX_TYPE_COMPONENT)).isEqualTo(expectedSize);
   }
 
-  private long count() {
-    return esTester.countDocuments(INDEX_TYPE_COMPONENT);
+  private void assertThatIndexContainsOnly(ComponentDto... expectedComponents) {
+    assertThat(es.getIds(INDEX_TYPE_COMPONENT)).containsExactlyInAnyOrder(
+      Arrays.stream(expectedComponents).map(ComponentDto::uuid).toArray(String[]::new));
   }
 
-  private void assertMatches(String nameQuery, int numberOfMatches) {
-    assertThat(
-      esTester.client()
-        .prepareSearch(INDEX_TYPE_COMPONENT)
-        .setQuery(termQuery(FIELD_NAME, nameQuery))
-        .get()
-        .getHits()
-        .getTotalHits()).isEqualTo(numberOfMatches);
+  private void assertThatComponentHasName(ComponentDto component, String expectedName) {
+    SearchHit[] hits = es.client()
+      .prepareSearch(INDEX_TYPE_COMPONENT)
+      .setQuery(termQuery(FIELD_NAME, expectedName))
+      .get()
+      .getHits()
+      .getHits();
+    assertThat(hits)
+      .extracting(SearchHit::getId)
+      .contains(component.uuid());
   }
-
-  private ComponentIndexer createIndexer() {
-    return new ComponentIndexer(dbTester.getDbClient(), esTester.client());
-  }
-
 }
index 7afbb927c5d636c5257b2a4dcdf1069c45239096..2ff7aa96d7eb041c5e1a429e0a004f1352d769df 100644 (file)
@@ -1119,7 +1119,7 @@ public class SearchProjectsActionTest {
     SnapshotDto analysis = db.components().insertSnapshot(project);
     Arrays.stream(measures).forEach(m -> db.measureDbTester().insertMeasure(project, analysis, m.metric, m.consumer));
     authorizationIndexerTester.allowOnlyAnyone(project);
-    projectMeasuresIndexer.indexProject(project.uuid(), PROJECT_CREATION);
+    projectMeasuresIndexer.indexOnAnalysis(project.uuid());
     return project;
   }
 
index 4f354df9bd102bb1ecccd5f5c2b2caed404ddc6d..c0c6518f4ce540b67199bb209e5a3de45c35a965 100644 (file)
@@ -42,7 +42,6 @@ import org.sonar.server.component.index.ComponentIndex;
 import org.sonar.server.component.index.ComponentIndexDefinition;
 import org.sonar.server.component.index.ComponentIndexer;
 import org.sonar.server.es.EsTester;
-import org.sonar.server.es.ProjectIndexer;
 import org.sonar.server.favorite.FavoriteFinder;
 import org.sonar.server.permission.index.AuthorizationTypeSupport;
 import org.sonar.server.permission.index.PermissionIndexerTester;
@@ -339,7 +338,7 @@ public class SuggestionsActionTest {
   @Test
   public void suggestions_without_query_should_return_empty_qualifiers() throws Exception {
     ComponentDto project = db.components().insertComponent(newPrivateProjectDto(organization));
-    componentIndexer.indexProject(project.projectUuid(), ProjectIndexer.Cause.PROJECT_CREATION);
+    componentIndexer.indexOnAnalysis(project.projectUuid());
     userSessionRule.addProjectPermission(USER, project);
 
     SuggestionsWsResponse response = ws.newRequest()
@@ -356,7 +355,7 @@ public class SuggestionsActionTest {
   public void suggestions_should_filter_allowed_qualifiers() {
     resourceTypes.setAllQualifiers(PROJECT, MODULE, FILE);
     ComponentDto project = db.components().insertComponent(newPrivateProjectDto(organization));
-    componentIndexer.indexProject(project.projectUuid(), ProjectIndexer.Cause.PROJECT_CREATION);
+    componentIndexer.indexOnAnalysis(project.projectUuid());
     userSessionRule.addProjectPermission(USER, project);
 
     SuggestionsWsResponse response = ws.newRequest()
@@ -444,7 +443,7 @@ public class SuggestionsActionTest {
     OrganizationDto organization1 = db.organizations().insert(o -> o.setKey("org-1").setName("Organization One"));
 
     ComponentDto project1 = db.components().insertComponent(newPrivateProjectDto(organization1).setName("Project1"));
-    componentIndexer.indexProject(project1.projectUuid(), ProjectIndexer.Cause.PROJECT_CREATION);
+    componentIndexer.indexOnAnalysis(project1.projectUuid());
     authorizationIndexerTester.allowOnlyAnyone(project1);
 
     SuggestionsWsResponse response = ws.newRequest()
@@ -464,11 +463,11 @@ public class SuggestionsActionTest {
     OrganizationDto organization2 = db.organizations().insert(o -> o.setKey("org-2").setName("Organization Two"));
 
     ComponentDto project1 = db.components().insertComponent(newPrivateProjectDto(organization1).setName("Project1"));
-    componentIndexer.indexProject(project1.projectUuid(), ProjectIndexer.Cause.PROJECT_CREATION);
+    componentIndexer.indexOnAnalysis(project1.projectUuid());
     authorizationIndexerTester.allowOnlyAnyone(project1);
 
     ComponentDto project2 = db.components().insertComponent(newPrivateProjectDto(organization2).setName("Project2"));
-    componentIndexer.indexProject(project2.projectUuid(), ProjectIndexer.Cause.PROJECT_CREATION);
+    componentIndexer.indexOnAnalysis(project2.projectUuid());
     authorizationIndexerTester.allowOnlyAnyone(project2);
 
     SuggestionsWsResponse response = ws.newRequest()
@@ -488,7 +487,7 @@ public class SuggestionsActionTest {
     ComponentDto project = db.components().insertComponent(newPrivateProjectDto(organization));
     db.components().insertComponent(newModuleDto(project).setName("Module1"));
     db.components().insertComponent(newModuleDto(project).setName("Module2"));
-    componentIndexer.indexProject(project.projectUuid(), ProjectIndexer.Cause.PROJECT_CREATION);
+    componentIndexer.indexOnAnalysis(project.projectUuid());
     authorizationIndexerTester.allowOnlyAnyone(project);
 
     SuggestionsWsResponse response = ws.newRequest()
@@ -514,7 +513,7 @@ public class SuggestionsActionTest {
     db.components().insertComponent(module1);
     ComponentDto module2 = newModuleDto(project).setName("Module2");
     db.components().insertComponent(module2);
-    componentIndexer.indexProject(project.projectUuid(), ProjectIndexer.Cause.PROJECT_CREATION);
+    componentIndexer.indexOnAnalysis(project.projectUuid());
     authorizationIndexerTester.allowOnlyAnyone(project);
 
     SuggestionsWsResponse response = ws.newRequest()
@@ -538,7 +537,7 @@ public class SuggestionsActionTest {
 
     ComponentDto nonFavorite = newModuleDto(project).setName("Module2");
     db.components().insertComponent(nonFavorite);
-    componentIndexer.indexProject(project.projectUuid(), ProjectIndexer.Cause.PROJECT_CREATION);
+    componentIndexer.indexOnAnalysis(project.projectUuid());
     authorizationIndexerTester.allowOnlyAnyone(project);
 
     SuggestionsWsResponse response = ws.newRequest()
@@ -555,7 +554,7 @@ public class SuggestionsActionTest {
   @Test
   public void should_return_empty_qualifiers() throws Exception {
     ComponentDto project = db.components().insertComponent(newPrivateProjectDto(organization));
-    componentIndexer.indexProject(project.projectUuid(), ProjectIndexer.Cause.PROJECT_CREATION);
+    componentIndexer.indexOnAnalysis(project.projectUuid());
     authorizationIndexerTester.allowOnlyAnyone(project);
 
     SuggestionsWsResponse response = ws.newRequest()
index 16b792b4174e73eeeb272233ede7d1e5da82a379..46bf97e6d7ef50e741c9f57cdb81c9b80852484b 100644 (file)
@@ -54,7 +54,7 @@ public class IndexAnalysisStepTest extends BaseStepTest {
 
     underTest.execute();
 
-    verify(componentIndexer).indexProject(PROJECT_UUID, ProjectIndexer.Cause.NEW_ANALYSIS);
+    verify(componentIndexer).indexOnAnalysis(PROJECT_UUID);
   }
 
   @Test
@@ -64,7 +64,7 @@ public class IndexAnalysisStepTest extends BaseStepTest {
 
     underTest.execute();
 
-    verify(componentIndexer).indexProject(PROJECT_UUID, ProjectIndexer.Cause.NEW_ANALYSIS);
+    verify(componentIndexer).indexOnAnalysis(PROJECT_UUID);
   }
 
   @Override
index 8848ae6d5e313788ab4c4f645892503ca67bd75b..7f63c806a08e2647974799fa61a1040c2bd0b8fc 100644 (file)
@@ -20,6 +20,8 @@
 package org.sonar.server.es;
 
 import com.google.common.collect.ImmutableMap;
+import java.util.ArrayList;
+import java.util.List;
 import org.elasticsearch.action.admin.indices.settings.get.GetSettingsResponse;
 import org.elasticsearch.action.index.IndexRequest;
 import org.elasticsearch.action.search.SearchRequestBuilder;
@@ -31,6 +33,7 @@ import org.sonar.api.utils.internal.TestSystem2;
 import org.sonar.db.DbTester;
 import org.sonar.server.es.BulkIndexer.Size;
 
+import static java.util.Collections.emptyMap;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.sonar.server.es.FakeIndexDefinition.INDEX;
 import static org.sonar.server.es.FakeIndexDefinition.INDEX_TYPE_FAKE;
@@ -46,7 +49,7 @@ public class BulkIndexerTest {
 
   @Test
   public void index_nothing() {
-    BulkIndexer indexer = new BulkIndexer(esTester.client(), INDEX, Size.REGULAR);
+    BulkIndexer indexer = new BulkIndexer(esTester.client(), INDEX_TYPE_FAKE, Size.REGULAR);
     indexer.start();
     indexer.stop();
 
@@ -55,7 +58,7 @@ public class BulkIndexerTest {
 
   @Test
   public void index_documents() {
-    BulkIndexer indexer = new BulkIndexer(esTester.client(), INDEX, Size.REGULAR);
+    BulkIndexer indexer = new BulkIndexer(esTester.client(), INDEX_TYPE_FAKE, Size.REGULAR);
     indexer.start();
     indexer.add(newIndexRequest(42));
     indexer.add(newIndexRequest(78));
@@ -73,7 +76,7 @@ public class BulkIndexerTest {
     // index has one replica
     assertThat(replicas()).isEqualTo(1);
 
-    BulkIndexer indexer = new BulkIndexer(esTester.client(), INDEX, Size.LARGE);
+    BulkIndexer indexer = new BulkIndexer(esTester.client(), INDEX_TYPE_FAKE, Size.LARGE);
     indexer.start();
 
     // replicas are temporarily disabled
@@ -106,11 +109,66 @@ public class BulkIndexerTest {
 
     SearchRequestBuilder req = esTester.client().prepareSearch(INDEX_TYPE_FAKE)
       .setQuery(QueryBuilders.rangeQuery(FakeIndexDefinition.INT_FIELD).gte(removeFrom));
-    BulkIndexer.delete(esTester.client(), INDEX, req);
+    BulkIndexer.delete(esTester.client(), INDEX_TYPE_FAKE, req);
 
     assertThat(count()).isEqualTo(removeFrom);
   }
 
+  @Test
+  public void listener_is_called_on_successful_requests() {
+    FakeListener listener = new FakeListener();
+    BulkIndexer indexer = new BulkIndexer(esTester.client(), INDEX_TYPE_FAKE, Size.REGULAR, listener);
+    indexer.start();
+    indexer.addDeletion(INDEX_TYPE_FAKE, "foo");
+    indexer.stop();
+    assertThat(listener.calledDocIds)
+      .containsExactlyInAnyOrder(new DocId(INDEX_TYPE_FAKE, "foo"));
+    assertThat(listener.calledResult.getSuccess()).isEqualTo(1);
+    assertThat(listener.calledResult.getTotal()).isEqualTo(1);
+  }
+
+  @Test
+  public void listener_is_called_even_if_deleting_a_doc_that_does_not_exist() {
+    FakeListener listener = new FakeListener();
+    BulkIndexer indexer = new BulkIndexer(esTester.client(), INDEX_TYPE_FAKE, Size.REGULAR, listener);
+    indexer.start();
+    indexer.add(newIndexRequestWithDocId("foo"));
+    indexer.add(newIndexRequestWithDocId("bar"));
+    indexer.stop();
+    assertThat(listener.calledDocIds)
+      .containsExactlyInAnyOrder(new DocId(INDEX_TYPE_FAKE, "foo"), new DocId(INDEX_TYPE_FAKE, "bar"));
+    assertThat(listener.calledResult.getSuccess()).isEqualTo(2);
+    assertThat(listener.calledResult.getTotal()).isEqualTo(2);
+  }
+
+  @Test
+  public void listener_is_not_called_with_errors() {
+    FakeListener listener = new FakeListener();
+    BulkIndexer indexer = new BulkIndexer(esTester.client(), INDEX_TYPE_FAKE, Size.REGULAR, listener);
+    indexer.start();
+    indexer.add(newIndexRequestWithDocId("foo"));
+    indexer.add(new IndexRequest("index_does_not_exist", "index_does_not_exist", "bar").source(emptyMap()));
+    indexer.stop();
+    assertThat(listener.calledDocIds).containsExactly(new DocId(INDEX_TYPE_FAKE, "foo"));
+    assertThat(listener.calledResult.getSuccess()).isEqualTo(1);
+    assertThat(listener.calledResult.getTotal()).isEqualTo(2);
+  }
+
+  private static class FakeListener implements IndexingListener {
+    private final List<DocId> calledDocIds = new ArrayList<>();
+    private IndexingResult calledResult;
+
+    @Override
+    public void onSuccess(List<DocId> docIds) {
+      calledDocIds.addAll(docIds);
+    }
+
+    @Override
+    public void onFinish(IndexingResult result) {
+      calledResult = result;
+    }
+  }
+
   private long count() {
     return esTester.countDocuments("fakes", "fake");
   }
@@ -125,4 +183,10 @@ public class BulkIndexerTest {
     return new IndexRequest(INDEX, INDEX_TYPE_FAKE.getType())
       .source(ImmutableMap.of(FakeIndexDefinition.INT_FIELD, intField));
   }
+
+  private IndexRequest newIndexRequestWithDocId(String id) {
+    return new IndexRequest(INDEX, INDEX_TYPE_FAKE.getType())
+      .id(id)
+      .source(ImmutableMap.of(FakeIndexDefinition.INT_FIELD, 42));
+  }
 }
index 3ae2027cad819e8b2bff359cfe17717f89fff935..56fd0b9d5b521885e42da0bffd1ab1ee4842981c 100644 (file)
@@ -23,16 +23,20 @@ import com.google.common.base.Function;
 import com.google.common.base.Throwables;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.List;
+import java.util.Map;
 import javax.annotation.Nonnull;
 import org.apache.commons.lang.math.RandomUtils;
 import org.apache.commons.lang.reflect.ConstructorUtils;
 import org.elasticsearch.action.admin.indices.delete.DeleteIndexResponse;
+import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsResponse;
 import org.elasticsearch.action.bulk.BulkRequestBuilder;
+import org.elasticsearch.action.bulk.BulkResponse;
 import org.elasticsearch.action.index.IndexRequest;
 import org.elasticsearch.action.search.SearchRequestBuilder;
 import org.elasticsearch.action.search.SearchResponse;
@@ -110,7 +114,10 @@ public class EsTester extends ExternalResource {
           .routing(doc.getRouting())
           .source(doc.getFields()));
       }
-      EsUtils.executeBulkRequest(bulk, "");
+      BulkResponse bulkResponse = bulk.get();
+      if (bulkResponse.hasFailures()) {
+        throw new IllegalStateException(bulkResponse.buildFailureMessage());
+      }
     } catch (Exception e) {
       throw Throwables.propagate(e);
     }
@@ -229,4 +236,21 @@ public class EsTester extends ExternalResource {
       }
     }
   }
+
+  public EsTester lockWrites(IndexType index) {
+    return setIndexSettings(index.getIndex(), ImmutableMap.of("index.blocks.write", "true"));
+  }
+
+  public EsTester unlockWrites(IndexType index) {
+    return setIndexSettings(index.getIndex(), ImmutableMap.of("index.blocks.write", "false"));
+  }
+
+  private EsTester setIndexSettings(String index, Map<String, Object> settings) {
+    UpdateSettingsResponse response = client.nativeClient().admin().indices()
+      .prepareUpdateSettings(index)
+      .setSettings(settings)
+      .get();
+    checkState(response.isAcknowledged());
+    return this;
+  }
 }
diff --git a/server/sonar-server/src/test/java/org/sonar/server/es/IndexTypeTest.java b/server/sonar-server/src/test/java/org/sonar/server/es/IndexTypeTest.java
new file mode 100644 (file)
index 0000000..4e8e71c
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+ package org.sonar.server.es;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class IndexTypeTest {
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Test
+  public void format_and_parse() {
+    IndexType type1 = new IndexType("foo", "bar");
+    assertThat(type1.format()).isEqualTo("foo/bar");
+
+    IndexType type2 = IndexType.parse(type1.format());
+    assertThat(type2.equals(type1)).isTrue();
+  }
+
+  @Test
+  public void parse_throws_IAE_if_invalid_format() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Unsupported IndexType value: foo");
+
+    IndexType.parse("foo");
+  }
+
+  @Test
+  public void equals_and_hashCode() {
+    IndexType type1 = new IndexType("foo", "bar");
+    IndexType type1b = new IndexType("foo", "bar");
+    IndexType type2 = new IndexType("foo", "baz");
+
+    assertThat(type1.equals(type1)).isTrue();
+    assertThat(type1.equals(type1b)).isTrue();
+    assertThat(type1.equals(type2)).isFalse();
+
+    assertThat(type1.hashCode()).isEqualTo(type1.hashCode());
+    assertThat(type1.hashCode()).isEqualTo(type1b.hashCode());
+  }
+}
index 2eb2c4ded8631c2aee64df156ebdb433274010ec..39fa5a4d3f6cb371c3bb0ffde2d1889e6c97b68a 100644 (file)
@@ -26,8 +26,19 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 public class IndexingResultTest {
 
+  private static final Offset<Double> DOUBLE_OFFSET = Offset.offset(0.000001d);
+
   private final IndexingResult underTest = new IndexingResult();
 
+  @Test
+  public void test_empty() {
+    assertThat(underTest.getFailures()).isEqualTo(0);
+    assertThat(underTest.getSuccess()).isEqualTo(0);
+    assertThat(underTest.getTotal()).isEqualTo(0);
+    assertThat(underTest.getSuccessRatio()).isEqualTo(1.0, DOUBLE_OFFSET);
+    assertThat(underTest.isSuccess()).isTrue();
+  }
+
   @Test
   public void test_success() {
     underTest.incrementRequests();
@@ -38,7 +49,7 @@ public class IndexingResultTest {
     assertThat(underTest.getFailures()).isEqualTo(0);
     assertThat(underTest.getSuccess()).isEqualTo(2);
     assertThat(underTest.getTotal()).isEqualTo(2);
-    assertThat(underTest.getFailureRatio()).isEqualTo(0.0, Offset.offset(0.000001d));
+    assertThat(underTest.getSuccessRatio()).isEqualTo(1.0, DOUBLE_OFFSET);
     assertThat(underTest.isSuccess()).isTrue();
   }
 
@@ -50,20 +61,22 @@ public class IndexingResultTest {
     assertThat(underTest.getFailures()).isEqualTo(2);
     assertThat(underTest.getSuccess()).isEqualTo(0);
     assertThat(underTest.getTotal()).isEqualTo(2);
-    assertThat(underTest.getFailureRatio()).isEqualTo(1.0, Offset.offset(0.000001d));
+    assertThat(underTest.getSuccessRatio()).isEqualTo(0.0, DOUBLE_OFFSET);
     assertThat(underTest.isSuccess()).isFalse();
   }
 
   @Test
   public void test_partial_failure() {
+    underTest.incrementRequests();
+    underTest.incrementRequests();
     underTest.incrementRequests();
     underTest.incrementRequests();
     underTest.incrementSuccess();
 
-    assertThat(underTest.getFailures()).isEqualTo(1);
+    assertThat(underTest.getFailures()).isEqualTo(3);
     assertThat(underTest.getSuccess()).isEqualTo(1);
-    assertThat(underTest.getTotal()).isEqualTo(2);
-    assertThat(underTest.getFailureRatio()).isEqualTo(0.5, Offset.offset(0.000001d));
+    assertThat(underTest.getTotal()).isEqualTo(4);
+    assertThat(underTest.getSuccessRatio()).isEqualTo(0.25, DOUBLE_OFFSET);
     assertThat(underTest.isSuccess()).isFalse();
   }
 
@@ -72,7 +85,7 @@ public class IndexingResultTest {
     assertThat(underTest.getFailures()).isEqualTo(0);
     assertThat(underTest.getSuccess()).isEqualTo(0);
     assertThat(underTest.getTotal()).isEqualTo(0);
-    assertThat(underTest.getFailureRatio()).isEqualTo(1);
+    assertThat(underTest.getSuccessRatio()).isEqualTo(1.0);
     assertThat(underTest.isSuccess()).isTrue();
   }
 }
diff --git a/server/sonar-server/src/test/java/org/sonar/server/es/OneToManyResilientIndexingListenerTest.java b/server/sonar-server/src/test/java/org/sonar/server/es/OneToManyResilientIndexingListenerTest.java
new file mode 100644 (file)
index 0000000..cff8f28
--- /dev/null
@@ -0,0 +1,120 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.es;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.es.EsQueueDto;
+import org.sonar.server.component.index.ComponentIndexDefinition;
+import org.sonar.server.issue.index.IssueIndexDefinition;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static java.util.stream.Collectors.toList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.server.issue.index.IssueIndexDefinition.INDEX_TYPE_ISSUE;
+
+public class OneToManyResilientIndexingListenerTest {
+
+  @Rule
+  public EsTester es = new EsTester(new IssueIndexDefinition(new MapSettings().asConfig()));
+  @Rule
+  public DbTester db = DbTester.create();
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Test
+  public void ES_QUEUE_rows_are_deleted_when_all_docs_are_successfully_indexed() {
+    EsQueueDto item1 = insertInQueue(INDEX_TYPE_ISSUE, "P1");
+    EsQueueDto item2 = insertInQueue(INDEX_TYPE_ISSUE, "P2");
+    EsQueueDto outOfScopeItem = insertInQueue(ComponentIndexDefinition.INDEX_TYPE_COMPONENT, "P1");
+    db.commit();
+
+    // does not contain outOfScopeItem
+    IndexingListener underTest = newListener(asList(item1, item2));
+
+    DocId issue1 = newDocId(INDEX_TYPE_ISSUE, "I1");
+    DocId issue2 = newDocId(INDEX_TYPE_ISSUE, "I2");
+    underTest.onSuccess(asList(issue1, issue2));
+    assertThatEsTableContainsOnly(item1, item2, outOfScopeItem);
+
+    // onFinish deletes all items
+    IndexingResult result = new IndexingResult();
+    result.incrementSuccess().incrementRequests();
+    result.incrementSuccess().incrementRequests();
+    underTest.onFinish(result);
+
+    assertThatEsTableContainsOnly(outOfScopeItem);
+  }
+
+  @Test
+  public void ES_QUEUE_rows_are_not_deleted_on_partial_error() {
+    EsQueueDto item1 = insertInQueue(INDEX_TYPE_ISSUE, "P1");
+    EsQueueDto item2 = insertInQueue(INDEX_TYPE_ISSUE, "P2");
+    EsQueueDto outOfScopeItem = insertInQueue(ComponentIndexDefinition.INDEX_TYPE_COMPONENT, "P1");
+    db.commit();
+
+    // does not contain outOfScopeItem
+    IndexingListener underTest = newListener(asList(item1, item2));
+
+    DocId issue1 = newDocId(INDEX_TYPE_ISSUE, "I1");
+    DocId issue2 = newDocId(INDEX_TYPE_ISSUE, "I2");
+    underTest.onSuccess(asList(issue1, issue2));
+    assertThatEsTableContainsOnly(item1, item2, outOfScopeItem);
+
+    // one failure among the 2 indexing requests of issues
+    IndexingResult result = new IndexingResult();
+    result.incrementSuccess().incrementRequests();
+    result.incrementRequests();
+    underTest.onFinish(result);
+
+    assertThatEsTableContainsOnly(item1, item2, outOfScopeItem);
+  }
+
+  private static DocId newDocId(IndexType indexType, String id) {
+    return new DocId(indexType, id);
+  }
+
+  private IndexingListener newListener(Collection<EsQueueDto> items) {
+    return new OneToManyResilientIndexingListener(db.getDbClient(), db.getSession(), items);
+  }
+
+  private EsQueueDto insertInQueue(IndexType indexType, String id) {
+    EsQueueDto item = EsQueueDto.create(indexType.format(), id);
+    db.getDbClient().esQueueDao().insert(db.getSession(), singletonList(item));
+    return item;
+  }
+
+  private void assertThatEsTableContainsOnly(EsQueueDto... expected) {
+    try (DbSession otherSession = db.getDbClient().openSession(false)) {
+      List<String> uuidsInDb = db.getDbClient().esQueueDao().selectForRecovery(otherSession, Long.MAX_VALUE, 10)
+        .stream().map(EsQueueDto::getUuid).collect(toList());
+      String expectedUuids[] = Arrays.stream(expected).map(EsQueueDto::getUuid).toArray(String[]::new);
+      assertThat(uuidsInDb).containsExactlyInAnyOrder(expectedUuids);
+    }
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/es/OneToOneResilientIndexingListenerTest.java b/server/sonar-server/src/test/java/org/sonar/server/es/OneToOneResilientIndexingListenerTest.java
new file mode 100644 (file)
index 0000000..4acf7de
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.es;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.es.EsQueueDto;
+import org.sonar.server.issue.index.IssueIndexDefinition;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static java.util.stream.Collectors.toList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.server.issue.index.IssueIndexDefinition.INDEX_TYPE_ISSUE;
+
+public class OneToOneResilientIndexingListenerTest {
+
+  @Rule
+  public EsTester es = new EsTester(new IssueIndexDefinition(new MapSettings().asConfig()));
+  @Rule
+  public DbTester db = DbTester.create();
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Test
+  public void onSuccess_deletes_rows_from_ES_QUEUE_table() {
+    EsQueueDto item1 = insertInQueue(INDEX_TYPE_ISSUE, "foo");
+    EsQueueDto item2 = insertInQueue(INDEX_TYPE_ISSUE, "bar");
+    EsQueueDto item3 = insertInQueue(INDEX_TYPE_ISSUE, "baz");
+    db.commit();
+
+    IndexingListener underTest = newListener(asList(item1, item2, item3));
+
+    underTest.onSuccess(emptyList());
+    assertThatEsTableContainsOnly(item1, item2, item3);
+
+    underTest.onSuccess(asList(toDocId(item1), toDocId(item3)));
+    assertThatEsTableContainsOnly(item2);
+
+    // onFinish does nothing
+    underTest.onFinish(new IndexingResult());
+    assertThatEsTableContainsOnly(item2);
+  }
+
+  /**
+   * ES_QUEUE can contain multiple times the same document, for instance
+   * when an issue has been updated multiple times in a row without
+   * being successfully indexed.
+   * Elasticsearch response does not make difference between the different
+   * occurrences (and nevertheless it would be useless). So all the
+   * occurrences are marked as successfully indexed if a single request
+   * passes.
+   */
+  @Test
+  public void onSuccess_deletes_all_the_rows_with_same_doc_id() {
+    EsQueueDto item1 = insertInQueue(INDEX_TYPE_ISSUE, "foo");
+    // same id as item1
+    EsQueueDto item2 = insertInQueue(INDEX_TYPE_ISSUE, item1.getDocId());
+    EsQueueDto item3 = insertInQueue(INDEX_TYPE_ISSUE, "bar");
+    db.commit();
+
+    IndexingListener underTest = newListener(asList(item1, item2, item3));
+
+    underTest.onSuccess(asList(toDocId(item1)));
+    assertThatEsTableContainsOnly(item3);
+  }
+
+  private static DocId toDocId(EsQueueDto dto) {
+    return new DocId(IndexType.parse(dto.getDocType()), dto.getDocId());
+  }
+
+  private IndexingListener newListener(Collection<EsQueueDto> items) {
+    return new OneToOneResilientIndexingListener(db.getDbClient(), db.getSession(), items);
+  }
+
+  private EsQueueDto insertInQueue(IndexType indexType, String id) {
+    EsQueueDto item = EsQueueDto.create(indexType.format(), id);
+    db.getDbClient().esQueueDao().insert(db.getSession(), singletonList(item));
+    return item;
+  }
+
+  private void assertThatEsTableContainsOnly(EsQueueDto... expected) {
+    try (DbSession otherSession = db.getDbClient().openSession(false)) {
+      List<String> uuidsInDb = db.getDbClient().esQueueDao().selectForRecovery(otherSession, Long.MAX_VALUE, 10)
+        .stream().map(EsQueueDto::getUuid).collect(toList());
+      String expectedUuids[] = Arrays.stream(expected).map(EsQueueDto::getUuid).toArray(String[]::new);
+      assertThat(uuidsInDb).containsExactlyInAnyOrder(expectedUuids);
+    }
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/es/ProjectIndexersImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/es/ProjectIndexersImplTest.java
new file mode 100644 (file)
index 0000000..2d95677
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+ package org.sonar.server.es;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import org.junit.Test;
+import org.sonar.db.DbSession;
+import org.sonar.db.es.EsQueueDto;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class ProjectIndexersImplTest {
+
+  @Test
+  public void commitAndIndex_calls_indexer_with_only_its_supported_items() {
+    EsQueueDto item1a = EsQueueDto.create("fake/fake1", "P1");
+    EsQueueDto item1b = EsQueueDto.create("fake/fake1", "P1");
+    EsQueueDto item2 = EsQueueDto.create("fake/fake2", "P1");
+    FakeIndexer indexer1 = new FakeIndexer(asList(item1a, item1b));
+    FakeIndexer indexer2 = new FakeIndexer(singletonList(item2));
+    DbSession dbSession = mock(DbSession.class);
+
+    ProjectIndexersImpl underTest = new ProjectIndexersImpl(indexer1, indexer2);
+    underTest.commitAndIndex(dbSession, singletonList("P1"), ProjectIndexer.Cause.PROJECT_CREATION);
+
+    assertThat(indexer1.calledItems).containsExactlyInAnyOrder(item1a, item1b);
+    assertThat(indexer2.calledItems).containsExactlyInAnyOrder(item2);
+  }
+
+  private static class FakeIndexer implements ProjectIndexer {
+
+    private final List<EsQueueDto> items;
+    private Collection<EsQueueDto> calledItems;
+
+    private FakeIndexer(List<EsQueueDto> items) {
+      this.items = items;
+    }
+
+    @Override
+    public void indexOnStartup(Set<IndexType> uninitializedIndexTypes) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Set<IndexType> getIndexTypes() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Collection<EsQueueDto> prepareForRecovery(DbSession dbSession, Collection<String> projectUuids, Cause cause) {
+      return items;
+    }
+
+    @Override
+    public IndexingResult index(DbSession dbSession, Collection<EsQueueDto> items) {
+      this.calledItems = items;
+      return new IndexingResult();
+    }
+
+    @Override
+    public void indexOnAnalysis(String projectUuid) {
+      throw new UnsupportedOperationException();
+    }
+  }
+}
index 423a185cacb2e1873e939703f59f712a532daac6..01d33c8bb98d76690e7102d7b5d5dc6b12c7068f 100644 (file)
  */
 package org.sonar.server.es;
 
+import com.google.common.collect.ImmutableSet;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
-import java.util.concurrent.CountDownLatch;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.IntStream;
@@ -33,50 +34,43 @@ import org.junit.Test;
 import org.junit.rules.DisableOnDebug;
 import org.junit.rules.TestRule;
 import org.junit.rules.Timeout;
+import org.sonar.api.config.Configuration;
 import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.utils.MessageException;
 import org.sonar.api.utils.internal.TestSystem2;
 import org.sonar.api.utils.log.LogTester;
 import org.sonar.api.utils.log.LoggerLevel;
 import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
 import org.sonar.db.es.EsQueueDto;
-import org.sonar.db.rule.RuleDto;
-import org.sonar.db.user.UserDto;
-import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
-import org.sonar.server.rule.index.RuleIndexDefinition;
 import org.sonar.server.rule.index.RuleIndexer;
-import org.sonar.server.user.index.UserIndexDefinition;
 import org.sonar.server.user.index.UserIndexer;
 
-import static java.util.Arrays.asList;
 import static java.util.stream.IntStream.rangeClosed;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.sonar.api.utils.log.LoggerLevel.ERROR;
 import static org.sonar.api.utils.log.LoggerLevel.INFO;
 import static org.sonar.api.utils.log.LoggerLevel.TRACE;
-import static org.sonar.core.util.stream.MoreCollectors.toArrayList;
 
 public class RecoveryIndexerTest {
 
   private static final long PAST = 1_000L;
+  private static final IndexType FOO_TYPE = new IndexType("foos", "foo");
+
   private TestSystem2 system2 = new TestSystem2().setNow(PAST);
   private MapSettings emptySettings = new MapSettings();
 
   @Rule
-  public final EsTester es = new EsTester(new UserIndexDefinition(emptySettings.asConfig()), new RuleIndexDefinition(emptySettings.asConfig()));
+  public EsTester es = new EsTester();
   @Rule
-  public final DbTester db = DbTester.create(system2);
+  public DbTester db = DbTester.create(system2);
   @Rule
-  public final LogTester logTester = new LogTester().setLevel(TRACE);
+  public LogTester logTester = new LogTester().setLevel(TRACE);
   @Rule
   public TestRule safeguard = new DisableOnDebug(Timeout.builder().withTimeout(60, TimeUnit.SECONDS).withLookingForStuckThread(true).build());
 
-  private UserIndexer mockedUserIndexer = mock(UserIndexer.class);
-  private RuleIndexer mockedRuleIndexer = mock(RuleIndexer.class);
-  private ActiveRuleIndexer mockedActiveRuleIndexer = mock(ActiveRuleIndexer.class);
   private RecoveryIndexer underTest;
 
   @After
@@ -88,9 +82,7 @@ public class RecoveryIndexerTest {
 
   @Test
   public void display_default_configuration_at_startup() {
-    UserIndexer userIndexer = new UserIndexer(db.getDbClient(), es.client());
-    RuleIndexer ruleIndexer = new RuleIndexer(es.client(), db.getDbClient());
-    underTest = newRecoveryIndexer(userIndexer, ruleIndexer, emptySettings);
+    underTest = newRecoveryIndexer(emptySettings.asConfig());
 
     underTest.start();
 
@@ -104,7 +96,7 @@ public class RecoveryIndexerTest {
     MapSettings settings = new MapSettings()
       .setProperty("sonar.search.recovery.initialDelayInMs", "0")
       .setProperty("sonar.search.recovery.delayInMs", "1");
-    underTest = spy(new RecoveryIndexer(system2, settings.asConfig(), db.getDbClient(), mockedUserIndexer, mockedRuleIndexer, mockedActiveRuleIndexer));
+    underTest = spy(new RecoveryIndexer(system2, settings.asConfig(), db.getDbClient()));
     AtomicInteger calls = new AtomicInteger(0);
     doAnswer(invocation -> {
       calls.incrementAndGet();
@@ -120,58 +112,50 @@ public class RecoveryIndexerTest {
   }
 
   @Test
-  public void successfully_index_RULE_records() {
-    EsQueueDto item1 = createUnindexedRule();
-    EsQueueDto item2 = createUnindexedRule();
-
-    ProxyRuleIndexer ruleIndexer = new ProxyRuleIndexer();
+  public void successfully_recover_indexing_requests() {
+    IndexType type1 = new IndexType("foos", "foo");
+    EsQueueDto item1a = insertItem(type1, "f1");
+    EsQueueDto item1b = insertItem(type1, "f2");
+    IndexType type2 = new IndexType("bars", "bar");
+    EsQueueDto item2 = insertItem(type2, "b1");
+
+    SuccessfulFakeIndexer indexer1 = new SuccessfulFakeIndexer(type1);
+    SuccessfulFakeIndexer indexer2 = new SuccessfulFakeIndexer(type2);
     advanceInTime();
 
-    underTest = newRecoveryIndexer(mockedUserIndexer, ruleIndexer);
+    underTest = newRecoveryIndexer(indexer1, indexer2);
     underTest.recover();
 
     assertThatQueueHasSize(0);
-    assertThat(ruleIndexer.called)
-      .extracting(EsQueueDto::getUuid)
-      .containsExactlyInAnyOrder(item1.getUuid(), item2.getUuid());
-
-    assertThatLogsContain(TRACE, "Elasticsearch recovery - processing 2 RULE");
-    assertThatLogsContain(INFO, "Elasticsearch recovery - 4 documents processed [0 failures]");
-  }
-
-  @Test
-  public void successfully_index_USER_records() {
-    EsQueueDto item1 = createUnindexedUser();
-    EsQueueDto item2 = createUnindexedUser();
+    assertThatLogsContain(INFO, "Elasticsearch recovery - 3 documents processed [0 failures]");
 
-    ProxyUserIndexer userIndexer = new ProxyUserIndexer();
-    advanceInTime();
-    underTest = newRecoveryIndexer(userIndexer, mockedRuleIndexer);
-    underTest.recover();
-
-    assertThatQueueHasSize(0);
-    assertThat(userIndexer.called)
+    assertThat(indexer1.called).hasSize(1);
+    assertThat(indexer1.called.get(0))
       .extracting(EsQueueDto::getUuid)
-      .containsExactlyInAnyOrder(item1.getUuid(), item2.getUuid());
+      .containsExactlyInAnyOrder(item1a.getUuid(), item1b.getUuid());
+    assertThatLogsContain(TRACE, "Elasticsearch recovery - processing 2 [foos/foo]");
 
-    assertThatLogsContain(TRACE, "Elasticsearch recovery - processing 2 USER");
-    assertThatLogsContain(INFO, "Elasticsearch recovery - 2 documents processed [0 failures]");
+    assertThat(indexer2.called).hasSize(1);
+    assertThat(indexer2.called.get(0))
+      .extracting(EsQueueDto::getUuid)
+      .containsExactlyInAnyOrder(item2.getUuid());
+    assertThatLogsContain(TRACE, "Elasticsearch recovery - processing 1 [bars/bar]");
   }
 
   @Test
   public void recent_records_are_not_recovered() {
-    createUnindexedUser();
-    createUnindexedUser();
+    EsQueueDto item = insertItem(FOO_TYPE, "f1");
 
-    ProxyUserIndexer userIndexer = new ProxyUserIndexer();
+    SuccessfulFakeIndexer indexer = new SuccessfulFakeIndexer(FOO_TYPE);
     // do not advance in time
-    underTest = newRecoveryIndexer(userIndexer, mockedRuleIndexer);
+
+    underTest = newRecoveryIndexer(indexer);
     underTest.recover();
 
-    assertThatQueueHasSize(2);
-    assertThat(userIndexer.called).isEmpty();
+    assertThatQueueHasSize(1);
+    assertThat(indexer.called).isEmpty();
 
-    assertThatLogsDoNotContain(TRACE, "Elasticsearch recovery - processing 2 USER");
+    assertThatLogsDoNotContain(TRACE, "Elasticsearch recovery - processing 2 [foos/foo]");
     assertThatLogsDoNotContain(INFO, "documents processed");
   }
 
@@ -187,13 +171,20 @@ public class RecoveryIndexerTest {
   }
 
   @Test
-  public void log_exception_on_recovery_failure() {
-    createUnindexedUser();
-    FailingOnceUserIndexer failingOnceUserIndexer = new FailingOnceUserIndexer();
+  public void hard_failures_are_logged_and_do_not_stop_recovery_scheduling() throws Exception {
+    insertItem(FOO_TYPE, "f1");
+
+    HardFailingFakeIndexer indexer = new HardFailingFakeIndexer(FOO_TYPE);
     advanceInTime();
 
-    underTest = newRecoveryIndexer(failingOnceUserIndexer, mockedRuleIndexer);
-    underTest.recover();
+    underTest = newRecoveryIndexer(indexer);
+    underTest.start();
+
+    // all runs fail, but they are still scheduled
+    // -> waiting for 2 runs
+    while (indexer.called.size() < 2) {
+      Thread.sleep(1L);
+    }
 
     // No rows treated
     assertThatQueueHasSize(1);
@@ -201,86 +192,87 @@ public class RecoveryIndexerTest {
   }
 
   @Test
-  public void scheduler_is_not_stopped_on_failures() throws Exception {
-    createUnindexedUser();
+  public void soft_failures_are_logged_and_do_not_stop_recovery_scheduling() throws Exception {
+    insertItem(FOO_TYPE, "f1");
+
+    SoftFailingFakeIndexer indexer = new SoftFailingFakeIndexer(FOO_TYPE);
     advanceInTime();
-    FailingUserIndexer userIndexer = new FailingUserIndexer();
 
-    underTest = newRecoveryIndexer(userIndexer, mockedRuleIndexer);
+    underTest = newRecoveryIndexer(indexer);
     underTest.start();
 
     // all runs fail, but they are still scheduled
     // -> waiting for 2 runs
-    while (userIndexer.called.size() < 2) {
+    while (indexer.called.size() < 2) {
       Thread.sleep(1L);
     }
+
+    // No rows treated
+    assertThatQueueHasSize(1);
+    assertThatLogsContain(INFO, "Elasticsearch recovery - 1 documents processed [1 failures]");
   }
 
   @Test
-  public void recovery_retries_on_next_run_if_failure() throws Exception {
-    createUnindexedUser();
-    advanceInTime();
-    FailingOnceUserIndexer userIndexer = new FailingOnceUserIndexer();
+  public void unsupported_types_are_kept_in_queue_for_manual_fix_operation() throws Exception {
+    insertItem(FOO_TYPE, "f1");
 
-    underTest = newRecoveryIndexer(userIndexer, mockedRuleIndexer);
-    underTest.start();
+    ResilientIndexer indexer = new SuccessfulFakeIndexer(new IndexType("bars", "bar"));
+    advanceInTime();
 
-    // first run fails, second run succeeds
-    userIndexer.counter.await(30, TimeUnit.SECONDS);
+    underTest = newRecoveryIndexer(indexer);
+    underTest.recover();
 
-    // First we expecting an exception at first run
-    // Then the second run must have treated all records
-    assertThatLogsContain(ERROR, "Elasticsearch recovery - fail to recover documents");
-    assertThatQueueHasSize(0);
+    assertThatQueueHasSize(1);
+    assertThatLogsContain(ERROR, "Elasticsearch recovery - ignore 1 items with unsupported type [foos/foo]");
   }
 
   @Test
   public void stop_run_if_too_many_failures() {
-    IntStream.range(0, 10).forEach(i -> createUnindexedUser());
+    IntStream.range(0, 10).forEach(i -> insertItem(FOO_TYPE, "" + i));
     advanceInTime();
 
     // 10 docs to process, by groups of 3.
     // The first group successfully recovers only 1 docs --> above 30% of failures --> stop run
-    PartiallyFailingUserIndexer failingAboveRatioUserIndexer = new PartiallyFailingUserIndexer(1);
+    PartiallyFailingIndexer indexer = new PartiallyFailingIndexer(FOO_TYPE, 1);
     MapSettings settings = new MapSettings()
       .setProperty("sonar.search.recovery.loopLimit", "3");
-    underTest = newRecoveryIndexer(failingAboveRatioUserIndexer, mockedRuleIndexer, settings);
+    underTest = newRecoveryIndexer(settings.asConfig(), indexer);
     underTest.recover();
 
     assertThatLogsContain(ERROR, "Elasticsearch recovery - too many failures [2/3 documents], waiting for next run");
     assertThatQueueHasSize(9);
 
     // The indexer must have been called once and only once.
-    assertThat(failingAboveRatioUserIndexer.called).hasSize(3);
+    assertThat(indexer.called).hasSize(3);
   }
 
   @Test
-  public void do_not_stop_run_if_success_rate_is_greater_than_ratio() {
-    IntStream.range(0, 10).forEach(i -> createUnindexedUser());
+  public void do_not_stop_run_if_success_rate_is_greater_than_circuit_breaker() {
+    IntStream.range(0, 10).forEach(i -> insertItem(FOO_TYPE, "" + i));
     advanceInTime();
 
     // 10 docs to process, by groups of 5.
     // Each group successfully recovers 4 docs --> below 30% of failures --> continue run
-    PartiallyFailingUserIndexer failingAboveRatioUserIndexer = new PartiallyFailingUserIndexer(4, 4, 2);
+    PartiallyFailingIndexer indexer = new PartiallyFailingIndexer(FOO_TYPE, 4, 4, 2);
     MapSettings settings = new MapSettings()
       .setProperty("sonar.search.recovery.loopLimit", "5");
-    underTest = newRecoveryIndexer(failingAboveRatioUserIndexer, mockedRuleIndexer, settings);
+    underTest = newRecoveryIndexer(settings.asConfig(), indexer);
     underTest.recover();
 
     assertThatLogsDoNotContain(ERROR, "too many failures");
     assertThatQueueHasSize(0);
-    assertThat(failingAboveRatioUserIndexer.indexed).hasSize(10);
-    assertThat(failingAboveRatioUserIndexer.called).hasSize(10 + 2 /* retries */);
+    assertThat(indexer.indexed).hasSize(10);
+    assertThat(indexer.called).hasSize(10 + 2 /* retries */);
   }
 
   @Test
   public void failing_always_on_same_document_does_not_generate_infinite_loop() {
-    EsQueueDto buggy = createUnindexedUser();
-    IntStream.range(0, 10).forEach(i -> createUnindexedUser());
+    EsQueueDto buggy = insertItem(FOO_TYPE, "buggy");
+    IntStream.range(0, 10).forEach(i -> insertItem(FOO_TYPE, "" + i));
     advanceInTime();
 
-    FailingAlwaysOnSameElementIndexer indexer = new FailingAlwaysOnSameElementIndexer(buggy);
-    underTest = newRecoveryIndexer(indexer, mockedRuleIndexer);
+    FailingAlwaysOnSameElementIndexer indexer = new FailingAlwaysOnSameElementIndexer(FOO_TYPE, buggy);
+    underTest = newRecoveryIndexer(indexer);
     underTest.recover();
 
     assertThatLogsContain(ERROR, "Elasticsearch recovery - too many failures [1/1 documents], waiting for next run");
@@ -289,119 +281,66 @@ public class RecoveryIndexerTest {
 
   @Test
   public void recover_multiple_times_the_same_document() {
-    UserDto user = db.users().insertUser();
-    EsQueueDto item1 = EsQueueDto.create(EsQueueDto.Type.USER, user.getLogin());
-    EsQueueDto item2 = EsQueueDto.create(EsQueueDto.Type.USER, user.getLogin());
-    EsQueueDto item3 = EsQueueDto.create(EsQueueDto.Type.USER, user.getLogin());
-    db.getDbClient().esQueueDao().insert(db.getSession(), asList(item1, item2, item3));
-    db.commit();
-
-    ProxyUserIndexer userIndexer = new ProxyUserIndexer();
+    EsQueueDto item1 = insertItem(FOO_TYPE, "f1");
+    EsQueueDto item2 = insertItem(FOO_TYPE, item1.getDocId());
+    EsQueueDto item3 = insertItem(FOO_TYPE, item1.getDocId());
     advanceInTime();
-    underTest = newRecoveryIndexer(userIndexer, mockedRuleIndexer);
+
+    SuccessfulFakeIndexer indexer = new SuccessfulFakeIndexer(FOO_TYPE);
+    underTest = newRecoveryIndexer(indexer);
     underTest.recover();
 
     assertThatQueueHasSize(0);
-    assertThat(userIndexer.called)
-      .extracting(EsQueueDto::getUuid)
+    assertThat(indexer.called).hasSize(1);
+    assertThat(indexer.called.get(0)).extracting(EsQueueDto::getUuid)
       .containsExactlyInAnyOrder(item1.getUuid(), item2.getUuid(), item3.getUuid());
 
-    assertThatLogsContain(TRACE, "Elasticsearch recovery - processing 3 USER");
-    assertThatLogsContain(INFO, "Elasticsearch recovery - 1 documents processed [0 failures]");
-  }
-
-  private class ProxyUserIndexer extends UserIndexer {
-    private final List<EsQueueDto> called = new ArrayList<>();
-
-    ProxyUserIndexer() {
-      super(db.getDbClient(), es.client());
-    }
-
-    @Override
-    public IndexingResult index(DbSession dbSession, Collection<EsQueueDto> items) {
-      called.addAll(items);
-      return super.index(dbSession, items);
-    }
-  }
-
-  private class ProxyRuleIndexer extends RuleIndexer {
-    private final List<EsQueueDto> called = new ArrayList<>();
-
-    ProxyRuleIndexer() {
-      super(es.client(), db.getDbClient());
-    }
-
-    @Override
-    public IndexingResult index(DbSession dbSession, Collection<EsQueueDto> items) {
-      called.addAll(items);
-      return super.index(dbSession, items);
-    }
-  }
-
-  private class FailingUserIndexer extends UserIndexer {
-    private final List<EsQueueDto> called = new ArrayList<>();
-
-    FailingUserIndexer() {
-      super(db.getDbClient(), es.client());
-    }
-
-    @Override
-    public IndexingResult index(DbSession dbSession, Collection<EsQueueDto> items) {
-      called.addAll(items);
-      throw new RuntimeException("boom");
-    }
-
+    assertThatLogsContain(TRACE, "Elasticsearch recovery - processing 3 [foos/foo]");
+    assertThatLogsContain(INFO, "Elasticsearch recovery - 3 documents processed [0 failures]");
   }
 
-  private class FailingOnceUserIndexer extends UserIndexer {
-    private final CountDownLatch counter = new CountDownLatch(2);
+  private class FailingAlwaysOnSameElementIndexer implements ResilientIndexer {
+    private final IndexType indexType;
+    private final EsQueueDto failing;
 
-    FailingOnceUserIndexer() {
-      super(db.getDbClient(), es.client());
+    FailingAlwaysOnSameElementIndexer(IndexType indexType, EsQueueDto failing) {
+      this.indexType = indexType;
+      this.failing = failing;
     }
 
     @Override
     public IndexingResult index(DbSession dbSession, Collection<EsQueueDto> items) {
-      try {
-        if (counter.getCount() == 2) {
-          throw new RuntimeException("boom");
+      IndexingResult result = new IndexingResult();
+      items.forEach(item -> {
+        result.incrementRequests();
+        if (!item.getUuid().equals(failing.getUuid())) {
+          result.incrementSuccess();
+          db.getDbClient().esQueueDao().delete(dbSession, item);
+          dbSession.commit();
         }
-        return super.index(dbSession, items);
-      } finally {
-        counter.countDown();
-      }
+      });
+      return result;
     }
-  }
-
-  private class FailingAlwaysOnSameElementIndexer extends UserIndexer {
-    private final EsQueueDto failing;
 
-    FailingAlwaysOnSameElementIndexer(EsQueueDto failing) {
-      super(db.getDbClient(), es.client());
-      this.failing = failing;
+    @Override
+    public void indexOnStartup(Set<IndexType> uninitializedIndexTypes) {
+      throw new UnsupportedOperationException();
     }
 
     @Override
-    public IndexingResult index(DbSession dbSession, Collection<EsQueueDto> items) {
-      List<EsQueueDto> filteredItems = items.stream().filter(
-        i -> !i.getUuid().equals(failing.getUuid())).collect(toArrayList());
-      IndexingResult result = super.index(dbSession, filteredItems);
-      if (result.getTotal() == items.size() - 1) {
-        // the failing item was in the items list
-        result.incrementRequests();
-      }
-
-      return result;
+    public Set<IndexType> getIndexTypes() {
+      return ImmutableSet.of(indexType);
     }
   }
 
-  private class PartiallyFailingUserIndexer extends UserIndexer {
+  private class PartiallyFailingIndexer implements ResilientIndexer {
+    private final IndexType indexType;
     private final List<EsQueueDto> called = new ArrayList<>();
     private final List<EsQueueDto> indexed = new ArrayList<>();
     private final Iterator<Integer> successfulReturns;
 
-    PartiallyFailingUserIndexer(int... successfulReturns) {
-      super(db.getDbClient(), es.client());
+    PartiallyFailingIndexer(IndexType indexType, int... successfulReturns) {
+      this.indexType = indexType;
       this.successfulReturns = IntStream.of(successfulReturns).iterator();
     }
 
@@ -420,6 +359,16 @@ public class RecoveryIndexerTest {
       dbSession.commit();
       return result;
     }
+
+    @Override
+    public void indexOnStartup(Set<IndexType> uninitializedIndexTypes) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Set<IndexType> getIndexTypes() {
+      return ImmutableSet.of(indexType);
+    }
   }
 
   private void advanceInTime() {
@@ -448,33 +397,104 @@ public class RecoveryIndexerTest {
     return newRecoveryIndexer(userIndexer, ruleIndexer);
   }
 
-  private RecoveryIndexer newRecoveryIndexer(UserIndexer userIndexer, RuleIndexer ruleIndexer) {
+  private RecoveryIndexer newRecoveryIndexer(ResilientIndexer... indexers) {
     MapSettings settings = new MapSettings()
       .setProperty("sonar.search.recovery.initialDelayInMs", "0")
       .setProperty("sonar.search.recovery.delayInMs", "1")
       .setProperty("sonar.search.recovery.minAgeInMs", "1");
-    return newRecoveryIndexer(userIndexer, ruleIndexer, settings);
+    return newRecoveryIndexer(settings.asConfig(), indexers);
   }
 
-  private RecoveryIndexer newRecoveryIndexer(UserIndexer userIndexer, RuleIndexer ruleIndexer, MapSettings settings) {
-    return new RecoveryIndexer(system2, settings.asConfig(), db.getDbClient(), userIndexer, ruleIndexer, mockedActiveRuleIndexer);
+  private RecoveryIndexer newRecoveryIndexer(Configuration config, ResilientIndexer... indexers) {
+    return new RecoveryIndexer(system2, config, db.getDbClient(), indexers);
   }
 
-  private EsQueueDto createUnindexedUser() {
-    UserDto user = db.users().insertUser();
-    EsQueueDto item = EsQueueDto.create(EsQueueDto.Type.USER, user.getLogin());
+  private EsQueueDto insertItem(IndexType indexType, String docUuid) {
+    EsQueueDto item = EsQueueDto.create(indexType.format(), docUuid);
     db.getDbClient().esQueueDao().insert(db.getSession(), item);
     db.commit();
-
     return item;
   }
 
-  private EsQueueDto createUnindexedRule() {
-    RuleDto rule = db.rules().insertRule();
-    EsQueueDto item = EsQueueDto.create(EsQueueDto.Type.RULE, rule.getKey().toString());
-    db.getDbClient().esQueueDao().insert(db.getSession(), item);
-    db.commit();
+  private class SuccessfulFakeIndexer implements ResilientIndexer {
+    private final Set<IndexType> types;
+    private final List<Collection<EsQueueDto>> called = new ArrayList<>();
 
-    return item;
+    private SuccessfulFakeIndexer(IndexType type) {
+      this.types = ImmutableSet.of(type);
+    }
+
+    @Override
+    public void indexOnStartup(Set<IndexType> uninitializedIndexTypes) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Set<IndexType> getIndexTypes() {
+      return types;
+    }
+
+    @Override
+    public IndexingResult index(DbSession dbSession, Collection<EsQueueDto> items) {
+      called.add(items);
+      IndexingResult result = new IndexingResult();
+      items.forEach(i -> result.incrementSuccess().incrementRequests());
+      db.getDbClient().esQueueDao().delete(dbSession, items);
+      dbSession.commit();
+      return result;
+    }
+  }
+
+  private class HardFailingFakeIndexer implements ResilientIndexer {
+    private final Set<IndexType> types;
+    private final List<Collection<EsQueueDto>> called = new ArrayList<>();
+
+    private HardFailingFakeIndexer(IndexType type) {
+      this.types = ImmutableSet.of(type);
+    }
+
+    @Override
+    public void indexOnStartup(Set<IndexType> uninitializedIndexTypes) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Set<IndexType> getIndexTypes() {
+      return types;
+    }
+
+    @Override
+    public IndexingResult index(DbSession dbSession, Collection<EsQueueDto> items) {
+      called.add(items);
+      // MessageException is used just to reduce noise in test logs
+      throw MessageException.of("BOOM");
+    }
+  }
+
+  private class SoftFailingFakeIndexer implements ResilientIndexer {
+    private final Set<IndexType> types;
+    private final List<Collection<EsQueueDto>> called = new ArrayList<>();
+
+    private SoftFailingFakeIndexer(IndexType type) {
+      this.types = ImmutableSet.of(type);
+    }
+
+    @Override
+    public void indexOnStartup(Set<IndexType> uninitializedIndexTypes) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Set<IndexType> getIndexTypes() {
+      return types;
+    }
+
+    @Override
+    public IndexingResult index(DbSession dbSession, Collection<EsQueueDto> items) {
+      called.add(items);
+      IndexingResult result = new IndexingResult();
+      items.forEach(i -> result.incrementRequests());
+      return result;
+    }
   }
 }
diff --git a/server/sonar-server/src/test/java/org/sonar/server/es/TestProjectIndexers.java b/server/sonar-server/src/test/java/org/sonar/server/es/TestProjectIndexers.java
new file mode 100644 (file)
index 0000000..957612a
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.es;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+import java.util.Collection;
+import org.sonar.db.DbSession;
+
+public class TestProjectIndexers implements ProjectIndexers {
+
+  private final ListMultimap<String, ProjectIndexer.Cause> calls = ArrayListMultimap.create();
+
+  @Override
+  public void commitAndIndex(DbSession dbSession, Collection<String> projectUuids, ProjectIndexer.Cause cause) {
+    dbSession.commit();
+    projectUuids.forEach(projectUuid -> calls.put(projectUuid, cause));
+
+  }
+
+  public boolean hasBeenCalled(String projectUuid, ProjectIndexer.Cause expectedCause) {
+    return calls.get(projectUuid).contains(expectedCause);
+  }
+
+  public boolean hasBeenCalled(String projectUuid) {
+    return calls.containsKey(projectUuid);
+  }
+}
index cc753f3b3e8be8018748310eb24116847b208dde..3ab87f778763451e39c926fcb9be73298077c175 100644 (file)
@@ -48,7 +48,6 @@ import org.sonar.server.tester.ServerTester;
 import org.sonar.server.tester.UserSessionRule;
 
 import static java.util.Arrays.asList;
-import static java.util.Collections.singletonList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.entry;
 
@@ -148,12 +147,17 @@ public class IssueServiceMediumTest {
     tester.get(ComponentDao.class).insert(session, project);
     session.commit();
 
-    tester.get(PermissionIndexer.class).indexProjectsByUuids(session, singletonList(project.uuid()));
+    indexPermissions();
     userSessionRule.logIn();
 
     return project;
   }
 
+  private void indexPermissions() {
+    PermissionIndexer permissionIndexer = tester.get(PermissionIndexer.class);
+    permissionIndexer.indexOnStartup(permissionIndexer.getIndexTypes());
+  }
+
   private ComponentDto newFile(ComponentDto project) {
     ComponentDto file = ComponentTesting.newFileDto(project, null);
     tester.get(ComponentDao.class).insert(session, file);
@@ -164,7 +168,7 @@ public class IssueServiceMediumTest {
   private IssueDto saveIssue(IssueDto issue) {
     tester.get(IssueDao.class).insert(session, issue);
     session.commit();
-    tester.get(IssueIndexer.class).index(asList(issue.getKey()));
+    tester.get(IssueIndexer.class).commitAndIndexIssues(session, asList(issue));
     return issue;
   }
 }
index 465d7ac0ca06c9cd55e87335a75c56d38ce8163c..f9ec6b8d7b69a70999c4b650ce5baca52f024894 100644 (file)
@@ -82,7 +82,7 @@ public class IssueUpdaterTest {
   private NotificationManager notificationManager = mock(NotificationManager.class);
   private ArgumentCaptor<IssueChangeNotification> notificationArgumentCaptor = ArgumentCaptor.forClass(IssueChangeNotification.class);
 
-  private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), new IssueIteratorFactory(dbClient));
+  private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbClient, new IssueIteratorFactory(dbClient));
   private IssueUpdater underTest = new IssueUpdater(dbClient,
     new ServerIssueStorage(system2, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), dbClient, issueIndexer), notificationManager);
 
index a429e5638be8197227aeb9a0832f6cf5ba7e13bc..cb287e0b7dd4342a9263b68445ee6837e6e3441d 100644 (file)
@@ -31,6 +31,7 @@ import org.sonar.api.rule.RuleKey;
 import org.sonar.api.rule.Severity;
 import org.sonar.api.utils.DateUtils;
 import org.sonar.api.utils.System2;
+import org.sonar.db.DbTester;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.component.ComponentTesting;
 import org.sonar.db.organization.OrganizationDto;
@@ -57,16 +58,18 @@ import static org.sonarqube.ws.client.issue.IssuesWsParameters.FACET_MODE_EFFORT
 
 public class IssueIndexDebtTest {
 
+  private System2 system2 = System2.INSTANCE;
+
   @Rule
   public EsTester es = new EsTester(new IssueIndexDefinition(new MapSettings().asConfig()), new ViewIndexDefinition(new MapSettings().asConfig()));
-
   @Rule
   public UserSessionRule userSessionRule = UserSessionRule.standalone();
+  @Rule
+  public DbTester db = DbTester.create(system2);
 
-  private System2 system2 = System2.INSTANCE;
-  private IssueIndex underTest;
-  private IssueIndexer issueIndexer = new IssueIndexer(es.client(), new IssueIteratorFactory(null));
+  private IssueIndexer issueIndexer = new IssueIndexer(es.client(), db.getDbClient(), new IssueIteratorFactory(db.getDbClient()));
   private PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, issueIndexer);
+  private IssueIndex underTest;
 
   @Before
   public void setUp() {
index 0a43cd2c2495adc5ed7fcfb526bc52c55f078a6b..416f253ef7e463c77fb99a90f1fd8a6ea85f9bbc 100644 (file)
@@ -91,7 +91,7 @@ public class IssueIndexTest {
   @Rule
   public ExpectedException expectedException = ExpectedException.none();
 
-  private IssueIndexer issueIndexer = new IssueIndexer(es.client(), new IssueIteratorFactory(db.getDbClient()));
+  private IssueIndexer issueIndexer = new IssueIndexer(es.client(), db.getDbClient(), new IssueIteratorFactory(db.getDbClient()));
   private ViewIndexer viewIndexer = new ViewIndexer(db.getDbClient(), es.client());
   private RuleIndexer ruleIndexer = new RuleIndexer(es.client(), db.getDbClient());
   private PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, issueIndexer);
index 7f4423215c928293f501a85ad871052abd86bf86..c6d3444809542a6c9f249805f1c2baf484a84145 100644 (file)
@@ -21,237 +21,465 @@ package org.sonar.server.issue.index;
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
 import java.util.List;
-import java.util.NoSuchElementException;
-import org.apache.commons.lang.StringUtils;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import org.elasticsearch.search.SearchHit;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.sonar.api.config.internal.MapSettings;
-import org.sonar.api.utils.System2;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.component.ComponentTesting;
+import org.sonar.db.es.EsQueueDto;
 import org.sonar.db.issue.IssueDto;
 import org.sonar.db.issue.IssueTesting;
 import org.sonar.db.organization.OrganizationDto;
-import org.sonar.db.rule.RuleDto;
+import org.sonar.db.rule.RuleDefinitionDto;
 import org.sonar.server.es.EsTester;
+import org.sonar.server.es.IndexingResult;
 import org.sonar.server.es.ProjectIndexer;
+import org.sonar.server.permission.index.AuthorizationScope;
+import org.sonar.server.permission.index.PermissionIndexerDao;
 
 import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.emptySet;
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assert.fail;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
+import static org.sonar.db.component.ComponentTesting.newFileDto;
 import static org.sonar.server.issue.IssueDocTesting.newDoc;
+import static org.sonar.server.issue.index.IssueIndexDefinition.INDEX_TYPE_ISSUE;
+import static org.sonar.server.permission.index.AuthorizationTypeSupport.TYPE_AUTHORIZATION;
 
 public class IssueIndexerTest {
 
-  private static final String A_PROJECT_UUID = "P1";
-
-  private System2 system2 = System2.INSTANCE;
-
   @Rule
-  public EsTester esTester = new EsTester(IssueIndexDefinition.createForTest(new MapSettings().asConfig()));
+  public EsTester es = new EsTester(IssueIndexDefinition.createForTest(new MapSettings().asConfig()));
   @Rule
-  public DbTester dbTester = DbTester.create(system2);
+  public DbTester db = DbTester.create();
   @Rule
   public ExpectedException expectedException = ExpectedException.none();
+  @Rule
+  public LogTester logTester = new LogTester();
 
-  private IssueIndexer underTest = new IssueIndexer(esTester.client(), new IssueIteratorFactory(dbTester.getDbClient()));
+  private OrganizationDto organization;
+  private IssueIndexer underTest = new IssueIndexer(es.client(), db.getDbClient(), new IssueIteratorFactory(db.getDbClient()));
+
+  @Before
+  public void setUp() {
+    organization = db.organizations().insert();
+  }
 
   @Test
-  public void index_on_startup() {
-    IssueIndexer indexer = spy(underTest);
-    doNothing().when(indexer).indexOnStartup(null);
-    indexer.indexOnStartup(null);
-    verify(indexer).indexOnStartup(null);
+  public void test_getIndexTypes() {
+    assertThat(underTest.getIndexTypes()).containsExactly(INDEX_TYPE_ISSUE);
   }
 
   @Test
-  public void index_nothing() {
-    underTest.index(Collections.emptyIterator());
+  public void test_getAuthorizationScope() {
+    AuthorizationScope scope = underTest.getAuthorizationScope();
+    assertThat(scope.getIndexType().getIndex()).isEqualTo(INDEX_TYPE_ISSUE.getIndex());
+    assertThat(scope.getIndexType().getType()).isEqualTo(TYPE_AUTHORIZATION);
+
+    Predicate<PermissionIndexerDao.Dto> projectPredicate = scope.getProjectPredicate();
+    PermissionIndexerDao.Dto project = new PermissionIndexerDao.Dto("P1", 1_000, Qualifiers.PROJECT);
+    PermissionIndexerDao.Dto file = new PermissionIndexerDao.Dto("F1", 1_000, Qualifiers.FILE);
+    assertThat(projectPredicate.test(project)).isTrue();
+    assertThat(projectPredicate.test(file)).isFalse();
+  }
+
+  @Test
+  public void indexOnStartup_scrolls_db_and_adds_all_issues_to_index() {
+    IssueDto issue1 = db.issues().insertIssue(organization);
+    IssueDto issue2 = db.issues().insertIssue(organization);
+
+    underTest.indexOnStartup(emptySet());
 
-    assertThat(esTester.countDocuments(IssueIndexDefinition.INDEX_TYPE_ISSUE)).isEqualTo(0L);
+    assertThatIndexHasOnly(issue1, issue2);
   }
 
   @Test
-  public void indexOnStartup_loads_and_indexes_all_issues() {
-    OrganizationDto org = dbTester.organizations().insert();
-    ComponentDto project = dbTester.components().insertPrivateProject(org);
-    ComponentDto dir = dbTester.components().insertComponent(ComponentTesting.newDirectory(project, "src/main/java/foo"));
-    ComponentDto file = dbTester.components().insertComponent(ComponentTesting.newFileDto(project, dir, "F1"));
-    RuleDto rule = dbTester.rules().insertRule();
-    IssueDto issue = dbTester.issues().insertIssue(IssueTesting.newDto(rule, file, project));
+  public void verify_indexed_fields() {
+    RuleDefinitionDto rule = db.rules().insert();
+    ComponentDto project = db.components().insertPrivateProject(organization);
+    ComponentDto dir = db.components().insertComponent(ComponentTesting.newDirectory(project, "src/main/java/foo"));
+    ComponentDto file = db.components().insertComponent(newFileDto(project, dir, "F1"));
+    IssueDto issue = db.issues().insertIssue(IssueTesting.newIssue(rule, project, file));
+
+    underTest.indexOnStartup(emptySet());
+
+    IssueDoc doc = es.getDocuments(INDEX_TYPE_ISSUE, IssueDoc.class).get(0);
+    assertThat(doc.getId()).isEqualTo(issue.getKey());
+    assertThat(doc.organizationUuid()).isEqualTo(organization.getUuid());
+    assertThat(doc.assignee()).isEqualTo(issue.getAssignee());
+    assertThat(doc.authorLogin()).isEqualTo(issue.getAuthorLogin());
+    assertThat(doc.componentUuid()).isEqualTo(issue.getComponentUuid());
+    assertThat(doc.closeDate()).isEqualTo(issue.getIssueCloseDate());
+    assertThat(doc.creationDate()).isEqualTo(issue.getIssueCreationDate());
+    assertThat(doc.directoryPath()).isEqualTo(dir.path());
+    assertThat(doc.filePath()).isEqualTo(file.path());
+    assertThat(doc.getParent()).isEqualTo(project.uuid());
+    assertThat(doc.getRouting()).isEqualTo(project.uuid());
+    assertThat(doc.language()).isEqualTo(issue.getLanguage());
+    assertThat(doc.line()).isEqualTo(issue.getLine());
+    // functional date
+    assertThat(doc.updateDate().getTime()).isEqualTo(issue.getIssueUpdateTime());
+  }
+
+  @Test
+  public void indexOnStartup_does_not_fail_on_errors_and_does_enable_recovery_mode() {
+    es.lockWrites(INDEX_TYPE_ISSUE);
+    db.issues().insertIssue(organization);
 
-    underTest.indexOnStartup(null);
+    underTest.indexOnStartup(emptySet());
 
-    List<IssueDoc> docs = esTester.getDocuments(IssueIndexDefinition.INDEX_TYPE_ISSUE, IssueDoc.class);
-    assertThat(docs).hasSize(1);
-    verifyDoc(docs.get(0), org, project, file, rule, issue);
+    assertThatIndexHasSize(0);
+    assertThatEsQueueTableHasSize(0);
   }
 
   @Test
-  public void index_loads_and_indexes_issues_with_specified_keys() {
-    OrganizationDto org = dbTester.organizations().insert();
-    ComponentDto project = dbTester.components().insertPrivateProject(org);
-    ComponentDto dir = dbTester.components().insertComponent(ComponentTesting.newDirectory(project, "src/main/java/foo"));
-    ComponentDto file = dbTester.components().insertComponent(ComponentTesting.newFileDto(project, dir, "F1"));
-    RuleDto rule = dbTester.rules().insertRule();
-    IssueDto issue1 = dbTester.issues().insertIssue(IssueTesting.newDto(rule, file, project));
-    IssueDto issue2 = dbTester.issues().insertIssue(IssueTesting.newDto(rule, file, project));
+  public void indexOnAnalysis_indexes_the_issues_of_project() {
+    RuleDefinitionDto rule = db.rules().insert();
+    ComponentDto project = db.components().insertPrivateProject(organization);
+    ComponentDto file = db.components().insertComponent(newFileDto(project));
+    IssueDto issue = db.issues().insertIssue(IssueTesting.newIssue(rule, project, file));
+    ComponentDto otherProject = db.components().insertPrivateProject(organization);
+    ComponentDto fileOnOtherProject = db.components().insertComponent(newFileDto(otherProject));
 
-    underTest.index(asList(issue1.getKey()));
+    underTest.indexOnAnalysis(project.uuid());
 
-    List<IssueDoc> docs = esTester.getDocuments(IssueIndexDefinition.INDEX_TYPE_ISSUE, IssueDoc.class);
-    assertThat(docs).hasSize(1);
-    verifyDoc(docs.get(0), org, project, file, rule, issue1);
+    assertThatIndexHasOnly(issue);
   }
 
   @Test
-  public void index_throws_NoSuchElementException_if_the_specified_key_does_not_exist() {
-    try {
-      underTest.index(asList("does_not_exist"));
-      fail();
-    } catch (NoSuchElementException e) {
-      assertThat(esTester.countDocuments(IssueIndexDefinition.INDEX_TYPE_ISSUE)).isEqualTo(0);
-    }
+  public void indexOnAnalysis_does_not_delete_orphan_docs() {
+    RuleDefinitionDto rule = db.rules().insert();
+    ComponentDto project = db.components().insertPrivateProject(organization);
+    ComponentDto file = db.components().insertComponent(newFileDto(project));
+    IssueDto issue = db.issues().insertIssue(IssueTesting.newIssue(rule, project, file));
+
+    // orphan in the project
+    addIssueToIndex(project.uuid(), "orphan");
+
+    underTest.indexOnAnalysis(project.uuid());
+
+    assertThat(es.getDocuments(INDEX_TYPE_ISSUE))
+      .extracting(SearchHit::getId)
+      .containsExactlyInAnyOrder(issue.getKey(), "orphan");
   }
 
+  /**
+   * Indexing recovery is handled by Compute Engine, without using
+   * the table es_queue
+   */
   @Test
-  public void indexProject_loads_and_indexes_issues_with_specified_project_uuid() {
-    OrganizationDto org = dbTester.organizations().insert();
-    ComponentDto project1 = dbTester.components().insertPrivateProject(org);
-    ComponentDto file1 = dbTester.components().insertComponent(ComponentTesting.newFileDto(project1));
-    ComponentDto project2 = dbTester.components().insertPrivateProject(org);
-    ComponentDto file2 = dbTester.components().insertComponent(ComponentTesting.newFileDto(project2));
-    RuleDto rule = dbTester.rules().insertRule();
-    IssueDto issue1 = dbTester.issues().insertIssue(IssueTesting.newDto(rule, file1, project1));
-    IssueDto issue2 = dbTester.issues().insertIssue(IssueTesting.newDto(rule, file2, project2));
+  public void indexOnAnalysis_does_not_fail_on_errors_and_does_not_enable_recovery_mode() {
+    es.lockWrites(INDEX_TYPE_ISSUE);
+    IssueDto issue = db.issues().insertIssue(organization);
 
-    underTest.indexProject(project1.projectUuid(), ProjectIndexer.Cause.NEW_ANALYSIS);
+    underTest.indexOnAnalysis(issue.getProjectUuid());
 
-    List<IssueDoc> docs = esTester.getDocuments(IssueIndexDefinition.INDEX_TYPE_ISSUE, IssueDoc.class);
-    assertThat(docs).hasSize(1);
-    verifyDoc(docs.get(0), org, project1, file1, rule, issue1);
+    assertThatIndexHasSize(0);
+    assertThatEsQueueTableHasSize(0);
   }
 
+
   @Test
-  public void indexProject_does_nothing_when_project_is_being_created() {
-    verifyThatProjectIsNotIndexed(ProjectIndexer.Cause.PROJECT_CREATION);
+  public void index_is_not_updated_when_creating_project() {
+    // it's impossible to already have an issue on a project
+    // that is being created, but it's just to verify that
+    // indexing is disabled
+    IssueDto issue = db.issues().insertIssue(organization);
+
+    IndexingResult result = indexProject(issue.getProjectUuid(), ProjectIndexer.Cause.PROJECT_CREATION);
+    assertThat(result.getTotal()).isEqualTo(0L);
+    assertThatIndexHasSize(0);
   }
 
   @Test
-  public void indexProject_does_nothing_when_project_is_being_renamed() {
-    verifyThatProjectIsNotIndexed(ProjectIndexer.Cause.PROJECT_KEY_UPDATE);
+  public void index_is_not_updated_when_updating_project_key() {
+    // issue is inserted to verify that indexing of project is not triggered
+    IssueDto issue = db.issues().insertIssue(organization);
+
+    IndexingResult result = indexProject(issue.getProjectUuid(), ProjectIndexer.Cause.PROJECT_KEY_UPDATE);
+    assertThat(result.getTotal()).isEqualTo(0L);
+    assertThatIndexHasSize(0);
   }
 
-  private void verifyThatProjectIsNotIndexed(ProjectIndexer.Cause cause) {
-    OrganizationDto org = dbTester.organizations().insert();
-    ComponentDto project = dbTester.components().insertPrivateProject(org);
-    ComponentDto file = dbTester.components().insertComponent(ComponentTesting.newFileDto(project));
-    RuleDto rule = dbTester.rules().insertRule();
-    IssueDto issue = dbTester.issues().insertIssue(IssueTesting.newDto(rule, file, project));
+  @Test
+  public void index_is_not_updated_when_updating_tags() {
+    // issue is inserted to verify that indexing of project is not triggered
+    IssueDto issue = db.issues().insertIssue(organization);
+
+    IndexingResult result = indexProject(issue.getProjectUuid(), ProjectIndexer.Cause.PROJECT_TAGS_UPDATE);
+    assertThat(result.getTotal()).isEqualTo(0L);
+    assertThatIndexHasSize(0);
+  }
+
+  @Test
+  public void index_is_updated_when_deleting_project() {
+    addIssueToIndex("P1", "I1");
+    assertThatIndexHasSize(1);
+
+    IndexingResult result = indexProject("P1", ProjectIndexer.Cause.PROJECT_DELETION);
 
-    underTest.indexProject(project.projectUuid(), cause);
+    assertThat(result.getTotal()).isEqualTo(1L);
+    assertThat(result.getSuccess()).isEqualTo(1L);
+    assertThatIndexHasSize(0);
+  }
 
-    assertThat(esTester.countDocuments(IssueIndexDefinition.INDEX_TYPE_ISSUE)).isEqualTo(0);
+  @Test
+  public void errors_during_project_deletion_are_recovered() {
+    addIssueToIndex("P1", "I1");
+    assertThatIndexHasSize(1);
+    es.lockWrites(INDEX_TYPE_ISSUE);
+
+    IndexingResult result = indexProject("P1", ProjectIndexer.Cause.PROJECT_DELETION);
+    assertThat(result.getTotal()).isEqualTo(1L);
+    assertThat(result.getFailures()).isEqualTo(1L);
+
+    // index is still read-only, fail to recover
+    result = recover();
+    assertThat(result.getTotal()).isEqualTo(1L);
+    assertThat(result.getFailures()).isEqualTo(1L);
+    assertThatIndexHasSize(1);
+
+    es.unlockWrites(INDEX_TYPE_ISSUE);
+
+    result = recover();
+    assertThat(result.getTotal()).isEqualTo(1L);
+    assertThat(result.getFailures()).isEqualTo(0L);
+    assertThatIndexHasSize(0);
   }
 
-  private static void verifyDoc(IssueDoc doc, OrganizationDto org, ComponentDto project, ComponentDto file, RuleDto rule, IssueDto issue) {
-    assertThat(doc.key()).isEqualTo(issue.getKey());
-    assertThat(doc.projectUuid()).isEqualTo(project.uuid());
-    assertThat(doc.componentUuid()).isEqualTo(file.uuid());
-    assertThat(doc.moduleUuid()).isEqualTo(project.uuid());
-    assertThat(doc.modulePath()).isEqualTo(file.moduleUuidPath());
-    assertThat(doc.directoryPath()).isEqualTo(StringUtils.substringBeforeLast(file.path(), "/"));
-    assertThat(doc.severity()).isEqualTo(issue.getSeverity());
-    assertThat(doc.ruleKey()).isEqualTo(rule.getKey());
-    assertThat(doc.organizationUuid()).isEqualTo(org.getUuid());
-    // functional date
-    assertThat(doc.updateDate().getTime()).isEqualTo(issue.getIssueUpdateTime());
+  @Test
+  public void commitAndIndexIssues_commits_db_transaction_and_adds_issues_to_index() {
+    RuleDefinitionDto rule = db.rules().insert();
+    ComponentDto project = db.components().insertPrivateProject(organization);
+    ComponentDto file = db.components().insertComponent(newFileDto(project));
+
+    // insert issues in db without committing
+    IssueDto issue1 = IssueTesting.newIssue(rule, project, file);
+    IssueDto issue2 = IssueTesting.newIssue(rule, project, file);
+    db.getDbClient().issueDao().insert(db.getSession(), issue1, issue2);
+
+    underTest.commitAndIndexIssues(db.getSession(), asList(issue1, issue2));
+
+    // issues are persisted and indexed
+    assertThatIndexHasOnly(issue1, issue2);
+    assertThatDbHasOnly(issue1, issue2);
+    assertThatEsQueueTableHasSize(0);
+  }
+
+  @Test
+  public void commitAndIndexIssues_removes_issue_from_index_if_it_does_not_exist_in_db() {
+    IssueDto issue1 = new IssueDto().setKee("I1").setProjectUuid("P1");
+    addIssueToIndex(issue1.getProjectUuid(), issue1.getKey());
+    IssueDto issue2 = db.issues().insertIssue(organization);
+
+    underTest.commitAndIndexIssues(db.getSession(), asList(issue1, issue2));
+
+    // issue1 is removed from index, issue2 is persisted and indexed
+    assertThatIndexHasOnly(issue2);
+    assertThatDbHasOnly(issue2);
+    assertThatEsQueueTableHasSize(0);
   }
 
   @Test
-  public void deleteProject_deletes_issues_of_a_specific_project() {
-    dbTester.prepareDbUnit(getClass(), "index.xml");
+  public void indexing_errors_during_commitAndIndexIssues_are_recovered() {
+    RuleDefinitionDto rule = db.rules().insert();
+    ComponentDto project = db.components().insertPrivateProject(organization);
+    ComponentDto file = db.components().insertComponent(newFileDto(project));
+
+    // insert issues in db without committing
+    IssueDto issue1 = IssueTesting.newIssue(rule, project, file);
+    IssueDto issue2 = IssueTesting.newIssue(rule, project, file);
+    db.getDbClient().issueDao().insert(db.getSession(), issue1, issue2);
+
+    // index is read-only
+    es.lockWrites(INDEX_TYPE_ISSUE);
+
+    underTest.commitAndIndexIssues(db.getSession(), asList(issue1, issue2));
 
-    underTest.indexOnStartup(null);
+    // issues are persisted but not indexed
+    assertThatIndexHasSize(0);
+    assertThatDbHasOnly(issue1, issue2);
+    assertThatEsQueueTableHasSize(2);
 
-    assertThat(esTester.countDocuments("issues", "issue")).isEqualTo(1);
+    // re-enable write on index
+    es.unlockWrites(INDEX_TYPE_ISSUE);
 
-    underTest.deleteProject("THE_PROJECT");
+    // emulate the recovery daemon
+    IndexingResult result = recover();
 
-    assertThat(esTester.countDocuments("issues", "issue")).isZero();
+    assertThatEsQueueTableHasSize(0);
+    assertThatIndexHasOnly(issue1, issue2);
+    assertThat(result.isSuccess()).isTrue();
+    assertThat(result.getTotal()).isEqualTo(2L);
   }
 
   @Test
-  public void deleteByKeys_deletes_docs_by_keys() throws Exception {
-    addIssue("P1", "Issue1");
-    addIssue("P1", "Issue2");
-    addIssue("P1", "Issue3");
-    addIssue("P2", "Issue4");
+  public void recovery_does_not_fail_if_unsupported_docIdType() {
+    EsQueueDto item = EsQueueDto.create(INDEX_TYPE_ISSUE.format(), "I1", "unknown", "P1");
+    db.getDbClient().esQueueDao().insert(db.getSession(), item);
+    db.commit();
 
-    verifyIssueKeys("Issue1", "Issue2", "Issue3", "Issue4");
+    recover();
 
-    underTest.deleteByKeys("P1", asList("Issue1", "Issue2"));
+    assertThat(logTester.logs(LoggerLevel.ERROR))
+      .filteredOn(l -> l.contains("Unsupported es_queue.doc_id_type for issues. Manual fix is required: "))
+      .hasSize(1);
+    assertThatEsQueueTableHasSize(1);
+  }
+
+  @Test
+  public void indexing_recovers_multiple_errors_on_the_same_issue() {
+    es.lockWrites(INDEX_TYPE_ISSUE);
+    IssueDto issue = db.issues().insertIssue(organization);
 
-    verifyIssueKeys("Issue3", "Issue4");
+    // three changes on the same issue
+    underTest.commitAndIndexIssues(db.getSession(), asList(issue));
+    underTest.commitAndIndexIssues(db.getSession(), asList(issue));
+    underTest.commitAndIndexIssues(db.getSession(), asList(issue));
+
+    assertThatIndexHasSize(0);
+    // three attempts of indexing are stored in es_queue recovery table
+    assertThatEsQueueTableHasSize(3);
+
+    es.unlockWrites(INDEX_TYPE_ISSUE);
+    recover();
+
+    assertThatIndexHasOnly(issue);
+    assertThatEsQueueTableHasSize(0);
   }
 
   @Test
-  public void deleteByKeys_deletes_more_than_one_thousand_issues_by_keys() throws Exception {
-    int numberOfIssues = 1010;
-    List<String> keys = new ArrayList<>(numberOfIssues);
-    IssueDoc[] issueDocs = new IssueDoc[numberOfIssues];
-    for (int i = 0; i < numberOfIssues; i++) {
-      String key = "Issue" + i;
-      issueDocs[i] = newDoc().setKey(key).setProjectUuid(A_PROJECT_UUID);
-      keys.add(key);
-    }
-    esTester.putDocuments(IssueIndexDefinition.INDEX_TYPE_ISSUE, issueDocs);
+  public void indexing_recovers_multiple_errors_on_the_same_project() {
+    RuleDefinitionDto rule = db.rules().insert();
+    ComponentDto project = db.components().insertPrivateProject(organization);
+    ComponentDto file = db.components().insertComponent(newFileDto(project));
+    IssueDto issue1 = db.issues().insertIssue(IssueTesting.newIssue(rule, project, file));
+    IssueDto issue2 = db.issues().insertIssue(IssueTesting.newIssue(rule, project, file));
+
+    es.lockWrites(INDEX_TYPE_ISSUE);
+
+    IndexingResult result = indexProject(project.uuid(), ProjectIndexer.Cause.PROJECT_DELETION);
+    assertThat(result.getTotal()).isEqualTo(2L);
+    assertThat(result.getFailures()).isEqualTo(2L);
+
+    // index is still read-only, fail to recover
+    result = recover();
+    assertThat(result.getTotal()).isEqualTo(2L);
+    assertThat(result.getFailures()).isEqualTo(2L);
+    assertThatIndexHasSize(0);
+
+    es.unlockWrites(INDEX_TYPE_ISSUE);
+
+    result = recover();
+    assertThat(result.getTotal()).isEqualTo(2L);
+    assertThat(result.getFailures()).isEqualTo(0L);
+    assertThatIndexHasSize(2);
+    assertThatEsQueueTableHasSize(0);
+  }
+
+  private IndexingResult indexProject(String projectUuid, ProjectIndexer.Cause cause) {
+    Collection<EsQueueDto> items = underTest.prepareForRecovery(db.getSession(), asList(projectUuid), cause);
+    db.commit();
+    return underTest.index(db.getSession(), items);
+  }
+
+  @Test
+  public void deleteByKeys_deletes_docs_by_keys() {
+    addIssueToIndex("P1", "Issue1");
+    addIssueToIndex("P1", "Issue2");
+    addIssueToIndex("P1", "Issue3");
+    addIssueToIndex("P2", "Issue4");
 
-    assertThat(esTester.countDocuments("issues", "issue")).isEqualTo(numberOfIssues);
-    underTest.deleteByKeys(A_PROJECT_UUID, keys);
-    assertThat(esTester.countDocuments("issues", "issue")).isZero();
+    assertThatIndexHasOnly("Issue1", "Issue2", "Issue3", "Issue4");
+
+    underTest.deleteByKeys("P1", asList("Issue1", "Issue2"));
+
+    assertThatIndexHasOnly("Issue3", "Issue4");
   }
 
   @Test
-  public void nothing_to_do_when_delete_issues_on_empty_list() throws Exception {
-    addIssue("P1", "Issue1");
-    addIssue("P1", "Issue2");
-    addIssue("P1", "Issue3");
+  public void deleteByKeys_does_not_recover_from_errors() {
+    addIssueToIndex("P1", "Issue1");
+    es.lockWrites(INDEX_TYPE_ISSUE);
 
-    verifyIssueKeys("Issue1", "Issue2", "Issue3");
+    underTest.deleteByKeys("P1", asList("Issue1"));
 
-    underTest.deleteByKeys("P1", Collections.emptyList());
+    assertThatIndexHasOnly("Issue1");
+    assertThatEsQueueTableHasSize(0);
+  }
 
-    verifyIssueKeys("Issue1", "Issue2", "Issue3");
+  @Test
+  public void nothing_to_do_when_delete_issues_on_empty_list() {
+    addIssueToIndex("P1", "Issue1");
+    addIssueToIndex("P1", "Issue2");
+    addIssueToIndex("P1", "Issue3");
+
+    underTest.deleteByKeys("P1", emptyList());
+
+    assertThatIndexHasOnly("Issue1", "Issue2", "Issue3");
   }
 
   /**
    * This is a technical constraint, to ensure, that the indexers can be called in any order, during startup.
    */
   @Test
-  public void index_issue_without_parent_should_work() {
+  public void parent_child_relationship_does_not_require_ordering_of_index_requests() {
     IssueDoc issueDoc = new IssueDoc();
     issueDoc.setKey("key");
-    issueDoc.setProjectUuid("non-exitsing-parent");
-    new IssueIndexer(esTester.client(), new IssueIteratorFactory(dbTester.getDbClient()))
+    issueDoc.setProjectUuid("parent-does-not-exist");
+    new IssueIndexer(es.client(), db.getDbClient(), new IssueIteratorFactory(db.getDbClient()))
       .index(asList(issueDoc).iterator());
 
-    assertThat(esTester.countDocuments(IssueIndexDefinition.INDEX_TYPE_ISSUE)).isEqualTo(1L);
+    assertThat(es.countDocuments(INDEX_TYPE_ISSUE)).isEqualTo(1L);
   }
 
-  private void addIssue(String projectUuid, String issueKey) throws Exception {
-    esTester.putDocuments(IssueIndexDefinition.INDEX_TYPE_ISSUE,
+  private void addIssueToIndex(String projectUuid, String issueKey) {
+    es.putDocuments(INDEX_TYPE_ISSUE,
       newDoc().setKey(issueKey).setProjectUuid(projectUuid));
   }
 
-  private void verifyIssueKeys(String... expectedKeys) {
-    List<IssueDoc> issues = esTester.getDocuments(IssueIndexDefinition.INDEX_TYPE_ISSUE, IssueDoc.class);
+  private void assertThatIndexHasSize(long expectedSize) {
+    assertThat(es.countDocuments(INDEX_TYPE_ISSUE)).isEqualTo(expectedSize);
+  }
+
+  private void assertThatIndexHasOnly(IssueDto... expectedIssues) {
+    assertThat(es.getDocuments(INDEX_TYPE_ISSUE))
+      .extracting(SearchHit::getId)
+      .containsExactlyInAnyOrder(Arrays.stream(expectedIssues).map(IssueDto::getKey).toArray(String[]::new));
+  }
+
+  private void assertThatIndexHasOnly(String... expectedKeys) {
+    List<IssueDoc> issues = es.getDocuments(INDEX_TYPE_ISSUE, IssueDoc.class);
     assertThat(issues).extracting(IssueDoc::key).containsOnly(expectedKeys);
   }
+
+  private void assertThatEsQueueTableHasSize(int expectedSize) {
+    assertThat(db.countRowsOfTable("es_queue")).isEqualTo(expectedSize);
+  }
+
+  private void assertThatDbHasOnly(IssueDto... expectedIssues) {
+    try (DbSession otherSession = db.getDbClient().openSession(false)) {
+      List<String> keys = Arrays.stream(expectedIssues).map(IssueDto::getKey).collect(Collectors.toList());
+      assertThat(db.getDbClient().issueDao().selectByKeys(otherSession, keys)).hasSize(expectedIssues.length);
+    }
+  }
+
+  private IndexingResult recover() {
+    Collection<EsQueueDto> items = db.getDbClient().esQueueDao().selectForRecovery(db.getSession(), System.currentTimeMillis() + 1_000L, 10);
+    return underTest.index(db.getSession(), items);
+  }
 }
index d4b849abaa455f6cb65b1f64d8f0597a10eb5e7d..752a57cc95e248dccdb5fff1517b6c52d77cf002 100644 (file)
@@ -92,7 +92,7 @@ public class AddCommentActionTest {
 
   private IssueDbTester issueDbTester = new IssueDbTester(dbTester);
 
-  private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), new IssueIteratorFactory(dbClient));
+  private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbClient, new IssueIteratorFactory(dbClient));
   private ServerIssueStorage serverIssueStorage = new ServerIssueStorage(system2, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), dbClient, issueIndexer);
   private IssueUpdater issueUpdater = new IssueUpdater(dbClient, serverIssueStorage, mock(NotificationManager.class));
   private OperationResponseWriter responseWriter = mock(OperationResponseWriter.class);
index ef9e92ab163997b93b257be7a8c2aadc09be04c7..20c5e39960bbce5606efcec5c9f18eeefea90909 100644 (file)
@@ -80,7 +80,7 @@ public class AssignActionTest {
   public DbTester db = DbTester.create(system2);
 
   private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
-  private IssueIndexer issueIndexer = new IssueIndexer(es.client(), new IssueIteratorFactory(db.getDbClient()));
+  private IssueIndexer issueIndexer = new IssueIndexer(es.client(), db.getDbClient(), new IssueIteratorFactory(db.getDbClient()));
   private OperationResponseWriter responseWriter = mock(OperationResponseWriter.class);
   private AssignAction underTest = new AssignAction(system2, userSession, db.getDbClient(), new IssueFinder(db.getDbClient(), userSession), new IssueFieldsSetter(),
     new IssueUpdater(db.getDbClient(),
index 64888625f323ff640c35cee860b88265bc1acfbd..e2830d2c3a0e56877330a760e719378dc112e119 100644 (file)
@@ -49,7 +49,7 @@ public class AuthorsActionTest {
   @Rule
   public UserSessionRule userSession = UserSessionRule.standalone();
 
-  private IssueIndexer issueIndexer = new IssueIndexer(es.client(), new IssueIteratorFactory(db.getDbClient()));
+  private IssueIndexer issueIndexer = new IssueIndexer(es.client(), db.getDbClient(), new IssueIteratorFactory(db.getDbClient()));
   private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSession, new AuthorizationTypeSupport(userSession));
   private IssueService issueService = new IssueService(issueIndex);
 
index c31b901b5f6b68602152201575c5cf6a0aaeee38..f8416a37848bf31c50cd2b74f32c04c8d4a765e2 100644 (file)
@@ -106,7 +106,7 @@ public class BulkChangeActionTest {
   private IssueFieldsSetter issueFieldsSetter = new IssueFieldsSetter();
   private IssueWorkflow issueWorkflow = new IssueWorkflow(new FunctionExecutor(issueFieldsSetter), issueFieldsSetter);
   private IssueStorage issueStorage = new ServerIssueStorage(system2, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), dbClient,
-    new IssueIndexer(es.client(), new IssueIteratorFactory(dbClient)));
+    new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient)));
   private NotificationManager notificationManager = mock(NotificationManager.class);
   private List<Action> actions = new ArrayList<>();
 
index 81d7e0b682c3ecbf32ed6b0db296b07982f7902a..5433a6f5979659f2f3c25cbcd8b6ceafe9e7b2e1 100644 (file)
@@ -102,7 +102,7 @@ public class DoTransitionActionTest {
   private IssueWorkflow workflow = new IssueWorkflow(new FunctionExecutor(updater), updater);
   private TransitionService transitionService = new TransitionService(userSession, workflow);
   private OperationResponseWriter responseWriter = mock(OperationResponseWriter.class);
-  private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), new IssueIteratorFactory(dbClient));
+  private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbClient, new IssueIteratorFactory(dbClient));
   private IssueUpdater issueUpdater = new IssueUpdater(dbClient,
     new ServerIssueStorage(system2, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), dbClient, issueIndexer), mock(NotificationManager.class));
   private ComponentDto project;
index a856c355ad7bcd99ab13fa8911a96e19ae4416bb..4e5ddce0cd366077b533df0d4f45ae9d0def8ddd 100644 (file)
@@ -20,9 +20,7 @@
 package org.sonar.server.issue.ws;
 
 import java.io.IOException;
-import java.util.Arrays;
 import java.util.List;
-import java.util.stream.Collectors;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.ClassRule;
@@ -129,7 +127,7 @@ public class SearchActionComponentsMediumTest {
     db.issueDao().insert(session, issue2);
     session.commit();
     indexIssues();
-    indexPermissionsOf(project, project2);
+    indexPermissions();
 
     WsTester.Result result = wsTester.newGetRequest(CONTROLLER_ISSUES, ACTION_SEARCH).execute();
     result.assertJson(this.getClass(), "issues_on_different_projects.json");
@@ -146,7 +144,7 @@ public class SearchActionComponentsMediumTest {
     db.issueDao().insert(session, issueInModule, issueInRootModule);
     session.commit();
     indexIssues();
-    indexPermissionsOf(project);
+    indexPermissions();
 
     WsActionTester actionTester = new WsActionTester(tester.get(SearchAction.class));
     SearchWsResponse searchResponse = actionTester.newRequest().executeProtobuf(SearchWsResponse.class);
@@ -170,7 +168,7 @@ public class SearchActionComponentsMediumTest {
     db.issueDao().insert(session, issue);
     session.commit();
     indexIssues();
-    indexPermissionsOf(project);
+    indexPermissions();
 
     wsTester.newGetRequest(CONTROLLER_ISSUES, ACTION_SEARCH)
       .setParam(IssuesWsParameters.PARAM_PROJECT_UUIDS, project.uuid())
@@ -212,7 +210,7 @@ public class SearchActionComponentsMediumTest {
     db.issueDao().insert(session, issueAfterLeak, issueBeforeLeak);
     session.commit();
     indexIssues();
-    indexPermissionsOf(project);
+    indexPermissions();
 
     wsTester.newGetRequest(CONTROLLER_ISSUES, ACTION_SEARCH)
       .setParam(IssuesWsParameters.PARAM_COMPONENT_UUIDS, project.uuid())
@@ -240,7 +238,7 @@ public class SearchActionComponentsMediumTest {
     db.issueDao().insert(session, issueAfterLeak, issueBeforeLeak);
     session.commit();
     indexIssues();
-    indexPermissionsOf(project);
+    indexPermissions();
 
     wsTester.newGetRequest(CONTROLLER_ISSUES, ACTION_SEARCH)
       .setParam(IssuesWsParameters.PARAM_COMPONENT_UUIDS, project.uuid())
@@ -265,7 +263,7 @@ public class SearchActionComponentsMediumTest {
     db.issueDao().insert(session, issue1, issue2, issue3);
     session.commit();
     indexIssues();
-    indexPermissionsOf(project1, project2, project3);
+    indexPermissions();
 
     wsTester.newGetRequest(CONTROLLER_ISSUES, ACTION_SEARCH)
       .setParam(IssuesWsParameters.PARAM_PROJECT_UUIDS, project1.uuid())
@@ -282,7 +280,7 @@ public class SearchActionComponentsMediumTest {
     db.issueDao().insert(session, issue);
     session.commit();
     indexIssues();
-    indexPermissionsOf(project);
+    indexPermissions();
 
     wsTester.newGetRequest(CONTROLLER_ISSUES, ACTION_SEARCH)
       .setParam(IssuesWsParameters.PARAM_FILE_UUIDS, file.uuid())
@@ -316,7 +314,7 @@ public class SearchActionComponentsMediumTest {
     db.issueDao().insert(session, issueOnFile, issueOnTest);
     session.commit();
     indexIssues();
-    indexPermissionsOf(project);
+    indexPermissions();
 
     wsTester.newGetRequest(CONTROLLER_ISSUES, ACTION_SEARCH)
       .setParam(IssuesWsParameters.PARAM_COMPONENTS, file.key())
@@ -341,7 +339,7 @@ public class SearchActionComponentsMediumTest {
     db.issueDao().insert(session, issue1, issue2);
     session.commit();
     indexIssues();
-    indexPermissionsOf(project);
+    indexPermissions();
 
     wsTester.newGetRequest(CONTROLLER_ISSUES, ACTION_SEARCH)
       .setParam(IssuesWsParameters.PARAM_COMPONENT_UUIDS, project.uuid())
@@ -360,7 +358,7 @@ public class SearchActionComponentsMediumTest {
     db.issueDao().insert(session, issue);
     session.commit();
     indexIssues();
-    indexPermissionsOf(project);
+    indexPermissions();
 
     wsTester.newGetRequest(CONTROLLER_ISSUES, ACTION_SEARCH)
       .setParam(IssuesWsParameters.PARAM_COMPONENT_UUIDS, directory.uuid())
@@ -397,7 +395,7 @@ public class SearchActionComponentsMediumTest {
     db.issueDao().insert(session, issue1);
     session.commit();
     indexIssues();
-    indexPermissionsOf(project);
+    indexPermissions();
 
     wsTester.newGetRequest(CONTROLLER_ISSUES, ACTION_SEARCH)
       .setParam(IssuesWsParameters.PARAM_COMPONENT_UUIDS, directory1.uuid())
@@ -447,7 +445,7 @@ public class SearchActionComponentsMediumTest {
     db.issueDao().insert(session, issue1, issue2);
     session.commit();
     indexIssues();
-    indexPermissionsOf(project);
+    indexPermissions();
 
     wsTester.newGetRequest(CONTROLLER_ISSUES, ACTION_SEARCH)
       .setParam(IssuesWsParameters.PARAM_COMPONENT_UUIDS, module.uuid())
@@ -466,7 +464,7 @@ public class SearchActionComponentsMediumTest {
     db.issueDao().insert(session, issue);
     session.commit();
     indexIssues();
-    indexPermissionsOf(project);
+    indexPermissions();
 
     userSessionRule.logIn("john");
     WsTester.Result result = wsTester.newGetRequest(CONTROLLER_ISSUES, ACTION_SEARCH)
@@ -482,7 +480,7 @@ public class SearchActionComponentsMediumTest {
     ComponentDto file = insertComponent(newFileDto(project, null, "F1").setKey("FK1"));
     ComponentDto view = insertComponent(ComponentTesting.newView(defaultOrganization, "V1").setKey("MyView"));
     indexView(view.uuid(), newArrayList(project.uuid()));
-    indexPermissionsOf(project, view);
+    indexPermissions();
 
     insertIssue(IssueTesting.newDto(newRule(), file, project).setKee("82fd47d4-b650-4037-80bc-7b112bd4eac2"));
 
@@ -505,7 +503,7 @@ public class SearchActionComponentsMediumTest {
     indexView(view.uuid(), newArrayList(project.uuid()));
     ComponentDto subView = insertComponent(ComponentTesting.newSubView(view, "SV1", "MySubView"));
     indexView(subView.uuid(), newArrayList(project.uuid()));
-    indexPermissionsOf(project, view);
+    indexPermissions();
 
     userSessionRule.logIn("john")
       .registerComponents(project, file, view, subView);
@@ -543,7 +541,7 @@ public class SearchActionComponentsMediumTest {
     RuleDto newRule = newRule();
     IssueDto issue1 = IssueTesting.newDto(newRule, file, project).setAuthorLogin("leia").setKee("2bd4eac2-b650-4037-80bc-7b112bd4eac2");
     IssueDto issue2 = IssueTesting.newDto(newRule, file, project).setAuthorLogin("luke@skywalker.name").setKee("82fd47d4-b650-4037-80bc-7b1182fd47d4");
-    indexPermissionsOf(project);
+    indexPermissions();
 
     db.issueDao().insert(session, issue1, issue2);
     session.commit();
@@ -572,8 +570,9 @@ public class SearchActionComponentsMediumTest {
     return rule;
   }
 
-  private void indexPermissionsOf(ComponentDto... rootComponents) {
-    tester.get(PermissionIndexer.class).indexProjectsByUuids(session, Arrays.stream(rootComponents).map(ComponentDto::uuid).collect(Collectors.toList()));
+  private void indexPermissions() {
+    PermissionIndexer permissionIndexer = tester.get(PermissionIndexer.class);
+    permissionIndexer.indexOnStartup(permissionIndexer.getIndexTypes());
   }
 
   private IssueDto insertIssue(IssueDto issue) {
index 1d873091674db05aad418889c0b38a9bf9f1dc5d..331e487caef40efeabbd3b8dfeebe1b4cc6c10e7 100644 (file)
@@ -57,7 +57,6 @@ import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.WsTester;
 
 import static java.util.Arrays.asList;
-import static java.util.Collections.singletonList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.sonar.api.web.UserRole.ISSUE_ADMIN;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_SEARCH;
@@ -124,7 +123,7 @@ public class SearchActionMediumTest {
     db.userDao().insert(session, new UserDto().setLogin("fabrice").setName("Fabrice").setEmail("fabrice@email.com"));
 
     ComponentDto project = insertComponent(ComponentTesting.newPublicProjectDto(otherOrganization2, "PROJECT_ID").setKey("PROJECT_KEY"));
-    indexPermissionsOf(project);
+    indexPermissions();
     ComponentDto file = insertComponent(ComponentTesting.newFileDto(project, null, "FILE_ID").setKey("FILE_KEY"));
     IssueDto issue = IssueTesting.newDto(newRule(), file, project)
       .setKee("82fd47d4-b650-4037-80bc-7b112bd4eac2")
@@ -153,7 +152,7 @@ public class SearchActionMediumTest {
     db.userDao().insert(session, new UserDto().setLogin("fabrice").setName("Fabrice").setEmail("fabrice@email.com"));
 
     ComponentDto project = insertComponent(ComponentTesting.newPublicProjectDto(otherOrganization2, "PROJECT_ID").setKey("PROJECT_KEY"));
-    indexPermissionsOf(project);
+    indexPermissions();
     ComponentDto file = insertComponent(ComponentTesting.newFileDto(project, null, "FILE_ID").setKey("FILE_KEY"));
     IssueDto issue = IssueTesting.newDto(newRule(), file, project)
       .setKee("82fd47d4-b650-4037-80bc-7b112bd4eac2");
@@ -190,7 +189,7 @@ public class SearchActionMediumTest {
     db.userDao().insert(session, new UserDto().setLogin("fabrice").setName("Fabrice").setEmail("fabrice@email.com"));
 
     ComponentDto project = insertComponent(ComponentTesting.newPublicProjectDto(otherOrganization1, "PROJECT_ID").setKey("PROJECT_KEY"));
-    indexPermissionsOf(project);
+    indexPermissions();
     ComponentDto file = insertComponent(ComponentTesting.newFileDto(project, null, "FILE_ID").setKey("FILE_KEY"));
     IssueDto issue = IssueTesting.newDto(newRule(), file, project)
       .setKee("82fd47d4-b650-4037-80bc-7b112bd4eac2");
@@ -225,7 +224,7 @@ public class SearchActionMediumTest {
     db.userDao().insert(session, new UserDto().setLogin("simon").setName("Simon").setEmail("simon@email.com"));
     db.userDao().insert(session, new UserDto().setLogin("fabrice").setName("Fabrice").setEmail("fabrice@email.com"));
     ComponentDto project = insertComponent(ComponentTesting.newPublicProjectDto(otherOrganization2, "PROJECT_ID").setKey("PROJECT_KEY").setLanguage("java"));
-    indexPermissionsOf(project);
+    indexPermissions();
     ComponentDto file = insertComponent(ComponentTesting.newFileDto(project, null, "FILE_ID").setKey("FILE_KEY").setLanguage("js"));
 
     IssueDto issue = IssueTesting.newDto(newRule(), file, project)
@@ -249,7 +248,7 @@ public class SearchActionMediumTest {
     db.userDao().insert(session, new UserDto().setLogin("fabrice").setName("Fabrice").setEmail("fabrice@email.com"));
     ComponentDto project = insertComponent(ComponentTesting.newPublicProjectDto(otherOrganization1, "PROJECT_ID").setKey("PROJECT_KEY").setLanguage("java"));
     grantPermissionToAnyone(project, ISSUE_ADMIN);
-    indexPermissionsOf(project);
+    indexPermissions();
     ComponentDto file = insertComponent(ComponentTesting.newFileDto(project, null, "FILE_ID").setKey("FILE_KEY").setLanguage("js"));
 
     IssueDto issue = IssueTesting.newDto(newRule(), file, project)
@@ -272,7 +271,7 @@ public class SearchActionMediumTest {
   public void issue_on_removed_file() throws Exception {
     RuleDto rule = newRule();
     ComponentDto project = insertComponent(ComponentTesting.newPublicProjectDto(otherOrganization2, "PROJECT_ID").setKey("PROJECT_KEY"));
-    indexPermissionsOf(project);
+    indexPermissions();
     ComponentDto removedFile = insertComponent(ComponentTesting.newFileDto(project, null).setUuid("REMOVED_FILE_ID")
       .setKey("REMOVED_FILE_KEY")
       .setEnabled(false));
@@ -298,7 +297,7 @@ public class SearchActionMediumTest {
   public void apply_paging_with_one_component() throws Exception {
     RuleDto rule = newRule();
     ComponentDto project = insertComponent(ComponentTesting.newPublicProjectDto(otherOrganization2, "PROJECT_ID").setKey("PROJECT_KEY"));
-    indexPermissionsOf(project);
+    indexPermissions();
     ComponentDto file = insertComponent(ComponentTesting.newFileDto(project, null, "FILE_ID").setKey("FILE_KEY"));
     for (int i = 0; i < SearchOptions.MAX_LIMIT + 1; i++) {
       IssueDto issue = IssueTesting.newDto(rule, file, project);
@@ -315,7 +314,7 @@ public class SearchActionMediumTest {
   @Test
   public void components_contains_sub_projects() throws Exception {
     ComponentDto project = insertComponent(ComponentTesting.newPublicProjectDto(otherOrganization1, "PROJECT_ID").setKey("ProjectHavingModule"));
-    indexPermissionsOf(project);
+    indexPermissions();
     ComponentDto module = insertComponent(ComponentTesting.newModuleDto(project).setKey("ModuleHavingFile"));
     ComponentDto file = insertComponent(ComponentTesting.newFileDto(module, null, "BCDE").setKey("FileLinkedToModule"));
     IssueDto issue = IssueTesting.newDto(newRule(), file, project);
@@ -331,7 +330,7 @@ public class SearchActionMediumTest {
   @Test
   public void display_facets() throws Exception {
     ComponentDto project = insertComponent(ComponentTesting.newPublicProjectDto(otherOrganization1, "PROJECT_ID").setKey("PROJECT_KEY"));
-    indexPermissionsOf(project);
+    indexPermissions();
     ComponentDto file = insertComponent(ComponentTesting.newFileDto(project, null, "FILE_ID").setKey("FILE_KEY"));
     IssueDto issue = IssueTesting.newDto(newRule(), file, project)
       .setIssueCreationDate(DateUtils.parseDate("2014-09-04"))
@@ -356,7 +355,7 @@ public class SearchActionMediumTest {
   @Test
   public void display_facets_in_effort_mode() throws Exception {
     ComponentDto project = insertComponent(ComponentTesting.newPublicProjectDto(otherOrganization2, "PROJECT_ID").setKey("PROJECT_KEY"));
-    indexPermissionsOf(project);
+    indexPermissions();
     ComponentDto file = insertComponent(ComponentTesting.newFileDto(project, null, "FILE_ID").setKey("FILE_KEY"));
     IssueDto issue = IssueTesting.newDto(newRule(), file, project)
       .setIssueCreationDate(DateUtils.parseDate("2014-09-04"))
@@ -382,7 +381,7 @@ public class SearchActionMediumTest {
   @Test
   public void display_zero_valued_facets_for_selected_items() throws Exception {
     ComponentDto project = insertComponent(ComponentTesting.newPublicProjectDto(otherOrganization1, "PROJECT_ID").setKey("PROJECT_KEY"));
-    indexPermissionsOf(project);
+    indexPermissions();
     ComponentDto file = insertComponent(ComponentTesting.newFileDto(project, null, "FILE_ID").setKey("FILE_KEY"));
     IssueDto issue = IssueTesting.newDto(newRule(), file, project)
       .setIssueCreationDate(DateUtils.parseDate("2014-09-04"))
@@ -424,7 +423,7 @@ public class SearchActionMediumTest {
     db.userDao().insert(session, new UserDto().setLogin("john").setName("John").setEmail("john@email.com"));
 
     ComponentDto project = insertComponent(ComponentTesting.newPublicProjectDto(defaultOrganization, "PROJECT_ID").setKey("PROJECT_KEY"));
-    indexPermissionsOf(project);
+    indexPermissions();
     ComponentDto file = insertComponent(ComponentTesting.newFileDto(project, null, "FILE_ID").setKey("FILE_KEY"));
     RuleDto rule = newRule();
     IssueDto issue1 = IssueTesting.newDto(rule, file, project)
@@ -469,7 +468,7 @@ public class SearchActionMediumTest {
     userSessionRule.logIn();
 
     ComponentDto project = insertComponent(ComponentTesting.newPublicProjectDto(otherOrganization1, "PROJECT_ID").setKey("PROJECT_KEY"));
-    indexPermissionsOf(project);
+    indexPermissions();
     ComponentDto file = insertComponent(ComponentTesting.newFileDto(project, null, "FILE_ID").setKey("FILE_KEY"));
     RuleDto rule = newRule();
     IssueDto issue1 = IssueTesting.newDto(rule, file, project)
@@ -500,7 +499,7 @@ public class SearchActionMediumTest {
     db.userDao().insert(session, new UserDto().setLogin("alice").setName("Alice").setEmail("alice@email.com"));
 
     ComponentDto project = insertComponent(ComponentTesting.newPublicProjectDto(otherOrganization2, "PROJECT_ID").setKey("PROJECT_KEY"));
-    indexPermissionsOf(project);
+    indexPermissions();
     ComponentDto file = insertComponent(ComponentTesting.newFileDto(project, null, "FILE_ID").setKey("FILE_KEY"));
     RuleDto rule = newRule();
     IssueDto issue1 = IssueTesting.newDto(rule, file, project)
@@ -544,7 +543,7 @@ public class SearchActionMediumTest {
   public void sort_by_updated_at() throws Exception {
     RuleDto rule = newRule();
     ComponentDto project = insertComponent(ComponentTesting.newPublicProjectDto(otherOrganization2, "PROJECT_ID").setKey("PROJECT_KEY"));
-    indexPermissionsOf(project);
+    indexPermissions();
     ComponentDto file = insertComponent(ComponentTesting.newFileDto(project, null, "FILE_ID").setKey("FILE_KEY"));
     db.issueDao().insert(session, IssueTesting.newDto(rule, file, project)
       .setKee("82fd47d4-b650-4037-80bc-7b112bd4eac1")
@@ -570,7 +569,7 @@ public class SearchActionMediumTest {
   public void paging() throws Exception {
     RuleDto rule = newRule();
     ComponentDto project = insertComponent(ComponentTesting.newPublicProjectDto(otherOrganization1, "PROJECT_ID").setKey("PROJECT_KEY"));
-    indexPermissionsOf(project);
+    indexPermissions();
     ComponentDto file = insertComponent(ComponentTesting.newFileDto(project, null, "FILE_ID").setKey("FILE_KEY"));
     for (int i = 0; i < 12; i++) {
       IssueDto issue = IssueTesting.newDto(rule, file, project);
@@ -592,7 +591,7 @@ public class SearchActionMediumTest {
   public void paging_with_page_size_to_minus_one() throws Exception {
     RuleDto rule = newRule();
     ComponentDto project = insertComponent(ComponentTesting.newPublicProjectDto(otherOrganization2, "PROJECT_ID").setKey("PROJECT_KEY"));
-    indexPermissionsOf(project);
+    indexPermissions();
     ComponentDto file = insertComponent(ComponentTesting.newFileDto(project, null, "FILE_ID").setKey("FILE_KEY"));
     for (int i = 0; i < 12; i++) {
       IssueDto issue = IssueTesting.newDto(rule, file, project);
@@ -614,7 +613,7 @@ public class SearchActionMediumTest {
   public void deprecated_paging() throws Exception {
     RuleDto rule = newRule();
     ComponentDto project = insertComponent(ComponentTesting.newPublicProjectDto(defaultOrganization, "PROJECT_ID").setKey("PROJECT_KEY"));
-    indexPermissionsOf(project);
+    indexPermissions();
     ComponentDto file = insertComponent(ComponentTesting.newFileDto(project, null, "FILE_ID").setKey("FILE_KEY"));
     for (int i = 0; i < 12; i++) {
       IssueDto issue = IssueTesting.newDto(rule, file, project);
@@ -643,7 +642,7 @@ public class SearchActionMediumTest {
   @Test
   public void display_deprecated_debt_fields() throws Exception {
     ComponentDto project = insertComponent(ComponentTesting.newPublicProjectDto(otherOrganization1, "PROJECT_ID").setKey("PROJECT_KEY"));
-    indexPermissionsOf(project);
+    indexPermissions();
     ComponentDto file = insertComponent(ComponentTesting.newFileDto(project, null, "FILE_ID").setKey("FILE_KEY"));
     IssueDto issue = IssueTesting.newDto(newRule(), file, project)
       .setIssueCreationDate(DateUtils.parseDate("2014-09-04"))
@@ -686,8 +685,9 @@ public class SearchActionMediumTest {
     return rule;
   }
 
-  private void indexPermissionsOf(ComponentDto project) {
-    tester.get(PermissionIndexer.class).indexProjectsByUuids(session, singletonList(project.uuid()));
+  private void indexPermissions() {
+    PermissionIndexer permissionIndexer = tester.get(PermissionIndexer.class);
+    permissionIndexer.indexOnStartup(permissionIndexer.getIndexTypes());
   }
 
   private void grantPermissionToAnyone(ComponentDto project, String permission) {
index 6cc8d39de358b09f07f7b9f0bec12e1257f57c98..42596d8151a2f6286e57a6dfab0ff65c902c906d 100644 (file)
@@ -90,7 +90,7 @@ public class SetSeverityActionTest {
   private OperationResponseWriter responseWriter = mock(OperationResponseWriter.class);
   private ArgumentCaptor<SearchResponseData> preloadedSearchResponseDataCaptor = ArgumentCaptor.forClass(SearchResponseData.class);
 
-  private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), new IssueIteratorFactory(dbClient));
+  private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbClient, new IssueIteratorFactory(dbClient));
   private WsActionTester tester = new WsActionTester(new SetSeverityAction(userSession, dbClient, new IssueFinder(dbClient, userSession), new IssueFieldsSetter(),
     new IssueUpdater(dbClient,
       new ServerIssueStorage(system2, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), dbClient, issueIndexer), mock(NotificationManager.class)),
index b30f4b72b6911fe7489bdf9d8c639fce71d0130d..614b3e90f0d893e0a7df7b72e3991b438b2e66b3 100644 (file)
@@ -85,7 +85,7 @@ public class SetTagsActionTest {
   private DbClient dbClient = db.getDbClient();
   private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
   private OperationResponseWriter responseWriter = mock(OperationResponseWriter.class);
-  private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), new IssueIteratorFactory(dbClient));
+  private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbClient, new IssueIteratorFactory(dbClient));
   private ArgumentCaptor<SearchResponseData> preloadedSearchResponseDataCaptor = ArgumentCaptor.forClass(SearchResponseData.class);
 
   private WsActionTester ws = new WsActionTester(new SetTagsAction(userSession, dbClient, new IssueFinder(dbClient, userSession), new IssueFieldsSetter(),
index efc09708b1044b25435025576d9db3e3230b096a..3f1e260e46e59f6e316063c136e482b3848c4fce 100644 (file)
@@ -91,7 +91,7 @@ public class SetTypeActionTest {
   private OperationResponseWriter responseWriter = mock(OperationResponseWriter.class);
   private ArgumentCaptor<SearchResponseData> preloadedSearchResponseDataCaptor = ArgumentCaptor.forClass(SearchResponseData.class);
 
-  private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), new IssueIteratorFactory(dbClient));
+  private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbClient, new IssueIteratorFactory(dbClient));
   private WsActionTester tester = new WsActionTester(new SetTypeAction(userSession, dbClient, new IssueFinder(dbClient, userSession), new IssueFieldsSetter(),
     new IssueUpdater(dbClient,
       new ServerIssueStorage(system2, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), dbClient, issueIndexer), mock(NotificationManager.class)),
index 356bfbf4ad89c55279116536e9403e2ebf46d578..b861d7840a0541cb1631baa506b064274a236165 100644 (file)
@@ -64,7 +64,7 @@ public class TagsActionTest {
   @Rule
   public EsTester esTester = new EsTester(new IssueIndexDefinition(settings.asConfig()), new RuleIndexDefinition(settings.asConfig()));
 
-  private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), new IssueIteratorFactory(dbTester.getDbClient()));
+  private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbTester.getDbClient(), new IssueIteratorFactory(dbTester.getDbClient()));
   private RuleIndexer ruleIndexer = new RuleIndexer(esTester.client(), dbTester.getDbClient());
   private PermissionIndexerTester permissionIndexerTester = new PermissionIndexerTester(esTester, issueIndexer);
   private IssueIndex issueIndex = new IssueIndex(esTester.client(), System2.INSTANCE, userSession, new AuthorizationTypeSupport(userSession));
@@ -233,7 +233,7 @@ public class TagsActionTest {
     IssueDto issue = dbTester.issues().insertIssue(organization, i -> i.setRule(rule).setTags(asList(tags)));
     ComponentDto project = dbTester.getDbClient().componentDao().selectByUuid(dbTester.getSession(), issue.getProjectUuid()).get();
     userSession.addProjectPermission(USER, project);
-    issueIndexer.index(Collections.singletonList(issue.getKey()));
+    issueIndexer.commitAndIndexIssues(dbTester.getSession(), Collections.singletonList(issue));
     return issue;
   }
 
index b237231746b8264c2f4f99bea882a31440c964b8..396c3517cc36ad0969689305135b6d6a846756be 100644 (file)
  */
 package org.sonar.server.measure.index;
 
-import java.util.Date;
+import java.util.Arrays;
+import java.util.Collection;
 import org.elasticsearch.action.search.SearchRequestBuilder;
+import org.elasticsearch.search.SearchHit;
 import org.junit.Rule;
 import org.junit.Test;
 import org.sonar.api.config.internal.MapSettings;
 import org.sonar.api.utils.System2;
+import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
-import org.sonar.db.component.ComponentDbTester;
 import org.sonar.db.component.ComponentDto;
-import org.sonar.db.component.ComponentTesting;
 import org.sonar.db.component.SnapshotDto;
+import org.sonar.db.es.EsQueueDto;
 import org.sonar.db.organization.OrganizationDto;
 import org.sonar.server.es.EsTester;
+import org.sonar.server.es.IndexingResult;
 import org.sonar.server.es.ProjectIndexer;
 
+import static java.util.Collections.emptyList;
+import static java.util.Collections.emptySet;
 import static java.util.Collections.singletonList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
-import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
 import static org.elasticsearch.index.query.QueryBuilders.termQuery;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
 import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto;
-import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_ANALYSED_AT;
-import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_KEY;
-import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_NAME;
+import static org.sonar.server.es.ProjectIndexer.Cause.PROJECT_CREATION;
+import static org.sonar.server.es.ProjectIndexer.Cause.PROJECT_DELETION;
+import static org.sonar.server.es.ProjectIndexer.Cause.PROJECT_KEY_UPDATE;
+import static org.sonar.server.es.ProjectIndexer.Cause.PROJECT_TAGS_UPDATE;
 import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_TAGS;
 import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.INDEX_TYPE_PROJECT_MEASURES;
 
@@ -54,134 +56,172 @@ public class ProjectMeasuresIndexerTest {
   private System2 system2 = System2.INSTANCE;
 
   @Rule
-  public EsTester esTester = new EsTester(new ProjectMeasuresIndexDefinition(new MapSettings().asConfig()));
-
+  public EsTester es = new EsTester(new ProjectMeasuresIndexDefinition(new MapSettings().asConfig()));
   @Rule
-  public DbTester dbTester = DbTester.create(system2);
-
-  private ComponentDbTester componentDbTester = new ComponentDbTester(dbTester);
-  private ProjectMeasuresIndexer underTest = new ProjectMeasuresIndexer(dbTester.getDbClient(), esTester.client());
+  public DbTester db = DbTester.create(system2);
 
-  @Test
-  public void index_on_startup() {
-    ProjectMeasuresIndexer indexer = spy(underTest);
-    doNothing().when(indexer).indexOnStartup(null);
-    indexer.indexOnStartup(null);
-    verify(indexer).indexOnStartup(null);
-  }
+  private ProjectMeasuresIndexer underTest = new ProjectMeasuresIndexer(db.getDbClient(), es.client());
 
   @Test
   public void index_nothing() {
-    underTest.indexOnStartup(null);
+    underTest.indexOnStartup(emptySet());
 
-    assertThat(esTester.countDocuments(INDEX_TYPE_PROJECT_MEASURES)).isZero();
+    assertThat(es.countDocuments(INDEX_TYPE_PROJECT_MEASURES)).isZero();
   }
 
   @Test
-  public void index_all_project() {
-    OrganizationDto organizationDto = dbTester.organizations().insert();
-    componentDbTester.insertProjectAndSnapshot(ComponentTesting.newPrivateProjectDto(organizationDto));
-    componentDbTester.insertProjectAndSnapshot(ComponentTesting.newPrivateProjectDto(organizationDto));
-    componentDbTester.insertProjectAndSnapshot(ComponentTesting.newPrivateProjectDto(organizationDto));
+  public void indexOnStartup_indexes_all_projects() {
+    OrganizationDto organization = db.organizations().insert();
+    SnapshotDto project1 = db.components().insertProjectAndSnapshot(newPrivateProjectDto(organization));
+    SnapshotDto project2 = db.components().insertProjectAndSnapshot(newPrivateProjectDto(organization));
+    SnapshotDto project3 = db.components().insertProjectAndSnapshot(newPrivateProjectDto(organization));
 
-    underTest.indexOnStartup(null);
+    underTest.indexOnStartup(emptySet());
 
-    assertThat(esTester.countDocuments(INDEX_TYPE_PROJECT_MEASURES)).isEqualTo(3);
+    assertThatIndexContainsOnly(project1, project2, project3);
   }
 
   /**
    * Provisioned projects don't have analysis yet
    */
   @Test
-  public void index_provisioned_projects() {
-    ComponentDto project = componentDbTester.insertPrivateProject();
+  public void indexOnStartup_indexes_provisioned_projects() {
+    ComponentDto project = db.components().insertPrivateProject();
 
-    underTest.indexOnStartup(null);
+    underTest.indexOnStartup(emptySet());
 
-    assertThat(esTester.getIds(INDEX_TYPE_PROJECT_MEASURES)).containsOnly(project.uuid());
+    assertThatIndexContainsOnly(project);
   }
 
   @Test
-  public void indexProject_indexes_provisioned_project() {
-    ComponentDto project = componentDbTester.insertPrivateProject();
+  public void indexOnAnalysis_indexes_provisioned_project() {
+    ComponentDto project1 = db.components().insertPrivateProject();
+    ComponentDto project2 = db.components().insertPrivateProject();
 
-    underTest.indexProject(project.uuid(), ProjectIndexer.Cause.PROJECT_CREATION);
+    underTest.indexOnAnalysis(project1.uuid());
 
-    assertThat(esTester.getIds(INDEX_TYPE_PROJECT_MEASURES)).containsOnly(project.uuid());
+    assertThatIndexContainsOnly(project1);
   }
 
   @Test
-  public void indexProject_indexes_project_when_its_key_is_updated() {
-    ComponentDto project = componentDbTester.insertPrivateProject();
+  public void update_index_when_project_key_is_updated() {
+    ComponentDto project = db.components().insertPrivateProject();
 
-    underTest.indexProject(project.uuid(), ProjectIndexer.Cause.PROJECT_KEY_UPDATE);
+    IndexingResult result = indexProject(project, PROJECT_KEY_UPDATE);
 
-    assertThat(esTester.getIds(INDEX_TYPE_PROJECT_MEASURES)).containsOnly(project.uuid());
+    assertThatIndexContainsOnly(project);
+    assertThat(result.getTotal()).isEqualTo(1L);
+    assertThat(result.getSuccess()).isEqualTo(1L);
   }
 
   @Test
-  public void index_one_project() throws Exception {
-    OrganizationDto organizationDto = dbTester.organizations().insert();
-    ComponentDto project = ComponentTesting.newPrivateProjectDto(organizationDto);
-    componentDbTester.insertProjectAndSnapshot(project);
-    componentDbTester.insertProjectAndSnapshot(ComponentTesting.newPrivateProjectDto(organizationDto));
+  public void update_index_when_project_is_created() {
+    ComponentDto project = db.components().insertPrivateProject();
 
-    underTest.indexProject(project.uuid(), ProjectIndexer.Cause.NEW_ANALYSIS);
+    IndexingResult result = indexProject(project, PROJECT_CREATION);
 
-    assertThat(esTester.getIds(INDEX_TYPE_PROJECT_MEASURES)).containsOnly(project.uuid());
+    assertThatIndexContainsOnly(project);
+    assertThat(result.getTotal()).isEqualTo(1L);
+    assertThat(result.getSuccess()).isEqualTo(1L);
   }
 
   @Test
-  public void update_existing_document_when_indexing_one_project() throws Exception {
-    String uuid = "PROJECT-UUID";
-    esTester.putDocuments(INDEX_TYPE_PROJECT_MEASURES, new ProjectMeasuresDoc()
-      .setId(uuid)
-      .setKey("Old Key")
-      .setName("Old Name")
-      .setTags(singletonList("old tag"))
-      .setAnalysedAt(new Date(1_000_000L)));
-    ComponentDto project = newPrivateProjectDto(dbTester.getDefaultOrganization(), uuid).setKey("New key").setName("New name").setTagsString("new tag");
-    SnapshotDto analysis = componentDbTester.insertProjectAndSnapshot(project);
-
-    underTest.indexProject(project.uuid(), ProjectIndexer.Cause.NEW_ANALYSIS);
-
-    assertThat(esTester.getIds(INDEX_TYPE_PROJECT_MEASURES)).containsOnly(uuid);
-    SearchRequestBuilder request = esTester.client()
-      .prepareSearch(INDEX_TYPE_PROJECT_MEASURES)
-      .setQuery(boolQuery().must(matchAllQuery()).filter(
-        boolQuery()
-          .must(termQuery("_id", uuid))
-          .must(termQuery(FIELD_KEY, "New key"))
-          .must(termQuery(FIELD_NAME, "New name"))
-          .must(termQuery(FIELD_TAGS, "new tag"))
-          .must(termQuery(FIELD_ANALYSED_AT, new Date(analysis.getCreatedAt())))));
-    assertThat(request.get().getHits()).hasSize(1);
+  public void update_index_when_project_tags_are_updated() {
+    ComponentDto project = db.components().insertPrivateProject(p -> p.setTagsString("foo"));
+    indexProject(project, PROJECT_CREATION);
+    assertThatProjectHasTag(project, "foo");
+
+    project.setTagsString("bar");
+    db.getDbClient().componentDao().updateTags(db.getSession(), project);
+    IndexingResult result = indexProject(project, PROJECT_TAGS_UPDATE);
+
+    assertThatProjectHasTag(project, "bar");
+    assertThat(result.getTotal()).isEqualTo(1L);
+    assertThat(result.getSuccess()).isEqualTo(1L);
   }
 
   @Test
-  public void delete_project() {
-    OrganizationDto organizationDto = dbTester.organizations().insert();
-    ComponentDto project1 = ComponentTesting.newPrivateProjectDto(organizationDto);
-    componentDbTester.insertProjectAndSnapshot(project1);
-    ComponentDto project2 = ComponentTesting.newPrivateProjectDto(organizationDto);
-    componentDbTester.insertProjectAndSnapshot(project2);
-    ComponentDto project3 = ComponentTesting.newPrivateProjectDto(organizationDto);
-    componentDbTester.insertProjectAndSnapshot(project3);
-    underTest.indexOnStartup(null);
+  public void delete_doc_from_index_when_project_is_deleted() {
+    ComponentDto project = db.components().insertPrivateProject();
+    indexProject(project, PROJECT_CREATION);
+    assertThatIndexContainsOnly(project);
 
-    underTest.deleteProject(project1.uuid());
+    db.getDbClient().componentDao().delete(db.getSession(), project.getId());
+    IndexingResult result = indexProject(project, PROJECT_DELETION);
 
-    assertThat(esTester.getIds(INDEX_TYPE_PROJECT_MEASURES)).containsOnly(project2.uuid(), project3.uuid());
+    assertThat(es.countDocuments(INDEX_TYPE_PROJECT_MEASURES)).isEqualTo(0);
+    assertThat(result.getTotal()).isEqualTo(1L);
+    assertThat(result.getSuccess()).isEqualTo(1L);
   }
 
   @Test
-  public void does_nothing_when_deleting_unknown_project() throws Exception {
-    ComponentDto project = ComponentTesting.newPrivateProjectDto(dbTester.organizations().insert());
-    componentDbTester.insertProjectAndSnapshot(project);
-    underTest.indexOnStartup(null);
+  public void do_nothing_if_no_projects_to_index() {
+    // this project should not be indexed
+    db.components().insertPrivateProject();
+
+    underTest.index(db.getSession(), emptyList());
+
+    assertThat(es.countDocuments(INDEX_TYPE_PROJECT_MEASURES)).isEqualTo(0);
+  }
+
+  @Test
+  public void errors_during_indexing_are_recovered() {
+    ComponentDto project = db.components().insertPrivateProject();
+    es.lockWrites(INDEX_TYPE_PROJECT_MEASURES);
+
+    IndexingResult result = indexProject(project, PROJECT_CREATION);
+    assertThat(result.getTotal()).isEqualTo(1L);
+    assertThat(result.getFailures()).isEqualTo(1L);
+
+    // index is still read-only, fail to recover
+    result = recover();
+    assertThat(result.getTotal()).isEqualTo(1L);
+    assertThat(result.getFailures()).isEqualTo(1L);
+    assertThat(es.countDocuments(INDEX_TYPE_PROJECT_MEASURES)).isEqualTo(0);
+    assertThatEsQueueTableHasSize(1);
+
+    es.unlockWrites(INDEX_TYPE_PROJECT_MEASURES);
+
+    result = recover();
+    assertThat(result.getTotal()).isEqualTo(1L);
+    assertThat(result.getFailures()).isEqualTo(0L);
+    assertThatEsQueueTableHasSize(0);
+    assertThatIndexContainsOnly(project);
+  }
 
-    underTest.deleteProject("UNKNOWN");
+  private IndexingResult indexProject(ComponentDto project, ProjectIndexer.Cause cause) {
+    DbSession dbSession = db.getSession();
+    Collection<EsQueueDto> items = underTest.prepareForRecovery(dbSession, singletonList(project.uuid()), cause);
+    dbSession.commit();
+    return underTest.index(dbSession, items);
+  }
+
+  private void assertThatProjectHasTag(ComponentDto project, String expectedTag) {
+    SearchRequestBuilder request = es.client()
+      .prepareSearch(INDEX_TYPE_PROJECT_MEASURES)
+      .setQuery(boolQuery().filter(termQuery(FIELD_TAGS, expectedTag)));
+    assertThat(request.get().getHits().getHits())
+      .extracting(SearchHit::getId)
+      .contains(project.uuid());
+  }
+
+  private void assertThatEsQueueTableHasSize(int expectedSize) {
+    assertThat(db.countRowsOfTable("es_queue")).isEqualTo(expectedSize);
+  }
 
-    assertThat(esTester.getIds(INDEX_TYPE_PROJECT_MEASURES)).containsOnly(project.uuid());
+  private void assertThatIndexContainsOnly(SnapshotDto... expectedProjects) {
+    assertThat(es.getIds(INDEX_TYPE_PROJECT_MEASURES)).containsExactlyInAnyOrder(
+      Arrays.stream(expectedProjects).map(SnapshotDto::getComponentUuid).toArray(String[]::new));
   }
+
+  private void assertThatIndexContainsOnly(ComponentDto... expectedProjects) {
+    assertThat(es.getIds(INDEX_TYPE_PROJECT_MEASURES)).containsExactlyInAnyOrder(
+      Arrays.stream(expectedProjects).map(ComponentDto::uuid).toArray(String[]::new));
+  }
+
+  private IndexingResult recover() {
+    Collection<EsQueueDto> items = db.getDbClient().esQueueDao().selectForRecovery(db.getSession(), System.currentTimeMillis() + 1_000L, 10);
+    return underTest.index(db.getSession(), items);
+  }
+
 }
index ae714b8162e344612878644435f9938142513be6..3dd184769bb998f573fd283c3e646faae528c56d 100644 (file)
@@ -38,13 +38,13 @@ import org.sonar.db.permission.template.PermissionTemplateDbTester;
 import org.sonar.db.permission.template.PermissionTemplateDto;
 import org.sonar.db.user.GroupDto;
 import org.sonar.db.user.UserDto;
-import org.sonar.server.permission.index.PermissionIndexer;
+import org.sonar.server.es.ProjectIndexers;
+import org.sonar.server.es.TestProjectIndexers;
 import org.sonar.server.permission.ws.template.DefaultTemplatesResolverRule;
 import org.sonar.server.tester.UserSessionRule;
 
 import static java.util.Collections.singletonList;
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
 import static org.sonar.core.permission.GlobalPermissions.SCAN_EXECUTION;
 
 public class PermissionTemplateServiceTest {
@@ -59,7 +59,7 @@ public class PermissionTemplateServiceTest {
   private UserSessionRule userSession = UserSessionRule.standalone();
   private PermissionTemplateDbTester templateDb = dbTester.permissionTemplates();
   private DbSession session = dbTester.getSession();
-  private PermissionIndexer permissionIndexer = mock(PermissionIndexer.class);
+  private ProjectIndexers projectIndexers = new TestProjectIndexers();
 
   private OrganizationDto organization;
   private ComponentDto privateProject;
@@ -68,7 +68,7 @@ public class PermissionTemplateServiceTest {
   private UserDto user;
   private UserDto creator;
 
-  private PermissionTemplateService underTest = new PermissionTemplateService(dbTester.getDbClient(), permissionIndexer, userSession, defaultTemplatesResolver);
+  private PermissionTemplateService underTest = new PermissionTemplateService(dbTester.getDbClient(), projectIndexers, userSession, defaultTemplatesResolver);
 
   @Before
   public void setUp() throws Exception {
@@ -85,7 +85,7 @@ public class PermissionTemplateServiceTest {
     PermissionTemplateDto permissionTemplate = dbTester.permissionTemplates().insertTemplate(organization);
     dbTester.permissionTemplates().addAnyoneToTemplate(permissionTemplate, "p1");
 
-    underTest.apply(session, permissionTemplate, singletonList(privateProject));
+    underTest.applyAndCommit(session, permissionTemplate, singletonList(privateProject));
 
     assertThat(selectProjectPermissionsOfGroup(organization, null, privateProject)).isEmpty();
   }
@@ -108,7 +108,7 @@ public class PermissionTemplateServiceTest {
       .forEach(perm -> dbTester.permissionTemplates().addAnyoneToTemplate(permissionTemplate, perm));
     dbTester.permissionTemplates().addAnyoneToTemplate(permissionTemplate, "p1");
 
-    underTest.apply(session, permissionTemplate, singletonList(publicProject));
+    underTest.applyAndCommit(session, permissionTemplate, singletonList(publicProject));
 
     assertThat(selectProjectPermissionsOfGroup(organization, null, publicProject))
       .containsOnly("p1", UserRole.ADMIN, UserRole.ISSUE_ADMIN, GlobalPermissions.SCAN_EXECUTION);
@@ -135,7 +135,7 @@ public class PermissionTemplateServiceTest {
       .forEach(perm -> dbTester.permissionTemplates().addGroupToTemplate(permissionTemplate, group, perm));
     dbTester.permissionTemplates().addGroupToTemplate(permissionTemplate, group, "p1");
 
-    underTest.apply(session, permissionTemplate, singletonList(privateProject));
+    underTest.applyAndCommit(session, permissionTemplate, singletonList(privateProject));
 
     assertThat(selectProjectPermissionsOfGroup(organization, group, privateProject))
       .containsOnly("p1", UserRole.USER, UserRole.CODEVIEWER, UserRole.ADMIN, UserRole.ISSUE_ADMIN, GlobalPermissions.SCAN_EXECUTION);
@@ -162,7 +162,7 @@ public class PermissionTemplateServiceTest {
       .forEach(perm -> dbTester.permissionTemplates().addGroupToTemplate(permissionTemplate, group, perm));
     dbTester.permissionTemplates().addGroupToTemplate(permissionTemplate, group, "p1");
 
-    underTest.apply(session, permissionTemplate, singletonList(publicProject));
+    underTest.applyAndCommit(session, permissionTemplate, singletonList(publicProject));
 
     assertThat(selectProjectPermissionsOfGroup(organization, group, publicProject))
       .containsOnly("p1", UserRole.ADMIN, UserRole.ISSUE_ADMIN, GlobalPermissions.SCAN_EXECUTION);
@@ -189,7 +189,7 @@ public class PermissionTemplateServiceTest {
       .forEach(perm -> dbTester.permissionTemplates().addUserToTemplate(permissionTemplate, user, perm));
     dbTester.permissionTemplates().addUserToTemplate(permissionTemplate, user, "p1");
 
-    underTest.apply(session, permissionTemplate, singletonList(publicProject));
+    underTest.applyAndCommit(session, permissionTemplate, singletonList(publicProject));
 
     assertThat(selectProjectPermissionsOfUser(user, publicProject))
       .containsOnly("p1", UserRole.ADMIN, UserRole.ISSUE_ADMIN, GlobalPermissions.SCAN_EXECUTION);
@@ -216,7 +216,7 @@ public class PermissionTemplateServiceTest {
       .forEach(perm -> dbTester.permissionTemplates().addUserToTemplate(permissionTemplate, user, perm));
     dbTester.permissionTemplates().addUserToTemplate(permissionTemplate, user, "p1");
 
-    underTest.apply(session, permissionTemplate, singletonList(privateProject));
+    underTest.applyAndCommit(session, permissionTemplate, singletonList(privateProject));
 
     assertThat(selectProjectPermissionsOfUser(user, privateProject))
       .containsOnly("p1", UserRole.USER, UserRole.CODEVIEWER, UserRole.ADMIN, UserRole.ISSUE_ADMIN, GlobalPermissions.SCAN_EXECUTION);
@@ -286,7 +286,7 @@ public class PermissionTemplateServiceTest {
     assertThat(selectProjectPermissionsOfGroup(organization, null, project)).isEmpty();
     assertThat(selectProjectPermissionsOfUser(user, project)).isEmpty();
 
-    underTest.apply(session, permissionTemplate, singletonList(project));
+    underTest.applyAndCommit(session, permissionTemplate, singletonList(project));
 
     assertThat(selectProjectPermissionsOfGroup(organization, adminGroup, project)).containsOnly("admin", "issueadmin");
     assertThat(selectProjectPermissionsOfGroup(organization, userGroup, project)).containsOnly("user", "codeviewer");
index 57e5e9b96d11e3ac34db67d04cfabb3de64ccfb8..cb6a69925b7bc1afe5a89a278d6ba300b71cceb3 100644 (file)
 package org.sonar.server.permission.index;
 
 import com.google.common.collect.ImmutableMap;
-import org.sonar.server.component.index.ComponentIndexDefinition;
-import org.sonar.server.es.BulkIndexer;
+import com.google.common.collect.ImmutableSet;
+import java.util.Collection;
+import java.util.Set;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.es.EsQueueDto;
 import org.sonar.server.es.EsClient;
+import org.sonar.server.es.IndexType;
+import org.sonar.server.es.IndexingResult;
 import org.sonar.server.es.ProjectIndexer;
 
-import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
-import static org.elasticsearch.index.query.QueryBuilders.termQuery;
-import static org.sonar.server.permission.index.FooIndexDefinition.FOO_INDEX;
-import static org.sonar.server.permission.index.FooIndexDefinition.FOO_TYPE;
 import static org.sonar.server.permission.index.FooIndexDefinition.INDEX_TYPE_FOO;
 
 public class FooIndexer implements ProjectIndexer, NeedAuthorizationIndexer {
 
   private static final AuthorizationScope AUTHORIZATION_SCOPE = new AuthorizationScope(INDEX_TYPE_FOO, p -> true);
 
+  private final DbClient dbClient;
   private final EsClient esClient;
 
-  public FooIndexer(EsClient esClient) {
+  public FooIndexer(DbClient dbClient, EsClient esClient) {
+    this.dbClient = dbClient;
     this.esClient = esClient;
   }
 
@@ -47,11 +51,16 @@ public class FooIndexer implements ProjectIndexer, NeedAuthorizationIndexer {
   }
 
   @Override
-  public void indexProject(String projectUuid, Cause cause) {
+  public void indexOnAnalysis(String projectUuid) {
     addToIndex(projectUuid, "bar");
     addToIndex(projectUuid, "baz");
   }
 
+  @Override
+  public Collection<EsQueueDto> prepareForRecovery(DbSession dbSession, Collection<String> projectUuids, Cause cause) {
+    throw new UnsupportedOperationException();
+  }
+
   private void addToIndex(String projectUuid, String name) {
     esClient.prepareIndex(INDEX_TYPE_FOO)
       .setRouting(projectUuid)
@@ -63,11 +72,17 @@ public class FooIndexer implements ProjectIndexer, NeedAuthorizationIndexer {
   }
 
   @Override
-  public void deleteProject(String projectUuid) {
-    BulkIndexer.delete(esClient, FOO_INDEX, esClient.prepareSearch(FOO_INDEX)
-      .setTypes(FOO_TYPE)
-      .setQuery(boolQuery()
-        .filter(
-          termQuery(ComponentIndexDefinition.FIELD_PROJECT_UUID, projectUuid))));
+  public void indexOnStartup(Set<IndexType> uninitializedIndexTypes) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Set<IndexType> getIndexTypes() {
+    return ImmutableSet.of(INDEX_TYPE_FOO);
+  }
+
+  @Override
+  public IndexingResult index(DbSession dbSession, Collection<EsQueueDto> items) {
+    throw new UnsupportedOperationException();
   }
 }
index 002b7c931e0be037a5c0317e0181ba5180cefb14..28d08710e544c4935d067432ca07a224de75b07a 100644 (file)
@@ -114,7 +114,7 @@ public class PermissionIndexerDaoTest {
   }
 
   @Test
-  public void selectByUuids() throws Exception {
+  public void selectByUuids() {
     insertTestDataForProjectsAndViews();
 
     Map<String, PermissionIndexerDao.Dto> dtos = underTest
@@ -147,6 +147,14 @@ public class PermissionIndexerDaoTest {
     isPublic(view2Authorization, VIEW);
   }
 
+  @Test
+  public void selectByUuids_returns_empty_list_when_project_does_not_exist() {
+    insertTestDataForProjectsAndViews();
+
+    List<PermissionIndexerDao.Dto> dtos = underTest.selectByUuids(dbClient, dbSession, asList("missing"));
+    assertThat(dtos).isEmpty();
+  }
+
   @Test
   public void select_by_projects_with_high_number_of_projects() throws Exception {
     List<String> projectUuids = new ArrayList<>();
index ff5edb6ad0713100c62a4fe2b223acc72e8878c6..2081c95ef7241d3a882b0eb36a0ad5d0bdf9020a 100644 (file)
  */
 package org.sonar.server.permission.index;
 
+import java.util.Collection;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.sonar.api.utils.System2;
+import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
-import org.sonar.db.component.ComponentDbTester;
 import org.sonar.db.component.ComponentDto;
+import org.sonar.db.es.EsQueueDto;
 import org.sonar.db.organization.OrganizationDto;
 import org.sonar.db.user.GroupDto;
-import org.sonar.db.user.UserDbTester;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.es.EsTester;
 import org.sonar.server.es.IndexType;
+import org.sonar.server.es.IndexingResult;
 import org.sonar.server.es.ProjectIndexer;
 import org.sonar.server.tester.UserSessionRule;
 
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.sonar.api.web.UserRole.ADMIN;
 import static org.sonar.api.web.UserRole.USER;
+import static org.sonar.server.es.ProjectIndexer.Cause.PERMISSION_CHANGE;
 
 public class PermissionIndexerTest {
 
@@ -46,23 +51,21 @@ public class PermissionIndexerTest {
   @Rule
   public ExpectedException expectedException = ExpectedException.none();
   @Rule
-  public DbTester dbTester = DbTester.create(System2.INSTANCE);
+  public DbTester db = DbTester.create(System2.INSTANCE);
   @Rule
-  public EsTester esTester = new EsTester(new FooIndexDefinition());
+  public EsTester es = new EsTester(new FooIndexDefinition());
   @Rule
   public UserSessionRule userSession = UserSessionRule.standalone();
 
-  private ComponentDbTester componentDbTester = new ComponentDbTester(dbTester);
-  private UserDbTester userDbTester = new UserDbTester(dbTester);
-  private FooIndex fooIndex = new FooIndex(esTester.client(), new AuthorizationTypeSupport(userSession));
-  private FooIndexer fooIndexer = new FooIndexer(esTester.client());
-  private PermissionIndexer underTest = new PermissionIndexer(dbTester.getDbClient(), esTester.client(), fooIndexer);
+  private FooIndex fooIndex = new FooIndex(es.client(), new AuthorizationTypeSupport(userSession));
+  private FooIndexer fooIndexer = new FooIndexer(db.getDbClient(), es.client());
+  private PermissionIndexer underTest = new PermissionIndexer(db.getDbClient(), es.client(), fooIndexer);
 
   @Test
-  public void initalizeOnStartup_grants_access_to_any_user_and_to_group_Anyone_on_public_projects() {
+  public void indexOnStartup_grants_access_to_any_user_and_to_group_Anyone_on_public_projects() {
     ComponentDto project = createAndIndexPublicProject();
-    UserDto user1 = userDbTester.insertUser();
-    UserDto user2 = userDbTester.insertUser();
+    UserDto user1 = db.users().insertUser();
+    UserDto user2 = db.users().insertUser();
 
     indexOnStartup();
 
@@ -72,12 +75,32 @@ public class PermissionIndexerTest {
   }
 
   @Test
-  public void initializeOnStartup_grants_access_to_user() {
+  public void deletion_resilience_will_deindex_projects() {
+    ComponentDto project1 = createUnindexedPublicProject();
+    ComponentDto project2 = createUnindexedPublicProject();
+    //UserDto user1 = db.users().insertUser();
+    indexOnStartup();
+    assertThat(es.countDocuments(INDEX_TYPE_FOO_AUTH)).isEqualTo(2);
+
+    // Simulate a indexation issue
+    db.getDbClient().componentDao().delete(db.getSession(), project1.getId());
+    underTest.prepareForRecovery(db.getSession(), asList(project1.uuid()), ProjectIndexer.Cause.PROJECT_DELETION);
+    assertThat(db.countRowsOfTable(db.getSession(), "es_queue")).isEqualTo(1);
+    Collection<EsQueueDto> esQueueDtos = db.getDbClient().esQueueDao().selectForRecovery(db.getSession(), Long.MAX_VALUE, 2);
+
+    underTest.index(db.getSession(), esQueueDtos);
+
+    assertThat(db.countRowsOfTable(db.getSession(), "es_queue")).isEqualTo(0);
+    assertThat(es.countDocuments(INDEX_TYPE_FOO_AUTH)).isEqualTo(1);
+  }
+
+  @Test
+  public void indexOnStartup_grants_access_to_user() {
     ComponentDto project = createAndIndexPrivateProject();
-    UserDto user1 = userDbTester.insertUser();
-    UserDto user2 = userDbTester.insertUser();
-    userDbTester.insertProjectPermissionOnUser(user1, USER, project);
-    userDbTester.insertProjectPermissionOnUser(user2, ADMIN, project);
+    UserDto user1 = db.users().insertUser();
+    UserDto user2 = db.users().insertUser();
+    db.users().insertProjectPermissionOnUser(user1, USER, project);
+    db.users().insertProjectPermissionOnUser(user2, ADMIN, project);
 
     indexOnStartup();
 
@@ -92,15 +115,15 @@ public class PermissionIndexerTest {
   }
 
   @Test
-  public void initializeOnStartup_grants_access_to_group_on_private_project() {
+  public void indexOnStartup_grants_access_to_group_on_private_project() {
     ComponentDto project = createAndIndexPrivateProject();
-    UserDto user1 = userDbTester.insertUser();
-    UserDto user2 = userDbTester.insertUser();
-    UserDto user3 = userDbTester.insertUser();
-    GroupDto group1 = userDbTester.insertGroup();
-    GroupDto group2 = userDbTester.insertGroup();
-    userDbTester.insertProjectPermissionOnGroup(group1, USER, project);
-    userDbTester.insertProjectPermissionOnGroup(group2, ADMIN, project);
+    UserDto user1 = db.users().insertUser();
+    UserDto user2 = db.users().insertUser();
+    UserDto user3 = db.users().insertUser();
+    GroupDto group1 = db.users().insertGroup();
+    GroupDto group2 = db.users().insertGroup();
+    db.users().insertProjectPermissionOnGroup(group1, USER, project);
+    db.users().insertProjectPermissionOnGroup(group2, ADMIN, project);
 
     indexOnStartup();
 
@@ -118,14 +141,14 @@ public class PermissionIndexerTest {
   }
 
   @Test
-  public void initializeOnStartup_grants_access_to_user_and_group() {
+  public void indexOnStartup_grants_access_to_user_and_group() {
     ComponentDto project = createAndIndexPrivateProject();
-    UserDto user1 = userDbTester.insertUser();
-    UserDto user2 = userDbTester.insertUser();
-    GroupDto group = userDbTester.insertGroup();
-    userDbTester.insertMember(group, user2);
-    userDbTester.insertProjectPermissionOnUser(user1, USER, project);
-    userDbTester.insertProjectPermissionOnGroup(group, USER, project);
+    UserDto user1 = db.users().insertUser();
+    UserDto user2 = db.users().insertUser();
+    GroupDto group = db.users().insertGroup();
+    db.users().insertMember(group, user2);
+    db.users().insertProjectPermissionOnUser(user1, USER, project);
+    db.users().insertProjectPermissionOnGroup(group, USER, project);
 
     indexOnStartup();
 
@@ -143,10 +166,10 @@ public class PermissionIndexerTest {
   }
 
   @Test
-  public void initializeOnStartup_does_not_grant_access_to_anybody_on_private_project() {
+  public void indexOnStartup_does_not_grant_access_to_anybody_on_private_project() {
     ComponentDto project = createAndIndexPrivateProject();
-    UserDto user = userDbTester.insertUser();
-    GroupDto group = userDbTester.insertGroup();
+    UserDto user = db.users().insertUser();
+    GroupDto group = db.users().insertGroup();
 
     indexOnStartup();
 
@@ -156,10 +179,10 @@ public class PermissionIndexerTest {
   }
 
   @Test
-  public void initializeOnStartup_grants_access_to_anybody_on_public_project() {
+  public void indexOnStartup_grants_access_to_anybody_on_public_project() {
     ComponentDto project = createAndIndexPublicProject();
-    UserDto user = userDbTester.insertUser();
-    GroupDto group = userDbTester.insertGroup();
+    UserDto user = db.users().insertUser();
+    GroupDto group = db.users().insertGroup();
 
     indexOnStartup();
 
@@ -169,26 +192,26 @@ public class PermissionIndexerTest {
   }
 
   @Test
-  public void initializeOnStartup_grants_access_to_anybody_on_view() {
-    ComponentDto project = createAndIndexView();
-    UserDto user = userDbTester.insertUser();
-    GroupDto group = userDbTester.insertGroup();
+  public void indexOnStartup_grants_access_to_anybody_on_view() {
+    ComponentDto view = createAndIndexView();
+    UserDto user = db.users().insertUser();
+    GroupDto group = db.users().insertGroup();
 
     indexOnStartup();
 
-    verifyAnyoneAuthorized(project);
-    verifyAuthorized(project, user);
-    verifyAuthorized(project, user, group);
+    verifyAnyoneAuthorized(view);
+    verifyAuthorized(view, user);
+    verifyAuthorized(view, user, group);
   }
 
   @Test
-  public void initializeOnStartup_grants_access_on_many_projects() {
-    UserDto user1 = userDbTester.insertUser();
-    UserDto user2 = userDbTester.insertUser();
+  public void indexOnStartup_grants_access_on_many_projects() {
+    UserDto user1 = db.users().insertUser();
+    UserDto user2 = db.users().insertUser();
     ComponentDto project = null;
-    for (int i = 0; i < PermissionIndexer.MAX_BATCH_SIZE + 10; i++) {
+    for (int i = 0; i < 10; i++) {
       project = createAndIndexPrivateProject();
-      userDbTester.insertProjectPermissionOnUser(user1, USER, project);
+      db.users().insertProjectPermissionOnUser(user1, USER, project);
     }
 
     indexOnStartup();
@@ -199,39 +222,121 @@ public class PermissionIndexerTest {
   }
 
   @Test
-  public void deleteProject_deletes_the_documents_related_to_the_project() {
-    ComponentDto project1 = createAndIndexPublicProject();
-    ComponentDto project2 = createAndIndexPublicProject();
+  public void public_projects_are_visible_to_anybody_whatever_the_organization() {
+    ComponentDto projectOnOrg1 = createAndIndexPublicProject(db.organizations().insert());
+    ComponentDto projectOnOrg2 = createAndIndexPublicProject(db.organizations().insert());
+    UserDto user = db.users().insertUser();
+
     indexOnStartup();
-    assertThat(esTester.countDocuments(INDEX_TYPE_FOO_AUTH)).isEqualTo(2);
 
-    underTest.deleteProject(project1.uuid());
-    assertThat(esTester.countDocuments(INDEX_TYPE_FOO_AUTH)).isEqualTo(1);
+    verifyAnyoneAuthorized(projectOnOrg1);
+    verifyAnyoneAuthorized(projectOnOrg2);
+    verifyAuthorized(projectOnOrg1, user);
+    verifyAuthorized(projectOnOrg2, user);
   }
 
   @Test
-  public void indexProject_does_nothing_because_authorizations_are_triggered_outside_standard_indexer_lifecycle() {
+  public void indexOnAnalysis_does_nothing_because_CE_does_not_touch_permissions() {
     ComponentDto project = createAndIndexPublicProject();
 
-    underTest.indexProject(project.uuid(), ProjectIndexer.Cause.NEW_ANALYSIS);
-    underTest.indexProject(project.uuid(), ProjectIndexer.Cause.PROJECT_CREATION);
-    underTest.indexProject(project.uuid(), ProjectIndexer.Cause.PROJECT_KEY_UPDATE);
+    underTest.indexOnAnalysis(project.uuid());
 
-    assertThat(esTester.countDocuments(INDEX_TYPE_FOO_AUTH)).isEqualTo(0);
+    assertThatAuthIndexHasSize(0);
+    verifyAnyoneNotAuthorized(project);
   }
 
   @Test
-  public void public_projects_are_visible_to_any_body_which_ever_the_organization() {
-    ComponentDto projectOnOrg1 = createAndIndexPublicProject(dbTester.organizations().insert());
-    ComponentDto projectOnOrg2 = createAndIndexPublicProject(dbTester.organizations().insert());
-    UserDto user = userDbTester.insertUser();
+  public void permissions_are_not_updated_on_project_tags_update() {
+    ComponentDto project = createAndIndexPublicProject();
 
-    indexOnStartup();
+    indexPermissions(project, ProjectIndexer.Cause.PROJECT_TAGS_UPDATE);
 
-    verifyAnyoneAuthorized(projectOnOrg1);
-    verifyAnyoneAuthorized(projectOnOrg2);
-    verifyAuthorized(projectOnOrg1, user);
-    verifyAuthorized(projectOnOrg2, user);
+    assertThatAuthIndexHasSize(0);
+    verifyAnyoneNotAuthorized(project);
+  }
+
+  @Test
+  public void permissions_are_not_updated_on_project_key_update() {
+    ComponentDto project = createAndIndexPublicProject();
+
+    indexPermissions(project, ProjectIndexer.Cause.PROJECT_TAGS_UPDATE);
+
+    assertThatAuthIndexHasSize(0);
+    verifyAnyoneNotAuthorized(project);
+  }
+
+  @Test
+  public void index_permissions_on_project_creation() {
+    ComponentDto project = createAndIndexPrivateProject();
+    UserDto user = db.users().insertUser();
+    db.users().insertProjectPermissionOnUser(user, USER, project);
+
+    indexPermissions(project, ProjectIndexer.Cause.PROJECT_CREATION);
+
+    assertThatAuthIndexHasSize(1);
+    verifyAuthorized(project, user);
+  }
+
+  @Test
+  public void index_permissions_on_permission_change() {
+    ComponentDto project = createAndIndexPrivateProject();
+    UserDto user1 = db.users().insertUser();
+    UserDto user2 = db.users().insertUser();
+    db.users().insertProjectPermissionOnUser(user1, USER, project);
+    indexPermissions(project, ProjectIndexer.Cause.PROJECT_CREATION);
+    verifyAuthorized(project, user1);
+    verifyNotAuthorized(project, user2);
+
+    db.users().insertProjectPermissionOnUser(user2, USER, project);
+    indexPermissions(project, PERMISSION_CHANGE);
+
+    verifyAuthorized(project, user1);
+    verifyAuthorized(project, user1);
+  }
+
+  @Test
+  public void delete_permissions_on_project_deletion() {
+    ComponentDto project = createAndIndexPrivateProject();
+    UserDto user = db.users().insertUser();
+    db.users().insertProjectPermissionOnUser(user, USER, project);
+    indexPermissions(project, ProjectIndexer.Cause.PROJECT_CREATION);
+    verifyAuthorized(project, user);
+
+    db.getDbClient().componentDao().delete(db.getSession(), project.getId());
+    indexPermissions(project, ProjectIndexer.Cause.PROJECT_DELETION);
+
+    verifyNotAuthorized(project, user);
+    assertThatAuthIndexHasSize(0);
+  }
+
+  @Test
+  public void errors_during_indexing_are_recovered() {
+    ComponentDto project = createAndIndexPublicProject();
+    es.lockWrites(INDEX_TYPE_FOO_AUTH);
+
+    IndexingResult result = indexPermissions(project, PERMISSION_CHANGE);
+    assertThat(result.getTotal()).isEqualTo(1L);
+    assertThat(result.getFailures()).isEqualTo(1L);
+
+    // index is still read-only, fail to recover
+    result = recover();
+    assertThat(result.getTotal()).isEqualTo(1L);
+    assertThat(result.getFailures()).isEqualTo(1L);
+    assertThatAuthIndexHasSize(0);
+    assertThatEsQueueTableHasSize(1);
+
+    es.unlockWrites(INDEX_TYPE_FOO_AUTH);
+
+    result = recover();
+    assertThat(result.getTotal()).isEqualTo(1L);
+    assertThat(result.getFailures()).isEqualTo(0L);
+    verifyAnyoneAuthorized(project);
+    assertThatEsQueueTableHasSize(0);
+  }
+
+  private void assertThatAuthIndexHasSize(int expectedSize) {
+    IndexType authIndexType = underTest.getIndexTypes().iterator().next();
+    assertThat(es.countDocuments(authIndexType)).isEqualTo(expectedSize);
   }
 
   private void indexOnStartup() {
@@ -239,22 +344,22 @@ public class PermissionIndexerTest {
   }
 
   private void verifyAuthorized(ComponentDto project, UserDto user) {
-    log_in(user);
+    logIn(user);
     verifyAuthorized(project, true);
   }
 
   private void verifyAuthorized(ComponentDto project, UserDto user, GroupDto group) {
-    log_in(user).setGroups(group);
+    logIn(user).setGroups(group);
     verifyAuthorized(project, true);
   }
 
   private void verifyNotAuthorized(ComponentDto project, UserDto user) {
-    log_in(user);
+    logIn(user);
     verifyAuthorized(project, false);
   }
 
   private void verifyNotAuthorized(ComponentDto project, UserDto user, GroupDto group) {
-    log_in(user).setGroups(group);
+    logIn(user).setGroups(group);
     verifyAuthorized(project, false);
   }
 
@@ -272,32 +377,54 @@ public class PermissionIndexerTest {
     assertThat(fooIndex.hasAccessToProject(project.uuid())).isEqualTo(expectedAccess);
   }
 
-  private UserSessionRule log_in(UserDto u) {
+  private UserSessionRule logIn(UserDto u) {
     userSession.logIn(u.getLogin()).setUserId(u.getId());
     return userSession;
   }
 
-  private ComponentDto createAndIndexPublicProject() {
-    ComponentDto project = componentDbTester.insertPublicProject();
-    fooIndexer.indexProject(project.uuid(), ProjectIndexer.Cause.PROJECT_CREATION);
+  private IndexingResult indexPermissions(ComponentDto project, ProjectIndexer.Cause cause) {
+    DbSession dbSession = db.getSession();
+    Collection<EsQueueDto> items = underTest.prepareForRecovery(dbSession, singletonList(project.uuid()), cause);
+    dbSession.commit();
+    return underTest.index(dbSession, items);
+  }
+
+  private ComponentDto createUnindexedPublicProject() {
+    ComponentDto project = db.components().insertPublicProject();
     return project;
   }
 
   private ComponentDto createAndIndexPrivateProject() {
-    ComponentDto project = componentDbTester.insertPrivateProject();
-    fooIndexer.indexProject(project.uuid(), ProjectIndexer.Cause.PROJECT_CREATION);
+    ComponentDto project = db.components().insertPrivateProject();
+    fooIndexer.indexOnAnalysis(project.uuid());
     return project;
   }
 
-  private ComponentDto createAndIndexView() {
-    ComponentDto project = componentDbTester.insertView();
-    fooIndexer.indexProject(project.uuid(), ProjectIndexer.Cause.PROJECT_CREATION);
+  private ComponentDto createAndIndexPublicProject() {
+    ComponentDto project = db.components().insertPublicProject();
+    fooIndexer.indexOnAnalysis(project.uuid());
     return project;
   }
 
+  private ComponentDto createAndIndexView() {
+    ComponentDto view = db.components().insertView();
+    fooIndexer.indexOnAnalysis(view.uuid());
+    return view;
+  }
+
   private ComponentDto createAndIndexPublicProject(OrganizationDto org) {
-    ComponentDto project = componentDbTester.insertPublicProject(org);
-    fooIndexer.indexProject(project.uuid(), ProjectIndexer.Cause.PROJECT_CREATION);
+    ComponentDto project = db.components().insertPublicProject(org);
+    fooIndexer.indexOnAnalysis(project.uuid());
     return project;
   }
+
+  private IndexingResult recover() {
+    Collection<EsQueueDto> items = db.getDbClient().esQueueDao().selectForRecovery(db.getSession(), System.currentTimeMillis() + 1_000L, 10);
+    return underTest.index(db.getSession(), items);
+  }
+
+  private void assertThatEsQueueTableHasSize(int expectedSize) {
+    assertThat(db.countRowsOfTable("es_queue")).isEqualTo(expectedSize);
+  }
+
 }
index cadf166786ccb113c60a544839ae6556bf2b31cc..1ed30d1d67b5eb9b7f0aa6d0380c106a7580c8c8 100644 (file)
@@ -30,10 +30,13 @@ import org.sonar.db.component.ResourceTypesRule;
 import org.sonar.db.organization.OrganizationDto;
 import org.sonar.db.permission.template.PermissionTemplateDto;
 import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.es.ProjectIndexersImpl;
 import org.sonar.server.organization.TestDefaultOrganizationProvider;
 import org.sonar.server.permission.GroupPermissionChanger;
 import org.sonar.server.permission.PermissionUpdater;
 import org.sonar.server.permission.UserPermissionChanger;
+import org.sonar.server.permission.index.FooIndexDefinition;
 import org.sonar.server.permission.index.PermissionIndexer;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.usergroups.DefaultGroupFinder;
@@ -41,7 +44,6 @@ import org.sonar.server.usergroups.ws.GroupWsSupport;
 import org.sonar.server.ws.TestRequest;
 import org.sonar.server.ws.WsActionTester;
 
-import static org.mockito.Mockito.mock;
 import static org.sonar.db.permission.OrganizationPermission.ADMINISTER;
 import static org.sonar.db.permission.template.PermissionTemplateTesting.newPermissionTemplateDto;
 
@@ -49,6 +51,9 @@ public abstract class BasePermissionWsTest<A extends PermissionsWsAction> {
 
   @Rule
   public DbTester db = DbTester.create(new AlwaysIncreasingSystem2());
+
+  @Rule
+  public EsTester esTester = new EsTester(new FooIndexDefinition());
   @Rule
   public ExpectedException expectedException = ExpectedException.none();
 
@@ -78,7 +83,7 @@ public abstract class BasePermissionWsTest<A extends PermissionsWsAction> {
 
   protected PermissionUpdater newPermissionUpdater() {
     return new PermissionUpdater(db.getDbClient(),
-      mock(PermissionIndexer.class),
+      new ProjectIndexersImpl(new PermissionIndexer(db.getDbClient(), esTester.client())),
       new UserPermissionChanger(db.getDbClient()),
       new GroupPermissionChanger(db.getDbClient()));
   }
index 7a546890321b6992fbbc3902d6ccc8f33fc8fb5d..62bd1caa1c8939cef51198f27800a813bd24e49c 100644 (file)
@@ -30,17 +30,16 @@ import org.sonar.db.permission.PermissionQuery;
 import org.sonar.db.permission.template.PermissionTemplateDto;
 import org.sonar.db.user.GroupDto;
 import org.sonar.db.user.UserDto;
+import org.sonar.server.es.TestProjectIndexers;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.permission.PermissionTemplateService;
-import org.sonar.server.permission.index.PermissionIndexer;
 import org.sonar.server.permission.ws.BasePermissionWsTest;
 import org.sonar.server.ws.TestRequest;
 import org.sonar.server.ws.TestResponse;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
 import static org.sonar.db.permission.OrganizationPermission.ADMINISTER;
 import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_PROJECT_ID;
 import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_PROJECT_KEY;
@@ -60,7 +59,7 @@ public class ApplyTemplateActionTest extends BasePermissionWsTest<ApplyTemplateA
   private PermissionTemplateDto template2;
 
   private PermissionTemplateService permissionTemplateService = new PermissionTemplateService(db.getDbClient(),
-    mock(PermissionIndexer.class), userSession, defaultTemplatesResolver);
+     new TestProjectIndexers(), userSession, defaultTemplatesResolver);
 
   @Override
   protected ApplyTemplateAction buildWsAction() {
index 1011572417e451bcd1dfc2a830d95e80a2c676d2..ed3bebb972bff324f8d23de159341ff073ea09bd 100644 (file)
@@ -31,15 +31,15 @@ import org.sonar.db.permission.PermissionQuery;
 import org.sonar.db.permission.template.PermissionTemplateDto;
 import org.sonar.db.user.GroupDto;
 import org.sonar.db.user.UserDto;
+import org.sonar.server.es.ProjectIndexers;
+import org.sonar.server.es.TestProjectIndexers;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.i18n.I18nRule;
 import org.sonar.server.permission.PermissionTemplateService;
-import org.sonar.server.permission.index.PermissionIndexer;
 import org.sonar.server.permission.ws.BasePermissionWsTest;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
 import static org.sonar.db.component.ComponentTesting.newView;
 import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_ORGANIZATION;
 import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_QUALIFIER;
@@ -58,12 +58,12 @@ public class BulkApplyTemplateActionTest extends BasePermissionWsTest<BulkApplyT
   private OrganizationDto organization;
   private PermissionTemplateDto template1;
   private PermissionTemplateDto template2;
-  private PermissionIndexer issuePermissionIndexer = mock(PermissionIndexer.class);
+  private ProjectIndexers projectIndexers = new TestProjectIndexers();
 
   @Override
   protected BulkApplyTemplateAction buildWsAction() {
     PermissionTemplateService permissionTemplateService = new PermissionTemplateService(db.getDbClient(),
-      issuePermissionIndexer, userSession, defaultTemplatesResolver);
+      projectIndexers, userSession, defaultTemplatesResolver);
     return new BulkApplyTemplateAction(db.getDbClient(), userSession, permissionTemplateService, newPermissionWsSupport(), new I18nRule(), newRootResourceTypes());
   }
 
index ed742f09207f5db2b8a10e9a4ac015821fe2e5f4..c661569fc45f970f7fd029528a1083c67cd904d9 100644 (file)
@@ -30,7 +30,7 @@ import org.sonar.db.DbTester;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.organization.OrganizationDto;
 import org.sonar.server.component.ComponentUpdater;
-import org.sonar.server.es.ProjectIndexer;
+import org.sonar.server.es.TestProjectIndexers;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.favorite.FavoriteUpdater;
@@ -82,13 +82,13 @@ public class CreateActionTest {
 
   private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
   private BillingValidationsProxy billingValidations = mock(BillingValidationsProxy.class);
-
+  private TestProjectIndexers projectIndexers = new TestProjectIndexers();
   private WsActionTester ws = new WsActionTester(
     new CreateAction(
       new ProjectsWsSupport(db.getDbClient(), billingValidations),
       db.getDbClient(), userSession,
       new ComponentUpdater(db.getDbClient(), i18n, system2, mock(PermissionTemplateService.class), new FavoriteUpdater(db.getDbClient()),
-        mock(ProjectIndexer.class)),
+        projectIndexers),
       defaultOrganizationProvider));
 
   @Test
index ea4b748a6da622ed845b2174f9ba7deed3be8dca..3854fb3c9268c6d362a49c7f58e8aa5f369b2674 100644 (file)
@@ -20,7 +20,6 @@
 package org.sonar.server.project.ws;
 
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.Random;
 import java.util.Set;
 import java.util.stream.IntStream;
@@ -45,13 +44,16 @@ import org.sonar.db.permission.UserPermissionDto;
 import org.sonar.db.user.GroupDto;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.component.TestComponentFinder;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.es.ProjectIndexer;
+import org.sonar.server.es.TestProjectIndexers;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.exceptions.UnauthorizedException;
 import org.sonar.server.organization.BillingValidations;
 import org.sonar.server.organization.BillingValidationsProxy;
-import org.sonar.server.permission.index.PermissionIndexer;
+import org.sonar.server.permission.index.FooIndexDefinition;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.TestRequest;
 import org.sonar.server.ws.WsActionTester;
@@ -64,8 +66,6 @@ import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.sonar.db.organization.OrganizationTesting.newOrganizationDto;
 
 public class UpdateVisibilityActionTest {
@@ -81,6 +81,8 @@ public class UpdateVisibilityActionTest {
   @Rule
   public DbTester dbTester = DbTester.create(System2.INSTANCE);
   @Rule
+  public EsTester esTester = new EsTester(new FooIndexDefinition());
+  @Rule
   public UserSessionRule userSessionRule = UserSessionRule.standalone()
     .logIn();
   @Rule
@@ -88,11 +90,11 @@ public class UpdateVisibilityActionTest {
 
   private DbClient dbClient = dbTester.getDbClient();
   private DbSession dbSession = dbTester.getSession();
-  private PermissionIndexer permissionIndexer = mock(PermissionIndexer.class);
+  private TestProjectIndexers projectIndexers = new TestProjectIndexers();
   private BillingValidationsProxy billingValidations = mock(BillingValidationsProxy.class);
 
-  private UpdateVisibilityAction underTest = new UpdateVisibilityAction(dbClient, TestComponentFinder.from(dbTester), userSessionRule, permissionIndexer,
-    new ProjectsWsSupport(dbClient, billingValidations));
+  private ProjectsWsSupport wsSupport = new ProjectsWsSupport(dbClient, billingValidations);
+  private UpdateVisibilityAction underTest = new UpdateVisibilityAction(dbClient, TestComponentFinder.from(dbTester), userSessionRule, projectIndexers, wsSupport);
   private WsActionTester actionTester = new WsActionTester(underTest);
 
   private final Random random = new Random();
@@ -444,7 +446,7 @@ public class UpdateVisibilityActionTest {
       .setParam(PARAM_VISIBILITY, initiallyPrivate ? PUBLIC : PRIVATE)
       .execute();
 
-    verify(permissionIndexer).indexProjectsByUuids(any(DbSession.class), eq(Collections.singletonList(project.uuid())));
+    assertThat(projectIndexers.hasBeenCalled(project.uuid(), ProjectIndexer.Cause.PERMISSION_CHANGE)).isTrue();
   }
 
   @Test
@@ -457,7 +459,7 @@ public class UpdateVisibilityActionTest {
       .setParam(PARAM_VISIBILITY, initiallyPrivate ? PRIVATE : PUBLIC)
       .execute();
 
-    verifyZeroInteractions(permissionIndexer);
+    assertThat(projectIndexers.hasBeenCalled(project.uuid())).isFalse();
   }
 
   @Test
@@ -470,7 +472,7 @@ public class UpdateVisibilityActionTest {
       .setParam(PARAM_VISIBILITY, PUBLIC)
       .execute();
 
-    verifyZeroInteractions(permissionIndexer);
+    assertThat(projectIndexers.hasBeenCalled(view.uuid())).isFalse();
   }
 
   @Test
index 9f2871c6b3b42d66f0163d9df020f5c851629459..7c35d20840c5e02dd0b08c6a335e8e1811a9a16f 100644 (file)
@@ -32,7 +32,7 @@ import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.server.component.TestComponentFinder;
-import org.sonar.server.es.ProjectIndexer;
+import org.sonar.server.es.TestProjectIndexers;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.tester.UserSessionRule;
@@ -41,14 +41,10 @@ import org.sonar.server.ws.TestResponse;
 import org.sonar.server.ws.WsActionTester;
 
 import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
-import static java.util.Collections.singletonList;
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
 import static org.sonar.core.util.Protobuf.setNullable;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
 import static org.sonar.db.component.ComponentTesting.newModuleDto;
-import static org.sonar.server.es.ProjectIndexer.Cause.PROJECT_TAGS_UPDATE;
 
 public class SetActionTest {
   @Rule
@@ -62,9 +58,9 @@ public class SetActionTest {
   private DbSession dbSession = db.getSession();
   private ComponentDto project;
 
-  private ProjectIndexer indexer = mock(ProjectIndexer.class);
+  private TestProjectIndexers projectIndexers = new TestProjectIndexers();
 
-  private WsActionTester ws = new WsActionTester(new SetAction(dbClient, TestComponentFinder.from(db), userSession, singletonList(indexer)));
+  private WsActionTester ws = new WsActionTester(new SetAction(dbClient, TestComponentFinder.from(db), userSession, projectIndexers));
 
   @Before
   public void setUp() {
@@ -76,7 +72,8 @@ public class SetActionTest {
     TestResponse response = call(project.key(), "finance , offshore, platform,   ,");
 
     assertTags(project.key(), "finance", "offshore", "platform");
-    verify(indexer).indexProject(project.uuid(), PROJECT_TAGS_UPDATE);
+    // FIXME verify(indexer).indexProject(project.uuid(), PROJECT_TAGS_UPDATE);
+
     assertThat(response.getStatus()).isEqualTo(HTTP_NO_CONTENT);
   }
 
index 216c7f428e032133e60bdcb5c93612224c0c6e28..32011df23d611002db376e42d51eb03bb3bb6a5e 100644 (file)
@@ -125,7 +125,7 @@ public class ActiveRuleIndexerTest {
 
     commitAndIndex(ar);
 
-    EsQueueDto expectedItem = EsQueueDto.create(EsQueueDto.Type.ACTIVE_RULE, "" + ar.getId(), "activeRuleId", ar.getRuleKey().toString());
+    EsQueueDto expectedItem = EsQueueDto.create(INDEX_TYPE_ACTIVE_RULE.format(), "" + ar.getId(), "activeRuleId", ar.getRuleKey().toString());
     assertThatEsQueueContainsExactly(expectedItem);
   }
 
@@ -144,7 +144,7 @@ public class ActiveRuleIndexerTest {
 
   @Test
   public void index_fails_and_deletes_doc_if_docIdType_is_unsupported() {
-    EsQueueDto item = EsQueueDto.create(EsQueueDto.Type.ACTIVE_RULE, "the_id", "unsupported", "the_routing");
+    EsQueueDto item = EsQueueDto.create(INDEX_TYPE_ACTIVE_RULE.format(), "the_id", "unsupported", "the_routing");
     db.getDbClient().esQueueDao().insert(db.getSession(), item);
 
     underTest.index(db.getSession(), asList(item));
diff --git a/server/sonar-server/src/test/java/org/sonar/server/rule/ws/SearchActionMediumTest.java b/server/sonar-server/src/test/java/org/sonar/server/rule/ws/SearchActionMediumTest.java
deleted file mode 100644 (file)
index 1d633e3..0000000
+++ /dev/null
@@ -1,244 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.server.rule.ws;
-
-public class SearchActionMediumTest {
-
-
-  //
-  // @Test
-  // public void search_profile_active_rules_with_inheritance() throws Exception {
-  // QProfileDto profile = QProfileTesting.newXooP1(defaultOrganizationDto);
-  // esTester.get(QualityProfileDao.class).insert(dbSession, profile);
-  //
-  // QProfileDto profile2 = QProfileTesting.newXooP2(defaultOrganizationDto).setParentKee(profile.getKee());
-  // esTester.get(QualityProfileDao.class).insert(dbSession, profile2);
-  //
-  // dbSession.commit();
-  //
-  // RuleDefinitionDto rule = RuleTesting.newXooX1().getDefinition();
-  // insertRule(rule);
-  //
-  // ActiveRuleDto activeRule = newActiveRule(profile, rule);
-  // esTester.get(ActiveRuleDao.class).insert(dbSession, activeRule);
-  // ActiveRuleDto activeRule2 = newActiveRule(profile2, rule).setInheritance(ActiveRuleDto.OVERRIDES).setSeverity(Severity.CRITICAL);
-  // esTester.get(ActiveRuleDao.class).insert(dbSession, activeRule2);
-  //
-  // dbSession.commit();
-  //
-  // activeRuleIndexer.index();
-  //
-  // WsTester.TestRequest request = esTester.wsTester().newGetRequest(API_ENDPOINT, API_SEARCH_METHOD);
-  // request.setParam(WebService.Param.TEXT_QUERY, "x1");
-  // request.setParam(PARAM_ACTIVATION, "true");
-  // request.setParam(PARAM_QPROFILE, profile2.getKee());
-  // request.setParam(WebService.Param.FIELDS, "actives");
-  // WsTester.Result result = request.execute();
-  // result.assertJson(this.getClass(), "search_profile_active_rules_inheritance.json");
-  // }
-  //
-  // @Test
-  // public void search_all_active_rules_params() throws Exception {
-  // QProfileDto profile = QProfileTesting.newXooP1(defaultOrganizationDto);
-  // esTester.get(QualityProfileDao.class).insert(dbSession, profile);
-  // RuleDefinitionDto rule = RuleTesting.newXooX1().getDefinition();
-  // insertRule(rule);
-  // dbSession.commit();
-  //
-  // RuleParamDto param = RuleParamDto.createFor(rule)
-  // .setDefaultValue("some value")
-  // .setType("string")
-  // .setDescription("My small description")
-  // .setName("my_var");
-  // ruleDao.insertRuleParam(dbSession, rule, param);
-  //
-  // RuleParamDto param2 = RuleParamDto.createFor(rule)
-  // .setDefaultValue("other value")
-  // .setType("integer")
-  // .setDescription("My small description")
-  // .setName("the_var");
-  // ruleDao.insertRuleParam(dbSession, rule, param2);
-  //
-  // ActiveRuleDto activeRule = newActiveRule(profile, rule);
-  // esTester.get(ActiveRuleDao.class).insert(dbSession, activeRule);
-  //
-  // ActiveRuleParamDto activeRuleParam = ActiveRuleParamDto.createFor(param)
-  // .setValue("The VALUE");
-  // esTester.get(ActiveRuleDao.class).insertParam(dbSession, activeRule, activeRuleParam);
-  //
-  // ActiveRuleParamDto activeRuleParam2 = ActiveRuleParamDto.createFor(param2)
-  // .setValue("The Other Value");
-  // esTester.get(ActiveRuleDao.class).insertParam(dbSession, activeRule, activeRuleParam2);
-  //
-  // dbSession.commit();
-  //
-  // activeRuleIndexer.index();
-  //
-  // WsTester.TestRequest request = esTester.wsTester().newGetRequest(API_ENDPOINT, API_SEARCH_METHOD);
-  // request.setParam(WebService.Param.TEXT_QUERY, "x1");
-  // request.setParam(PARAM_ACTIVATION, "true");
-  // request.setParam(WebService.Param.FIELDS, "params");
-  // WsTester.Result result = request.execute();
-  //
-  // result.assertJson(this.getClass(), "search_active_rules_params.json");
-  // }
-  //
-  // @Test
-  // public void get_note_as_markdown_and_html() throws Exception {
-  // QProfileDto profile = QProfileTesting.newXooP1("org-123");
-  // esTester.get(QualityProfileDao.class).insert(dbSession, profile);
-  // RuleDto rule = RuleTesting.newXooX1(defaultOrganizationDto).setNoteData("this is *bold*");
-  // insertRule(rule.getDefinition());
-  // ruleDao.insertOrUpdate(dbSession, rule.getMetadata().setRuleId(rule.getId()));
-  //
-  // dbSession.commit();
-  //
-  // activeRuleIndexer.index();
-  //
-  // WsTester.TestRequest request = esTester.wsTester().newGetRequest(API_ENDPOINT, API_SEARCH_METHOD);
-  // request.setParam(WebService.Param.FIELDS, "htmlNote, mdNote");
-  // WsTester.Result result = request.execute();
-  // result.assertJson(this.getClass(), "get_note_as_markdown_and_html.json");
-  // }
-  //
-  // @Test
-  // public void filter_by_tags() throws Exception {
-  // insertRule(RuleTesting.newRule()
-  // .setRepositoryKey("xoo").setRuleKey("x1")
-  // .setSystemTags(ImmutableSet.of("tag1")));
-  // insertRule(RuleTesting.newRule()
-  // .setSystemTags(ImmutableSet.of("tag2")));
-  //
-  // activeRuleIndexer.index();
-  //
-  // WsTester.TestRequest request = esTester.wsTester().newGetRequest(API_ENDPOINT, API_SEARCH_METHOD);
-  // request.setParam(PARAM_TAGS, "tag1");
-  // request.setParam(WebService.Param.FIELDS, "sysTags, tags");
-  // request.setParam(WebService.Param.FACETS, "tags");
-  // WsTester.Result result = request.execute();
-  // result.assertJson(this.getClass(), "filter_by_tags.json");
-  // }
-  //
-  // @Test
-  // public void severities_facet_should_have_all_severities() throws Exception {
-  // WsTester.TestRequest request = esTester.wsTester().newGetRequest(API_ENDPOINT, API_SEARCH_METHOD);
-  // request.setParam(WebService.Param.FACETS, "severities");
-  // request.execute().assertJson(this.getClass(), "severities_facet.json");
-  // }
-  //
- //
-  //
-  // @Test
-  // public void sort_by_name() throws Exception {
-  // insertRule(RuleTesting.newXooX1()
-  // .setName("Dodgy - Consider returning a zero length array rather than null ")
-  // .getDefinition());
-  // insertRule(RuleTesting.newXooX2()
-  // .setName("Bad practice - Creates an empty zip file entry")
-  // .getDefinition());
-  // insertRule(RuleTesting.newXooX3()
-  // .setName("XPath rule")
-  // .getDefinition());
-  //
-  // dbSession.commit();
-  //
-  // // 1. Sort Name Asc
-  // WsTester.TestRequest request = esTester.wsTester().newGetRequest(API_ENDPOINT, API_SEARCH_METHOD);
-  // request.setParam(WebService.Param.FIELDS, "");
-  // request.setParam(WebService.Param.SORT, "name");
-  // request.setParam(WebService.Param.ASCENDING, "true");
-  //
-  // WsTester.Result result = request.execute();
-  // result.assertJson("{\"total\":3,\"p\":1,\"ps\":100,\"rules\":[{\"key\":\"xoo:x2\"},{\"key\":\"xoo:x1\"},{\"key\":\"xoo:x3\"}]}");
-  //
-  // // 2. Sort Name DESC
-  // request = esTester.wsTester().newGetRequest(API_ENDPOINT, API_SEARCH_METHOD);
-  // request.setParam(WebService.Param.FIELDS, "");
-  // request.setParam(WebService.Param.SORT, RuleIndexDefinition.FIELD_RULE_NAME);
-  // request.setParam(WebService.Param.ASCENDING, "false");
-  //
-  // result = request.execute();
-  // result.assertJson("{\"total\":3,\"p\":1,\"ps\":100,\"rules\":[{\"key\":\"xoo:x3\"},{\"key\":\"xoo:x1\"},{\"key\":\"xoo:x2\"}]}");
-  // }
-  //
-  // @Test
-  // public void available_since() throws Exception {
-  // Date since = new Date();
-  // insertRule(RuleTesting.newXooX1()
-  // .setUpdatedAt(since.getTime())
-  // .setCreatedAt(since.getTime())
-  // .getDefinition());
-  // insertRule(RuleTesting.newXooX2()
-  // .setUpdatedAt(since.getTime())
-  // .setCreatedAt(since.getTime())
-  // .getDefinition());
-  //
-  // dbSession.commit();
-  // dbSession.clearCache();
-  //
-  // // 1. find today's rules
-  // WsTester.TestRequest request = esTester.wsTester().newGetRequest(API_ENDPOINT, API_SEARCH_METHOD);
-  // request.setParam(WebService.Param.FIELDS, "");
-  // request.setParam(PARAM_AVAILABLE_SINCE, DateUtils.formatDate(since));
-  // request.setParam(WebService.Param.SORT, RuleIndexDefinition.FIELD_RULE_KEY);
-  // WsTester.Result result = request.execute();
-  // result.assertJson("{\"total\":2,\"p\":1,\"ps\":100,\"rules\":[{\"key\":\"xoo:x1\"},{\"key\":\"xoo:x2\"}]}");
-  //
-  // // 2. no rules since tomorrow
-  // request = esTester.wsTester().newGetRequest(API_ENDPOINT, API_SEARCH_METHOD);
-  // request.setParam(WebService.Param.FIELDS, "");
-  // request.setParam(PARAM_AVAILABLE_SINCE, DateUtils.formatDate(DateUtils.addDays(since, 1)));
-  // result = request.execute();
-  // result.assertJson("{\"total\":0,\"p\":1,\"ps\":100,\"rules\":[]}");
-  // }
-  //
-  // @Test
-  // public void search_rules_with_deprecated_fields() throws Exception {
-  // RuleDto ruleDto = RuleTesting.newXooX1(defaultOrganizationDto)
-  // .setDefRemediationFunction(DebtRemediationFunction.Type.LINEAR_OFFSET.name())
-  // .setDefRemediationGapMultiplier("1h")
-  // .setDefRemediationBaseEffort("15min")
-  // .setRemediationFunction(DebtRemediationFunction.Type.LINEAR_OFFSET.name())
-  // .setRemediationGapMultiplier("2h")
-  // .setRemediationBaseEffort("25min");
-  // insertRule(ruleDto.getDefinition());
-  // ruleDao.insertOrUpdate(dbSession, ruleDto.getMetadata().setRuleId(ruleDto.getId()));
-  // dbSession.commit();
-  //
-  // WsTester.TestRequest request = esTester.wsTester()
-  // .newGetRequest(API_ENDPOINT, API_SEARCH_METHOD)
-  // .setParam(WebService.Param.FIELDS, "name,defaultDebtRemFn,debtRemFn,effortToFixDescription,debtOverloaded");
-  // WsTester.Result result = request.execute();
-  //
-  // result.assertJson(getClass(), "search_rules_with_deprecated_fields.json");
-  // }
-  //
-  // private ActiveRuleDto newActiveRule(QProfileDto profile, RuleDefinitionDto rule) {
-  // return ActiveRuleDto.createFor(profile, rule)
-  // .setInheritance(null)
-  // .setSeverity("BLOCKER");
-  // }
-  //
-  // private void insertRule(RuleDefinitionDto definition) {
-  // ruleDao.insert(dbSession, definition);
-  // dbSession.commit();
-  // ruleIndexer.indexRuleDefinition(definition.getKey());
-  // }
-}
index 1922c16d74567473d2dd58f638d970a1159f044e..9bd4ffd8472642af09550a3508bc0f4146b2cc98 100644 (file)
@@ -36,8 +36,9 @@ import org.sonar.api.config.internal.MapSettings;
 import org.sonar.api.utils.System2;
 import org.sonar.db.DbTester;
 import org.sonar.db.protobuf.DbFileSources;
+import org.sonar.server.es.BulkIndexer;
 import org.sonar.server.es.EsTester;
-import org.sonar.server.es.ProjectIndexer;
+import org.sonar.server.es.IndexingListener;
 import org.sonar.server.source.index.FileSourcesUpdaterHelper;
 import org.sonar.server.test.db.TestTesting;
 
@@ -93,7 +94,7 @@ public class TestIndexerTest {
 
     TestTesting.updateDataColumn(db.getSession(), "FILE_UUID", TestTesting.newRandomTests(3));
 
-    underTest.indexProject("PROJECT_UUID", ProjectIndexer.Cause.NEW_ANALYSIS);
+    underTest.indexOnAnalysis("PROJECT_UUID");
     assertThat(countDocuments()).isEqualTo(3);
   }
 
@@ -103,7 +104,7 @@ public class TestIndexerTest {
 
     TestTesting.updateDataColumn(db.getSession(), "FILE_UUID", TestTesting.newRandomTests(3));
 
-    underTest.indexProject("UNKNOWN", ProjectIndexer.Cause.NEW_ANALYSIS);
+    underTest.indexOnAnalysis("UNKNOWN");
     assertThat(countDocuments()).isZero();
   }
 
@@ -127,7 +128,7 @@ public class TestIndexerTest {
         .setExecutionTimeMs(123_456L)
         .addCoveredFile(DbFileSources.Test.CoveredFile.newBuilder().setFileUuid("MAIN_UUID_1").addCoveredLine(42))
         .build()));
-    underTest.index(Iterators.singletonIterator(dbRow));
+    underTest.doIndex(Iterators.singletonIterator(dbRow), BulkIndexer.Size.REGULAR, IndexingListener.NOOP);
 
     assertThat(countDocuments()).isEqualTo(2L);
 
@@ -162,21 +163,21 @@ public class TestIndexerTest {
     assertThat(document.get(FIELD_FILE_UUID)).isEqualTo("F2");
   }
 
-  @Test
-  public void delete_project_by_uuid() throws Exception {
-    indexTest("P1", "F1", "T1", "U111");
-    indexTest("P1", "F1", "T2", "U112");
-    indexTest("P1", "F2", "T1", "U121");
-    indexTest("P2", "F3", "T1", "U231");
-
-    underTest.deleteProject("P1");
-
-    List<SearchHit> hits = getDocuments();
-    assertThat(hits).hasSize(1);
-    Map<String, Object> document = hits.get(0).getSource();
-    assertThat(hits).hasSize(1);
-    assertThat(document.get(FIELD_PROJECT_UUID)).isEqualTo("P2");
-  }
+//  @Test
+//  public void delete_project_by_uuid() throws Exception {
+//    indexTest("P1", "F1", "T1", "U111");
+//    indexTest("P1", "F1", "T2", "U112");
+//    indexTest("P1", "F2", "T1", "U121");
+//    indexTest("P2", "F3", "T1", "U231");
+//
+//    underTest.deleteProject("P1");
+//
+//    List<SearchHit> hits = getDocuments();
+//    assertThat(hits).hasSize(1);
+//    Map<String, Object> document = hits.get(0).getSource();
+//    assertThat(hits).hasSize(1);
+//    assertThat(document.get(FIELD_PROJECT_UUID)).isEqualTo("P2");
+//  }
 
   private void indexTest(String projectUuid, String fileUuid, String testName, String uuid) throws IOException {
     es.client().prepareIndex(INDEX_TYPE_TEST)
index 53c1e5f3c06ce9f5497782d5aff322e85ff28ef2..e416e031fa0bff9c7617706589e25599037735a8 100644 (file)
@@ -49,7 +49,7 @@ import org.sonar.server.permission.index.PermissionIndexer;
 import org.sonar.server.tester.UserSessionRule;
 
 import static com.google.common.collect.Lists.newArrayList;
-import static java.util.Arrays.asList;
+import static java.util.Collections.emptySet;
 import static org.assertj.core.api.Assertions.assertThat;
 
 public class ViewIndexerTest {
@@ -67,13 +67,13 @@ public class ViewIndexerTest {
 
   private DbClient dbClient = dbTester.getDbClient();
   private DbSession dbSession = dbTester.getSession();
-  private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), new IssueIteratorFactory(dbClient));
+  private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbClient, new IssueIteratorFactory(dbClient));
   private PermissionIndexer permissionIndexer = new PermissionIndexer(dbClient, esTester.client(), issueIndexer);
   private ViewIndexer underTest = new ViewIndexer(dbClient, esTester.client());
 
   @Test
   public void index_nothing() {
-    underTest.indexOnStartup(null);
+    underTest.indexOnStartup(emptySet());
     assertThat(esTester.countDocuments(ViewIndexDefinition.INDEX_TYPE_VIEW)).isEqualTo(0L);
   }
 
@@ -81,7 +81,7 @@ public class ViewIndexerTest {
   public void index() {
     dbTester.prepareDbUnit(getClass(), "index.xml");
 
-    underTest.indexOnStartup(null);
+    underTest.indexOnStartup(emptySet());
 
     List<ViewDoc> docs = esTester.getDocuments(ViewIndexDefinition.INDEX_TYPE_VIEW, ViewDoc.class);
     assertThat(docs).hasSize(4);
@@ -124,7 +124,7 @@ public class ViewIndexerTest {
   @Test
   public void clear_views_lookup_cache_on_index_view_uuid() {
     IssueIndex issueIndex = new IssueIndex(esTester.client(), System2.INSTANCE, userSessionRule, new AuthorizationTypeSupport(userSessionRule));
-    IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), new IssueIteratorFactory(dbClient));
+    IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbClient, new IssueIteratorFactory(dbClient));
 
     String viewUuid = "ABCD";
 
@@ -132,7 +132,7 @@ public class ViewIndexerTest {
     dbClient.ruleDao().insert(dbSession, rule.getDefinition());
     ComponentDto project1 = addProjectWithIssue(rule, dbTester.organizations().insert());
     issueIndexer.indexOnStartup(issueIndexer.getIndexTypes());
-    permissionIndexer.indexProjectsByUuids(dbSession, asList(project1.uuid()));
+    permissionIndexer.indexOnStartup(permissionIndexer.getIndexTypes());
 
     OrganizationDto organizationDto = dbTester.organizations().insert();
     ComponentDto view = ComponentTesting.newView(organizationDto, "ABCD");
@@ -150,7 +150,7 @@ public class ViewIndexerTest {
     // Add a project to the view and index it again
     ComponentDto project2 = addProjectWithIssue(rule, organizationDto);
     issueIndexer.indexOnStartup(issueIndexer.getIndexTypes());
-    permissionIndexer.indexProjectsByUuids(dbSession, asList(project2.uuid()));
+    permissionIndexer.indexOnStartup(permissionIndexer.getIndexTypes());
 
     ComponentDto techProject2 = ComponentTesting.newProjectCopy("EFGH", project2, view);
     dbClient.componentDao().insert(dbSession, techProject2);
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/project/BulkDeleteRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/project/BulkDeleteRequest.java
new file mode 100644 (file)
index 0000000..de16549
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonarqube.ws.client.project;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+public class BulkDeleteRequest {
+
+  private final String organization;
+  private final Collection<String> projectKeys;
+
+  private BulkDeleteRequest(Builder builder) {
+    this.organization = builder.organization;
+    this.projectKeys = builder.projectKeys;
+  }
+
+  @CheckForNull
+  public String getOrganization() {
+    return organization;
+  }
+
+  public Collection<String> getProjectKeys() {
+    return projectKeys;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    private String organization;
+    private final Collection<String> projectKeys = new ArrayList<>();
+
+    private Builder() {
+    }
+
+    public Builder setOrganization(@Nullable String s) {
+      this.organization = s;
+      return this;
+    }
+
+    public Builder setProjectKeys(Collection<String> s) {
+      this.projectKeys.addAll(s);
+      return this;
+    }
+
+    public BulkDeleteRequest build() {
+      return new BulkDeleteRequest(this);
+    }
+  }
+}
index 3546d933f9d8d079e5dcdb8e60a89dadf8ca8cd9..c21edd90113f5038deda392a7e93efffdfe28208 100644 (file)
@@ -49,7 +49,6 @@ public class DeleteRequest {
   }
 
   public static class Builder {
-    private String id;
     private String key;
 
     private Builder() {
index f46f0f5701e1c55515213377782389eb3cda38ba..71b31ef1d1d8ac2d666adaa2e06f9490f3c2e483 100644 (file)
@@ -49,6 +49,7 @@ import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_VISIBIL
 
 /**
  * Maps web service {@code api/projects}.
+ *
  * @since 5.5
  */
 public class ProjectsService extends BaseService {
@@ -81,6 +82,14 @@ public class ProjectsService extends BaseService {
       .setParam("key", request.getKey()));
   }
 
+  public void bulkDelete(BulkDeleteRequest request) {
+    PostRequest post = new PostRequest(path("bulk_delete"))
+      .setParam("organization", request.getOrganization())
+      .setParam("projects", String.join(",", request.getProjectKeys()));
+
+    call(post);
+  }
+
   public void updateKey(UpdateKeyWsRequest request) {
     PostRequest post = new PostRequest(path(ACTION_UPDATE_KEY))
       .setParam(PARAM_PROJECT_ID, request.getId())
index b329c02b7745e70e6aa279d1435f205ba727931e..8420491f422fabf30b8b2fcad6e12eb3a387383b 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonarqube.ws.client.project;
 
+import java.util.Arrays;
 import org.junit.Rule;
 import org.junit.Test;
 import org.sonarqube.ws.WsProjects;
@@ -130,6 +131,15 @@ public class ProjectsServiceTest {
     assertThat(serviceTester.getPostRequest().getParams()).containsOnly(entry("key", "project_key"));
   }
 
+  @Test
+  public void bulk_delete() {
+    BulkDeleteRequest request = BulkDeleteRequest.builder().setProjectKeys(Arrays.asList("p1", "p2")).setOrganization("my-org").build();
+    underTest.bulkDelete(request);
+
+    assertThat(serviceTester.getPostRequest().getPath()).isEqualTo("api/projects/bulk_delete");
+    assertThat(serviceTester.getPostRequest().getParams()).containsOnly(entry("organization", "my-org"), entry("projects", "p1,p2"));
+  }
+
   @Test
   public void search() {
     underTest.search(SearchWsRequest.builder()
index b48b1283b8a99d65a2f12ccc7df44f54e1d48392..f1e0fb726fc58393a96d0858f38ba2c8beac3672 100644 (file)
@@ -38,7 +38,7 @@ import org.sonarqube.tests.measure.SincePreviousVersionHistoryTest;
 import org.sonarqube.tests.measure.SinceXDaysHistoryTest;
 import org.sonarqube.tests.measure.TimeMachineTest;
 import org.sonarqube.tests.projectAdministration.BackgroundTasksTest;
-import org.sonarqube.tests.projectAdministration.BulkDeletionTest;
+import org.sonarqube.tests.projectAdministration.ProjectBulkDeletionPageTest;
 import org.sonarqube.tests.projectAdministration.ProjectAdministrationTest;
 import org.sonarqube.tests.projectAdministration.ProjectLinksPageTest;
 import org.sonarqube.tests.projectSearch.ProjectsPageTest;
@@ -65,7 +65,7 @@ import static util.ItUtils.xooPlugin;
   UsersPageTest.class,
   ProjectVisibilityTest.class,
   // project administration
-  BulkDeletionTest.class,
+  ProjectBulkDeletionPageTest.class,
   ProjectAdministrationTest.class,
   ProjectLinksPageTest.class,
   BackgroundTasksTest.class,
index eae3e358b8a80213c4eb04f04d017d5e83b4869a..cf02b5934158cfa2e9e960cc8788809eee0ac1fd 100644 (file)
@@ -20,6 +20,8 @@
 package org.sonarqube.tests;
 
 import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.util.NetworkUtils;
+import java.net.InetAddress;
 import org.junit.ClassRule;
 import org.junit.runner.RunWith;
 import org.junit.runners.Suite;
@@ -31,6 +33,8 @@ import org.sonarqube.tests.organization.OrganizationMembershipUiTest;
 import org.sonarqube.tests.organization.OrganizationTest;
 import org.sonarqube.tests.organization.PersonalOrganizationTest;
 import org.sonarqube.tests.organization.RootUserOnOrganizationTest;
+import org.sonarqube.tests.projectAdministration.ProjectDeletionTest;
+import org.sonarqube.tests.projectAdministration.ProjectProvisioningTest;
 import org.sonarqube.tests.projectSearch.LeakProjectsPageTest;
 import org.sonarqube.tests.projectSearch.SearchProjectsTest;
 import org.sonarqube.tests.qualityProfile.BuiltInQualityProfilesTest;
@@ -65,12 +69,22 @@ import static util.ItUtils.xooPlugin;
   IssueTagsTest.class,
   LeakProjectsPageTest.class,
   SearchProjectsTest.class,
-  RulesWsTest.class
+  RulesWsTest.class,
+  ProjectDeletionTest.class,
+  ProjectProvisioningTest.class
 })
 public class Category6Suite {
 
+  public static final int SEARCH_HTTP_PORT = NetworkUtils.getNextAvailablePort(InetAddress.getLoopbackAddress());
+
   @ClassRule
   public static final Orchestrator ORCHESTRATOR = Orchestrator.builderEnv()
+
+    // for ES resiliency tests
+    .setServerProperty("sonar.search.httpPort", "" + SEARCH_HTTP_PORT)
+    .setServerProperty("sonar.search.recovery.delayInMs", "1000")
+    .setServerProperty("sonar.search.recovery.minAgeInMs", "3000")
+
     .addPlugin(xooPlugin())
     .addPlugin(pluginArtifact("base-auth-plugin"))
     .addPlugin(pluginArtifact("fake-billing-plugin"))
diff --git a/tests/src/test/java/org/sonarqube/tests/Elasticsearch.java b/tests/src/test/java/org/sonarqube/tests/Elasticsearch.java
new file mode 100644 (file)
index 0000000..60df943
--- /dev/null
@@ -0,0 +1,51 @@
+package org.sonarqube.tests;
+
+import java.net.InetAddress;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Helper to directly access Elasticsearch. It requires the HTTP port
+ * to be open.
+ */
+public class Elasticsearch {
+
+  private final int httpPort;
+
+  Elasticsearch(int httpPort) {
+    this.httpPort = httpPort;
+  }
+
+  /**
+   * Forbid indexing requests on the specified index. Index becomes read-only.
+   */
+  public void lockWrites(String index) throws Exception {
+    putIndexSetting(httpPort, index, "blocks.write", "true");
+  }
+
+  /**
+   * Enable indexing requests on the specified index.
+   * @see #lockWrites(String)
+   */
+  public void unlockWrites(String index) throws Exception {
+    putIndexSetting(httpPort, index, "blocks.write", "false");
+  }
+
+  private void putIndexSetting(int searchHttpPort, String index, String key, String value) throws Exception {
+    Request.Builder request = new Request.Builder()
+      .url("http://" + InetAddress.getLoopbackAddress().getHostAddress() + ":" + searchHttpPort + "/" + index + "/_settings")
+      .put(RequestBody.create(MediaType.parse("application/json"), "{" +
+        "    \"index\" : {" +
+        "        \"" + key + "\" : \"" + value + "\"" +
+        "    }" +
+        "}"));
+    OkHttpClient okClient = new OkHttpClient.Builder().build();
+    Response response = okClient.newCall(request.build()).execute();
+    assertThat(response.isSuccessful()).isTrue();
+  }
+}
index f6a8f55162aba3db5a581fbf1a8b14b444a798d2..7ab41b836d556ffa2fb4c08176f9fb48fd0d9f76 100644 (file)
@@ -22,23 +22,24 @@ package org.sonarqube.tests;
 import com.sonar.orchestrator.Orchestrator;
 import javax.annotation.Nullable;
 import org.junit.rules.ExternalResource;
-import org.sonarqube.ws.client.WsClient;
 import org.sonarqube.pageobjects.Navigation;
+import org.sonarqube.ws.client.WsClient;
 import util.selenium.Selenese;
 
+import static java.util.Objects.requireNonNull;
 import static util.ItUtils.newUserWsClient;
 
 /**
  * This JUnit rule wraps an {@link Orchestrator} instance and provides :
  * <ul>
- *   <li>enabling the organization feature by default</li>
- *   <li>clean-up of organizations between tests</li>
- *   <li>clean-up of users between tests</li>
- *   <li>clean-up of session when opening a browser (cookies, local storage)</li>
- *   <li>quick access to {@link WsClient} instances</li>
- *   <li>helpers to generate organizations and users</li>
+ * <li>enabling the organization feature by default</li>
+ * <li>clean-up of organizations between tests</li>
+ * <li>clean-up of users between tests</li>
+ * <li>clean-up of session when opening a browser (cookies, local storage)</li>
+ * <li>quick access to {@link WsClient} instances</li>
+ * <li>helpers to generate organizations and users</li>
  * </ul>
- *
+ * <p>
  * Recommendation is to define a {@code @Rule} instance. If not possible, then
  * {@code @ClassRule} must be used through a {@link org.junit.rules.RuleChain}
  * around {@link Orchestrator}.
@@ -49,6 +50,7 @@ public class Tester extends ExternalResource implements Session {
 
   // configuration before startup
   private boolean disableOrganizations = false;
+  private Elasticsearch elasticsearch = null;
 
   // initialized in #before()
   private boolean beforeCalled = false;
@@ -64,6 +66,18 @@ public class Tester extends ExternalResource implements Session {
     return this;
   }
 
+  /**
+   * Enables Elasticsearch debugging, see {@link #elasticsearch()}.
+   *
+   * The property "sonar.search.httpPort" must be defined before
+   * starting SonarQube server.
+   */
+  public Tester setElasticsearchHttpPort(int port) {
+    verifyNotStarted();
+    elasticsearch = new Elasticsearch(port);
+    return this;
+  }
+
   @Override
   protected void before() {
     verifyNotStarted();
@@ -98,6 +112,10 @@ public class Tester extends ExternalResource implements Session {
     return new SessionImpl(orchestrator, login, password);
   }
 
+  public Elasticsearch elasticsearch() {
+    return requireNonNull(elasticsearch, "Elasticsearch HTTP port is not defined. See #setElasticsearchHttpPort()");
+  }
+
   /**
    * Open a new browser session. Cookies are deleted.
    */
diff --git a/tests/src/test/java/org/sonarqube/tests/projectAdministration/BulkDeletionTest.java b/tests/src/test/java/org/sonarqube/tests/projectAdministration/BulkDeletionTest.java
deleted file mode 100644 (file)
index e31020a..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonarqube.tests.projectAdministration;
-
-import com.sonar.orchestrator.Orchestrator;
-import com.sonar.orchestrator.build.SonarScanner;
-import org.sonarqube.tests.Category1Suite;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.ClassRule;
-import org.junit.Rule;
-import org.junit.Test;
-import util.user.UserRule;
-
-import static util.ItUtils.projectDir;
-import static util.selenium.Selenese.runSelenese;
-
-public class BulkDeletionTest {
-
-  private static final String ADMIN_USER_LOGIN = "admin-user";
-
-  @ClassRule
-  public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
-
-  @Rule
-  public UserRule userRule = UserRule.from(orchestrator);
-
-  @Before
-  public void deleteData() {
-    orchestrator.resetData();
-    userRule.createAdminUser(ADMIN_USER_LOGIN, ADMIN_USER_LOGIN);
-  }
-
-  @After
-  public void deleteAdminUser() {
-    userRule.resetUsers();
-  }
-
-  /**
-   * SONAR-2614, SONAR-3805
-   */
-  @Test
-  public void test_bulk_deletion_on_selected_projects() throws Exception {
-    // we must have several projects to test the bulk deletion
-    executeBuild("cameleon-1", "Sample-Project");
-    executeBuild("cameleon-2", "Foo-Application");
-    executeBuild("cameleon-3", "Bar-Sonar-Plugin");
-
-    runSelenese(orchestrator, "/projectAdministration/BulkDeletionTest/bulk-delete-filter-projects.html");
-  }
-
-  private void executeBuild(String projectKey, String projectName) {
-    orchestrator.executeBuild(
-      SonarScanner.create(projectDir("shared/xoo-sample"))
-        .setProjectKey(projectKey)
-        .setProjectName(projectName));
-  }
-
-}
diff --git a/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectBulkDeletionPageTest.java b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectBulkDeletionPageTest.java
new file mode 100644 (file)
index 0000000..c0cc2a7
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonarqube.tests.projectAdministration;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.sonarqube.tests.Category1Suite;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import util.user.UserRule;
+
+import static util.ItUtils.projectDir;
+import static util.selenium.Selenese.runSelenese;
+
+public class ProjectBulkDeletionPageTest {
+
+  private static final String ADMIN_USER_LOGIN = "admin-user";
+
+  @ClassRule
+  public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
+
+  @Rule
+  public UserRule userRule = UserRule.from(orchestrator);
+
+  @Before
+  public void deleteData() {
+    orchestrator.resetData();
+    userRule.createAdminUser(ADMIN_USER_LOGIN, ADMIN_USER_LOGIN);
+  }
+
+  @After
+  public void deleteAdminUser() {
+    userRule.resetUsers();
+  }
+
+  /**
+   * SONAR-2614, SONAR-3805
+   */
+  @Test
+  public void test_bulk_deletion_on_selected_projects() throws Exception {
+    // we must have several projects to test the bulk deletion
+    executeBuild("cameleon-1", "Sample-Project");
+    executeBuild("cameleon-2", "Foo-Application");
+    executeBuild("cameleon-3", "Bar-Sonar-Plugin");
+
+    runSelenese(orchestrator, "/projectAdministration/ProjectBulkDeletionPageTest/bulk-delete-filter-projects.html");
+  }
+
+  private void executeBuild(String projectKey, String projectName) {
+    orchestrator.executeBuild(
+      SonarScanner.create(projectDir("shared/xoo-sample"))
+        .setProjectKey(projectKey)
+        .setProjectName(projectName));
+  }
+
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectDeletionTest.java b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectDeletionTest.java
new file mode 100644 (file)
index 0000000..d9ea32b
--- /dev/null
@@ -0,0 +1,197 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonarqube.tests.projectAdministration;
+
+import com.sonar.orchestrator.Orchestrator;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.DisableOnDebug;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+import org.sonarqube.tests.Category6Suite;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.Organizations;
+import org.sonarqube.ws.WsComponents;
+import org.sonarqube.ws.WsProjects;
+import org.sonarqube.ws.WsProjects.CreateWsResponse.Project;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.WsResponse;
+import org.sonarqube.ws.client.component.SearchProjectsRequest;
+import org.sonarqube.ws.client.project.BulkDeleteRequest;
+import org.sonarqube.ws.client.project.CreateRequest;
+import org.sonarqube.ws.client.project.DeleteRequest;
+import org.sonarqube.ws.client.project.SearchWsRequest;
+import util.ItUtils;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ProjectDeletionTest {
+
+  @ClassRule
+  public static final Orchestrator orchestrator = Category6Suite.ORCHESTRATOR;
+
+  @Rule
+  public TestRule safeguard = new DisableOnDebug(Timeout.seconds(300));
+  @Rule
+  public Tester tester = new Tester(orchestrator)
+    .setElasticsearchHttpPort(Category6Suite.SEARCH_HTTP_PORT);
+
+  @Test
+  public void deletion_removes_project_from_search_engines() {
+    Organizations.Organization organization = tester.organizations().generate();
+    Project project1 = createProject(organization, "one", "Foo");
+    Project project2 = createProject(organization, "two", "Bar");
+    assertThatProjectIsSearchable(organization, "Foo");
+    assertThatProjectIsSearchable(organization, "Bar");
+
+    deleteProject(project1);
+    assertThatProjectIsNotSearchable(organization, project1.getName());
+    assertThatProjectIsSearchable(organization, project2.getName());
+
+    deleteProject(project2);
+    assertThatProjectIsNotSearchable(organization, project1.getName());
+    assertThatProjectIsNotSearchable(organization, project2.getName());
+  }
+
+  @Test
+  public void indexing_errors_are_recovered_asynchronously_when_deleting_project() throws Exception {
+    Organizations.Organization organization = tester.organizations().generate();
+    Project project = createProject(organization, "one", "Foo");
+
+    tester.elasticsearch().lockWrites("components");
+    tester.elasticsearch().lockWrites("projectmeasures");
+    deleteProject(project);
+    // WS reloads from database the results returned by Elasticsearch. That's
+    // why the project does not appear in search engine.
+    // However this test is still useful to verify that WS do not
+    // fail during this Elasticsearch inconsistency.
+    assertThatProjectIsNotSearchable(organization, project.getName());
+
+    tester.elasticsearch().unlockWrites("components");
+    tester.elasticsearch().unlockWrites("projectmeasures");
+    // TODO verify that recovery daemon successfully updated indices
+  }
+
+  @Test
+  public void bulk_deletion_removes_projects_from_search_engines() {
+    Organizations.Organization organization = tester.organizations().generate();
+    Project project1 = createProject(organization, "one", "Foo");
+    Project project2 = createProject(organization, "two", "Bar");
+    Project project3 = createProject(organization, "three", "Baz");
+
+    bulkDeleteProjects(organization, project1, project3);
+    assertThatProjectIsNotSearchable(organization, project1.getName());
+    assertThatProjectIsSearchable(organization, project2.getName());
+    assertThatProjectIsNotSearchable(organization, project3.getName());
+  }
+
+  @Test
+  public void indexing_errors_are_recovered_asynchronously_when_bulk_deleting_projects() throws Exception {
+    Organizations.Organization organization = tester.organizations().generate();
+    Project project1 = createProject(organization, "one", "Foo");
+    Project project2 = createProject(organization, "two", "Bar");
+    Project project3 = createProject(organization, "three", "Baz");
+
+    tester.elasticsearch().lockWrites("components");
+    tester.elasticsearch().lockWrites("projectmeasures");
+    bulkDeleteProjects(organization, project1, project3);
+
+    // WS reloads from database the results returned by Elasticsearch. That's
+    // why the project does not appear in search engine.
+    // However this test is still useful to verify that WS do not
+    // fail during this Elasticsearch inconsistency.
+    assertThatProjectIsNotSearchable(organization, project1.getName());
+    assertThatProjectIsSearchable(organization, project2.getName());
+    assertThatProjectIsNotSearchable(organization, project3.getName());
+
+    tester.elasticsearch().unlockWrites("components");
+    tester.elasticsearch().unlockWrites("projectmeasures");
+    // TODO verify that recovery daemon successfully updated indices
+  }
+
+  private void deleteProject(Project project) {
+    tester.wsClient().projects().delete(DeleteRequest.builder().setKey(project.getKey()).build());
+  }
+
+  private void bulkDeleteProjects(Organizations.Organization organization, Project... projects) {
+    BulkDeleteRequest request = BulkDeleteRequest.builder()
+      .setOrganization(organization.getKey())
+      .setProjectKeys(Arrays.stream(projects).map(Project::getKey).collect(Collectors.toList()))
+      .build();
+    tester.wsClient().projects().bulkDelete(request);
+  }
+
+  private Project createProject(Organizations.Organization organization, String key, String name) {
+    CreateRequest createRequest = CreateRequest.builder().setKey(key).setName(name).setOrganization(organization.getKey()).build();
+    return tester.wsClient().projects().create(createRequest).getProject();
+  }
+
+  private void assertThatProjectIsSearchable(Organizations.Organization organization, String name) {
+    assertThat(isInProjectsSearch(organization, name)).isTrue();
+    assertThat(isInComponentSearchProjects(name)).isTrue();
+    assertThat(isInComponentSuggestions(name)).isTrue();
+  }
+
+  private void assertThatProjectIsNotSearchable(Organizations.Organization organization, String name) {
+    assertThat(isInProjectsSearch(organization, name)).isFalse();
+    assertThat(isInComponentSearchProjects(name)).isFalse();
+    assertThat(isInComponentSuggestions(name)).isFalse();
+  }
+
+  /**
+   * Projects administration page - uses database
+   */
+  private boolean isInProjectsSearch(Organizations.Organization organization, String name) {
+    WsProjects.SearchWsResponse response = tester.wsClient().projects().search(
+      SearchWsRequest.builder().setOrganization(organization.getKey()).setQuery(name).setQualifiers(singletonList("TRK")).build());
+    return response.getComponentsCount() > 0;
+  }
+
+  /**
+   * Projects page - api/components/search_projects - uses ES + DB
+   */
+  private boolean isInComponentSearchProjects(String name) {
+    WsComponents.SearchProjectsWsResponse response = tester.wsClient().components().searchProjects(
+      SearchProjectsRequest.builder().setFilter("query=\"" + name + "\"").build());
+    return response.getComponentsCount() > 0;
+  }
+
+  /**
+   * Top-right search engine - api/components/suggestions - uses ES + DB
+   */
+  private boolean isInComponentSuggestions(String name) {
+    GetRequest request = new GetRequest("api/components/suggestions").setParam("s", name);
+    WsResponse response = tester.wsClient().wsConnector().call(request);
+    Map<String, Object> json = ItUtils.jsonToMap(response.content());
+    Collection<Map<String, Object>> results = (Collection<Map<String, Object>>) json.get("results");
+    Collection items = results.stream()
+      .filter(map -> "TRK".equals(map.get("q")))
+      .map(map -> (Collection) map.get("items"))
+      .findFirst()
+      .orElseThrow(() -> new IllegalStateException("missing field results/[q=TRK]"));
+    return !items.isEmpty();
+  }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectProvisioningTest.java b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectProvisioningTest.java
new file mode 100644 (file)
index 0000000..1ff7a8f
--- /dev/null
@@ -0,0 +1,133 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonarqube.tests.projectAdministration;
+
+import com.sonar.orchestrator.Orchestrator;
+import java.util.Collection;
+import java.util.Map;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.DisableOnDebug;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+import org.sonarqube.tests.Category6Suite;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.Organizations;
+import org.sonarqube.ws.WsComponents;
+import org.sonarqube.ws.WsProjects;
+import org.sonarqube.ws.WsProjects.CreateWsResponse.Project;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.WsResponse;
+import org.sonarqube.ws.client.component.SearchProjectsRequest;
+import org.sonarqube.ws.client.project.CreateRequest;
+import org.sonarqube.ws.client.project.SearchWsRequest;
+import util.ItUtils;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ProjectProvisioningTest {
+
+  @ClassRule
+  public static final Orchestrator orchestrator = Category6Suite.ORCHESTRATOR;
+
+  @Rule
+  public TestRule safeguard = new DisableOnDebug(Timeout.seconds(300));
+  @Rule
+  public Tester tester = new Tester(orchestrator)
+    .setElasticsearchHttpPort(Category6Suite.SEARCH_HTTP_PORT);
+
+  @Test
+  public void provisioned_project_is_available_in_search_engines() {
+    Organizations.Organization organization = tester.organizations().generate();
+
+    createProject(organization, "one", "Foo");
+
+    assertThat(isInProjectsSearch(organization, "Foo")).isTrue();
+    assertThat(isInComponentSearchProjects("Foo")).isTrue();
+    assertThat(isInComponentSuggestions("Foo")).isTrue();
+  }
+
+  @Test
+  public void indexing_errors_are_recovered_asynchronously_when_provisioning_project() throws Exception {
+    tester.elasticsearch().lockWrites("components");
+    tester.elasticsearch().lockWrites("projectmeasures");
+
+    Organizations.Organization organization = tester.organizations().generate();
+    Project project = createProject(organization, "one", "Foo");
+
+    // no ES requests but only DB
+    assertThat(isInProjectsSearch(organization, project.getName())).isTrue();
+
+    // these WS use ES so they are temporarily inconsistent
+    assertThat(isInComponentSearchProjects(project.getName())).isFalse();
+    assertThat(isInComponentSuggestions(project.getName())).isFalse();
+
+    tester.elasticsearch().unlockWrites("components");
+    tester.elasticsearch().unlockWrites("projectmeasures");
+
+    boolean found = false;
+    while (!found) {
+      // recovery daemon runs every second (see Category6Suite)
+      Thread.sleep(1_000L);
+      found = isInComponentSearchProjects(project.getName()) && isInComponentSuggestions(project.getName());
+    }
+  }
+
+  private Project createProject(Organizations.Organization organization, String key, String name) {
+    CreateRequest createRequest = CreateRequest.builder().setKey(key).setName(name).setOrganization(organization.getKey()).build();
+    return tester.wsClient().projects().create(createRequest).getProject();
+  }
+
+  /**
+   * Projects administration page - uses database
+   */
+  private boolean isInProjectsSearch(Organizations.Organization organization, String name) {
+    WsProjects.SearchWsResponse response = tester.wsClient().projects().search(
+      SearchWsRequest.builder().setOrganization(organization.getKey()).setQuery(name).setQualifiers(singletonList("TRK")).build());
+    return response.getComponentsCount() > 0;
+  }
+
+  /**
+   * Projects page - api/components/search_projects - uses ES + DB
+   */
+  private boolean isInComponentSearchProjects(String name) {
+    WsComponents.SearchProjectsWsResponse response = tester.wsClient().components().searchProjects(
+      SearchProjectsRequest.builder().setFilter("query=\"" + name + "\"").build());
+    return response.getComponentsCount() > 0;
+  }
+
+  /**
+   * Top-right search engine - api/components/suggestions - uses ES + DB
+   */
+  private boolean isInComponentSuggestions(String name) {
+    GetRequest request = new GetRequest("api/components/suggestions").setParam("s", name);
+    WsResponse response = tester.wsClient().wsConnector().call(request);
+    Map<String, Object> json = ItUtils.jsonToMap(response.content());
+    Collection<Map<String, Object>> results = (Collection<Map<String, Object>>) json.get("results");
+    Collection items = results.stream()
+      .filter(map -> "TRK".equals(map.get("q")))
+      .map(map -> (Collection) map.get("items"))
+      .findFirst()
+      .orElseThrow(() -> new IllegalStateException("missing field results/[q=TRK]"));
+    return !items.isEmpty();
+  }
+}
diff --git a/tests/src/test/resources/projectAdministration/BulkDeletionTest/bulk-delete-filter-projects.html b/tests/src/test/resources/projectAdministration/BulkDeletionTest/bulk-delete-filter-projects.html
deleted file mode 100644 (file)
index b6256e4..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head profile="http://selenium-ide.openqa.org/profiles/test-case">
-    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
-    <title>bulk-delete-filter-projects</title>
-</head>
-<body>
-<table cellpadding="1" cellspacing="1" border="1">
-    <tbody>
-    <tr>
-       <td>open</td>
-       <td>/sessions/logout</td>
-       <td></td>
-</tr>
-<tr>
-       <td>open</td>
-       <td>/sessions/login</td>
-       <td></td>
-</tr>
-<tr>
-       <td>type</td>
-       <td>login</td>
-       <td>admin-user</td>
-</tr>
-<tr>
-       <td>type</td>
-       <td>password</td>
-       <td>admin-user</td>
-</tr>
-<tr>
-       <td>clickAndWait</td>
-       <td>commit</td>
-       <td></td>
-</tr>
-<tr>
-       <td>waitForElementPresent</td>
-       <td>css=.js-user-authenticated</td>
-       <td></td>
-</tr>
-<tr>
-       <td>open</td>
-       <td>/projects_admin</td>
-       <td></td>
-</tr>
-<tr>
-       <td>waitForText</td>
-       <td>content</td>
-       <td>*Bar-Sonar-Plugin*Foo-Application*Sample-Project*</td>
-</tr>
-<tr>
-       <td>type</td>
-       <td>css=.search-box-input</td>
-       <td>s</td>
-</tr>
-<tr>
-       <td>click</td>
-       <td>css=.search-box-submit</td>
-       <td></td>
-</tr>
-<tr>
-       <td>waitForText</td>
-       <td>content</td>
-       <td>*Bar-Sonar-Plugin*Sample-Project*</td>
-</tr>
-<tr>
-       <td>waitForText</td>
-       <td>content</td>
-       <td>*cameleon-3*cameleon-1*</td>
-</tr>
-<tr>
-       <td>assertTextNotPresent</td>
-       <td>content</td>
-       <td>*Foo-Application*</td>
-</tr>
-</tbody>
-</table>
-</body>
-</html>
diff --git a/tests/src/test/resources/projectAdministration/ProjectBulkDeletionPageTest/bulk-delete-filter-projects.html b/tests/src/test/resources/projectAdministration/ProjectBulkDeletionPageTest/bulk-delete-filter-projects.html
new file mode 100644 (file)
index 0000000..b6256e4
--- /dev/null
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+    <title>bulk-delete-filter-projects</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+    <tbody>
+    <tr>
+       <td>open</td>
+       <td>/sessions/logout</td>
+       <td></td>
+</tr>
+<tr>
+       <td>open</td>
+       <td>/sessions/login</td>
+       <td></td>
+</tr>
+<tr>
+       <td>type</td>
+       <td>login</td>
+       <td>admin-user</td>
+</tr>
+<tr>
+       <td>type</td>
+       <td>password</td>
+       <td>admin-user</td>
+</tr>
+<tr>
+       <td>clickAndWait</td>
+       <td>commit</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForElementPresent</td>
+       <td>css=.js-user-authenticated</td>
+       <td></td>
+</tr>
+<tr>
+       <td>open</td>
+       <td>/projects_admin</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForText</td>
+       <td>content</td>
+       <td>*Bar-Sonar-Plugin*Foo-Application*Sample-Project*</td>
+</tr>
+<tr>
+       <td>type</td>
+       <td>css=.search-box-input</td>
+       <td>s</td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>css=.search-box-submit</td>
+       <td></td>
+</tr>
+<tr>
+       <td>waitForText</td>
+       <td>content</td>
+       <td>*Bar-Sonar-Plugin*Sample-Project*</td>
+</tr>
+<tr>
+       <td>waitForText</td>
+       <td>content</td>
+       <td>*cameleon-3*cameleon-1*</td>
+</tr>
+<tr>
+       <td>assertTextNotPresent</td>
+       <td>content</td>
+       <td>*Foo-Application*</td>
+</tr>
+</tbody>
+</table>
+</body>
+</html>