Introduce all-project-changes-deleted-from-index event

When deleting a project, its related changes are also removed from the
index. This used to be notified via single change-index events, for each
change deleted from the index.

This however was potentially causing a large number of events to be
published and consumed, making it impractical and inefficient to process
one-by-one.

Leverage a new hook introduced at I4c8a53629 to trigger a new dedicated
event that can be processed by consumers to delete changes in bulk from
their index.

Depends-On: I4c8a536290800d7b93b9f62d2e7ed959fceebb6b
Bug: Issue 440670678
Change-Id: I7a171837bca5103094caa9c302afb6409f075094
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/AllProjectChangesDeletedFromIndexEvent.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/AllProjectChangesDeletedFromIndexEvent.java
new file mode 100644
index 0000000..e94c5fb
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/AllProjectChangesDeletedFromIndexEvent.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2025 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.googlesource.gerrit.plugins.deleteproject;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.server.events.ProjectEvent;
+
+public class AllProjectChangesDeletedFromIndexEvent extends ProjectEvent {
+  public static final String TYPE = "all-project-changes-deleted-from-index";
+  public String projectName;
+
+  public AllProjectChangesDeletedFromIndexEvent() {
+    super(TYPE);
+  }
+
+  @Override
+  public NameKey getProjectNameKey() {
+    return Project.nameKey(projectName);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProject.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProject.java
index 75a948a..255fe3d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProject.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProject.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.events.ChangeIndexedListener;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -37,7 +38,8 @@
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 
 @Singleton
-class DeleteProject implements RestModifyView<ProjectResource, Input> {
+class DeleteProject implements RestModifyView<ProjectResource, Input>, ChangeIndexedListener {
+
   static class Input {
     boolean preserve;
     boolean force;
@@ -125,4 +127,19 @@
       deleteLog.onDelete((IdentifiedUser) userProvider.get(), project.getNameKey(), input, ex);
     }
   }
+
+  @Override
+  public void onChangeIndexed(String projectName, int id) {}
+
+  @Override
+  public void onChangeDeleted(int id) {}
+
+  @Override
+  public void onAllChangesDeletedForProject(String projectName) {
+    AllProjectChangesDeletedFromIndexEvent event = new AllProjectChangesDeletedFromIndexEvent();
+    event.projectName = projectName;
+    event.instanceId = instanceId;
+
+    dispatcher.get().postEvent(Project.nameKey(projectName), event);
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/PluginModule.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/PluginModule.java
index a2e3f15..cd0510e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/PluginModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/PluginModule.java
@@ -20,7 +20,9 @@
 
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.config.CapabilityDefinition;
+import com.google.gerrit.extensions.events.ChangeIndexedListener;
 import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.RestApiModule;
 import com.google.gerrit.server.events.EventTypes;
 import com.google.inject.AbstractModule;
@@ -62,6 +64,10 @@
     }
 
     EventTypes.register(ProjectDeletedEvent.TYPE, ProjectDeletedEvent.class);
+    EventTypes.register(
+        AllProjectChangesDeletedFromIndexEvent.TYPE, AllProjectChangesDeletedFromIndexEvent.class);
+
+    DynamicSet.bind(binder(), ChangeIndexedListener.class).to(DeleteProject.class);
 
     install(
         new RestApiModule() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/database/DatabaseDeleteHandler.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/database/DatabaseDeleteHandler.java
index 99dc164..a40cab3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/database/DatabaseDeleteHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/database/DatabaseDeleteHandler.java
@@ -27,8 +27,7 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.index.change.ChangeIndex;
-import com.google.gerrit.server.index.change.ChangeIndexCollection;
+import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeNotes.Factory.ChangeNotesResult;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -44,7 +43,7 @@
   private static final FluentLogger log = FluentLogger.forEnclosingClass();
 
   private final StarredChangesWriter starredChangesWriter;
-  private final ChangeIndexCollection indexes;
+  private final ChangeIndexer indexer;
   private final Provider<InternalAccountQuery> accountQueryProvider;
   private final Provider<AccountsUpdate> accountsUpdateProvider;
   private final ChangeNotes.Factory schemaFactoryNoteDb;
@@ -53,13 +52,13 @@
   @Inject
   public DatabaseDeleteHandler(
       StarredChangesWriter starredChangesWriter,
-      ChangeIndexCollection indexes,
+      ChangeIndexer indexer,
       ChangeNotes.Factory schemaFactoryNoteDb,
       GitRepositoryManager repoManager,
       Provider<InternalAccountQuery> accountQueryProvider,
       @UserInitiated Provider<AccountsUpdate> accountsUpdateProvider) {
     this.starredChangesWriter = starredChangesWriter;
-    this.indexes = indexes;
+    this.indexer = indexer;
     this.accountQueryProvider = accountQueryProvider;
     this.accountsUpdateProvider = accountsUpdateProvider;
     this.schemaFactoryNoteDb = schemaFactoryNoteDb;
@@ -67,7 +66,7 @@
   }
 
   public void delete(Project project) throws IOException {
-    deleteChangesFromIndex(project);
+    indexer.deleteAllForProject(project.getNameKey());
     unstarChanges(getChangesListFromNoteDb(project));
     deleteProjectWatches(project);
   }
@@ -84,12 +83,6 @@
     }
   }
 
-  private void deleteChangesFromIndex(Project project) {
-    for (ChangeIndex i : indexes.getWriteIndexes()) {
-      i.deleteAllForProject(project.getNameKey());
-    }
-  }
-
   private void unstarChanges(List<Change.Id> changeIds) {
     for (Change.Id id : changeIds) {
       try {
diff --git a/src/test/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProjectIT.java b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProjectIT.java
index 9c684ef..fa63d85 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProjectIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProjectIT.java
@@ -23,6 +23,7 @@
 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
 
 import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestPlugin;
@@ -37,6 +38,7 @@
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.events.Event;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.deleteproject.DeleteProject.Input;
 import java.io.File;
@@ -286,6 +288,31 @@
     assertThat(parentFolder.toFile().exists()).isFalse();
   }
 
+  @Test
+  @UseLocalDisk
+  public void testHttpDeleteProjectEmitsAllProjectChangesDeletedFromIndex() throws Exception {
+    RestResponse r = httpDeleteProjectHelper(false);
+    r.assertNoContent();
+
+    ImmutableList<Event> events =
+        eventRecorder.getGenericEvents(AllProjectChangesDeletedFromIndexEvent.TYPE, 1);
+
+    assertThat(((AllProjectChangesDeletedFromIndexEvent) events.getFirst()).getProjectNameKey())
+        .isEqualTo(project);
+  }
+
+  @Test
+  @UseLocalDisk
+  public void testSshDeleteProjectEmitsAllProjectChangesDeletedFromIndex() throws Exception {
+    adminSshSession.exec(createDeleteCommand(project.get()));
+
+    ImmutableList<Event> events =
+        eventRecorder.getGenericEvents(AllProjectChangesDeletedFromIndexEvent.TYPE, 1);
+
+    assertThat(((AllProjectChangesDeletedFromIndexEvent) events.getFirst()).getProjectNameKey())
+        .isEqualTo(project);
+  }
+
   private File verifyProjectRepoExists(Project.NameKey name) throws IOException {
     File projectDir;
     try (Repository projectRepo = repoManager.openRepository(name)) {