Merge branch 'stable-3.11' into stable-3.12

* stable-3.11:
  Fix pull replication filtering for deleted refs
  Revert "Do not filter out refs removals when filtering pull-replication fetches"

Change-Id: Ib85084ac3496992dc089215982f43ba4eed34e37
diff --git a/README.md b/README.md
index 1e6882c..2e13f45 100644
--- a/README.md
+++ b/README.md
@@ -32,21 +32,38 @@
 The multi-site plugin can only be built in tree mode, by cloning
 Gerrit and the multi-site plugin code, and checking them out on the desired branch.
 
-Example of cloning Gerrit and multi-site for a stable-2.16 build:
+To build the multi-site plugin in addition to the gerrit core plugins also the
+following plugins need to be present in the plugins directory:
+
+- pull-replication
+- healthcheck
+- events-broker
+- global-refdb
+
+Example of cloning Gerrit, the multi-site plugin and the plugins it depends on
+for a stable-3.11 build:
 
 ```
-git clone -b stable-2.16 https://gerrit.googlesource.com/gerrit
-git clone -b stable-2.16 https://gerrit.googlesource.com/plugins/multi-site
+git clone --recurse-submodules -b stable-3.11 https://gerrit.googlesource.com/gerrit
+git clone -b stable-3.11 https://gerrit.googlesource.com/plugins/multi-site
+git clone -b stable-3.11 https://gerrit.googlesource.com/modules/events-broker
+git clone -b stable-3.11 https://gerrit.googlesource.com/modules/global-refdb
+git clone -b stable-3.11 https://gerrit.googlesource.com/plugins/healthcheck
+git clone -b stable-3.11 https://gerrit.googlesource.com/plugins/pull-replication
 
 cd gerrit/plugins
 ln -s ../../multi-site .
+ln -s ../../events-broker .
+ln -s ../../global-refdb .
+ln -s ../../healthcheck .
+ln -s ../../pull-replication .
 ```
 
 Example of building the multi-site plugin:
 
 ```
 cd gerrit
-bazel build plugins/multi-site
+bazelisk build plugins/multi-site
 ```
 
 The multi-site.jar plugin is generated to `bazel-bin/plugins/multi-site/multi-site.jar`.
@@ -55,13 +72,13 @@
 
 ```
 cd gerrit
-bazel test plugins/multi-site:multi_site_tests
+bazelisk test plugins/multi-site/...
 ```
 
 **NOTE**: The multi-site tests include also the use of Docker containers for
 instantiating and using a Kafka/Zookeeper broker. Make sure you have a Docker
 daemon running (/var/run/docker.sock accessible) or a DOCKER_HOST pointing to
-a Docker server.
+a Docker server. In addition docker-compose needs to be installed.
 
 ## Pre-requisites
 
diff --git a/setup_local_env/setup.sh b/setup_local_env/setup.sh
index 3e79f14..d7ab200 100755
--- a/setup_local_env/setup.sh
+++ b/setup_local_env/setup.sh
@@ -16,7 +16,7 @@
 
 
 SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
-GERRIT_BRANCH=stable-3.11
+GERRIT_BRANCH=stable-3.12
 GERRIT_CI=https://gerrit-ci.gerritforge.com/view/Plugins-$GERRIT_BRANCH/job
 LAST_BUILD=lastSuccessfulBuild/artifact/bazel-bin/plugins
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/Configuration.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/Configuration.java
index 4415aea..1be09aa 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/Configuration.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/Configuration.java
@@ -58,6 +58,8 @@
   private static final String REPLICATION_LAG_REFRESH_INTERVAL = "replicationLagRefreshInterval";
   private static final String REPLICATION_LAG_ENABLED = "replicationLagEnabled";
   private static final Duration REPLICATION_LAG_REFRESH_INTERVAL_DEFAULT = Duration.ofSeconds(60);
