Merge "Merge branch 'stable-3.8' into stable-3.9" into stable-3.9
diff --git a/docker-compose.kafka-broker.yaml b/docker-compose.kafka-broker.yaml
deleted file mode 100644
index d3fc713..0000000
--- a/docker-compose.kafka-broker.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-version: '3'
-services:
-  zookeeper:
-    image: wurstmeister/zookeeper:latest
-    ports:
-      - "2181:2181"
-  kafka:
-    image: wurstmeister/kafka:2.13-2.6.3
-    ports:
-      - "9092:9092"
-    environment:
-      KAFKA_ADVERTISED_HOST_NAME: 127.0.0.1
-      KAFKA_CREATE_TOPICS: "gerrit:1:1"
-      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
diff --git a/e2e-tests/docker-compose-kafka.yaml b/e2e-tests/docker-compose-kafka.yaml
index addb8d5..b02b58d 100644
--- a/e2e-tests/docker-compose-kafka.yaml
+++ b/e2e-tests/docker-compose-kafka.yaml
@@ -1,12 +1,11 @@
 version: '3'
 services:
   kafka:
-    image: wurstmeister/kafka:2.13-2.6.3
+    image: bitnami/kafka:3.6.0
     depends_on:
       - zookeeper
     environment:
-      KAFKA_ADVERTISED_HOST_NAME: kafka
-      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
+      - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
 
   gerrit1:
     depends_on:
diff --git a/e2e-tests/docker-compose.yaml b/e2e-tests/docker-compose.yaml
index 3abb757..2ac7b06 100644
--- a/e2e-tests/docker-compose.yaml
+++ b/e2e-tests/docker-compose.yaml
@@ -1,7 +1,9 @@
 version: '3'
 services:
   zookeeper:
-    image: wurstmeister/zookeeper:latest
+    image: bitnami/zookeeper:3.8.3
+    environment:
+      ALLOW_ANONYMOUS_LOGIN: "true"
 
   gerrit1:
     image: gerritcodereview/gerrit:${GERRIT_IMAGE}
diff --git a/setup_local_env/configs/gerrit.config b/setup_local_env/configs/gerrit.config
index 8a176f2..c92c60d 100644
--- a/setup_local_env/configs/gerrit.config
+++ b/setup_local_env/configs/gerrit.config
@@ -1,3 +1,5 @@
+[core]
+    usePerRequestRefCache = false
 [gerrit]
     basePath = git
     serverId = 69ec38f0-350e-4d9c-96d4-bc956f2faaac
@@ -39,8 +41,6 @@
     directory = cache
 [plugins]
     allowRemoteAdmin = true
-[plugin "websession-flatfile"]
-    directory = $FAKE_NFS
 [plugin "events-kafka"]
     sendAsync = true
     bootstrapServers = $BROKER_HOST:$BROKER_PORT
diff --git a/setup_local_env/docker-compose-core.yaml b/setup_local_env/docker-compose-core.yaml
index dab168d..f8ef318 100644
--- a/setup_local_env/docker-compose-core.yaml
+++ b/setup_local_env/docker-compose-core.yaml
@@ -1,10 +1,12 @@
 version: '3'
 services:
   zookeeper:
-    image: wurstmeister/zookeeper:latest
+    image: bitnami/zookeeper:3.8.3
     ports:
       - "2181:2181"
     container_name: zk_test_node
+    environment:
+      ALLOW_ANONYMOUS_LOGIN: "true"
   prometheus:
     container_name: prometheus_test_node
     image: prom/prometheus:v2.16.0
diff --git a/setup_local_env/docker-compose-kafka.yaml b/setup_local_env/docker-compose-kafka.yaml
index 2a5b0fc..70fda58 100644
--- a/setup_local_env/docker-compose-kafka.yaml
+++ b/setup_local_env/docker-compose-kafka.yaml
@@ -1,13 +1,13 @@
 version: '3'
 services:
   kafka:
-    image: wurstmeister/kafka:2.13-2.6.3
+    image: bitnami/kafka:3.6.0
     ports:
       - "9092:9092"
     container_name: kafka_test_node
     environment:
-      KAFKA_ADVERTISED_HOST_NAME: 127.0.0.1
-      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
+      - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092
+      - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
     networks:
       - setup_local_env_default
 networks:
diff --git a/setup_local_env/setup.sh b/setup_local_env/setup.sh
index 0e84603..96a4204 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.8
+GERRIT_BRANCH=stable-3.9
 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 5c443e7..e3f53ab 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/Configuration.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/Configuration.java
@@ -306,12 +306,15 @@
     static final String INDEX_SECTION = "index";
     static final String MAX_TRIES_KEY = "maxTries";
     static final String RETRY_INTERVAL_KEY = "retryInterval";
+    static final String SYNCHRONIZE_FORCED_KEY = "synchronizeForced";
+    static final boolean DEFAULT_SYNCHRONIZE_FORCED = true;
 
     private final int threadPoolSize;
     private final int retryInterval;
     private final int maxTries;
 
     private final int numStripedLocks;
