Use stream events to delete repositories

This functionality in combination with events-broker and multi-site
provides a backfill mechanism for REST API calls missed when the node
was unreachable.

Co-Authored-By: Marcin Czech <maczech@gmail.com>
Bug: Issue 40014693
Change-Id: I31cb7378ba7aa5a5532ba7007fc5780db2723908
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionAction.java
index 6a21104..18574fd 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionAction.java
@@ -39,12 +39,12 @@
 import org.eclipse.jgit.transport.URIish;
 
 @Singleton
-class ProjectDeletionAction
+public class ProjectDeletionAction
     implements RestModifyView<ProjectResource, ProjectDeletionAction.DeleteInput> {
   private static final PluginPermission DELETE_PROJECT =
       new PluginPermission("delete-project", "deleteProject");
 
-  static class DeleteInput {}
+  public static class DeleteInput {}
 
   private final Provider<CurrentUser> userProvider;
   private final GerritConfigOps gerritConfigOps;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListener.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListener.java
index f863148..4c9793c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListener.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListener.java
@@ -25,7 +25,9 @@
 import com.google.gerrit.entities.Project.NameKey;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.config.GerritInstanceId;
 import com.google.gerrit.server.data.RefUpdateAttribute;
@@ -37,9 +39,12 @@
 import com.google.gerrit.server.events.RefUpdatedEvent;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.restapi.project.ProjectsCollection;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.name.Named;
+import com.googlesource.gerrit.plugins.deleteproject.ProjectDeletedEvent;
 import com.googlesource.gerrit.plugins.replication.pull.ApplyObjectsCacheKey;
 import com.googlesource.gerrit.plugins.replication.pull.FetchOne;
 import com.googlesource.gerrit.plugins.replication.pull.Source;
@@ -47,6 +52,7 @@
 import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction;
 import com.googlesource.gerrit.plugins.replication.pull.api.FetchJob;
 import com.googlesource.gerrit.plugins.replication.pull.api.FetchJob.Factory;
+import com.googlesource.gerrit.plugins.replication.pull.api.ProjectDeletionAction;
 import com.googlesource.gerrit.plugins.replication.pull.api.ProjectInitializationAction;
 import com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationApiRequestMetrics;
 import com.googlesource.gerrit.plugins.replication.pull.api.UpdateHeadCommand;
@@ -68,6 +74,8 @@
   private final String instanceId;
   private final WorkQueue workQueue;
   private final Cache<ApplyObjectsCacheKey, Long> refUpdatesSucceededCache;
+  private final ProjectDeletionAction projectDeletionAction;
+  private final ProjectsCollection projectsCollection;
 
   @Inject
   public StreamEventListener(
@@ -79,7 +87,9 @@
       Provider<PullReplicationApiRequestMetrics> metricsProvider,
       SourcesCollection sources,
       ExcludedRefsFilter excludedRefsFilter,
-      @Named(APPLY_OBJECTS_CACHE) Cache<ApplyObjectsCacheKey, Long> refUpdatesSucceededCache) {
+      @Named(APPLY_OBJECTS_CACHE) Cache<ApplyObjectsCacheKey, Long> refUpdatesSucceededCache,
+      ProjectDeletionAction projectDeletionAction,
+      ProjectsCollection projectsCollection) {
     this.instanceId = instanceId;
     this.updateHeadCommand = updateHeadCommand;
     this.projectInitializationAction = projectInitializationAction;
@@ -89,6 +99,8 @@
     this.sources = sources;
     this.refsFilter = excludedRefsFilter;
     this.refUpdatesSucceededCache = refUpdatesSucceededCache;
+    this.projectDeletionAction = projectDeletionAction;
+    this.projectsCollection = projectsCollection;
 
     requireNonNull(
         Strings.emptyToNull(this.instanceId), "gerrit.instanceId cannot be null or empty");
@@ -173,6 +185,25 @@
             "Failed to update HEAD on project: %s", headUpdatedEvent.projectName);
         throw e;
       }
+    } else if (event instanceof ProjectDeletedEvent) {
+      deleteProject((ProjectDeletedEvent) event);
+    }
+  }
+
+  protected void deleteProject(ProjectEvent projectDeletedEvent) {
+    try {
+      ProjectResource projectResource =
+          projectsCollection.parse(
+              TopLevelResource.INSTANCE,
+              IdString.fromDecoded(projectDeletedEvent.getProjectNameKey().get()));
+      projectDeletionAction.apply(projectResource, new ProjectDeletionAction.DeleteInput());
+    } catch (ResourceNotFoundException e) {
+      logger.atFine().withCause(e).log(
+          "Repository not found whilst trying to delete project:%s",
+          projectDeletedEvent.getProjectNameKey().get());
+    } catch (Exception e) {
+      logger.atSevere().withCause(e).log(
+          "Cannot delete project:%s", projectDeletedEvent.getProjectNameKey().get());
     }
   }
 
@@ -202,6 +233,12 @@
           && source.wouldCreateProject(projectCreatedEvent.getProjectNameKey());
     }
 
+    if (event instanceof ProjectDeletedEvent) {
+      ProjectDeletedEvent projectDeletedEvent = (ProjectDeletedEvent) event;
+
+      return source.wouldDeleteProject(projectDeletedEvent.getProjectNameKey());
+    }
+
     ProjectEvent projectEvent = (ProjectEvent) event;
     return source.wouldFetchProject(projectEvent.getProjectNameKey());
   }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListenerTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListenerTest.java
index a60fed6..980c509 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListenerTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListenerTest.java
@@ -31,12 +31,14 @@
 import com.google.gerrit.server.events.ProjectHeadUpdatedEvent;
 import com.google.gerrit.server.events.RefUpdatedEvent;
 import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.restapi.project.ProjectsCollection;
 import com.googlesource.gerrit.plugins.replication.pull.ApplyObjectsCacheKey;
 import com.googlesource.gerrit.plugins.replication.pull.FetchOne;
 import com.googlesource.gerrit.plugins.replication.pull.Source;
 import com.googlesource.gerrit.plugins.replication.pull.SourcesCollection;
 import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction;
 import com.googlesource.gerrit.plugins.replication.pull.api.FetchJob;
+import com.googlesource.gerrit.plugins.replication.pull.api.ProjectDeletionAction;
 import com.googlesource.gerrit.plugins.replication.pull.api.ProjectInitializationAction;
 import com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationApiRequestMetrics;
 import com.googlesource.gerrit.plugins.replication.pull.api.UpdateHeadCommand;
@@ -72,6 +74,9 @@
   @Mock private SourcesCollection sources;
   @Mock private Source source;
   @Mock private ExcludedRefsFilter refsFilter;
+  @Mock private ProjectDeletionAction projectDeletionAction;
+  @Mock private ProjectsCollection projectsCollection;
+
   private Cache<ApplyObjectsCacheKey, Long> cache;
 
   private StreamEventListener objectUnderTest;
@@ -98,7 +103,9 @@
             () -> metrics,
             sources,
             refsFilter,
-            cache);
+            cache,
+            projectDeletionAction,
+            projectsCollection);
   }
 
   @Test