+  private static final String PUSH_REPLICATION_FILTER_ENABLED = "pushReplicationFilterEnabled";
+  private static final String PULL_REPLICATION_FILTER_ENABLED = "pullReplicationFilterEnabled";
   private static final String LOCAL_REF_LOCK_TIMEOUT = "localRefLockTimeout";
   private static final Duration LOCAL_REF_LOCK_TIMEOUT_DEFAULT = Duration.ofSeconds(30);
 
@@ -81,6 +83,8 @@
   private final Config multiSiteConfig;
   private final Supplier<Duration> replicationLagRefreshInterval;
   private final Supplier<Boolean> replicationLagEnabled;
+  private final Supplier<Boolean> pushReplicationFilterEnabled;
+  private final Supplier<Boolean> pullReplicationFilterEnabled;
   private final Supplier<Long> localRefLockTimeoutMsec;
 
   @Inject
@@ -121,6 +125,19 @@
                 lazyMultiSiteCfg
                     .get()
                     .getBoolean(REF_DATABASE, null, REPLICATION_LAG_ENABLED, true));
+
+    pushReplicationFilterEnabled =
+        memoize(
+            () ->
+                lazyMultiSiteCfg
+                    .get()
+                    .getBoolean(REF_DATABASE, null, PUSH_REPLICATION_FILTER_ENABLED, true));
+    pullReplicationFilterEnabled =
+        memoize(
+            () ->
+                lazyMultiSiteCfg
+                    .get()
+                    .getBoolean(REF_DATABASE, null, PULL_REPLICATION_FILTER_ENABLED, true));
     localRefLockTimeoutMsec =
         memoize(
             () ->
@@ -173,6 +190,14 @@
     return replicationLagEnabled.get();
   }
 
