diff options
author | Eric Hartmann <hartmann.eric@gmail.com> | 2017-07-12 06:52:41 +0200 |
---|---|---|
committer | Simon Brandhof <simon.brandhof@sonarsource.com> | 2017-07-22 00:31:15 +0200 |
commit | 329a3c594a5f5b39858d3f587249c8d6accb072f (patch) | |
tree | 1798cdb5b34b4fbb387f2f3d83b0909752a472e4 /server | |
parent | fa6d3bdff62c8c12ea3c910b871b6eb0461752b2 (diff) | |
download | sonarqube-329a3c594a5f5b39858d3f587249c8d6accb072f.tar.gz sonarqube-329a3c594a5f5b39858d3f587249c8d6accb072f.zip |
SONAR-9514 SONAR-9516 SONAR-9517 ES resilience from POST WS
Diffstat (limited to 'server')
103 files changed, 2832 insertions, 1561 deletions
diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java index 59e18e5f049..eae580c3a3e 100644 --- a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java +++ b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java @@ -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, diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java index ad62ace71c2..17eee92bbe0 100644 --- a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java +++ b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java @@ -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 diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDao.java index f23f1fe91a8..7caca7706ba 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDao.java @@ -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); } /** diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentMapper.java index 0cfc494d2dd..c476910b2ed 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentMapper.java @@ -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); diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/es/EsQueueDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/es/EsQueueDto.java index 63751bf5de5..b0449159aef 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/es/EsQueueDto.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/es/EsQueueDto.java @@ -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); } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java index 94c1c6b072b..40d148ec741 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java @@ -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) { diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java index 77355540661..d08d71999ab 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java @@ -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, diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/component/ComponentMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/component/ComponentMapper.xml index eb361e3a45d..f9a99818714 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/component/ComponentMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/component/ComponentMapper.xml @@ -417,7 +417,7 @@ </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 diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml index 5211809d2b8..744482c6cb1 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml @@ -65,6 +65,38 @@ </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, @@ -150,7 +182,7 @@ 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 diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDaoTest.java index ee6b738ce57..c9c83436a8e 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDaoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDaoTest.java @@ -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); } diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentTesting.java b/server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentTesting.java index f15288642bc..3445b4778b4 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentTesting.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentTesting.java @@ -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) diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/es/EsQueueDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/es/EsQueueDaoTest.java index 373a88b2013..6a5cf1531f6 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/es/EsQueueDaoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/es/EsQueueDaoTest.java @@ -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)) diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ComponentCleanerService.java b/server/sonar-server/src/main/java/org/sonar/server/component/ComponentCleanerService.java index 7dd68335f9a..33d357e3a75 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/ComponentCleanerService.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ComponentCleanerService.java @@ -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) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ComponentService.java b/server/sonar-server/src/main/java/org/sonar/server/component/ComponentService.java index a3ad4ebd213..1cf8b58feff 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/ComponentService.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ComponentService.java @@ -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) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ComponentUpdater.java b/server/sonar-server/src/main/java/org/sonar/server/component/ComponentUpdater.java index 759033e7f20..115a7e28f4a 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/ComponentUpdater.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ComponentUpdater.java @@ -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)); - } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexer.java index 2f02c9871fc..cce63e7e2c7 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexer.java @@ -19,31 +19,39 @@ */ 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) diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ws/SuggestionsAction.java b/server/sonar-server/src/main/java/org/sonar/server/component/ws/SuggestionsAction.java index ced1b25dda1..00df270a275 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/ws/SuggestionsAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ws/SuggestionsAction.java @@ -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) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/BaseIssuesLoader.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/BaseIssuesLoader.java index 93404c0b81e..eef3eea4d0b 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/BaseIssuesLoader.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/BaseIssuesLoader.java @@ -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 diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/IndexAnalysisStep.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/IndexAnalysisStep.java index 39f9631c3b7..bc2ec1621cd 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/IndexAnalysisStep.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/IndexAnalysisStep.java @@ -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); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/BulkIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/es/BulkIndexer.java index ee167116e17..a844a5f1d59 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/es/BulkIndexer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/es/BulkIndexer.java @@ -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 index 00000000000..98365a1c64e --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/es/DocId.java @@ -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; + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/EsUtils.java b/server/sonar-server/src/main/java/org/sonar/server/es/EsUtils.java index 529329e74e4..7fe8e4205bf 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/es/EsUtils.java +++ b/server/sonar-server/src/main/java/org/sonar/server/es/EsUtils.java @@ -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); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/IndexType.java b/server/sonar-server/src/main/java/org/sonar/server/es/IndexType.java index 4ead86465ef..01805216250 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/es/IndexType.java +++ b/server/sonar-server/src/main/java/org/sonar/server/es/IndexType.java @@ -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 diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/IndexerStartupTask.java b/server/sonar-server/src/main/java/org/sonar/server/es/IndexerStartupTask.java index 717f290d8be..c30695280bc 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/es/IndexerStartupTask.java +++ b/server/sonar-server/src/main/java/org/sonar/server/es/IndexerStartupTask.java @@ -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); diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/IndexingListener.java b/server/sonar-server/src/main/java/org/sonar/server/es/IndexingListener.java index 639931780bd..ce74bd9cdde 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/es/IndexingListener.java +++ b/server/sonar-server/src/main/java/org/sonar/server/es/IndexingListener.java @@ -19,13 +19,23 @@ */ 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 + } + }; } diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/IndexingResult.java b/server/sonar-server/src/main/java/org/sonar/server/es/IndexingResult.java index e66d842ed4b..b178ddc5077 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/es/IndexingResult.java +++ b/server/sonar-server/src/main/java/org/sonar/server/es/IndexingResult.java @@ -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 index 00000000000..f777222fba1 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/es/OneToManyResilientIndexingListener.java @@ -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/ResiliencyIndexingListener.java b/server/sonar-server/src/main/java/org/sonar/server/es/OneToOneResilientIndexingListener.java index cc29e14a9f1..be7ffddb65d 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/es/ResiliencyIndexingListener.java +++ b/server/sonar-server/src/main/java/org/sonar/server/es/OneToOneResilientIndexingListener.java @@ -21,6 +21,7 @@ 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; @@ -29,35 +30,43 @@ import org.sonar.db.DbSession; import org.sonar.db.es.EsQueueDto; /** - * Clean-up the db table es_queue when documents + * 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 ResiliencyIndexingListener implements IndexingListener { +public class OneToOneResilientIndexingListener implements IndexingListener { private final DbClient dbClient; private final DbSession dbSession; - private final Collection<EsQueueDto> items; + private final Multimap<DocId, EsQueueDto> itemsById; - public ResiliencyIndexingListener(DbClient dbClient, DbSession dbSession, Collection<EsQueueDto> items) { + public OneToOneResilientIndexingListener(DbClient dbClient, DbSession dbSession, Collection<EsQueueDto> items) { this.dbClient = dbClient; this.dbSession = dbSession; - this.items = items; + this.itemsById = items.stream() + .collect(MoreCollectors.index(i -> new DocId(IndexType.parse(i.getDocType()), i.getDocId()), Function.identity())); } @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() + 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(docIds.size())); + .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()) + } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/ProjectIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/es/ProjectIndexer.java index 0c4a11fd9fc..c36c7a5076e 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/es/ProjectIndexer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/es/ProjectIndexer.java @@ -19,6 +19,10 @@ */ 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 index 00000000000..51a9b34bd76 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/es/ProjectIndexers.java @@ -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 index 00000000000..d26ba078d9c --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/es/ProjectIndexersImpl.java @@ -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)); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/RecoveryIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/es/RecoveryIndexer.java index 709f652e88c..3cb27312679 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/es/RecoveryIndexer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/es/RecoveryIndexer.java @@ -20,9 +20,12 @@ 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/ResilientIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/es/ResilientIndexer.java index b717c831e09..a54e4317f94 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/es/ResilientIndexer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/es/ResilientIndexer.java @@ -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 diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/IssueStorage.java b/server/sonar-server/src/main/java/org/sonar/server/issue/IssueStorage.java index 2d039842c3c..3097b673276 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/IssueStorage.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/IssueStorage.java @@ -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 } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ServerIssueStorage.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ServerIssueStorage.java index 6e0fe687338..74ca0c4448f 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ServerIssueStorage.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ServerIssueStorage.java @@ -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) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueDoc.java b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueDoc.java index 3a93ccd41ab..eef37179e5b 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueDoc.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueDoc.java @@ -272,4 +272,5 @@ public class IssueDoc extends BaseDoc { setField(IssueIndexDefinition.FIELD_ISSUE_ORGANIZATION_UUID, s); return this; } + } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndexer.java index 5dbf88356bb..45d2c4708ba 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndexer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndexer.java @@ -19,42 +19,63 @@ */ 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); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexer.java index 84d52030769..fbb1cbb6822 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexer.java @@ -20,30 +20,38 @@ 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) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/permission/PermissionTemplateService.java b/server/sonar-server/src/main/java/org/sonar/server/permission/PermissionTemplateService.java index 875d1b0a93a..82735ce115f 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/permission/PermissionTemplateService.java +++ b/server/sonar-server/src/main/java/org/sonar/server/permission/PermissionTemplateService.java @@ -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()); diff --git a/server/sonar-server/src/main/java/org/sonar/server/permission/PermissionUpdater.java b/server/sonar-server/src/main/java/org/sonar/server/permission/PermissionUpdater.java index 52d76459fec..9238fd5e36e 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/permission/PermissionUpdater.java +++ b/server/sonar-server/src/main/java/org/sonar/server/permission/PermissionUpdater.java @@ -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) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/permission/index/AuthorizationTypeSupport.java b/server/sonar-server/src/main/java/org/sonar/server/permission/index/AuthorizationTypeSupport.java index 749bd51ba09..90f7289ab37 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/permission/index/AuthorizationTypeSupport.java +++ b/server/sonar-server/src/main/java/org/sonar/server/permission/index/AuthorizationTypeSupport.java @@ -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"; diff --git a/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java index 8c7ad3e6693..078fe2f1091 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java @@ -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); + } + } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexerDao.java b/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexerDao.java index 55d575a8269..a854f9caaca 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexerDao.java +++ b/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexerDao.java @@ -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)); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/permission/ws/template/ApplyTemplateAction.java b/server/sonar-server/src/main/java/org/sonar/server/permission/ws/template/ApplyTemplateAction.java index 747a427312f..65f57f7ccaf 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/permission/ws/template/ApplyTemplateAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/permission/ws/template/ApplyTemplateAction.java @@ -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)); } } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/permission/ws/template/BulkApplyTemplateAction.java b/server/sonar-server/src/main/java/org/sonar/server/permission/ws/template/BulkApplyTemplateAction.java index 27fbaf744d9..e56b103db7b 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/permission/ws/template/BulkApplyTemplateAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/permission/ws/template/BulkApplyTemplateAction.java @@ -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); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/BackendCleanup.java b/server/sonar-server/src/main/java/org/sonar/server/platform/BackendCleanup.java index d465a9a8ba5..a508d4de517 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/BackendCleanup.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/BackendCleanup.java @@ -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 diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index 8145619988e..5a8c8ca0a44 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -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); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/project/ws/UpdateVisibilityAction.java b/server/sonar-server/src/main/java/org/sonar/server/project/ws/UpdateVisibilityAction.java index 6e117741622..990839e226c 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/project/ws/UpdateVisibilityAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/project/ws/UpdateVisibilityAction.java @@ -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); } } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/projecttag/ws/SetAction.java b/server/sonar-server/src/main/java/org/sonar/server/projecttag/ws/SetAction.java index 8fdffb9b20b..71fab7c6114 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/projecttag/ws/SetAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/projecttag/ws/SetAction.java @@ -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(); diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/index/ActiveRuleIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/index/ActiveRuleIndexer.java index 5f768e0f88b..1a4610573a5 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/index/ActiveRuleIndexer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/index/ActiveRuleIndexer.java @@ -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); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/rule/index/RuleIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/rule/index/RuleIndexer.java index edb24020163..c6ceb47cb8d 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/rule/index/RuleIndexer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/rule/index/RuleIndexer.java @@ -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()); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/test/index/TestIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/test/index/TestIndexer.java index 30919221b0a..ebd02c87a23 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/test/index/TestIndexer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/test/index/TestIndexer.java @@ -19,30 +19,39 @@ */ 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; @@ -53,73 +62,106 @@ public class TestIndexer implements ProjectIndexer, StartupIndexer { } @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); } @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); + } } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndexer.java index 7cead64cb48..50d43af95af 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndexer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndexer.java @@ -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()); diff --git a/server/sonar-server/src/main/java/org/sonar/server/view/index/ViewIndex.java b/server/sonar-server/src/main/java/org/sonar/server/view/index/ViewIndex.java index de300419b2f..4b7dc2f4936 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/view/index/ViewIndex.java +++ b/server/sonar-server/src/main/java/org/sonar/server/view/index/ViewIndex.java @@ -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); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/view/index/ViewIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/view/index/ViewIndexer.java index 2c92637976c..313599b80ea 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/view/index/ViewIndexer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/view/index/ViewIndexer.java @@ -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(); diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ComponentCleanerServiceTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ComponentCleanerServiceTest.java index a2fb921d485..1a53c05b7ed 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ComponentCleanerServiceTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ComponentCleanerServiceTest.java @@ -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) { diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceTest.java index fd7b5fcabae..b8928a8419f 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceTest.java @@ -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() { diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceUpdateKeyTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceUpdateKeyTest.java index 6b2e0933d7c..b4208705488 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceUpdateKeyTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceUpdateKeyTest.java @@ -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 diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ComponentUpdaterTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ComponentUpdaterTest.java index fae662acb0a..868e7bf7066 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ComponentUpdaterTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ComponentUpdaterTest.java @@ -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(); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexerTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexerTest.java index cf69abaaf33..959ec117e9e 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexerTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexerTest.java @@ -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()); - } - } diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java index 7afbb927c5d..2ff7aa96d7e 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java @@ -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; } diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ws/SuggestionsActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ws/SuggestionsActionTest.java index 4f354df9bd1..c0c6518f4ce 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ws/SuggestionsActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ws/SuggestionsActionTest.java @@ -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() diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/IndexAnalysisStepTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/IndexAnalysisStepTest.java index 16b792b4174..46bf97e6d7e 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/IndexAnalysisStepTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/IndexAnalysisStepTest.java @@ -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 diff --git a/server/sonar-server/src/test/java/org/sonar/server/es/BulkIndexerTest.java b/server/sonar-server/src/test/java/org/sonar/server/es/BulkIndexerTest.java index 8848ae6d5e3..7f63c806a08 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/es/BulkIndexerTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/es/BulkIndexerTest.java @@ -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)); + } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/es/EsTester.java b/server/sonar-server/src/test/java/org/sonar/server/es/EsTester.java index 3ae2027cad8..56fd0b9d5b5 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/es/EsTester.java +++ b/server/sonar-server/src/test/java/org/sonar/server/es/EsTester.java @@ -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 index 00000000000..4e8e71c7cad --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/es/IndexTypeTest.java @@ -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()); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/es/IndexingResultTest.java b/server/sonar-server/src/test/java/org/sonar/server/es/IndexingResultTest.java index 2eb2c4ded86..39fa5a4d3f6 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/es/IndexingResultTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/es/IndexingResultTest.java @@ -26,9 +26,20 @@ 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(); 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,7 +61,7 @@ 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(); } @@ -58,12 +69,14 @@ public class IndexingResultTest { 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 index 00000000000..cff8f286f2a --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/es/OneToManyResilientIndexingListenerTest.java @@ -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 index 00000000000..4acf7decbb3 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/es/OneToOneResilientIndexingListenerTest.java @@ -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 index 00000000000..2d956778031 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/es/ProjectIndexersImplTest.java @@ -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(); + } + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/es/RecoveryIndexerTest.java b/server/sonar-server/src/test/java/org/sonar/server/es/RecoveryIndexerTest.java index 423a185cacb..01d33c8bb98 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/es/RecoveryIndexerTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/es/RecoveryIndexerTest.java @@ -19,11 +19,12 @@ */ 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 index 00000000000..957612a0588 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/es/TestProjectIndexers.java @@ -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); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/IssueServiceMediumTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/IssueServiceMediumTest.java index cc753f3b3e8..3ab87f77876 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/IssueServiceMediumTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/IssueServiceMediumTest.java @@ -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; } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/IssueUpdaterTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/IssueUpdaterTest.java index 465d7ac0ca0..f9ec6b8d7b6 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/IssueUpdaterTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/IssueUpdaterTest.java @@ -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); diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexDebtTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexDebtTest.java index a429e5638be..cb287e0b7dd 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexDebtTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexDebtTest.java @@ -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() { diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexTest.java index 0a43cd2c249..416f253ef7e 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexTest.java @@ -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); diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexerTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexerTest.java index 7f4423215c9..c6d34448095 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexerTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexerTest.java @@ -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); + } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AddCommentActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AddCommentActionTest.java index d4b849abaa4..752a57cc95e 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AddCommentActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AddCommentActionTest.java @@ -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); diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AssignActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AssignActionTest.java index ef9e92ab163..20c5e39960b 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AssignActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AssignActionTest.java @@ -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(), diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AuthorsActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AuthorsActionTest.java index 64888625f32..e2830d2c3a0 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AuthorsActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AuthorsActionTest.java @@ -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); diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java index c31b901b5f6..f8416a37848 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java @@ -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<>(); diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java index 81d7e0b682c..5433a6f5979 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java @@ -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; diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsMediumTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsMediumTest.java index a856c355ad7..4e5ddce0cd3 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsMediumTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsMediumTest.java @@ -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) { diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java index 1d873091674..331e487caef 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java @@ -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) { diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetSeverityActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetSeverityActionTest.java index 6cc8d39de35..42596d8151a 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetSeverityActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetSeverityActionTest.java @@ -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)), diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTagsActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTagsActionTest.java index b30f4b72b69..614b3e90f0d 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTagsActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTagsActionTest.java @@ -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(), diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java index efc09708b10..3f1e260e46e 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java @@ -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)), diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/TagsActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/TagsActionTest.java index 356bfbf4ad8..b861d7840a0 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/TagsActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/TagsActionTest.java @@ -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; } diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexerTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexerTest.java index b237231746b..396c3517cc3 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexerTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexerTest.java @@ -19,33 +19,35 @@ */ 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); + } + } diff --git a/server/sonar-server/src/test/java/org/sonar/server/permission/PermissionTemplateServiceTest.java b/server/sonar-server/src/test/java/org/sonar/server/permission/PermissionTemplateServiceTest.java index ae714b8162e..3dd184769bb 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/permission/PermissionTemplateServiceTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/permission/PermissionTemplateServiceTest.java @@ -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"); diff --git a/server/sonar-server/src/test/java/org/sonar/server/permission/index/FooIndexer.java b/server/sonar-server/src/test/java/org/sonar/server/permission/index/FooIndexer.java index 57e5e9b96d1..cb6a69925b7 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/permission/index/FooIndexer.java +++ b/server/sonar-server/src/test/java/org/sonar/server/permission/index/FooIndexer.java @@ -20,24 +20,28 @@ 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(); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/permission/index/PermissionIndexerDaoTest.java b/server/sonar-server/src/test/java/org/sonar/server/permission/index/PermissionIndexerDaoTest.java index 002b7c931e0..28d08710e54 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/permission/index/PermissionIndexerDaoTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/permission/index/PermissionIndexerDaoTest.java @@ -114,7 +114,7 @@ public class PermissionIndexerDaoTest { } @Test - public void selectByUuids() throws Exception { + public void selectByUuids() { insertTestDataForProjectsAndViews(); Map<String, PermissionIndexerDao.Dto> dtos = underTest @@ -148,6 +148,14 @@ public class PermissionIndexerDaoTest { } @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<>(); for (int i = 0; i < 350; i++) { diff --git a/server/sonar-server/src/test/java/org/sonar/server/permission/index/PermissionIndexerTest.java b/server/sonar-server/src/test/java/org/sonar/server/permission/index/PermissionIndexerTest.java index ff5edb6ad07..2081c95ef72 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/permission/index/PermissionIndexerTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/permission/index/PermissionIndexerTest.java @@ -19,25 +19,30 @@ */ 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); + } + } diff --git a/server/sonar-server/src/test/java/org/sonar/server/permission/ws/BasePermissionWsTest.java b/server/sonar-server/src/test/java/org/sonar/server/permission/ws/BasePermissionWsTest.java index cadf166786c..1ed30d1d67b 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/permission/ws/BasePermissionWsTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/permission/ws/BasePermissionWsTest.java @@ -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())); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/permission/ws/template/ApplyTemplateActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/permission/ws/template/ApplyTemplateActionTest.java index 7a546890321..62bd1caa1c8 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/permission/ws/template/ApplyTemplateActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/permission/ws/template/ApplyTemplateActionTest.java @@ -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() { diff --git a/server/sonar-server/src/test/java/org/sonar/server/permission/ws/template/BulkApplyTemplateActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/permission/ws/template/BulkApplyTemplateActionTest.java index 1011572417e..ed3bebb972b 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/permission/ws/template/BulkApplyTemplateActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/permission/ws/template/BulkApplyTemplateActionTest.java @@ -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()); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/project/ws/CreateActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/project/ws/CreateActionTest.java index ed742f09207..c661569fc45 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/project/ws/CreateActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/project/ws/CreateActionTest.java @@ -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 diff --git a/server/sonar-server/src/test/java/org/sonar/server/project/ws/UpdateVisibilityActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/project/ws/UpdateVisibilityActionTest.java index ea4b748a6da..3854fb3c926 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/project/ws/UpdateVisibilityActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/project/ws/UpdateVisibilityActionTest.java @@ -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 diff --git a/server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/SetActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/SetActionTest.java index 9f2871c6b3b..7c35d20840c 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/SetActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/SetActionTest.java @@ -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); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/index/ActiveRuleIndexerTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/index/ActiveRuleIndexerTest.java index 216c7f428e0..32011df23d6 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/index/ActiveRuleIndexerTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/index/ActiveRuleIndexerTest.java @@ -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 index 1d633e366f8..00000000000 --- a/server/sonar-server/src/test/java/org/sonar/server/rule/ws/SearchActionMediumTest.java +++ /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()); - // } -} diff --git a/server/sonar-server/src/test/java/org/sonar/server/test/index/TestIndexerTest.java b/server/sonar-server/src/test/java/org/sonar/server/test/index/TestIndexerTest.java index 1922c16d745..9bd4ffd8472 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/test/index/TestIndexerTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/test/index/TestIndexerTest.java @@ -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) diff --git a/server/sonar-server/src/test/java/org/sonar/server/view/index/ViewIndexerTest.java b/server/sonar-server/src/test/java/org/sonar/server/view/index/ViewIndexerTest.java index 53c1e5f3c06..e416e031fa0 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/view/index/ViewIndexerTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/view/index/ViewIndexerTest.java @@ -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); |