+    private final boolean synchronizeForced;
 
     private Index(Supplier<Config> cfg) {
       super(cfg, INDEX_SECTION);
@@ -322,6 +325,8 @@
       maxTries = getInt(cfg, INDEX_SECTION, null, MAX_TRIES_KEY, DEFAULT_INDEX_MAX_TRIES);
       numStripedLocks =
           getInt(cfg, INDEX_SECTION, null, NUM_STRIPED_LOCKS, DEFAULT_NUM_STRIPED_LOCKS);
+      synchronizeForced =
+          getBoolean(cfg, INDEX_SECTION, null, SYNCHRONIZE_FORCED_KEY, DEFAULT_SYNCHRONIZE_FORCED);
     }
 
     public int threadPoolSize() {
@@ -339,6 +344,10 @@
     public int numStripedLocks() {
       return numStripedLocks;
     }
+
+    public boolean synchronizeForced() {
+      return synchronizeForced;
+    }
   }
 
   public static class Broker {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/event/EventModule.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/event/EventModule.java
index 53bcbc0..8c67823 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/event/EventModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/event/EventModule.java
@@ -16,11 +16,10 @@
 
 import com.gerritforge.gerrit.eventbroker.publisher.StreamEventPublisherConfig;
 import com.gerritforge.gerrit.eventbroker.publisher.StreamEventPublisherModule;
+import com.google.gerrit.extensions.events.GitBatchRefUpdateListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.server.events.EventListener;
 import com.google.inject.Inject;
-import com.google.inject.Scopes;
 import com.google.inject.multibindings.OptionalBinder;
 import com.googlesource.gerrit.plugins.multisite.Configuration;
 import com.googlesource.gerrit.plugins.multisite.forwarder.events.EventTopic;
@@ -37,8 +36,6 @@
 
   @Override
   protected void configure() {
-    DynamicSet.bind(binder(), EventListener.class).to(ProjectVersionRefUpdateImpl.class);
-
     bind(StreamEventPublisherConfig.class)
         .toInstance(
             new StreamEventPublisherConfig(
@@ -50,11 +47,9 @@
     OptionalBinder<ProjectVersionRefUpdate> projectVersionRefUpdateBinder =
         OptionalBinder.newOptionalBinder(binder(), ProjectVersionRefUpdate.class);
     if (configuration.getSharedRefDbConfiguration().getSharedRefDb().isEnabled()) {
-      DynamicSet.bind(binder(), EventListener.class).to(ProjectVersionRefUpdateImpl.class);
-      projectVersionRefUpdateBinder
-          .setBinding()
-          .to(ProjectVersionRefUpdateImpl.class)
-          .in(Scopes.SINGLETON);
+      DynamicSet.bind(binder(), GitBatchRefUpdateListener.class)
+          .to(ProjectVersionRefUpdateImpl.class);
+      projectVersionRefUpdateBinder.setBinding().to(ProjectVersionRefUpdateImpl.class);
     }
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/index/ChangeCheckerImpl.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/index/ChangeCheckerImpl.java
index 4779258..7b90468 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/index/ChangeCheckerImpl.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/index/ChangeCheckerImpl.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.server.DraftCommentsReader;
 import com.google.gerrit.server.change.ChangeFinder;
 import com.google.gerrit.server.config.GerritInstanceId;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.util.ManualRequestContext;
@@ -32,6 +33,7 @@
 import java.util.Objects;
 import java.util.Optional;
 import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -47,6 +49,7 @@
   private final String changeId;
   private final ChangeFinder changeFinder;
   private final String instanceId;
+  private final boolean enableDraftCommentEvents;
   private Optional<Long> computedChangeTs = Optional.empty();
   private Optional<ChangeNotes> changeNotes = Optional.empty();
 
@@ -61,6 +64,7 @@
       ChangeFinder changeFinder,
       OneOffRequestContext oneOffReqCtx,
       @GerritInstanceId String instanceId,
+      @GerritServerConfig Config config,
       @Assisted String changeId) {
     this.changeFinder = changeFinder;
     this.gitRepoMgr = gitRepoMgr;
@@ -68,6 +72,8 @@
     this.oneOffReqCtx = oneOffReqCtx;
     this.changeId = changeId;
     this.instanceId = instanceId;
+    this.enableDraftCommentEvents =
+        config.getBoolean("event", "stream-events", "enableDraftCommentEvents", false);
   }
 
   @Override
@@ -177,20 +183,22 @@
   }
 
   private Optional<Long> computeLastChangeTs() {
-    return getChangeNotes().map(notes -> getTsFromChangeAndDraftComments(notes));
+    return getChangeNotes().map(this::getTsFromChangeAndDraftComments);
   }
 
   private long getTsFromChangeAndDraftComments(ChangeNotes notes) {
     Change change = notes.getChange();
     Timestamp changeTs = Timestamp.from(change.getLastUpdatedOn());
-    try {
-      for (HumanComment comment :
-          draftCommentsReader.getDraftsByChangeForAllAuthors(changeNotes.get())) {
-        Timestamp commentTs = comment.writtenOn;
-        changeTs = commentTs.after(changeTs) ? commentTs : changeTs;
+    if (enableDraftCommentEvents) {
+      try {
+        for (HumanComment comment :
+            draftCommentsReader.getDraftsByChangeForAllAuthors(changeNotes.get())) {
+          Timestamp commentTs = comment.writtenOn;
+          changeTs = commentTs.after(changeTs) ? commentTs : changeTs;
+        }
+      } catch (StorageException e) {
+        log.warn("Unable to access draft comments for change {}", change, e);
       }
-    } catch (StorageException e) {
-      log.warn("Unable to access draft comments for change {}", change, e);
     }
     return changeTs.getTime() / 1000;
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/index/CurrentRequestContext.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/index/CurrentRequestContext.java
new file mode 100644
index 0000000..d6598b9
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/index/CurrentRequestContext.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2023 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.multisite.index;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.OneOffRequestContext;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.multisite.Configuration;
+import java.util.function.Consumer;
+
+@Singleton
+public class CurrentRequestContext {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private ThreadLocalRequestContext threadLocalCtx;
+  private Configuration cfg;
+  private OneOffRequestContext oneOffCtx;
+
+  @Inject
+  public CurrentRequestContext(
+      ThreadLocalRequestContext threadLocalCtx, Configuration cfg, OneOffRequestContext oneOffCtx) {
+    this.threadLocalCtx = threadLocalCtx;
+    this.cfg = cfg;
+    this.oneOffCtx = oneOffCtx;
+  }
+
+  public void onlyWithContext(Consumer<RequestContext> body) {
+    RequestContext ctx = threadLocalCtx.getContext();
+    if (ctx == null && !cfg.index().synchronizeForced()) {
+      logger.atFine().log("No context, skipping event (index.synchronizeForced is false)");
+      return;
+    }
+
+    if (ctx == null) {
+      try (ManualRequestContext manualCtx = oneOffCtx.open()) {
+        body.accept(manualCtx);
+      }
+    } else {
+      body.accept(ctx);
+    }
+  }
+}
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 ee16b07..9aee56e 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
@@ -48,6 +48,7 @@
   private final ChangeCheckerImpl.Factory changeChecker;
   private final GroupChecker groupChecker;
   private final String instanceId;
+  private final CurrentRequestContext currCtx;
 
   @Inject
   IndexEventHandler(
@@ -55,27 +56,32 @@
       DynamicSet<IndexEventForwarder> forwarders,
       ChangeCheckerImpl.Factory changeChecker,
       GroupChecker groupChecker,
-      @GerritInstanceId String instanceId) {
+      @GerritInstanceId String instanceId,
+      CurrentRequestContext currCtx) {
     this.forwarders = forwarders;
     this.executor = executor;
     this.changeChecker = changeChecker;
     this.groupChecker = groupChecker;
     this.instanceId = instanceId;
+    this.currCtx = currCtx;
   }
 
   @Override
   public void onAccountIndexed(int id) {
-    if (!Context.isForwardedEvent()) {
-      IndexAccountTask task = new IndexAccountTask(new AccountIndexEvent(id, instanceId));
-      if (queuedTasks.add(task)) {
-        executor.execute(task);
-      }
-    }
+    currCtx.onlyWithContext(
+        (ctx) -> {
+          if (!Context.isForwardedEvent()) {
+            IndexAccountTask task = new IndexAccountTask(new AccountIndexEvent(id, instanceId));
+            if (queuedTasks.add(task)) {
+              executor.execute(task);
+            }
+          }
+        });
   }
 
   @Override
   public void onChangeIndexed(String projectName, int id) {
-    executeIndexChangeTask(projectName, id);
+    currCtx.onlyWithContext((ctx) -> executeIndexChangeTask(projectName, id));
   }
 
   @Override
@@ -178,7 +184,9 @@
 
     @Override
     public String toString() {
-      return String.format("Index change %s in target instance", changeIndexEvent.changeId);
+      return String.format(
+          "Index change %s for project %s produced by instance %s",
+          changeIndexEvent.changeId, changeIndexEvent.projectName, changeIndexEvent.instanceId);
     }
   }
 
@@ -209,7 +217,9 @@
 
     @Override
     public String toString() {
-      return String.format("Index change %s in target instance", changeIndexEvent.changeId);
+      return String.format(
+          "Index change %s for project %s produced by instance %s",
+          changeIndexEvent.changeId, changeIndexEvent.projectName, changeIndexEvent.instanceId);
     }
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/MultisiteReplicationFetchFilter.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/MultisiteReplicationFetchFilter.java
index 98d1320..3fc57ae 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/MultisiteReplicationFetchFilter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/MultisiteReplicationFetchFilter.java
@@ -14,6 +14,7 @@
 
 package com.googlesource.gerrit.plugins.multisite.validation;
 
+import static com.googlesource.gerrit.plugins.multisite.validation.ProjectVersionRefUpdate.MULTI_SITE_VERSIONING_REF;
 import static com.googlesource.gerrit.plugins.replication.pull.PullReplicationLogger.repLog;
 
 import com.gerritforge.gerrit.globalrefdb.GlobalRefDbLockException;
@@ -64,6 +65,9 @@
           .filter(ref -> !hasBeenRemovedFromGlobalRefDb(projectName, ref))
           .filter(
               ref -> {
+                if (shouldNotBeTrackedAnymoreOnGlobalRefDb(ref)) {
+                  return true;
+                }
                 Optional<ObjectId> localRefOid =
                     getLocalSha1IfEqualsToExistingGlobalRefDb(
                         repository, projectName, refDb, ref, true);
@@ -76,7 +80,7 @@
                             ref,
                             oid.getName()));
 
-                return localRefOid.isEmpty();
+                return !localRefOid.isPresent();
               })
           .collect(Collectors.toSet());
     } catch (IOException ioe) {
@@ -87,6 +91,19 @@
     }
   }
 
