Merge "Allow to reindex without notifying listeners"
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 4cd5bb2..37121350 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -1728,7 +1728,7 @@
 
 This endpoint allows to trigger background reindexing of an index version.  It is
 also supported to specify whether to reuse existing up-to-date (non-stale) index
-documents.
+documents and whether to notifyListeners or not.
 
 .Request
 ----
@@ -1736,7 +1736,8 @@
   Content-Type: application/json; charset=UTF-8
 
   {
-    "reuse": "true"
+    "reuse": "true",
+    "notifyListeners": "false"
   }
 ----
 
diff --git a/java/com/google/gerrit/index/SiteIndexer.java b/java/com/google/gerrit/index/SiteIndexer.java
index 32b4b21..bfb4407 100644
--- a/java/com/google/gerrit/index/SiteIndexer.java
+++ b/java/com/google/gerrit/index/SiteIndexer.java
@@ -76,6 +76,16 @@
   /** Indexes all entities for the provided index. */
   public abstract Result indexAll(I index);
 
+  /**
+   * Indexes all entities for the provided index.
+   *
+   * <p>NOTE: This method does not implement the 'notifyListeners' logic which is effectively
+   * ignored and all listeners are always notified.
+   */
+  public Result indexAll(I index, @SuppressWarnings("unused") boolean notifyListeners) {
+    return indexAll(index);
+  }
+
   protected final void addErrorListener(
       ListenableFuture<?> future, String desc, ProgressMonitor progress, AtomicBoolean ok) {
     future.addListener(
diff --git a/java/com/google/gerrit/server/index/IndexVersionReindexer.java b/java/com/google/gerrit/server/index/IndexVersionReindexer.java
index 5d136e8..84be97e 100644
--- a/java/com/google/gerrit/server/index/IndexVersionReindexer.java
+++ b/java/com/google/gerrit/server/index/IndexVersionReindexer.java
@@ -35,14 +35,14 @@
   }
 
   public <K, V, I extends Index<K, V>> Future<SiteIndexer.Result> reindex(
-      IndexDefinition<K, V, I> def, int version, boolean reuse) {
+      IndexDefinition<K, V, I> def, int version, boolean reuse, boolean notifyListeners) {
     I index = def.getIndexCollection().getWriteIndex(version);
     SiteIndexer<K, V, I> siteIndexer = def.getSiteIndexer(reuse);
     return executor.submit(
         () -> {
           String name = def.getName();
           logger.atInfo().log("Starting reindex of %s version %d", name, version);
-          SiteIndexer.Result result = siteIndexer.indexAll(index);
+          SiteIndexer.Result result = siteIndexer.indexAll(index, notifyListeners);
           if (result.success()) {
             logger.atInfo().log("Reindex %s version %s complete", name, version);
           } else {
diff --git a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index 925f68d..19a0223 100644
--- a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -177,6 +177,11 @@
 
   @Override
   public Result indexAll(ChangeIndex index) {
+    return indexAll(index, true);
+  }
+
+  @Override
+  public Result indexAll(ChangeIndex index, boolean notifyListeners) {
     // The simplest approach to distribute indexing would be to let each thread grab a project
     // and index it fully. But if a site has one big project and 100s of small projects, then
     // in the beginning all CPUs would be busy reindexing projects. But soon enough all small
@@ -199,7 +204,7 @@
     failedTask = mpm.beginSubTask("failed", MultiProgressMonitor.UNKNOWN);
     List<ListenableFuture<?>> futures;
     try {
-      futures = new SliceScheduler(index, ok).schedule();
+      futures = new SliceScheduler(index, ok, notifyListeners).schedule();
     } catch (ProjectsCollectionFailure e) {
       logger.atSevere().log("%s", e.getMessage());
       return Result.create(sw, false, 0, 0);
@@ -359,6 +364,7 @@
   private class SliceScheduler {
     final ChangeIndex index;
     final AtomicBoolean ok;
+    final boolean notifyListeners;
     final AtomicInteger changeCount = new AtomicInteger(0);
     final AtomicInteger projectsFailed = new AtomicInteger(0);
     final List<ListenableFuture<?>> sliceIndexerFutures = new ArrayList<>();
@@ -366,9 +372,10 @@
     VolatileTask projTask = mpm.beginVolatileSubTask("project-slices");
     Task slicingProjects;
 
-    public SliceScheduler(ChangeIndex index, AtomicBoolean ok) {
+    public SliceScheduler(ChangeIndex index, AtomicBoolean ok, boolean notifyListeners) {
       this.index = index;
       this.ok = ok;
+      this.notifyListeners = notifyListeners;
     }
 
     private List<ListenableFuture<?>> schedule() throws ProjectsCollectionFailure {
@@ -376,7 +383,7 @@
       int projectCount = projects.size();
       slicingProjects = mpm.beginSubTask("Slicing projects", projectCount);
       for (Project.NameKey name : projects) {
-        sliceCreationFutures.add(executor.submit(new ProjectSliceCreator(name)));
+        sliceCreationFutures.add(executor.submit(new ProjectSliceCreator(name, notifyListeners)));
       }
 
       try {
@@ -407,9 +414,11 @@
 
     private class ProjectSliceCreator implements Callable<Void> {
       final Project.NameKey name;
+      final boolean notifyListeners;
 
-      public ProjectSliceCreator(Project.NameKey name) {
+      public ProjectSliceCreator(Project.NameKey name, boolean notifyListeners) {
         this.name = name;
+        this.notifyListeners = notifyListeners;
       }
 
       @Override
@@ -434,9 +443,10 @@
               ChangeIndexer indexer;
               if (reuseExistingDocuments) {
                 indexer =
-                    indexerFactory.create(executor, index, stalenessCheckerFactory.create(index));
+                    indexerFactory.create(
+                        executor, index, stalenessCheckerFactory.create(index), notifyListeners);
               } else {
-                indexer = indexerFactory.create(executor, index);
+                indexer = indexerFactory.create(executor, index, notifyListeners);
               }
               ListenableFuture<?> future =
                   executor.submit(reindexProjectSlice(indexer, projectSlice, doneTask, failedTask));
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index 4ec390d..1e7d0f2 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -71,7 +71,13 @@
     ChangeIndexer create(ListeningExecutorService executor, ChangeIndex index);
 
     ChangeIndexer create(
-        ListeningExecutorService executor, ChangeIndex index, StalenessChecker stalenessChecker);
+        ListeningExecutorService executor, ChangeIndex index, boolean notifyListeners);
+
+    ChangeIndexer create(
+        ListeningExecutorService executor,
+        ChangeIndex index,
+        StalenessChecker stalenessChecker,
+        boolean notifyListeners);
 
     ChangeIndexer create(ListeningExecutorService executor, ChangeIndexCollection indexes);
   }
@@ -87,6 +93,7 @@
   private final StalenessChecker stalenessChecker;
   private final boolean autoReindexIfStale;
   private final IsFirstInsertForEntry isFirstInsertForEntry;
+  private final boolean notifyListeners;
 
   private final Map<Change.Id, IndexTask> queuedIndexTasks = new ConcurrentHashMap<>();
   private final Set<ReindexIfStaleTask> queuedReindexIfStaleTasks =
@@ -104,6 +111,33 @@
       @Assisted ListeningExecutorService executor,
       @Assisted ChangeIndex index,
       IsFirstInsertForEntry isFirstInsertForEntry) {
+    this(
+        cfg,
+        changeDataFactory,
+        notesFactory,
+        context,
+        indexedListeners,
+        stalenessChecker,
+        batchExecutor,
+        executor,
+        index,
+        true,
+        isFirstInsertForEntry);
+  }
+
+  @AssistedInject
+  ChangeIndexer(
+      @GerritServerConfig Config cfg,
+      ChangeData.Factory changeDataFactory,
+      ChangeNotes.Factory notesFactory,
+      ThreadLocalRequestContext context,
+      PluginSetContext<ChangeIndexedListener> indexedListeners,
+      StalenessChecker stalenessChecker,
+      @IndexExecutor(BATCH) ListeningExecutorService batchExecutor,
+      @Assisted ListeningExecutorService executor,
+      @Assisted ChangeIndex index,
+      @Assisted boolean notifyListeners,
+      IsFirstInsertForEntry isFirstInsertForEntry) {
     this.executor = executor;
     this.changeDataFactory = changeDataFactory;
     this.notesFactory = notesFactory;
@@ -115,6 +149,7 @@
     this.index = index;
     this.indexes = null;
     this.isFirstInsertForEntry = isFirstInsertForEntry;
+    this.notifyListeners = notifyListeners;
   }
 
   @AssistedInject
@@ -128,7 +163,8 @@
       IsFirstInsertForEntry isFirstInsertForEntry,
       @Assisted ListeningExecutorService executor,
       @Assisted ChangeIndex index,
-      @Assisted StalenessChecker stalenessChecker) {
+      @Assisted StalenessChecker stalenessChecker,
+      @Assisted boolean notifyListeners) {
     this.executor = executor;
     this.changeDataFactory = changeDataFactory;
     this.notesFactory = notesFactory;
@@ -140,6 +176,7 @@
     this.index = index;
     this.indexes = null;
     this.stalenessChecker = stalenessChecker;
+    this.notifyListeners = notifyListeners;
   }
 
   @AssistedInject
@@ -165,6 +202,7 @@
     this.index = null;
     this.indexes = indexes;
     this.isFirstInsertForEntry = isFirstInsertForEntry;
+    this.notifyListeners = true;
   }
 
   private static boolean autoReindexIfStale(Config cfg) {
@@ -290,19 +328,27 @@
   }
 
   private void fireChangeScheduledForIndexingEvent(String projectName, int id) {
-    indexedListeners.runEach(l -> l.onChangeScheduledForIndexing(projectName, id));
+    if (notifyListeners) {
+      indexedListeners.runEach(l -> l.onChangeScheduledForIndexing(projectName, id));
+    }
   }
 
   private void fireChangeIndexedEvent(String projectName, int id) {
-    indexedListeners.runEach(l -> l.onChangeIndexed(projectName, id));
+    if (notifyListeners) {
+      indexedListeners.runEach(l -> l.onChangeIndexed(projectName, id));
+    }
   }
 
   private void fireChangeScheduledForDeletionFromIndexEvent(int id) {
-    indexedListeners.runEach(l -> l.onChangeScheduledForDeletionFromIndex(id));
+    if (notifyListeners) {
+      indexedListeners.runEach(l -> l.onChangeScheduledForDeletionFromIndex(id));
+    }
   }
 
   private void fireChangeDeletedFromIndexEvent(int id) {
-    indexedListeners.runEach(l -> l.onChangeDeleted(id));
+    if (notifyListeners) {
+      indexedListeners.runEach(l -> l.onChangeDeleted(id));
+    }
   }
 
   /**
diff --git a/java/com/google/gerrit/server/restapi/config/ReindexIndexVersion.java b/java/com/google/gerrit/server/restapi/config/ReindexIndexVersion.java
index d923155..21cd1c1 100644
--- a/java/com/google/gerrit/server/restapi/config/ReindexIndexVersion.java
+++ b/java/com/google/gerrit/server/restapi/config/ReindexIndexVersion.java
@@ -25,7 +25,8 @@
 
 public class ReindexIndexVersion implements RestModifyView<IndexVersionResource, Input> {
   public static class Input {
-    boolean reuse;
+    public boolean reuse;
+    public boolean notifyListeners;
   }
 
   private final IndexVersionReindexer indexVersionReindexer;
@@ -41,7 +42,7 @@
     IndexDefinition<?, ?, ?> def = rsrc.getIndexDefinition();
     int version = rsrc.getIndex().getSchema().getVersion();
     @SuppressWarnings("unused")
-    var unused = indexVersionReindexer.reindex(def, version, input.reuse);
+    var unused = indexVersionReindexer.reindex(def, version, input.reuse, input.notifyListeners);
     return Response.accepted(
         String.format("Index %s version %d submitted for reindexing", def.getName(), version));
   }
diff --git a/javatests/com/google/gerrit/acceptance/server/index/change/BUILD b/javatests/com/google/gerrit/acceptance/server/index/BUILD
similarity index 81%
rename from javatests/com/google/gerrit/acceptance/server/index/change/BUILD
rename to javatests/com/google/gerrit/acceptance/server/index/BUILD
index 3be5249..1d4ef02 100644
--- a/javatests/com/google/gerrit/acceptance/server/index/change/BUILD
+++ b/javatests/com/google/gerrit/acceptance/server/index/BUILD
@@ -1,7 +1,7 @@
 load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
 
 acceptance_tests(
-    srcs = glob(["*IT.java"]),
+    srcs = glob(["**/*IT.java"]),
     group = "server_index",
     labels = ["server"],
 )
diff --git a/javatests/com/google/gerrit/acceptance/server/index/ReindexIndexVersionIT.java b/javatests/com/google/gerrit/acceptance/server/index/ReindexIndexVersionIT.java
new file mode 100644
index 0000000..d433ca7
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/index/ReindexIndexVersionIT.java
@@ -0,0 +1,87 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.server.index;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.extensions.events.ChangeIndexedListener;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.index.Index;
+import com.google.gerrit.index.IndexDefinition;
+import com.google.gerrit.server.config.IndexVersionResource;
+import com.google.gerrit.server.restapi.config.ReindexIndexVersion;
+import com.google.inject.Inject;
+import java.util.Collection;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ReindexIndexVersionIT extends AbstractDaemonTest {
+
+  @Inject private ReindexIndexVersion reindexIndexVersion;
+  @Inject private Collection<IndexDefinition<?, ?, ?>> indexDefs;
+  @Inject private ExtensionRegistry extensionRegistry;
+
+  private IndexDefinition<?, ?, ?> def;
+  private Index<?, ?> changeIndex;
+  private Change.Id C1;
+  private Change.Id C2;
+
+  private ChangeIndexedListener changeIndexedListener;
+  private ReindexIndexVersion.Input input = new ReindexIndexVersion.Input();
+
+  @Before
+  public void setUp() throws Exception {
+    def = indexDefs.stream().filter(i -> i.getName().equals("changes")).findFirst().get();
+    changeIndex = def.getIndexCollection().getSearchIndex();
+    C1 = createChange().getChange().getId();
+    C2 = createChange().getChange().getId();
+    changeIndexedListener = mock(ChangeIndexedListener.class);
+    input = new ReindexIndexVersion.Input();
+  }
+
+  @Test
+  public void reindexWithListenerNotification() throws Exception {
+    input.notifyListeners = true;
+    reindex();
+    verify(changeIndexedListener, times(1)).onChangeIndexed(project.get(), C1.get());
+    verify(changeIndexedListener, times(1)).onChangeIndexed(project.get(), C2.get());
+  }
+
+  @Test
+  public void reindexWithoutListenerNotification() throws Exception {
+    input.notifyListeners = false;
+    reindex();
+    verifyNoInteractions(changeIndexedListener);
+  }
+
+  private void reindex() throws ResourceNotFoundException {
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(changeIndexedListener)) {
+      Response<?> rsp =
+          reindexIndexVersion.apply(new IndexVersionResource(def, changeIndex), input);
+      assertThat(rsp.statusCode()).isEqualTo(HttpServletResponse.SC_ACCEPTED);
+    }
+  }
+}