+  public boolean pushReplicationFilterEnabled() {
+    return pushReplicationFilterEnabled.get();
+  }
+
+  public boolean pullRepllicationFilterEnabled() {
+    return pullReplicationFilterEnabled.get();
+  }
+
   public long localRefLockTimeoutMsec() {
     return localRefLockTimeoutMsec.get();
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/PluginModule.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/PluginModule.java
index db06c9f..3ced8c6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/PluginModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/PluginModule.java
@@ -35,14 +35,10 @@
 
 public class PluginModule extends LifecycleModule {
   private static final FluentLogger log = FluentLogger.forEnclosingClass();
-  private static final String[] FILTER_MODULES_CLASS_NAMES =
-      new String[] {
-        /* Class names are defined as String for avoiding this class failing to load
-         * if either replication or pull-replication plugins are missing.
-         */
-        "com.googlesource.gerrit.plugins.multisite.validation.PullReplicationFilterModule",
-        "com.googlesource.gerrit.plugins.multisite.validation.PushReplicationFilterModule"
-      };
+  public static final String PULL_REPLICATION_FILTER_MODULE =
+      "com.googlesource.gerrit.plugins.multisite.validation.PullReplicationFilterModule";
+  public static final String PUSH_REPLICATION_FILTER_MODULE =
+      "com.googlesource.gerrit.plugins.multisite.validation.PushReplicationFilterModule";
 
   private final Configuration config;
   private final WorkQueue workQueue;
@@ -89,25 +85,33 @@
   private Iterable<AbstractModule> detectFilterModules() {
     ImmutableList.Builder<AbstractModule> filterModulesBuilder = ImmutableList.builder();
 
-    for (String filterClassName : FILTER_MODULES_CLASS_NAMES) {
-      try {
-        @SuppressWarnings("unchecked")
-        Class<AbstractModule> filterClass = (Class<AbstractModule>) Class.forName(filterClassName);
-
-        AbstractModule filterModule = parentInjector.getInstance(filterClass);
-        // Check if the filterModule would be valid for creating a child Guice Injector
-        parentInjector.createChildInjector(filterModule);
-
-        filterModulesBuilder.add(filterModule);
-      } catch (NoClassDefFoundError | ClassNotFoundException e) {
-        log.atFine().withCause(e).log(
-            "Not loading %s because of missing the associated replication plugin", filterClassName);
-      } catch (Exception e) {
-        throw new ProvisionException(
-            "Unable to instantiate replication filter " + filterClassName, e);
-      }
+    if (config.pushReplicationFilterEnabled()) {
+      bindReplicationFilterClass(PUSH_REPLICATION_FILTER_MODULE, filterModulesBuilder);
+    }
+    if (config.pullRepllicationFilterEnabled()) {
+      bindReplicationFilterClass(PULL_REPLICATION_FILTER_MODULE, filterModulesBuilder);
     }
 
     return filterModulesBuilder.build();
   }
+
+  private void bindReplicationFilterClass(
+      String filterClassName, ImmutableList.Builder<AbstractModule> filterModulesBuilder) {
+    try {
+      @SuppressWarnings("unchecked")
+      Class<AbstractModule> filterClass = (Class<AbstractModule>) Class.forName(filterClassName);
+
+      AbstractModule filterModule = parentInjector.getInstance(filterClass);
+      // Check if the filterModule would be valid for creating a child Guice Injector
+      parentInjector.createChildInjector(filterModule);
+
+      filterModulesBuilder.add(filterModule);
+    } catch (NoClassDefFoundError | ClassNotFoundException e) {
+      log.atWarning().withCause(e).log(
+          "Not loading %s because of missing the associated replication plugin", filterClassName);
+    } catch (Exception e) {
+      throw new ProvisionException(
+          "Unable to instantiate replication filter " + filterClassName, e);
+    }
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedEventHandler.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedEventDispatcher.java
similarity index 87%
rename from src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedEventHandler.java
rename to src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedEventDispatcher.java
index e2e3954..aeee827 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedEventHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedEventDispatcher.java
@@ -22,23 +22,24 @@
 import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.multisite.forwarder.router.StreamEventRouter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
  * Dispatch event to the {@link EventDispatcher}. This class is meant to be used on the receiving
- * side of the {@link StreamEventForwarder} since it will prevent dispatched event to be forwarded
+ * side of the {@link StreamEventRouter} since it will prevent dispatched event to be forwarded
  * again causing an infinite forwarding loop between the 2 nodes.
  */
 @Singleton
-public class ForwardedEventHandler {
-  private static final Logger log = LoggerFactory.getLogger(ForwardedEventHandler.class);
+public class ForwardedEventDispatcher {
+  private static final Logger log = LoggerFactory.getLogger(ForwardedEventDispatcher.class);
 
   private final DynamicItem<EventDispatcher> dispatcher;
   private final OneOffRequestContext oneOffCtx;
 
   @Inject
-  public ForwardedEventHandler(
+  public ForwardedEventDispatcher(
       DynamicItem<EventDispatcher> dispatcher, OneOffRequestContext oneOffCtx) {
     this.dispatcher = dispatcher;
     this.oneOffCtx = oneOffCtx;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedIndexChangeHandler.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedIndexChangeHandler.java
index 15f7c13..8a52eb5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedIndexChangeHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedIndexChangeHandler.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Splitter;
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.util.ManualRequestContext;
@@ -103,6 +104,28 @@
     }
   }
 
+  /**
+   * Deletes all change index entries associated with the given project.
+   *
+   * <p>This method is invoked when a project-wide deletion event is received from a peer in a
+   * multi-site setup. It removes all changes for the specified project from the local index.
+   *
+   * <p>The method temporarily marks the execution context as a forwarded event to prevent the
+   * resulting index updates from being re-forwarded to other peers, which would otherwise create
+   * event loops.
+   *
+   * @param projectName the name of the project whose changes should be removed from the index
+   */
+  public void deleteAllForProject(String projectName) {
+    try {
+      Context.setForwardedEvent(true);
+      log.debug("Deleting all change indexes for project {}", projectName);
+      indexer.deleteAllForProject(Project.nameKey(projectName));
+    } finally {
+      Context.unsetForwardedEvent();
+    }
+  }
+
   @Override
   protected void reindex(String id) {
     try (ManualRequestContext ctx = oneOffCtx.open()) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/events/ChangeIndexEvent.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/events/ChangeIndexEvent.java
index 64fbdfb..af4c0bc 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/events/ChangeIndexEvent.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/events/ChangeIndexEvent.java
@@ -22,12 +22,37 @@
 
 public class ChangeIndexEvent extends IndexEvent {
   static final String TYPE = "change-index";
+  private static final int ALL_CHANGES_FOR_PROJECT = 0;
 
   public String projectName;
   public int changeId;
   public String targetSha;
   public boolean deleted;
 
+  /**
+   * Creates a {@link ChangeIndexEvent} that represents the deletion of all changes belonging to the
+   * given project.
+   *
+   * @param projectName the name of the project
+   * @param instanceId the originating instance identifier
+   * @return an event marking all project changes as deleted
+   */
+  public static ChangeIndexEvent allChangesDeletedForProject(
+      String projectName, String instanceId) {
+    return new ChangeIndexEvent(
+        projectName, ALL_CHANGES_FOR_PROJECT, /* deleted */ true, instanceId);
+  }
+
+  /**
+   * Checks whether the given event represents the deletion of all changes for a project.
+   *
+   * @param event the event to check
+   * @return {@code true} if the event signals a project-wide deletion, {@code false} otherwise
+   */
+  public static boolean isAllChangesDeletedForProject(ChangeIndexEvent event) {
+    return event.deleted && event.changeId == ALL_CHANGES_FOR_PROJECT;
+  }
+
   public ChangeIndexEvent(String projectName, int changeId, boolean deleted, String instanceId) {
     super(TYPE, instanceId);
     this.projectName = projectName;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/router/IndexEventRouter.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/router/IndexEventRouter.java
index a647bce..a23ca33 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/router/IndexEventRouter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/router/IndexEventRouter.java
@@ -71,11 +71,16 @@
   public void route(IndexEvent sourceEvent) throws IOException {
     if (sourceEvent instanceof ChangeIndexEvent) {
       ChangeIndexEvent changeIndexEvent = (ChangeIndexEvent) sourceEvent;
-      ForwardedIndexingHandler.Operation operation = changeIndexEvent.deleted ? DELETE : INDEX;
-      indexChangeHandler.index(
-          changeIndexEvent.projectName + "~" + changeIndexEvent.changeId,
-          operation,
-          Optional.of(changeIndexEvent));
+
+      if (ChangeIndexEvent.isAllChangesDeletedForProject(changeIndexEvent)) {
+        indexChangeHandler.deleteAllForProject(changeIndexEvent.projectName);
+      } else {
+        ForwardedIndexingHandler.Operation operation = changeIndexEvent.deleted ? DELETE : INDEX;
+        indexChangeHandler.index(
+            changeIndexEvent.projectName + "~" + changeIndexEvent.changeId,
+            operation,
+            Optional.of(changeIndexEvent));
+      }
     } else if (sourceEvent instanceof AccountIndexEvent) {
       AccountIndexEvent accountIndexEvent = (AccountIndexEvent) sourceEvent;
       indexAccountHandler.indexAsync(Account.id(accountIndexEvent.accountId), INDEX);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/router/StreamEventRouter.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/router/StreamEventRouter.java
index 95a3e66..c88f5cb 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/router/StreamEventRouter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/router/StreamEventRouter.java
@@ -17,18 +17,18 @@
 import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
-import com.googlesource.gerrit.plugins.multisite.forwarder.ForwardedEventHandler;
+import com.googlesource.gerrit.plugins.multisite.forwarder.ForwardedEventDispatcher;
 import com.googlesource.gerrit.plugins.replication.events.RefReplicationDoneEvent;
 import java.io.IOException;
 
 public class StreamEventRouter implements ForwardedEventRouter<Event> {
-  private final ForwardedEventHandler streamEventHandler;
+  private final ForwardedEventDispatcher forwardedEventDispatcher;
   private final IndexEventRouter indexEventRouter;
 
   @Inject
   public StreamEventRouter(
-      ForwardedEventHandler streamEventHandler, IndexEventRouter indexEventRouter) {
-    this.streamEventHandler = streamEventHandler;
+      ForwardedEventDispatcher forwardedEventDispatcher, IndexEventRouter indexEventRouter) {
+    this.forwardedEventDispatcher = forwardedEventDispatcher;
     this.indexEventRouter = indexEventRouter;
   }
 
@@ -44,6 +44,6 @@
       indexEventRouter.onRefReplicated((RefReplicationDoneEvent) sourceEvent);
     }
 
-    streamEventHandler.dispatch(sourceEvent);
+    forwardedEventDispatcher.dispatch(sourceEvent);
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/index/FetchRefReplicatedEventHandler.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/index/FetchRefReplicatedEventHandler.java
index 83076bf..69cdb06 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/index/FetchRefReplicatedEventHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/index/FetchRefReplicatedEventHandler.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.server.config.GerritInstanceId;
 import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.EventListener;
@@ -28,6 +29,7 @@
 import com.googlesource.gerrit.plugins.replication.pull.FetchRefReplicatedEvent;
 import com.googlesource.gerrit.plugins.replication.pull.ReplicationState;
 import java.io.IOException;
+import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 
@@ -63,7 +65,8 @@
             fetchRefReplicatedEvent.ref.startsWith(":")
                 ? fetchRefReplicatedEvent.ref.substring(1)
                 : fetchRefReplicatedEvent.ref;
-        if (!RefNames.isNoteDbMetaRef(refName)
+        boolean isMetaRef = RefNames.isNoteDbMetaRef(refName);
+        if (!RefNames.isRefsChanges(refName)
             || !fetchRefReplicatedEvent
                 .getStatus()
                 .equals(ReplicationState.RefFetchResult.SUCCEEDED.toString())) {
@@ -71,6 +74,9 @@
         }
 
         Project.NameKey projectNameKey = fetchRefReplicatedEvent.getProjectNameKey();
+        logger.atFine().log(
+            "Indexing ref '%s' for project %s",
+            fetchRefReplicatedEvent.getRefName(), projectNameKey.get());
         Change.Id changeId = Change.Id.fromRef(refName);
 
         try (Repository repo = gitRepositoryManager.openRepository(projectNameKey)) {
@@ -93,12 +99,32 @@
               // so that multi-site can use it for computing the virtual change-id and assuring the
               // delete
               // of the imported changes as well.
-              changeIndexer.delete(changeId);
+              if (isMetaRef) {
+                catchStorageExceptionForMissingUnknown(
+                    () -> changeIndexer.delete(changeId),
+                    "Skipping deleting from index of "
+                        + projectNameKey
+                        + "~"
+                        + changeId.get()
+                        + " because its patch-sets aren't replicated yet");
+              } else {
+                changeIndexer.delete(changeId);
+              }
             } else {
               logger.atInfo().log(
                   "Indexing change '%s' following update of '%s' on project %s",
                   changeId, refName, projectNameKey.get());
-              changeIndexer.index(projectNameKey, changeId);
+              if (isMetaRef) {
+                catchStorageExceptionForMissingUnknown(
+                    () -> changeIndexer.index(projectNameKey, changeId),
+                    "Skipping indexing of "
+                        + projectNameKey
+                        + "~"
+                        + changeId.get()
+                        + " because its patch-sets aren't replicated yet");
+              } else {
+                changeIndexer.index(projectNameKey, changeId);
+              }
             }
           } else {
             logger.atWarning().log(
@@ -114,6 +140,23 @@
     }
   }
 
+  private void catchStorageExceptionForMissingUnknown(Runnable lambda, String infoMessage)
+      throws RuntimeException {
+    try {
+      lambda.run();
+    } catch (StorageException se) {
+      Throwable cause = se.getCause();
+      while (cause != null) {
+        if (cause instanceof MissingObjectException) {
+          logger.atInfo().log("%s", infoMessage);
+          return;
+        }
+        cause = cause.getCause();
+      }
+      throw se;
+    }
+  }
+
   private boolean isLocalEvent(Event event) {
     return instanceId.equals(event.instanceId);
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/index/IndexEventHandler.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/index/IndexEventHandler.java
index 9aee56e..83f610d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/index/IndexEventHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/index/IndexEventHandler.java
@@ -85,6 +85,11 @@
   }
 
   @Override
+  public void onAllChangesDeletedForProject(String projectName) {
+    currCtx.onlyWithContext((ctx) -> executeAllChangesDeletedForProject(projectName));
+  }
+
+  @Override
   public void onChangeDeleted(int id) {
     executeDeleteChangeTask(id);
   }
@@ -111,6 +116,17 @@
     }
   }
 
+  private void executeAllChangesDeletedForProject(String projectName) {
+    if (!Context.isForwardedEvent()) {
+      IndexChangeTask task =
+          new IndexChangeTask(
+              ChangeIndexEvent.allChangesDeletedForProject(projectName, instanceId));
+      if (queuedTasks.add(task)) {
+        executor.execute(task);
+      }
+    }
+  }
+
   private void executeIndexChangeTask(String projectName, int id) {
     if (!Context.isForwardedEvent()) {
       ChangeChecker checker = changeChecker.create(projectName + "~" + id);
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 55e5041..ed72405 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -95,6 +95,16 @@
 :   Enable the use of a shared ref-database
     Defaults: true
 
+```ref-database.pushReplicationFilterClassEnabled```
+:   Enable the filtering of push replication events checking their
+    up-to-date status with the global-refdb.
+    Defaults: true
+
+```ref-database.pullReplicationFilterClassEnabled```
+:   Enable the filtering of pull replication events checking their
+    up-to-date status with the global-refdb.
+    Defaults: true
+
 ```ref-database.localRefLockTimeout```
 :   Timeout waiting for a local ref to become available to accept
     updates or for starting a replication task.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/event/IndexEventRouterTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/event/IndexEventRouterTest.java
index 4ffdbbc..8239cf2 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/event/IndexEventRouterTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/event/IndexEventRouterTest.java
@@ -20,7 +20,7 @@
 
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.config.AllUsersName;
-import com.googlesource.gerrit.plugins.multisite.forwarder.ForwardedEventHandler;
+import com.googlesource.gerrit.plugins.multisite.forwarder.ForwardedEventDispatcher;
 import com.googlesource.gerrit.plugins.multisite.forwarder.ForwardedIndexAccountHandler;
 import com.googlesource.gerrit.plugins.multisite.forwarder.ForwardedIndexChangeHandler;
 import com.googlesource.gerrit.plugins.multisite.forwarder.ForwardedIndexGroupHandler;
@@ -50,7 +50,7 @@
   @Mock private ForwardedIndexChangeHandler indexChangeHandler;
   @Mock private ForwardedIndexGroupHandler indexGroupHandler;
   @Mock private ForwardedIndexProjectHandler indexProjectHandler;
-  @Mock private ForwardedEventHandler forwardedEventHandler;
+  @Mock private ForwardedEventDispatcher forwardedEventDispatcher;
   private AllUsersName allUsersName = new AllUsersName("All-Users");
 
   @Before
@@ -79,7 +79,7 @@
   @Test
   public void streamEventRouterShouldTriggerAccountIndexFlush() throws Exception {
 
-    StreamEventRouter streamEventRouter = new StreamEventRouter(forwardedEventHandler, router);
+    StreamEventRouter streamEventRouter = new StreamEventRouter(forwardedEventDispatcher, router);
 
     final AccountIndexEvent event = new AccountIndexEvent(1, INSTANCE_ID);
     router.route(event);
@@ -154,4 +154,16 @@
     verifyNoInteractions(
         indexAccountHandler, indexChangeHandler, indexGroupHandler, indexProjectHandler);
   }
+
+  @Test
+  public void routerShouldSendEventsToTheAppropriateHandler_allChangesDeletedForProject()
+      throws Exception {
+    ChangeIndexEvent event =
+        ChangeIndexEvent.allChangesDeletedForProject("projectName", INSTANCE_ID);
+    router.route(event);
+
+    verify(indexChangeHandler).deleteAllForProject(event.projectName);
+
+    verifyNoInteractions(indexAccountHandler, indexGroupHandler, indexProjectHandler);
+  }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/event/StreamEventRouterTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/event/StreamEventRouterTest.java
index ac90436..8d02c20 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/event/StreamEventRouterTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/event/StreamEventRouterTest.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.events.CommentAddedEvent;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.googlesource.gerrit.plugins.multisite.forwarder.ForwardedEventHandler;
+import com.googlesource.gerrit.plugins.multisite.forwarder.ForwardedEventDispatcher;
 import com.googlesource.gerrit.plugins.multisite.forwarder.router.IndexEventRouter;
 import com.googlesource.gerrit.plugins.multisite.forwarder.router.StreamEventRouter;
 import org.junit.Before;
@@ -34,19 +34,19 @@
 public class StreamEventRouterTest {
 
   private StreamEventRouter router;
-  @Mock private ForwardedEventHandler streamEventHandler;
+  @Mock private ForwardedEventDispatcher forwardedEventDispatcher;
   @Mock private IndexEventRouter indexEventRouter;
 
   @Before
   public void setUp() {
-    router = new StreamEventRouter(streamEventHandler, indexEventRouter);
+    router = new StreamEventRouter(forwardedEventDispatcher, indexEventRouter);
   }
 
   @Test
   public void routerShouldSendEventsToTheAppropriateHandler_StreamEvent() throws Exception {
     final CommentAddedEvent event = new CommentAddedEvent(aChange());
     router.route(event);
-    verify(streamEventHandler).dispatch(event);
+    verify(forwardedEventDispatcher).dispatch(event);
   }
 
   private Change aChange() {
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedEventHandlerTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedEventDispatcherTest.java
similarity index 87%
rename from src/test/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedEventHandlerTest.java
rename to src/test/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedEventDispatcherTest.java
index 73ef353..bf70f62 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedEventHandlerTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedEventDispatcherTest.java
@@ -31,24 +31,24 @@
 import org.mockito.junit.MockitoJUnitRunner;
 
 @RunWith(MockitoJUnitRunner.class)
-public class ForwardedEventHandlerTest {
+public class ForwardedEventDispatcherTest {
 
   @Rule public ExpectedException exception = ExpectedException.none();
   @Mock private DynamicItem<EventDispatcher> dispatcherMockItem;
   @Mock private EventDispatcher dispatcherMock;
   @Mock OneOffRequestContext oneOffCtxMock;
-  private ForwardedEventHandler handler;
+  private ForwardedEventDispatcher forwardedEventDispatcher;
 
   @Before
   public void setUp() throws Exception {
     when(dispatcherMockItem.get()).thenReturn(dispatcherMock);
-    handler = new ForwardedEventHandler(dispatcherMockItem, oneOffCtxMock);
+    forwardedEventDispatcher = new ForwardedEventDispatcher(dispatcherMockItem, oneOffCtxMock);
   }
 
   @Test
   public void testSuccessfulDispatching() throws Exception {
     Event event = new ProjectCreatedEvent();
-    handler.dispatch(event);
+    forwardedEventDispatcher.dispatch(event);
     verify(dispatcherMock).postEvent(event);
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedIndexChangeHandlerTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedIndexChangeHandlerTest.java
index 7133a0d..984e39f 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedIndexChangeHandlerTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedIndexChangeHandlerTest.java
@@ -203,6 +203,13 @@
     verify(indexerMock, times(1)).index(any(ChangeNotes.class));
   }
 
+  @Test
+  public void shouldDeleteAllForProject() {
+    handler.deleteAllForProject(TEST_PROJECT);
+
+    verify(indexerMock, times(1)).deleteAllForProject(Project.nameKey(TEST_PROJECT));
+  }
+
   private void setupChangeAccessRelatedMocks(boolean changeExist, boolean changeUpToDate)
       throws Exception {
     setupChangeAccessRelatedMocks(
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/index/FetchRefReplicatedEventHandlerTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/index/FetchRefReplicatedEventHandlerTest.java
index ed2f958..f7ef474 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/index/FetchRefReplicatedEventHandlerTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/index/FetchRefReplicatedEventHandlerTest.java
@@ -14,9 +14,12 @@
 
 package com.googlesource.gerrit.plugins.multisite.index;
 
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
@@ -24,12 +27,15 @@
 
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.googlesource.gerrit.plugins.replication.pull.Context;
 import com.googlesource.gerrit.plugins.replication.pull.FetchRefReplicatedEvent;
 import com.googlesource.gerrit.plugins.replication.pull.ReplicationState;
 import com.googlesource.gerrit.plugins.replication.pull.ReplicationState.RefFetchResult;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.URIish;
@@ -98,6 +104,65 @@
   }
 
   @Test
+  public void onEventShouldIgnoreMissingObjectWhenIndexExistingChangeMeta() {
+    Project.NameKey projectNameKey = Project.nameKey("testProject");
+    String ref = "refs/changes/41/41/meta";
+    Change.Id changeId = Change.Id.fromRef(ref);
+    try {
+      Context.setLocalEvent(true);
+      StorageException storageExceptionForMissingObject =
+          new StorageException(
+              "Test storage exception",
+              new MissingObjectException(ObjectId.zeroId(), "Missing unknown"));
+      doThrow(storageExceptionForMissingObject)
+          .when(changeIndexerMock)
+          .index(eq(projectNameKey), eq(changeId));
+      fetchRefReplicatedEventHandler.onEvent(
+          newFetchRefReplicatedEvent(
+              projectNameKey,
+              ref,
+              ReplicationState.RefFetchResult.SUCCEEDED,
+              LOCAL_INSTANCE_ID,
+              RefUpdate.Result.FAST_FORWARD));
+      verify(changeIndexerMock, times(1)).index(eq(projectNameKey), eq(changeId));
+    } finally {
+      Context.unsetLocalEvent();
+    }
+  }
+
+  @Test
+  public void onEventShouldThrowStorageExceptionWhenIndexPatchSetsWithMissingRefs() {
+    Project.NameKey projectNameKey = Project.nameKey("testProject");
+    String ref = "refs/changes/41/41/1";
+    Change.Id changeId = Change.Id.fromRef(ref);
+    try {
+      Context.setLocalEvent(true);
+      StorageException storageExceptionForMissingObject =
+          new StorageException(
+              "Test storage exception",
+              new MissingObjectException(ObjectId.zeroId(), "Missing unknown"));
+      doThrow(storageExceptionForMissingObject)
+          .when(changeIndexerMock)
+          .index(eq(projectNameKey), eq(changeId));
+      StorageException indexException =
+          assertThrows(
+              StorageException.class,
+              () ->
+                  fetchRefReplicatedEventHandler.onEvent(
+                      newFetchRefReplicatedEvent(
+                          projectNameKey,
+                          ref,
+                          RefFetchResult.SUCCEEDED,
+                          LOCAL_INSTANCE_ID,
+                          RefUpdate.Result.FAST_FORWARD)));
+      assertThat(indexException).isEqualTo(storageExceptionForMissingObject);
+      verify(changeIndexerMock, times(1)).index(eq(projectNameKey), eq(changeId));
+    } finally {
+      Context.unsetLocalEvent();
+    }
+  }
+
+  @Test
   public void onEventShouldNotIndexIfNotLocalEvent() {
     Project.NameKey projectNameKey = Project.nameKey("testProject");
     String ref = "refs/changes/41/41/meta";
@@ -113,7 +178,7 @@
   }
 
   @Test
-  public void onEventShouldIndexOnlyMetaRef() {
+  public void onEventShouldIndexOnPatchSetsRef() {
     Project.NameKey projectNameKey = Project.nameKey("testProject");
     String ref = "refs/changes/41/41/1";
     Change.Id changeId = Change.Id.fromRef(ref);
@@ -124,7 +189,7 @@
             ReplicationState.RefFetchResult.SUCCEEDED,
             LOCAL_INSTANCE_ID,
             RefUpdate.Result.FAST_FORWARD));
-    verify(changeIndexerMock, never()).index(eq(projectNameKey), eq(changeId));
+    verify(changeIndexerMock).index(eq(projectNameKey), eq(changeId));
   }
 
   @Test