+  /*
+   * Since ac43a5f94c773c9db7a73d44035961d69d13fa53 the 'refs/multi-site/version' is
+   * not updated anymore on the global-refdb; however, the values stored already
+   * on the global-refdb could get in the way and prevent replication from happening
+   * as expected.
+   *
+   * Exclude the 'refs/multi-site/version' from local vs. global refdb checking
+   * pretending that the global-refdb for that ref did not exist.
+   */
+  private boolean shouldNotBeTrackedAnymoreOnGlobalRefDb(String ref) {
+    return MULTI_SITE_VERSIONING_REF.equals(ref);
+  }
+
   /* If the ref to fetch has been set to all zeros on the global-refdb, it means
    * that whatever is the situation locally, we do not need to fetch it:
    * - If the remote still has it, fetching it will be useless because the global
@@ -129,7 +146,7 @@
                           .orElse(false))
               .map(Ref::getObjectId);
 
-      if (localRefObjectId.isEmpty() && retryWithRandomSleep) {
+      if (!localRefObjectId.isPresent() && retryWithRandomSleep) {
         randomSleepForMitigatingConditionWhereLocalRefHaveJustBeenChanged(
             projectName, localRefObjectId, ref);
         localRefObjectId =
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdateImpl.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdateImpl.java
index aa589b6..adbef1b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdateImpl.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdateImpl.java
@@ -25,15 +25,12 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.Project.NameKey;
-import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.server.config.GerritInstanceId;
-import com.google.gerrit.server.events.Event;
-import com.google.gerrit.server.events.EventListener;
-import com.google.gerrit.server.events.RefUpdatedEvent;
+import com.google.gerrit.extensions.events.GitBatchRefUpdateListener;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.multisite.ProjectVersionLogger;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
@@ -45,7 +42,9 @@
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 
-public class ProjectVersionRefUpdateImpl implements EventListener, ProjectVersionRefUpdate {
+@Singleton
+public class ProjectVersionRefUpdateImpl
+    implements GitBatchRefUpdateListener, ProjectVersionRefUpdate {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
   private static final Set<RefUpdate.Result> SUCCESSFUL_RESULTS =
       ImmutableSet.of(RefUpdate.Result.NEW, RefUpdate.Result.FORCED, RefUpdate.Result.NO_CHANGE);
@@ -53,7 +52,6 @@
   private final GitRepositoryManager gitRepositoryManager;
   private final GitReferenceUpdated gitReferenceUpdated;
   private final ProjectVersionLogger verLogger;
-  private final String nodeInstanceId;
 
   protected final SharedRefDatabaseWrapper sharedRefDb;
 
@@ -62,61 +60,43 @@
       GitRepositoryManager gitRepositoryManager,
       SharedRefDatabaseWrapper sharedRefDb,
       GitReferenceUpdated gitReferenceUpdated,
-      ProjectVersionLogger verLogger,
-      @GerritInstanceId String nodeInstanceId) {
+      ProjectVersionLogger verLogger) {
     this.gitRepositoryManager = gitRepositoryManager;
     this.sharedRefDb = sharedRefDb;
     this.gitReferenceUpdated = gitReferenceUpdated;
     this.verLogger = verLogger;
-    this.nodeInstanceId = nodeInstanceId;
   }
 
   @Override
-  public void onEvent(Event event) {
-    logger.atFine().log("Processing event type: %s", event.type);
+  public void onGitBatchRefUpdate(Event event) {
     // Producer of the Event use RefUpdatedEvent to trigger the version update
-    if (nodeInstanceId.equals(event.instanceId) && event instanceof RefUpdatedEvent) {
-      updateProducerProjectVersionUpdate((RefUpdatedEvent) event);
-    }
+    updateProducerProjectVersionUpdate(event);
   }
 
-  private boolean isSpecialRefName(String refName) {
-    return refName.startsWith(RefNames.REFS_SEQUENCES)
-        || refName.startsWith(RefNames.REFS_STARRED_CHANGES)
-        || refName.equals(MULTI_SITE_VERSIONING_REF);
-  }
-
-  private void updateProducerProjectVersionUpdate(RefUpdatedEvent refUpdatedEvent) {
-    String refName = refUpdatedEvent.getRefName();
-
-    if (isSpecialRefName(refName)) {
+  private void updateProducerProjectVersionUpdate(Event refUpdatedEvent) {
+    if (refUpdatedEvent.getRefNames().stream()
+        .allMatch(refName -> refName.equals(MULTI_SITE_VERSIONING_REF))) {
       logger.atFine().log(
           "Found a special ref name %s, skipping update for %s",
-          refName, refUpdatedEvent.getProjectNameKey().get());
+          MULTI_SITE_VERSIONING_REF, refUpdatedEvent.getProjectName());
       return;
     }
+
     try {
-      Project.NameKey projectNameKey = refUpdatedEvent.getProjectNameKey();
+      Project.NameKey projectNameKey = Project.nameKey(refUpdatedEvent.getProjectName());
       long newVersion = getCurrentGlobalVersionNumber();
 
-      Optional<RefUpdate> newProjectVersionRefUpdate =
-          updateLocalProjectVersion(projectNameKey, newVersion);
+      RefUpdate newProjectVersionRefUpdate = updateLocalProjectVersion(projectNameKey, newVersion);
 
-      if (newProjectVersionRefUpdate.isPresent()) {
-        verLogger.log(projectNameKey, newVersion, 0L);
+      verLogger.log(projectNameKey, newVersion, 0L);
 
-        if (updateSharedProjectVersion(projectNameKey, newVersion)) {
-          gitReferenceUpdated.fire(projectNameKey, newProjectVersionRefUpdate.get(), null);
-        }
-      } else {
-        logger.atWarning().log(
-            "Ref %s not found on projet %s: skipping project version update",
-            refUpdatedEvent.getRefName(), projectNameKey);
+      if (updateSharedProjectVersion(projectNameKey, newVersion)) {
+        gitReferenceUpdated.fire(projectNameKey, newProjectVersionRefUpdate, null);
       }
     } catch (LocalProjectVersionUpdateException | SharedProjectVersionUpdateException e) {
       logger.atSevere().withCause(e).log(
           "Issue encountered when updating version for project %s",
-          refUpdatedEvent.getProjectNameKey());
+          refUpdatedEvent.getProjectName());
     }
   }
 
@@ -249,8 +229,7 @@
   }
 
   @SuppressWarnings("FloggerLogString")
-  private Optional<RefUpdate> updateLocalProjectVersion(
-      Project.NameKey projectNameKey, long newVersionNumber)
+  private RefUpdate updateLocalProjectVersion(Project.NameKey projectNameKey, long newVersionNumber)
       throws LocalProjectVersionUpdateException {
     logger.atFine().log(
         "Updating local version for project %s with version %d",
@@ -268,7 +247,7 @@
         throw new LocalProjectVersionUpdateException(message);
       }
 
-      return Optional.of(refUpdate);
+      return refUpdate;
     } catch (IOException e) {
       String message = "Cannot create versioning command for " + projectNameKey.get();
       logger.atSevere().withCause(e).log(message);
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 0cfd6ea..3baa911 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -44,6 +44,11 @@
     service such as ElasticSearch.
     Defaults to true.
 
+```index.synchronizeForced```
+:   Whether to synchronize forced index events. E.g. on-line reindex
+automatically triggered upon version upgrades.
+Defaults to true.
+
 ```index.threadPoolSize```
 :   Maximum number of threads used to send index events to the target instance.
     Defaults to 4.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/ConfigurationTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/ConfigurationTest.java
index 0863f55..3a24773 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/ConfigurationTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/ConfigurationTest.java
@@ -21,7 +21,9 @@
 import static com.googlesource.gerrit.plugins.multisite.Configuration.Event.EVENT_SECTION;
 import static com.googlesource.gerrit.plugins.multisite.Configuration.Forwarding.DEFAULT_SYNCHRONIZE;
 import static com.googlesource.gerrit.plugins.multisite.Configuration.Forwarding.SYNCHRONIZE_KEY;
+import static com.googlesource.gerrit.plugins.multisite.Configuration.Index.DEFAULT_SYNCHRONIZE_FORCED;
 import static com.googlesource.gerrit.plugins.multisite.Configuration.Index.INDEX_SECTION;
+import static com.googlesource.gerrit.plugins.multisite.Configuration.Index.SYNCHRONIZE_FORCED_KEY;
 import static com.googlesource.gerrit.plugins.multisite.Configuration.THREAD_POOL_SIZE_KEY;
 
 import com.google.common.collect.ImmutableList;
@@ -129,4 +131,13 @@
     replicationConfig.setBoolean("gerrit", null, "replicateOnStartup", true);
     assertThat(new Configuration(globalPluginConfig, replicationConfig).validate()).isNotEmpty();
   }
+
+  @Test
+  public void testGetIndexSynchronizeForced() throws Exception {
+    assertThat(getConfiguration().index().synchronizeForced())
+        .isEqualTo(DEFAULT_SYNCHRONIZE_FORCED);
+
+    globalPluginConfig.setBoolean(INDEX_SECTION, null, SYNCHRONIZE_FORCED_KEY, false);
+    assertThat(getConfiguration().index().synchronizeForced()).isFalse();
+  }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/index/GroupCheckerImplTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/index/GroupCheckerImplTest.java
index 09f8f59..c612119 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/index/GroupCheckerImplTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/index/GroupCheckerImplTest.java
@@ -30,7 +30,6 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectIdRef;
 import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.Repository;
 import org.junit.Before;
 import org.junit.Test;
@@ -45,13 +44,11 @@
 
   GroupCheckerImpl objectUnderTest;
   @Mock private GitRepositoryManager repoManagerMock;
-  @Mock private RefDatabase refDatabaseMock;
   @Mock private Repository repoMock;
 
   @Before
   public void setUp() throws Exception {
     doReturn(repoMock).when(repoManagerMock).openRepository(allUsers);
-    doReturn(refDatabaseMock).when(repoMock).getRefDatabase();
     objectUnderTest = new GroupCheckerImpl(repoManagerMock, allUsers);
   }
 
@@ -118,6 +115,6 @@
       throws IOException {
     String groupRefName = RefNames.refsGroups(AccountGroup.uuid(groupUUID.toString()));
     ObjectIdRef.Unpeeled aRef = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, groupRefName, objectId);
-    doReturn(objectId == null ? null : aRef).when(refDatabaseMock).exactRef(groupRefName);
+    doReturn(objectId == null ? null : aRef).when(repoMock).exactRef(groupRefName);
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/index/IndexEventHandlerTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/index/IndexEventHandlerTest.java
index ae5851d..7e27eeb 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/index/IndexEventHandlerTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/index/IndexEventHandlerTest.java
@@ -15,14 +15,26 @@
 package com.googlesource.gerrit.plugins.multisite.index;
 
 import static com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationEndpoints.APPLY_OBJECT_API_ENDPOINT;
-import static org.mockito.Mockito.anyString;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
 
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.util.OneOffRequestContext;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
+import com.googlesource.gerrit.plugins.multisite.Configuration;
 import com.googlesource.gerrit.plugins.multisite.forwarder.Context;
 import com.googlesource.gerrit.plugins.multisite.forwarder.IndexEventForwarder;
+import com.googlesource.gerrit.plugins.multisite.forwarder.events.ChangeIndexEvent;
+import java.io.IOException;
+import java.util.Optional;
+import java.util.function.Consumer;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -33,21 +45,34 @@
 public class IndexEventHandlerTest {
 
   private static final String INSTANCE_ID = "instance-id";
+  private static final String PROJECT_NAME = "test_project";
+  private static final int CHANGE_ID = 1;
 
   private IndexEventHandler eventHandler;
 
   @Mock private IndexEventForwarder forwarder;
   @Mock private ChangeCheckerImpl.Factory changeChecker;
+  @Mock private ChangeChecker changeCheckerMock;
+  @Mock private RequestContext mockCtx;
+
+  private CurrentRequestContext currCtx =
+      new CurrentRequestContext(null, null, null) {
+        @Override
+        public void onlyWithContext(Consumer<RequestContext> body) {
+          body.accept(mockCtx);
+        }
+      };
 
   @Before
-  public void setUp() {
+  public void setUp() throws IOException {
     eventHandler =
         new IndexEventHandler(
             MoreExecutors.directExecutor(),
             asDynamicSet(forwarder),
             changeChecker,
             new TestGroupChecker(true),
-            INSTANCE_ID);
+            INSTANCE_ID,
+            currCtx);
   }
 
   private DynamicSet<IndexEventForwarder> asDynamicSet(IndexEventForwarder forwarder) {
@@ -62,17 +87,54 @@
     String currentThreadName = Thread.currentThread().getName();
     try {
       Thread.currentThread().setName("pull-replication~" + APPLY_OBJECT_API_ENDPOINT);
-      int changeId = 1;
       Context.setForwardedEvent(false);
       lenient()
           .when(changeChecker.create(anyString()))
           .thenThrow(
               new IllegalStateException("Change indexing event should have not been triggered"));
 
-      eventHandler.onChangeIndexed("test_project", changeId);
+      eventHandler.onChangeIndexed(PROJECT_NAME, CHANGE_ID);
       verifyNoInteractions(changeChecker);
     } finally {
       Thread.currentThread().setName(currentThreadName);
     }
   }
+
+  @Test
+  public void shouldNotForwardIndexChangeWhenContextIsMissingAndForcedIndexingDisabled()
+      throws Exception {
+    eventHandler = createIndexEventHandler(changeChecker, false);
+    eventHandler.onChangeIndexed(PROJECT_NAME, CHANGE_ID);
+    verifyNoInteractions(changeChecker);
+    verifyNoInteractions(forwarder);
+  }
+
+  @Test
+  public void shouldForwardIndexChangeWhenContextIsMissingAndForcedIndexingEnabled()
+      throws Exception {
+    when(changeChecker.create(any())).thenReturn(changeCheckerMock);
+    when(changeCheckerMock.newIndexEvent(PROJECT_NAME, CHANGE_ID, false))
+        .thenReturn(Optional.of(new ChangeIndexEvent(PROJECT_NAME, CHANGE_ID, false, INSTANCE_ID)));
+    eventHandler = createIndexEventHandler(changeChecker, true);
+    eventHandler.onChangeIndexed(PROJECT_NAME, CHANGE_ID);
+    verify(changeCheckerMock).newIndexEvent(PROJECT_NAME, CHANGE_ID, false);
+    verify(forwarder).index(any(), any());
+  }
+
+  private IndexEventHandler createIndexEventHandler(
+      ChangeCheckerImpl.Factory changeChecker, boolean synchronizeForced) {
+    ThreadLocalRequestContext threadLocalCtxMock = mock(ThreadLocalRequestContext.class);
+    OneOffRequestContext oneOffCtxMock = mock(OneOffRequestContext.class);
+    Configuration cfgMock = mock(Configuration.class);
+    Configuration.Index cfgIndex = mock(Configuration.Index.class);
+    when(cfgMock.index()).thenReturn(cfgIndex);
+    when(cfgIndex.synchronizeForced()).thenReturn(synchronizeForced);
+    return new IndexEventHandler(
+        MoreExecutors.directExecutor(),
+        asDynamicSet(forwarder),
+        changeChecker,
+        new TestGroupChecker(true),
+        INSTANCE_ID,
+        new CurrentRequestContext(threadLocalCtxMock, cfgMock, oneOffCtxMock));
+  }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultisiteReplicationFetchFilterTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultisiteReplicationFetchFilterTest.java
index 9a7a3b3..e09f6f6 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultisiteReplicationFetchFilterTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultisiteReplicationFetchFilterTest.java
@@ -170,6 +170,24 @@
   }
 
   @Test
+  public void shouldNotFilterOutWhenRefsMultisiteVersionIsPresentInSharedRefDb() throws Exception {
+    String refsMultisiteVersionRef = ProjectVersionRefUpdate.MULTI_SITE_VERSIONING_REF;
+    RevCommit multiSiteVersionRef = newRef(refsMultisiteVersionRef);
+
+    doReturn(Optional.of(multiSiteVersionRef.getId().getName()))
+        .when(sharedRefDatabaseMock)
+        .get(eq(projectName), eq(refsMultisiteVersionRef), eq(String.class));
+
+    Set<String> refsToFetch = Set.of(refsMultisiteVersionRef);
+
+    MultisiteReplicationFetchFilter fetchFilter =
+        new MultisiteReplicationFetchFilter(sharedRefDatabaseMock, gitRepositoryManager, config);
+    Set<String> filteredRefsToFetch = fetchFilter.filter(project, refsToFetch);
+
+    assertThat(filteredRefsToFetch).hasSize(1);
+  }
+
+  @Test
   public void shouldFilterOutWhenRefIsDeletedInTheSharedRefDb() throws Exception {
     String temporaryOutdated = "refs/heads/temporaryOutdated";
     newRef(temporaryOutdated);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdateTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdateTest.java
index 1cfb2cd..e2e3113 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdateTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdateTest.java
@@ -28,11 +28,9 @@
 import static org.mockito.Mockito.when;
 
 import com.gerritforge.gerrit.globalrefdb.validation.SharedRefDatabaseWrapper;
-import com.google.common.base.Suppliers;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.server.data.RefUpdateAttribute;
-import com.google.gerrit.server.events.RefUpdatedEvent;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.events.GitBatchRefUpdateListener;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
@@ -44,6 +42,7 @@
 import java.io.UnsupportedEncodingException;
 import java.nio.charset.StandardCharsets;
 import java.util.Optional;
+import java.util.Set;
 import org.eclipse.jgit.errors.LargeObjectException;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
@@ -51,6 +50,7 @@
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.ReceiveCommand;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -61,11 +61,14 @@
 @RunWith(MockitoJUnitRunner.class)
 public class ProjectVersionRefUpdateTest implements RefFixture {
 
-  private static final String DEFAULT_INSTANCE_ID = "instance-id";
+  private static final int A_TEST_ACCOUNT_ID = 1;
+  private static final GitReferenceUpdated.UpdatedRef A_TEST_UPDATED_REF =
+      new GitReferenceUpdated.UpdatedRef(
+          A_TEST_REF_NAME, ObjectId.zeroId(), ObjectId.zeroId(), ReceiveCommand.Type.UPDATE);
 
   @Rule public InMemoryTestEnvironment testEnvironment = new InMemoryTestEnvironment();
 
-  @Mock RefUpdatedEvent refUpdatedEvent;
+  @Mock GitBatchRefUpdateListener.Event refUpdatedEvent;
   @Mock SharedRefDatabaseWrapper sharedRefDb;
   @Mock GitReferenceUpdated gitReferenceUpdated;
   @Mock ProjectVersionLogger verLogger;
@@ -78,7 +81,6 @@
 
   @Before
   public void setUp() throws Exception {
-    refUpdatedEvent.instanceId = DEFAULT_INSTANCE_ID;
     InMemoryRepository inMemoryRepo = repoManager.createRepository(A_TEST_PROJECT_NAME_KEY);
     project = projectConfigFactory.create(A_TEST_PROJECT_NAME_KEY);
     project.load(inMemoryRepo);
@@ -95,12 +97,11 @@
         .thenReturn(Optional.of("" + (masterCommit.getCommitTime() - 1)));
     when(sharedRefDb.compareAndPut(any(Project.NameKey.class), any(String.class), any(), any()))
         .thenReturn(true);
-    when(refUpdatedEvent.getProjectNameKey()).thenReturn(A_TEST_PROJECT_NAME_KEY);
-    when(refUpdatedEvent.getRefName()).thenReturn(A_TEST_REF_NAME);
+    when(refUpdatedEvent.getProjectName()).thenReturn(A_TEST_PROJECT_NAME);
+    when(refUpdatedEvent.getRefNames()).thenReturn(Set.of(A_TEST_REF_NAME));
 
-    new ProjectVersionRefUpdateImpl(
-            repoManager, sharedRefDb, gitReferenceUpdated, verLogger, DEFAULT_INSTANCE_ID)
-        .onEvent(refUpdatedEvent);
+    new ProjectVersionRefUpdateImpl(repoManager, sharedRefDb, gitReferenceUpdated, verLogger)
+        .onGitBatchRefUpdate(refUpdatedEvent);
 
     Ref ref = repo.getRepository().findRef(MULTI_SITE_VERSIONING_REF);
 
@@ -117,19 +118,47 @@
   }
 
   @Test
+  public void producerShouldUpdateProjectVersionOnceUponMultipleRefUpdatedEvent() {
+    when(sharedRefDb.compareAndPut(any(Project.NameKey.class), any(String.class), any(), any()))
+        .thenReturn(true);
+    when(refUpdatedEvent.getProjectName()).thenReturn(A_TEST_PROJECT_NAME);
+    when(refUpdatedEvent.getRefNames())
+        .thenReturn(Set.of(A_TEST_REF_NAME, A_REF_NAME_OF_A_PATCHSET));
+
+    new ProjectVersionRefUpdateImpl(repoManager, sharedRefDb, gitReferenceUpdated, verLogger)
+        .onGitBatchRefUpdate(refUpdatedEvent);
+
+    verify(sharedRefDb, atMost(1))
+        .compareAndPut(any(Project.NameKey.class), any(Ref.class), any(ObjectId.class));
+  }
+
+  @Test
+  public void producerShouldUpdateProjectVersionOnceUponMultipleRefWithOneMetaUpdatedEvent() {
+    when(sharedRefDb.compareAndPut(any(Project.NameKey.class), any(String.class), any(), any()))
+        .thenReturn(true);
+    when(refUpdatedEvent.getProjectName()).thenReturn(A_TEST_PROJECT_NAME);
+    when(refUpdatedEvent.getRefNames())
+        .thenReturn(Set.of(A_TEST_REF_NAME, MULTI_SITE_VERSIONING_REF));
+
+    new ProjectVersionRefUpdateImpl(repoManager, sharedRefDb, gitReferenceUpdated, verLogger)
+        .onGitBatchRefUpdate(refUpdatedEvent);
+
+    verify(sharedRefDb, atMost(1))
+        .compareAndPut(any(Project.NameKey.class), any(Ref.class), any(ObjectId.class));
+  }
+
+  @Test
   public void producerShouldUsePutInsteadOfCompareAndPutWhenExtendedGlobalRefDb()
       throws IOException {
     when(sharedRefDb.isSetOperationSupported()).thenReturn(true);
-    RefUpdatedEvent refUpdatedEvent = new RefUpdatedEvent();
-    refUpdatedEvent.instanceId = DEFAULT_INSTANCE_ID;
-    RefUpdateAttribute refUpdatedAttribute = new RefUpdateAttribute();
-    refUpdatedAttribute.project = A_TEST_PROJECT_NAME_KEY.get();
-    refUpdatedAttribute.refName = A_TEST_REF_NAME;
-    refUpdatedEvent.refUpdate = Suppliers.memoize(() -> refUpdatedAttribute);
+    GitBatchRefUpdateListener.Event refUpdatedEvent =
+        new GitReferenceUpdated.GitBatchRefUpdateEvent(
+            A_TEST_PROJECT_NAME_KEY,
+            Set.of(A_TEST_UPDATED_REF),
+            new AccountInfo(A_TEST_ACCOUNT_ID));
 
-    new ProjectVersionRefUpdateImpl(
-            repoManager, sharedRefDb, gitReferenceUpdated, verLogger, DEFAULT_INSTANCE_ID)
-        .onEvent(refUpdatedEvent);
+    new ProjectVersionRefUpdateImpl(repoManager, sharedRefDb, gitReferenceUpdated, verLogger)
+        .onGitBatchRefUpdate(refUpdatedEvent);
 
     Ref ref = repo.getRepository().findRef(MULTI_SITE_VERSIONING_REF);
 
@@ -160,13 +189,12 @@
         .thenReturn(Optional.of("" + (masterCommit.getCommitTime() - 1)));
     when(sharedRefDb.compareAndPut(any(Project.NameKey.class), any(String.class), any(), any()))
         .thenReturn(true);
-    when(refUpdatedEvent.getProjectNameKey()).thenReturn(A_TEST_PROJECT_NAME_KEY);
-    when(refUpdatedEvent.getRefName()).thenReturn(A_TEST_REF_NAME);
+    when(refUpdatedEvent.getProjectName()).thenReturn(A_TEST_PROJECT_NAME);
+    when(refUpdatedEvent.getRefNames()).thenReturn(Set.of(A_TEST_REF_NAME));
 
     ProjectVersionRefUpdateImpl projectVersion =
-        new ProjectVersionRefUpdateImpl(
-            repoManager, sharedRefDb, gitReferenceUpdated, verLogger, DEFAULT_INSTANCE_ID);
-    projectVersion.onEvent(refUpdatedEvent);
+        new ProjectVersionRefUpdateImpl(repoManager, sharedRefDb, gitReferenceUpdated, verLogger);
+    projectVersion.onGitBatchRefUpdate(refUpdatedEvent);
 
     Ref ref = repo.getRepository().findRef(MULTI_SITE_VERSIONING_REF);
 
@@ -197,12 +225,11 @@
 
     when(sharedRefDb.compareAndPut(any(Project.NameKey.class), any(String.class), any(), any()))
         .thenReturn(true);
-    when(refUpdatedEvent.getProjectNameKey()).thenReturn(A_TEST_PROJECT_NAME_KEY);
-    when(refUpdatedEvent.getRefName()).thenReturn(A_TEST_REF_NAME);
+    when(refUpdatedEvent.getProjectName()).thenReturn(A_TEST_PROJECT_NAME);
+    when(refUpdatedEvent.getRefNames()).thenReturn(Set.of(A_TEST_REF_NAME));
 
-    new ProjectVersionRefUpdateImpl(
-            repoManager, sharedRefDb, gitReferenceUpdated, verLogger, DEFAULT_INSTANCE_ID)
-        .onEvent(refUpdatedEvent);
+    new ProjectVersionRefUpdateImpl(repoManager, sharedRefDb, gitReferenceUpdated, verLogger)
+        .onGitBatchRefUpdate(refUpdatedEvent);
 
     Ref ref = repo.getRepository().findRef(MULTI_SITE_VERSIONING_REF);
 
@@ -218,63 +245,19 @@
     verify(verLogger).log(A_TEST_PROJECT_NAME_KEY, storedVersion, 0);
   }
 
-  @Test
-  public void producerShouldNotUpdateProjectVersionUponSequenceRefUpdatedEvent() throws Exception {
-    producerShouldNotUpdateProjectVersionUponMagicRefUpdatedEvent(RefNames.REFS_SEQUENCES);
-  }
-
-  @Test
-  public void producerShouldNotUpdateProjectVersionUponStarredChangesRefUpdatedEvent()
-      throws Exception {
-    producerShouldNotUpdateProjectVersionUponMagicRefUpdatedEvent(RefNames.REFS_STARRED_CHANGES);
-  }
-
   private long readLongObject(ObjectLoader loader)
       throws LargeObjectException, UnsupportedEncodingException {
     String boutString = new String(loader.getBytes(), StandardCharsets.UTF_8.name());
     return Long.parseLong(boutString);
   }
 
-  private void producerShouldNotUpdateProjectVersionUponMagicRefUpdatedEvent(String magicRefPrefix)
-      throws Exception {
-    String magicRefName = magicRefPrefix + "/foo";
-    when(refUpdatedEvent.getProjectNameKey()).thenReturn(A_TEST_PROJECT_NAME_KEY);
-    when(refUpdatedEvent.getRefName()).thenReturn(magicRefName);
-    repo.branch(magicRefName).commit().create();
-
-    new ProjectVersionRefUpdateImpl(
-            repoManager, sharedRefDb, gitReferenceUpdated, verLogger, DEFAULT_INSTANCE_ID)
-        .onEvent(refUpdatedEvent);
-
-    Ref ref = repo.getRepository().findRef(MULTI_SITE_VERSIONING_REF);
-    assertThat(ref).isNull();
-
-    verifyNoInteractions(verLogger);
-  }
-
-  @Test
-  public void producerShouldNotUpdateProjectVersionUponForwardedRefUpdatedEvent()
-      throws IOException {
-    refUpdatedEvent.instanceId = "instance-id-2";
-
-    new ProjectVersionRefUpdateImpl(
-            repoManager, sharedRefDb, gitReferenceUpdated, verLogger, DEFAULT_INSTANCE_ID)
-        .onEvent(refUpdatedEvent);
-
-    Ref ref = repo.getRepository().findRef(MULTI_SITE_VERSIONING_REF);
-    assertThat(ref).isNull();
-
-    verifyNoInteractions(verLogger);
-  }
-
   @Test
   public void shouldNotUpdateProjectVersionWhenProjectDoesntExist() throws IOException {
-    when(refUpdatedEvent.getProjectNameKey()).thenReturn(Project.nameKey("aNonExistentProject"));
-    when(refUpdatedEvent.getRefName()).thenReturn(A_TEST_REF_NAME);
+    when(refUpdatedEvent.getProjectName()).thenReturn("aNonExistentProject");
+    when(refUpdatedEvent.getRefNames()).thenReturn(Set.of(A_TEST_REF_NAME));
 
-    new ProjectVersionRefUpdateImpl(
-            repoManager, sharedRefDb, gitReferenceUpdated, verLogger, DEFAULT_INSTANCE_ID)
-        .onEvent(refUpdatedEvent);
+    new ProjectVersionRefUpdateImpl(repoManager, sharedRefDb, gitReferenceUpdated, verLogger)
+        .onGitBatchRefUpdate(refUpdatedEvent);
 
     Ref ref = repo.getRepository().findRef(MULTI_SITE_VERSIONING_REF);
     assertThat(ref).isNull();
@@ -288,8 +271,7 @@
         .thenReturn(Optional.of("123"));
 
     Optional<Long> version =
-        new ProjectVersionRefUpdateImpl(
-                repoManager, sharedRefDb, gitReferenceUpdated, verLogger, DEFAULT_INSTANCE_ID)
+        new ProjectVersionRefUpdateImpl(repoManager, sharedRefDb, gitReferenceUpdated, verLogger)
             .getProjectRemoteVersion(A_TEST_PROJECT_NAME);
 
     assertThat(version.isPresent()).isTrue();