Merge branch 'stable-3.8'

Also adapt to the latest changes in master where
CommentsUtil.draftByChange has been replaced by
DraftCommentsReader.getDraftsByChangeForAllAuthors.

* stable-3.8:
  Add newline at EOF as required by HAProxy
  Use GerritForge's archive-ci for downloading artifacts
  Fix issue with disabling ref-database
  Use a bucketed metric for the project-specific replication lag
  Fix pull replication fetch filter for non-existing refs on global-refdb
  Bump global-refdb to v3.4.8.6
  Address review comments of Change 376577
  Parse refs/multi-site/version as a Long BLOB value
  Bump global-refdb/events-broker to v3.8.0-rc5
  Bump events-broker to v3.7.2
  Use msec-level precision for refs/multi-site/version value
  Remove unused flogger
  Create replication lag metric per repository
  Fix errorprone errors [FloggerLogString]
  Fix duplicate log appender creation
  Allow running docker with sudo
  Use docker compose v2 format
  Move EventHandler binding from lib module to plugin module
  Do not forward indexing events from pull-replication/apply-object
  Add pull-replication plugin in local environment test
  Use archive-ci.gerritforge.com for downloading plugins
  Add handling of multi-base local disk repositories

Change-Id: I7b19c415c62970f22c1e48c55b54867dc5783f11
diff --git a/e2e-tests/test.sh b/e2e-tests/test.sh
index abdc5c5..e1c3bf3 100755
--- a/e2e-tests/test.sh
+++ b/e2e-tests/test.sh
@@ -16,7 +16,7 @@
 
 LOCATION="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
 LOCAL_ENV="$( cd "${LOCATION}/../setup_local_env" >/dev/null 2>&1 && pwd )"
-GERRIT_BRANCH=master
+GERRIT_BRANCH=stable-3.6
 GERRIT_CI=https://gerrit-ci.gerritforge.com/view/Plugins-$GERRIT_BRANCH/job
 LAST_BUILD=lastSuccessfulBuild/artifact/bazel-bin/plugins
 DEF_MULTISITE_LOCATION=${LOCATION}/../../../bazel-bin/plugins/multi-site/multi-site.jar
@@ -232,7 +232,7 @@
   -O $COMMON_PLUGINS/websession-broker.jar || { echo >&2 "Cannot download websession-broker plugin: Check internet connection. Aborting"; exit 1; }
 
 echo "Downloading healthcheck plugin $GERRIT_BRANCH"
-wget $GERRIT_CI/plugin-healthcheck-bazel-master/$LAST_BUILD/healthcheck/healthcheck.jar \
+wget $GERRIT_CI/plugin-healthcheck-bazel-$GERRIT_BRANCH/$LAST_BUILD/healthcheck/healthcheck.jar \
   -O $COMMON_PLUGINS/healthcheck.jar || { echo >&2 "Cannot download healthcheck plugin: Check internet connection. Aborting"; exit 1; }
 
 echo "Downloading zookeeper plugin $GERRIT_BRANCH"
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
index 5dfb485..b4fc691 100644
--- a/external_plugin_deps.bzl
+++ b/external_plugin_deps.bzl
@@ -3,13 +3,13 @@
 def external_plugin_deps():
     maven_jar(
         name = "global-refdb",
-        artifact = "com.gerritforge:global-refdb:3.7.2.1",
-        sha1 = "be8177669a281f8d14e9e3b3231ee86a806710d3",
+        artifact = "com.gerritforge:global-refdb:3.8.0-rc5",
+        sha1 = "3f54f99e4ddf454c119e8ac3183bba7e9d7c7897",
     )
 
     maven_jar(
         name = "events-broker",
-        artifact = "com.gerritforge:events-broker:3.6.3",
-        sha1 = "2a78d4492810d5b4280c6a92e6b8bbdadaffe7d2",
+        artifact = "com.gerritforge:events-broker:3.8.0-rc5",
+        sha1 = "d62a2e5e49a6f77fff1221d05835bc84481bcb0a",
     )
 
diff --git a/setup_local_env/setup.sh b/setup_local_env/setup.sh
index 9b0d868..1f464af 100755
--- a/setup_local_env/setup.sh
+++ b/setup_local_env/setup.sh
@@ -26,7 +26,7 @@
   type haproxy >/dev/null 2>&1 || { echo >&2 "Require haproxy but it's not installed. Aborting."; exit 1; }
   type java >/dev/null 2>&1 || { echo >&2 "Require java but it's not installed. Aborting."; exit 1; }
   type docker >/dev/null 2>&1 || { echo >&2 "Require docker but it's not installed. Aborting."; exit 1; }
-  type docker-compose >/dev/null 2>&1 || { echo >&2 "Require docker-compose but it's not installed. Aborting."; exit 1; }
+  [ $($SUDO docker --version | awk '{print $3}' | cut -d '.' -f 1) -ge 20 ] || { echo >&2 "Require docker v20 or later. Aborting."; exit 1; }
   type wget >/dev/null 2>&1 || { echo >&2 "Require wget but it's not installed. Aborting."; exit 1; }
   type envsubst >/dev/null 2>&1 || { echo >&2 "Require envsubst but it's not installed. Aborting."; exit 1; }
   type openssl >/dev/null 2>&1 || { echo >&2 "Require openssl but it's not installed. Aborting."; exit 1; }
@@ -159,7 +159,7 @@
 }
 
 function is_docker_desktop {
-  echo $(docker info | grep "Operating System: Docker Desktop" | wc -l)
+  echo $($SUDO docker info | grep "Operating System: Docker Desktop" | wc -l)
 }
 
 function docker_host_env {
@@ -198,9 +198,10 @@
   kill $(ps -ax | grep haproxy | grep "gerrit_setup/ha-proxy-config" | awk '{print $1}') 2> /dev/null
 
   echo "Stopping $BROKER_TYPE docker container"
-  docker-compose -f "${SCRIPT_DIR}/docker-compose-${BROKER_TYPE}.yaml" down 2> /dev/null
+  printenv > "${SCRIPT_DIR}/docker-compose-${BROKER_TYPE}.env"
+  $SUDO docker compose -f "${SCRIPT_DIR}/docker-compose-${BROKER_TYPE}.yaml" --env-file "${SCRIPT_DIR}/docker-compose-${BROKER_TYPE}.env" down 2> /dev/null
   echo "Stopping core docker containers"
-  docker-compose -f "${SCRIPT_DIR}/docker-compose-core.yaml" down 2> /dev/null
+  $SUDO docker compose -f "${SCRIPT_DIR}/docker-compose-core.yaml" --env-file "${SCRIPT_DIR}/docker-compose-${BROKER_TYPE}.env" down 2> /dev/null
 
   echo "Stopping GERRIT instances"
   $1/bin/gerrit.sh stop 2> /dev/null
@@ -212,7 +213,7 @@
 
 function check_if_container_is_running {
   local container=$1;
-  echo $(docker inspect "$container" 2> /dev/null | grep '"Running": true' | wc -l)
+  echo $($SUDO docker inspect "$container" 2> /dev/null | grep '"Running": true' | wc -l)
 }
 
 function ensure_docker_compose_is_up_and_running {
@@ -222,8 +223,9 @@
 
   local is_container_running=$(check_if_container_is_running "$container_name")
   if [ "$is_container_running" -lt 1 ];then
+    printenv > "${SCRIPT_DIR}/${docker_compose_file}.env"
     echo "[$log_label] Starting docker containers"
-    docker-compose -f "${SCRIPT_DIR}/${docker_compose_file}" up -d
+    $SUDO docker compose -f "${SCRIPT_DIR}/${docker_compose_file}" --env-file "${SCRIPT_DIR}/${docker_compose_file}.env" up -d
 
     echo "[$log_label] Waiting for docker containers to start..."
     while [[ $(check_if_container_is_running "$container_name") -lt 1 ]];do sleep 10s; done
@@ -272,6 +274,8 @@
     echo
     echo "[--broker-type]                 events broker type; 'kafka', 'kinesis' or 'gcloud-pubsub'. Default 'kafka'"
     echo
+    echo "[--sudo]                        run docker commands with sudo"
+    echo
     exit 0
   ;;
   "--new-deployment")
@@ -368,6 +372,11 @@
       exit 1
     fi
   ;;
+  "--sudo" )
+    SUDO=sudo
+    shift
+    shift
+  ;;
   *     )
     echo "Unknown option argument: $1"
     shift
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 3e75cf9..5c443e7 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/Configuration.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/Configuration.java
@@ -167,6 +167,13 @@
           null,
           SharedRefDbConfiguration.SharedRefDatabase.ENABLE_KEY,
           true);
+      if (cfg instanceof FileBasedConfig) {
+        try {
+          ((FileBasedConfig) cfg).save();
+        } catch (IOException e) {
+          throw new IllegalStateException("Error while enabling global-refdb by default", e);
+        }
+      }
     }
     return cfg;
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/GitModule.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/GitModule.java
index 38cddd5..96bb4e8 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/GitModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/GitModule.java
@@ -15,24 +15,33 @@
 package com.googlesource.gerrit.plugins.multisite;
 
 import com.gerritforge.gerrit.globalrefdb.validation.SharedRefDbConfiguration;
+import com.google.gerrit.server.ModuleImpl;
+import com.google.gerrit.server.config.RepositoryConfig;
+import com.google.gerrit.server.git.GitRepositoryManagerModule;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.multisite.validation.ValidationModule;
 
+@ModuleImpl(name = GitRepositoryManagerModule.MANAGER_MODULE)
 public class GitModule extends AbstractModule {
   private final Configuration config;
+  private final RepositoryConfig repoConfig;
 
   @Inject
-  public GitModule(Configuration config) {
+  public GitModule(Configuration config, RepositoryConfig repoConfig) {
     this.config = config;
+    this.repoConfig = repoConfig;
   }
 
   @Override
   protected void configure() {
-    bind(SharedRefDbConfiguration.class).toInstance(config.getSharedRefDbConfiguration());
+    SharedRefDbConfiguration sharedRefDbConfiguration = config.getSharedRefDbConfiguration();
+    bind(SharedRefDbConfiguration.class).toInstance(sharedRefDbConfiguration);
     bind(ProjectVersionLogger.class).to(Log4jProjectVersionLogger.class);
-    if (config.getSharedRefDbConfiguration().getSharedRefDb().isEnabled()) {
-      install(new ValidationModule(config));
+    if (sharedRefDbConfiguration.getSharedRefDb().isEnabled()) {
+      install(new ValidationModule(config, repoConfig));
+    } else {
+      install(new GitRepositoryManagerModule(repoConfig));
     }
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/LibModuleLogFile.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/LibModuleLogFile.java
index 106bcde..7c88185 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/LibModuleLogFile.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/LibModuleLogFile.java
@@ -23,10 +23,11 @@
 public abstract class LibModuleLogFile {
 
   public LibModuleLogFile(SystemLog systemLog, String logName, Layout layout) {
-    AsyncAppender asyncAppender = systemLog.createAsyncAppender(logName, layout, true, true);
     Logger logger = LogManager.getLogger(logName);
-    logger.removeAppender(logName);
-    logger.addAppender(asyncAppender);
-    logger.setAdditivity(false);
+    if (logger.getAppender(logName) == null) {
+      AsyncAppender asyncAppender = systemLog.createAsyncAppender(logName, layout, true, true);
+      logger.addAppender(asyncAppender);
+      logger.setAdditivity(false);
+    }
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/Module.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/Module.java
index afdb41c..9392077 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/Module.java
@@ -21,7 +21,6 @@
 import com.google.inject.spi.Message;
 import com.googlesource.gerrit.plugins.multisite.broker.BrokerModule;
 import com.googlesource.gerrit.plugins.multisite.cache.CacheModule;
-import com.googlesource.gerrit.plugins.multisite.event.EventModule;
 import com.googlesource.gerrit.plugins.multisite.forwarder.ForwarderModule;
 import com.googlesource.gerrit.plugins.multisite.forwarder.router.RouterModule;
 import com.googlesource.gerrit.plugins.multisite.index.IndexModule;
@@ -56,7 +55,6 @@
       brokerRouterNeeded = true;
     }
     if (config.event().synchronize()) {
-      install(new EventModule(config));
       brokerRouterNeeded = true;
     }
     if (config.index().synchronize()) {
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 c650691..db06c9f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/PluginModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/PluginModule.java
@@ -30,6 +30,7 @@
 import com.googlesource.gerrit.plugins.multisite.consumer.MultiSiteConsumerRunner;
 import com.googlesource.gerrit.plugins.multisite.consumer.ReplicationStatusModule;
 import com.googlesource.gerrit.plugins.multisite.consumer.SubscriberModule;
+import com.googlesource.gerrit.plugins.multisite.event.EventModule;
 import com.googlesource.gerrit.plugins.multisite.forwarder.broker.BrokerForwarderModule;
 
 public class PluginModule extends LifecycleModule {
@@ -56,10 +57,10 @@
 
   @Override
   protected void configure() {
-
     if (config.index().synchronize()
         || config.cache().synchronize()
         || config.event().synchronize()) {
+      install(new EventModule(config));
       bind(BrokerApiWrapper.class).in(Scopes.SINGLETON);
       install(new SubscriberModule());
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/consumer/ReplicationStatus.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/consumer/ReplicationStatus.java
index acdd76e..0253588 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/consumer/ReplicationStatus.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/consumer/ReplicationStatus.java
@@ -20,6 +20,8 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.events.ProjectDeletedListener;
+import com.google.gerrit.metrics.CallbackMetric1;
+import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.serialize.JavaCacheSerializer;
 import com.google.gerrit.server.cache.serialize.StringCacheSerializer;
@@ -76,6 +78,8 @@
 
   private final Configuration config;
 
+  private final MetricMaker metricMaker;
+
   @Inject
   public ReplicationStatus(
       @Named(REPLICATION_STATUS) Cache<String, Long> cache,
@@ -83,16 +87,22 @@
       ProjectVersionLogger verLogger,
       ProjectCache projectCache,
       @Named(REPLICATION_STATUS) ScheduledExecutorService statusScheduler,
-      Configuration config) {
+      Configuration config,
+      MetricMaker metricMaker) {
     this.cache = cache;
     this.projectVersionRefUpdate = projectVersionRefUpdate;
     this.verLogger = verLogger;
     this.projectCache = projectCache;
     this.statusScheduler = statusScheduler;
     this.config = config;
+    this.metricMaker = metricMaker;
   }
 
   public Long getMaxLag() {
+    return getMaxLagMillis() / 1000;
+  }
+
+  public Long getMaxLagMillis() {
     Collection<Long> lags = replicationStatusPerProject.values();
     if (lags.isEmpty()) {
       return 0L;
@@ -152,6 +162,23 @@
   }
 
   @VisibleForTesting
+  Runnable replicationLagMetricPerProject(CallbackMetric1<String, Long> metricCallback) {
+    return () -> {
+      if (replicationStatusPerProject.isEmpty()) {
+        metricCallback.forceCreate("");
+      } else {
+        replicationStatusPerProject.entrySet().stream()
+            .filter(e -> e.getValue() > 0)
+            .forEach(
+                e ->
+                    metricCallback.set(
+                        SubscriberMetrics.sanitizeProjectName(e.getKey()), e.getValue()));
+        metricCallback.prune();
+      }
+    };
+  }
+
+  @VisibleForTesting
   public void doUpdateLag(Project.NameKey projectName, Long lag) {
     cache.put(projectName.get(), lag);
     replicationStatusPerProject.put(projectName.get(), lag);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/consumer/SubscriberMetrics.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/consumer/SubscriberMetrics.java
index cce74f9..bb971ff 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/consumer/SubscriberMetrics.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/consumer/SubscriberMetrics.java
@@ -14,13 +14,15 @@
 
 package com.googlesource.gerrit.plugins.multisite.consumer;
 
-import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.metrics.CallbackMetric1;
 import com.google.gerrit.metrics.Counter1;
 import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.ProjectEvent;
 import com.google.gerrit.server.events.RefUpdatedEvent;
+import com.google.gerrit.server.logging.Metadata;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.multisite.MultiSiteMetrics;
@@ -28,19 +30,27 @@
 import com.googlesource.gerrit.plugins.replication.events.RefReplicatedEvent;
 import com.googlesource.gerrit.plugins.replication.events.RefReplicationDoneEvent;
 import com.googlesource.gerrit.plugins.replication.events.ReplicationScheduledEvent;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 @Singleton
 public class SubscriberMetrics extends MultiSiteMetrics {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
   private static final String SUBSCRIBER_SUCCESS_COUNTER = "subscriber_msg_consumer_counter";
   private static final String SUBSCRIBER_FAILURE_COUNTER =
       "subscriber_msg_consumer_failure_counter";
-  private static final String REPLICATION_LAG_SEC =
+  public static final String REPLICATION_LAG_SEC =
       "multi_site/subscriber/subscriber_replication_status/sec_behind";
+  private static final String REPLICATION_LAG_MSEC =
+      "multi_site/subscriber/subscriber_replication_status/msec_behind";
+  private static final String REPLICATION_LAG_MSEC_PROJECT =
+      "multi_site/subscriber/subscriber_replication_status/msec_behind/per_project";
 
   private final Counter1<String> subscriberSuccessCounter;
   private final Counter1<String> subscriberFailureCounter;
   private final ReplicationStatus replicationStatus;
+  private static final Pattern isValidMetricNamePattern = Pattern.compile("[a-zA-Z0-9_-]");
+  private static final Field<String> PROJECT_NAME =
+      Field.ofString("project_name", Metadata.Builder::cacheName).build();
 
   @Inject
   public SubscriberMetrics(MetricMaker metricMaker, ReplicationStatus replicationStatus) {
@@ -65,6 +75,51 @@
         Long.class,
         new Description("Replication lag (sec)").setGauge().setUnit(Description.Units.SECONDS),
         replicationStatus::getMaxLag);
+    metricMaker.newCallbackMetric(
+        REPLICATION_LAG_MSEC,
+        Long.class,
+        new Description("Replication lag (msec)")
+            .setGauge()
+            .setUnit(Description.Units.MILLISECONDS),
+        replicationStatus::getMaxLagMillis);
+
+    CallbackMetric1<String, Long> metrics =
+        metricMaker.newCallbackMetric(
+            SubscriberMetrics.REPLICATION_LAG_MSEC_PROJECT,
+            Long.class,
+            new Description("Per-project replication lag (msec)")
+                .setGauge()
+                .setUnit(Description.Units.MILLISECONDS),
+            PROJECT_NAME);
+    metricMaker.newTrigger(metrics, replicationStatus.replicationLagMetricPerProject(metrics));
+  }
+
+  /**
+   * Ensures that the generated metric is compatible with prometheus metric names, as the set of
+   * values that represent a valid metric name are different from the ones that represent a valid
+   * project name. Main differences: - All _ are replaced with __ - All characters that aren't a
+   * letter(uppercase or lowercase) or a hyphen(-) are replaced with `_<hex_code>` This is to avoid
+   * all chances of name clashes when modifying a project name.
+   *
+   * @param name name of the metric to sanitize
+   * @return sanitized metric name
+   */
+  public static String sanitizeProjectName(String name) {
+    StringBuilder sanitizedName = new StringBuilder();
+    for (int i = 0; i < name.length(); i++) {
+      Character c = name.charAt(i);
+      Matcher matcher = isValidMetricNamePattern.matcher(String.valueOf(c));
+      if (matcher.find()) {
+        if (c == '_') {
+          sanitizedName.append("__");
+        } else {
+          sanitizedName.append(c);
+        }
+      } else {
+        sanitizedName.append("_").append(Integer.toHexString((int) c));
+      }
+    }
+    return sanitizedName.toString();
   }
 
   public void incrementSubscriberConsumedMessage() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/Context.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/Context.java
index 1c29d75..269388c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/Context.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/Context.java
@@ -14,14 +14,22 @@
 
 package com.googlesource.gerrit.plugins.multisite.forwarder;
 
+import static com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationEndpoints.APPLY_OBJECTS_API_ENDPOINT;
+import static com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationEndpoints.APPLY_OBJECT_API_ENDPOINT;
+
 /** Allows to tag a forwarded event to avoid infinitely looping events. */
 public class Context {
+  public static final String PULL_REPLICATION_PLUGIN_NAME = "pull-replication";
   private static final ThreadLocal<Boolean> forwardedEvent = ThreadLocal.withInitial(() -> false);
 
   private Context() {}
 
   public static Boolean isForwardedEvent() {
-    return forwardedEvent.get();
+    return forwardedEvent.get()
+        ||
+        // When the event is a result of pull-replication event, is considered as
+        // "forwarded" action because did not happen on this node.
+        isPullReplicationApplyObjectIndexing();
   }
 
   public static void setForwardedEvent(Boolean b) {
@@ -31,4 +39,10 @@
   public static void unsetForwardedEvent() {
     forwardedEvent.remove();
   }
+
+  public static boolean isPullReplicationApplyObjectIndexing() {
+    String threadName = Thread.currentThread().getName();
+    return threadName.contains(PULL_REPLICATION_PLUGIN_NAME + "~" + APPLY_OBJECT_API_ENDPOINT)
+        || threadName.contains(PULL_REPLICATION_PLUGIN_NAME + "~" + APPLY_OBJECTS_API_ENDPOINT);
+  }
 }
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 7373bcc..4779258 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
@@ -17,7 +17,7 @@
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.HumanComment;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.server.CommentsUtil;
+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.git.GitRepositoryManager;
@@ -42,7 +42,7 @@
 public class ChangeCheckerImpl implements ChangeChecker {
   private static final Logger log = LoggerFactory.getLogger(ChangeCheckerImpl.class);
   private final GitRepositoryManager gitRepoMgr;
-  private final CommentsUtil commentsUtil;
+  private final DraftCommentsReader draftCommentsReader;
   private final OneOffRequestContext oneOffReqCtx;
   private final String changeId;
   private final ChangeFinder changeFinder;
@@ -57,14 +57,14 @@
   @Inject
   public ChangeCheckerImpl(
       GitRepositoryManager gitRepoMgr,
-      CommentsUtil commentsUtil,
+      DraftCommentsReader draftCommentsReader,
       ChangeFinder changeFinder,
       OneOffRequestContext oneOffReqCtx,
       @GerritInstanceId String instanceId,
       @Assisted String changeId) {
     this.changeFinder = changeFinder;
     this.gitRepoMgr = gitRepoMgr;
-    this.commentsUtil = commentsUtil;
+    this.draftCommentsReader = draftCommentsReader;
     this.oneOffReqCtx = oneOffReqCtx;
     this.changeId = changeId;
     this.instanceId = instanceId;
@@ -184,7 +184,8 @@
     Change change = notes.getChange();
     Timestamp changeTs = Timestamp.from(change.getLastUpdatedOn());
     try {
-      for (HumanComment comment : commentsUtil.draftByChange(changeNotes.get())) {
+      for (HumanComment comment :
+          draftCommentsReader.getDraftsByChangeForAllAuthors(changeNotes.get())) {
         Timestamp commentTs = comment.writtenOn;
         changeTs = commentTs.after(changeTs) ? commentTs : changeTs;
       }
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 4d349f0..98d1320 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
@@ -65,7 +65,8 @@
           .filter(
               ref -> {
                 Optional<ObjectId> localRefOid =
-                    getSha1IfUpToDateWithGlobalRefDb(repository, projectName, refDb, ref, true);
+                    getLocalSha1IfEqualsToExistingGlobalRefDb(
+                        repository, projectName, refDb, ref, true);
                 localRefOid.ifPresent(
                     oid ->
                         repLog.info(
@@ -111,7 +112,7 @@
         .orElse(false);
   }
 
-  private Optional<ObjectId> getSha1IfUpToDateWithGlobalRefDb(
+  private Optional<ObjectId> getLocalSha1IfEqualsToExistingGlobalRefDb(
       Repository repository,
       String projectName,
       RefDatabase refDb,
@@ -120,14 +121,19 @@
     try {
       Optional<ObjectId> localRefObjectId =
           Optional.ofNullable(refDb.exactRef(ref))
-              .filter(r -> sharedRefDb.isUpToDate(Project.nameKey(projectName), r))
+              .filter(
+                  r ->
+                      sharedRefDb
+                          .get(Project.nameKey(projectName), r.getName(), String.class)
+                          .map(sharedRefObjId -> r.getObjectId().getName().equals(sharedRefObjId))
+                          .orElse(false))
               .map(Ref::getObjectId);
 
       if (localRefObjectId.isEmpty() && retryWithRandomSleep) {
         randomSleepForMitigatingConditionWhereLocalRefHaveJustBeenChanged(
             projectName, localRefObjectId, ref);
         localRefObjectId =
-            getSha1IfUpToDateWithGlobalRefDb(repository, projectName, refDb, ref, false);
+            getLocalSha1IfEqualsToExistingGlobalRefDb(repository, projectName, refDb, ref, false);
       }
 
       return 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 48edd81..03271aa 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
@@ -20,6 +20,7 @@
 
 import com.gerritforge.gerrit.globalrefdb.GlobalRefDbSystemError;
 import com.gerritforge.gerrit.globalrefdb.validation.SharedRefDatabaseWrapper;
+import com.google.common.base.Predicates;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Project;
@@ -30,11 +31,11 @@
 import com.google.gerrit.server.events.RefUpdatedEvent;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.notedb.IntBlob;
 import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.multisite.ProjectVersionLogger;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.util.Optional;
 import java.util.Set;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -218,9 +219,9 @@
   public Optional<Long> getProjectLocalVersion(String projectName) {
     try (Repository repository =
         gitRepositoryManager.openRepository(Project.NameKey.parse(projectName))) {
-      Optional<IntBlob> blob = IntBlob.parse(repository, MULTI_SITE_VERSIONING_REF);
+      Optional<Long> blob = longBlobParse(repository, MULTI_SITE_VERSIONING_REF);
       if (blob.isPresent()) {
-        Long repoVersion = Integer.toUnsignedLong(blob.get().value());
+        Long repoVersion = blob.get();
         logger.atFine().log("Local project '%s' has version %d", projectName, repoVersion);
         return Optional.of(repoVersion);
       }
@@ -232,6 +233,22 @@
     return Optional.empty();
   }
 
+  private Optional<Long> longBlobParse(Repository repo, String refName) throws IOException {
+    return Optional.ofNullable(repo.exactRef(refName))
+        .map(
+            (ref) -> {
+              try {
+                return Long.parseLong(
+                    new String(repo.open(ref.getObjectId()).getBytes(), StandardCharsets.UTF_8));
+              } catch (IOException e) {
+                logger.atSevere().withCause(e).log(
+                    "Unable to extract long BLOB from %s:%s", repo.getDirectory(), ref);
+                return null;
+              }
+            })
+        .filter(Predicates.notNull());
+  }
+
   /* (non-Javadoc)
    * @see com.googlesource.gerrit.plugins.multisite.validation.ProjectVersionRefUpdate#getProjectRemoteVersion(java.lang.String)
    */
@@ -286,7 +303,7 @@
   }
 
   private long getCurrentGlobalVersionNumber() {
-    return System.currentTimeMillis() / 1000;
+    return System.currentTimeMillis();
   }
 
   private Boolean isSuccessful(RefUpdate.Result result) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/RepositoryManagerModule.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/RepositoryManagerModule.java
new file mode 100644
index 0000000..b71ca67
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/RepositoryManagerModule.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2022 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.validation;
+
+import com.gerritforge.gerrit.globalrefdb.validation.SharedRefDbGitRepositoryManager;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.RepositoryConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.gerrit.server.git.MultiBaseLocalDiskRepositoryManager;
+
+class RepositoryManagerModule extends LifecycleModule {
+  private final RepositoryConfig cfg;
+
+  RepositoryManagerModule(RepositoryConfig cfg) {
+    this.cfg = cfg;
+  }
+
+  @Override
+  protected void configure() {
+    bind(GitRepositoryManager.class).to(SharedRefDbGitRepositoryManager.class);
+
+    // part responsible for physical repositories handling
+    listener().to(LocalDiskRepositoryManager.Lifecycle.class);
+    if (!cfg.getAllBasePaths().isEmpty()) {
+      bind(LocalDiskRepositoryManager.class).to(MultiBaseLocalDiskRepositoryManager.class);
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationModule.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationModule.java
index e9d3e1a..fc4d505 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationModule.java
@@ -30,7 +30,7 @@
 import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedRefEnforcement;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.config.RepositoryConfig;
 import com.google.inject.Scopes;
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Names;
@@ -38,9 +38,11 @@
 
 public class ValidationModule extends FactoryModule {
   private final Configuration cfg;
+  private final RepositoryConfig repoConfig;
 
-  public ValidationModule(Configuration cfg) {
+  public ValidationModule(Configuration cfg, RepositoryConfig repoConfig) {
     this.cfg = cfg;
+    this.repoConfig = repoConfig;
   }
 
   @Override
@@ -62,7 +64,7 @@
             ImmutableSet.of(
                 ProjectVersionRefUpdate.MULTI_SITE_VERSIONING_REF,
                 ProjectVersionRefUpdate.MULTI_SITE_VERSIONING_VALUE_REF));
-    bind(GitRepositoryManager.class).to(SharedRefDbGitRepositoryManager.class);
+    install(new RepositoryManagerModule(repoConfig));
 
     if (cfg.getSharedRefDbConfiguration().getSharedRefDb().getEnforcementRules().isEmpty()) {
       bind(SharedRefEnforcement.class).to(DefaultSharedRefEnforcement.class).in(Scopes.SINGLETON);
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index 159e4a0..d7c5648 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -99,3 +99,7 @@
 * Subscriber replication lag (sec behind the producer)
 
 `metric=site/multi_site/subscriber/subscriber_replication_status/sec_behind, type=com.google.gerrit.metrics.dropwizard.CallbackMetricImpl`
+
+* Subscriber replication lag (millisec behind the producer)
+
+`metric=site/multi_site/subscriber/subscriber_replication_status/msec_behind, type=com.google.gerrit.metrics.dropwizard.CallbackMetricImpl`
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/GitModuleTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/GitModuleTest.java
new file mode 100644
index 0000000..7faccae
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/GitModuleTest.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2022 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.gerritforge.gerrit.globalrefdb.validation.SharedRefDbGitRepositoryManager;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.config.GlobalPluginConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.gerrit.server.git.MultiBaseLocalDiskRepositoryManager;
+import com.google.inject.Inject;
+import org.junit.Test;
+
+@UseLocalDisk
+@NoHttpd
+public class GitModuleTest extends AbstractDaemonTest {
+  @Inject private GitRepositoryManager gitRepoManager;
+  @Inject private LocalDiskRepositoryManager wrapped;
+
+  @Test
+  @GerritConfig(
+      name = "gerrit.installDbModule",
+      value = "com.googlesource.gerrit.plugins.multisite.GitModule")
+  public void shouldUseLocalDiskRepositoryManagerByDefault() {
+    assertThat(gitRepoManager).isInstanceOf(SharedRefDbGitRepositoryManager.class);
+    assertThat(wrapped).isNotInstanceOf(MultiBaseLocalDiskRepositoryManager.class);
+  }
+
+  @Test
+  @GerritConfig(
+      name = "gerrit.installDbModule",
+      value = "com.googlesource.gerrit.plugins.multisite.GitModule")
+  @GerritConfig(name = "repository.r1.basePath", value = "/tmp/git1")
+  public void shouldUseMultiBaseLocalDiskRepositoryManagerWhenItIsConfigured() {
+    assertThat(gitRepoManager).isInstanceOf(SharedRefDbGitRepositoryManager.class);
+    assertThat(wrapped).isInstanceOf(MultiBaseLocalDiskRepositoryManager.class);
+  }
+
+  @Test
+  @GerritConfig(
+      name = "gerrit.installDbModule",
+      value = "com.googlesource.gerrit.plugins.multisite.GitModule")
+  @GlobalPluginConfig(
+      pluginName = Configuration.PLUGIN_NAME,
+      name = "ref-database.enabled",
+      value = "false")
+  public void shouldInstallDefaultGerritGitManagerWhenRefDbIsDisabled() {
+    assertThat(gitRepoManager).isInstanceOf(LocalDiskRepositoryManager.class);
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/consumer/CallbackMetricMaker.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/consumer/CallbackMetricMaker.java
new file mode 100644
index 0000000..a197c91
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/consumer/CallbackMetricMaker.java
@@ -0,0 +1,26 @@
+package com.googlesource.gerrit.plugins.multisite.consumer;
+
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.DisabledMetricMaker;
+import com.google.gerrit.metrics.Field;
+import org.junit.Ignore;
+
+@Ignore
+public class CallbackMetricMaker extends DisabledMetricMaker {
+  private int callbackMetricCounter = 0;
+
+  public int getCallbackMetricCounter() {
+    return callbackMetricCounter;
+  }
+
+  @Override
+  public <F1> Counter1<F1> newCounter(String name, Description desc, Field<F1> field1) {
+    callbackMetricCounter += 1;
+    return super.newCounter(name, desc, field1);
+  }
+
+  public void resetCallbackMetricCounter() {
+    callbackMetricCounter = 0;
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/consumer/ReplicationStatusTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/consumer/ReplicationStatusTest.java
index e1cd127..7828062 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/consumer/ReplicationStatusTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/consumer/ReplicationStatusTest.java
@@ -15,8 +15,11 @@
 package com.googlesource.gerrit.plugins.multisite.consumer;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import com.google.common.cache.Cache;
@@ -24,6 +27,8 @@
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.events.ProjectDeletedListener;
+import com.google.gerrit.metrics.CallbackMetric1;
+import com.google.gerrit.metrics.DisabledMetricMaker;
 import com.google.gerrit.server.project.ProjectCache;
 import com.googlesource.gerrit.plugins.multisite.Configuration;
 import com.googlesource.gerrit.plugins.multisite.ProjectVersionLogger;
@@ -43,6 +48,7 @@
   @Mock private ProjectVersionLogger verLogger;
   @Mock private ProjectCache projectCache;
   @Mock private ProjectVersionRefUpdate projectVersionRefUpdate;
+  @Mock private CallbackMetric1<String, Long> perProjectReplicationLagMetricCallback;
   private ReplicationStatus objectUnderTest;
   private Cache<String, Long> replicationStatusCache;
 
@@ -59,7 +65,8 @@
             verLogger,
             projectCache,
             Executors.newScheduledThreadPool(1),
-            new Configuration(new Config(), new Config()));
+            new Configuration(new Config(), new Config()),
+            new DisabledMetricMaker());
   }
 
   @Test
@@ -68,6 +75,14 @@
     replicationStatusCache.put("projectB", 3L);
 
     objectUnderTest.start();
+    assertThat(objectUnderTest.getMaxLagMillis()).isEqualTo(10L);
+  }
+
+  @Test
+  public void shouldConvertMillisLagFromPersistedCacheOnStartToSecs() {
+    replicationStatusCache.put("projectA", 10000L);
+
+    objectUnderTest.start();
     assertThat(objectUnderTest.getMaxLag()).isEqualTo(10L);
   }
 
@@ -78,7 +93,7 @@
     objectUnderTest.start();
 
     objectUnderTest.doUpdateLag(Project.nameKey("projectA"), 20L);
-    assertThat(objectUnderTest.getMaxLag()).isEqualTo(20L);
+    assertThat(objectUnderTest.getMaxLagMillis()).isEqualTo(20L);
   }
 
   @Test
@@ -163,9 +178,25 @@
         .thenReturn(Optional.of(projectRemoteVersion));
 
     objectUnderTest.updateReplicationLag(Project.nameKey(projectName));
+    objectUnderTest.replicationLagMetricPerProject(perProjectReplicationLagMetricCallback).run();
 
     assertThat(replicationStatusCache.getIfPresent(projectName))
         .isEqualTo(projectRemoteVersion - projectLocalVersion);
+    verify(perProjectReplicationLagMetricCallback)
+        .set(eq(projectName), eq(projectRemoteVersion - projectLocalVersion));
+  }
+
+  @Test
+  public void shouldNotGenerateCallbackMetricIfNoReplicationLag() {
+    String projectName = "projectA";
+    long projectLatestVersion = 10L;
+    when(projectVersionRefUpdate.getProjectLocalVersion(eq(projectName)))
+        .thenReturn(Optional.of(projectLatestVersion));
+
+    objectUnderTest.updateReplicationLag(Project.nameKey(projectName));
+
+    assertThat(replicationStatusCache.getIfPresent(projectName)).isNull();
+    verify(perProjectReplicationLagMetricCallback, never()).set(any(), any());
   }
 
   @SuppressWarnings("unchecked")
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/consumer/SubscriberMetricsTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/consumer/SubscriberMetricsTest.java
index 8d0ac81..6a03a6c 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/consumer/SubscriberMetricsTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/consumer/SubscriberMetricsTest.java
@@ -22,6 +22,7 @@
 import com.google.common.base.Suppliers;
 import com.google.common.cache.CacheBuilder;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.metrics.DisabledMetricMaker;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.server.data.RefUpdateAttribute;
 import com.google.gerrit.server.events.Event;
@@ -63,14 +64,14 @@
             verLogger,
             projectCache,
             Executors.newScheduledThreadPool(1),
-            new com.googlesource.gerrit.plugins.multisite.Configuration(
-                new Config(), new Config()));
+            new com.googlesource.gerrit.plugins.multisite.Configuration(new Config(), new Config()),
+            new DisabledMetricMaker());
     metrics = new SubscriberMetrics(metricMaker, replicationStatus);
   }
 
   @Test
   public void shouldLogProjectVersionWhenReceivingRefUpdatedEventWithoutLag() {
-    Optional<Long> globalRefDbVersion = Optional.of(System.currentTimeMillis() / 1000);
+    Optional<Long> globalRefDbVersion = Optional.of(System.currentTimeMillis());
     when(projectVersionRefUpdate.getProjectRemoteVersion(A_TEST_PROJECT_NAME))
         .thenReturn(globalRefDbVersion);
     when(projectVersionRefUpdate.getProjectLocalVersion(A_TEST_PROJECT_NAME))
@@ -85,7 +86,7 @@
 
   @Test
   public void shouldLogProjectVersionWhenReceivingRefUpdatedEventWithALag() {
-    Optional<Long> globalRefDbVersion = Optional.of(System.currentTimeMillis() / 1000);
+    Optional<Long> globalRefDbVersion = Optional.of(System.currentTimeMillis());
     long replicationLag = 60;
     when(projectVersionRefUpdate.getProjectRemoteVersion(A_TEST_PROJECT_NAME))
         .thenReturn(globalRefDbVersion.map(ts -> ts + replicationLag));
@@ -103,7 +104,7 @@
   public void
       shouldLogUponProjectDeletionSuccessWhenLocalVersionDoesNotExistAndSubscriberMetricsExist()
           throws Exception {
-    long nowSecs = System.currentTimeMillis() / 1000;
+    long nowSecs = System.currentTimeMillis();
     long replicationLagSecs = 60;
     Optional<Long> globalRefDbVersion = Optional.of(nowSecs);
     when(projectVersionRefUpdate.getProjectRemoteVersion(A_TEST_PROJECT_NAME))
@@ -145,7 +146,7 @@
   @Test
   public void shouldNotLogUponProjectDeletionSuccessWhenLocalVersionStillExists() throws Exception {
     Event eventMessage = projectDeletionSuccess();
-    Optional<Long> anyRefVersionValue = Optional.of(System.currentTimeMillis() / 1000);
+    Optional<Long> anyRefVersionValue = Optional.of(System.currentTimeMillis());
     when(projectVersionRefUpdate.getProjectLocalVersion(A_TEST_PROJECT_NAME))
         .thenReturn(anyRefVersionValue);
 
@@ -156,7 +157,7 @@
 
   @Test
   public void shouldRemoveProjectMetricsUponProjectDeletionSuccess() throws Exception {
-    long nowSecs = System.currentTimeMillis() / 1000;
+    long nowSecs = System.currentTimeMillis();
     long replicationLagSecs = 60;
     Optional<Long> globalRefDbVersion = Optional.of(nowSecs);
     when(projectVersionRefUpdate.getProjectRemoteVersion(A_TEST_PROJECT_NAME))
@@ -182,6 +183,34 @@
     assertThat(replicationStatus.getLocalVersion(A_TEST_PROJECT_NAME)).isNull();
   }
 
+  @Test
+  public void shouldDoNothingIfNameIsValid() {
+    String validProjectName = "aValidProject-Name123";
+
+    assertThat(metrics.sanitizeProjectName(validProjectName)).isEqualTo(validProjectName);
+  }
+
+  @Test
+  public void shouldSanitizeNameWithDot() {
+    String validProjectName = "nameWithA.InTheMiddle";
+
+    assertThat(metrics.sanitizeProjectName(validProjectName)).isEqualTo("nameWithA_2eInTheMiddle");
+  }
+
+  @Test
+  public void shouldSanitizeNameWithSlash() {
+    String validProjectName = "nameWithA/InTheMiddle";
+
+    assertThat(metrics.sanitizeProjectName(validProjectName)).isEqualTo("nameWithA_2fInTheMiddle");
+  }
+
+  @Test
+  public void shouldDoubleUnderscoresInName() {
+    String validProjectName = "nameWithA_InTheMiddle";
+
+    assertThat(metrics.sanitizeProjectName(validProjectName)).isEqualTo("nameWithA__InTheMiddle");
+  }
+
   private ProjectDeletionReplicationSucceededEvent projectDeletionSuccess()
       throws URISyntaxException {
     return new ProjectDeletionReplicationSucceededEvent(
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
new file mode 100644
index 0000000..ae5851d
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/index/IndexEventHandlerTest.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2020 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 static com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationEndpoints.APPLY_OBJECT_API_ENDPOINT;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.googlesource.gerrit.plugins.multisite.forwarder.Context;
+import com.googlesource.gerrit.plugins.multisite.forwarder.IndexEventForwarder;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class IndexEventHandlerTest {
+
+  private static final String INSTANCE_ID = "instance-id";
+
+  private IndexEventHandler eventHandler;
+
+  @Mock private IndexEventForwarder forwarder;
+  @Mock private ChangeCheckerImpl.Factory changeChecker;
+
+  @Before
+  public void setUp() {
+    eventHandler =
+        new IndexEventHandler(
+            MoreExecutors.directExecutor(),
+            asDynamicSet(forwarder),
+            changeChecker,
+            new TestGroupChecker(true),
+            INSTANCE_ID);
+  }
+
+  private DynamicSet<IndexEventForwarder> asDynamicSet(IndexEventForwarder forwarder) {
+    DynamicSet<IndexEventForwarder> result = new DynamicSet<>();
+    result.add("multi-site", forwarder);
+    return result;
+  }
+
+  @Test
+  public void shouldNotForwardIndexChangeIfCurrentThreadIsPullReplicationApplyObject()
+      throws Exception {
+    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);
+      verifyNoInteractions(changeChecker);
+    } finally {
+      Thread.currentThread().setName(currentThreadName);
+    }
+  }
+}
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 3e80b51..9a7a3b3 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
@@ -34,6 +34,7 @@
 import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -68,10 +69,18 @@
 
   @Test
   public void shouldReturnEmptyRefsWhenAllUpToDate() throws Exception {
-    newRef("refs/heads/foo");
-    newRef("refs/heads/bar");
-    Set<String> refs = Set.of("refs/heads/foo", "refs/heads/bar");
-    doReturn(true).when(sharedRefDatabaseMock).isUpToDate(eq(projectName), any());
+    String fooRefName = "refs/heads/foo";
+    ObjectId fooObjectId = newRef(fooRefName).getId();
+    String barRefName = "refs/heads/bar";
+    ObjectId barObjectId = newRef(barRefName).getId();
+    Set<String> refs = Set.of(fooRefName, barRefName);
+
+    doReturn(Optional.of(fooObjectId.getName()))
+        .when(sharedRefDatabaseMock)
+        .get(eq(projectName), eq(fooRefName), eq(String.class));
+    doReturn(Optional.of(barObjectId.getName()))
+        .when(sharedRefDatabaseMock)
+        .get(eq(projectName), eq(barRefName), eq(String.class));
 
     MultisiteReplicationFetchFilter fetchFilter =
         new MultisiteReplicationFetchFilter(sharedRefDatabaseMock, gitRepositoryManager, config);
@@ -84,13 +93,19 @@
   public void shouldFilterOutOneUpToDateRef() throws Exception {
     String refUpToDate = "refs/heads/uptodate";
     String outdatedRef = "refs/heads/outdated";
-    newRef(refUpToDate);
-    newRef(outdatedRef);
+    ObjectId upToDateObjectId = newRef(refUpToDate).getId();
+    newRef(outdatedRef).getId();
     Set<String> refsToFetch = Set.of(refUpToDate, outdatedRef);
-    SharedRefDatabaseWrapper sharedRefDatabase = new FakeSharedRefDatabaseWrapper(outdatedRef);
+
+    doReturn(Optional.of(upToDateObjectId.getName()))
+        .when(sharedRefDatabaseMock)
+        .get(eq(projectName), eq(refUpToDate), eq(String.class));
+    doReturn(Optional.of(AN_OUTDATED_OBJECT_ID.getName()))
+        .when(sharedRefDatabaseMock)
+        .get(eq(projectName), eq(outdatedRef), eq(String.class));
 
     MultisiteReplicationFetchFilter fetchFilter =
-        new MultisiteReplicationFetchFilter(sharedRefDatabase, gitRepositoryManager, config);
+        new MultisiteReplicationFetchFilter(sharedRefDatabaseMock, gitRepositoryManager, config);
     Set<String> filteredRefsToFetch = fetchFilter.filter(project, refsToFetch);
 
     assertThat(filteredRefsToFetch).containsExactly(outdatedRef);
@@ -99,10 +114,13 @@
   @Test
   public void shouldLoadLocalVersionAndFilterOut() throws Exception {
     String temporaryOutdated = "refs/heads/temporaryOutdated";
-    newRef(temporaryOutdated);
+    RevCommit localRef = newRef(temporaryOutdated);
 
     Set<String> refsToFetch = Set.of(temporaryOutdated);
-    doReturn(false).doReturn(true).when(sharedRefDatabaseMock).isUpToDate(eq(projectName), any());
+    doReturn(Optional.empty())
+        .doReturn(Optional.of(localRef.getId().getName()))
+        .when(sharedRefDatabaseMock)
+        .get(eq(projectName), eq(temporaryOutdated), eq(String.class));
 
     MultisiteReplicationFetchFilter fetchFilter =
         new MultisiteReplicationFetchFilter(sharedRefDatabaseMock, gitRepositoryManager, config);
@@ -110,7 +128,7 @@
 
     assertThat(filteredRefsToFetch).isEmpty();
 
-    verify(sharedRefDatabaseMock, times(2)).isUpToDate(any(), any());
+    verify(sharedRefDatabaseMock, times(2)).get(any(), any(), any());
   }
 
   @Test
@@ -119,14 +137,18 @@
     newRef(temporaryOutdated);
 
     Set<String> refsToFetch = Set.of(temporaryOutdated);
-    doReturn(false).doReturn(false).when(sharedRefDatabaseMock).isUpToDate(eq(projectName), any());
+    doReturn(Optional.of(AN_OUTDATED_OBJECT_ID.getName()))
+        .doReturn(Optional.of(AN_OUTDATED_OBJECT_ID.getName()))
+        .when(sharedRefDatabaseMock)
+        .get(eq(projectName), eq(temporaryOutdated), eq(String.class));
 
     MultisiteReplicationFetchFilter fetchFilter =
         new MultisiteReplicationFetchFilter(sharedRefDatabaseMock, gitRepositoryManager, config);
     Set<String> filteredRefsToFetch = fetchFilter.filter(project, refsToFetch);
 
     assertThat(filteredRefsToFetch).hasSize(1);
-    verify(sharedRefDatabaseMock, times(2)).isUpToDate(any(), any());
+    verify(sharedRefDatabaseMock, times(3))
+        .get(eq(projectName), eq(temporaryOutdated), eq(String.class));
   }
 
   @Test
@@ -134,6 +156,10 @@
     String temporaryOutdated = "refs/heads/temporaryOutdated";
     newRef(temporaryOutdated);
 
+    doReturn(Optional.empty())
+        .when(sharedRefDatabaseMock)
+        .get(eq(projectName), eq(temporaryOutdated), eq(String.class));
+
     Set<String> refsToFetch = Set.of(temporaryOutdated);
 
     MultisiteReplicationFetchFilter fetchFilter =
@@ -151,7 +177,7 @@
     Set<String> refsToFetch = Set.of(temporaryOutdated);
     doReturn(Optional.of(ObjectId.zeroId().getName()))
         .when(sharedRefDatabaseMock)
-        .get(eq(projectName), any(), any());
+        .get(eq(projectName), eq(temporaryOutdated), eq(String.class));
 
     MultisiteReplicationFetchFilter fetchFilter =
         new MultisiteReplicationFetchFilter(sharedRefDatabaseMock, gitRepositoryManager, config);
@@ -208,7 +234,7 @@
     assertThat(filteredRefsToFetch).hasSize(0);
   }
 
-  private void newRef(String refName) throws Exception {
-    repo.branch(refName).commit().create();
+  private RevCommit newRef(String refName) throws Exception {
+    return repo.branch(refName).commit().create();
   }
 }
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 92015ce..a997f9f 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
@@ -143,9 +143,10 @@
     when(refUpdatedEvent.getProjectNameKey()).thenReturn(A_TEST_PROJECT_NAME_KEY);
     when(refUpdatedEvent.getRefName()).thenReturn(A_TEST_REF_NAME);
 
-    new ProjectVersionRefUpdateImpl(
-            repoManager, sharedRefDb, gitReferenceUpdated, verLogger, DEFAULT_INSTANCE_ID)
-        .onEvent(refUpdatedEvent);
+    ProjectVersionRefUpdateImpl projectVersion =
+        new ProjectVersionRefUpdateImpl(
+            repoManager, sharedRefDb, gitReferenceUpdated, verLogger, DEFAULT_INSTANCE_ID);
+    projectVersion.onEvent(refUpdatedEvent);
 
     Ref ref = repo.getRepository().findRef(MULTI_SITE_VERSIONING_REF);
 
@@ -156,6 +157,10 @@
 
     ObjectLoader loader = repo.getRepository().open(ref.getObjectId());
     long storedVersion = readLongObject(loader);
+
+    Optional<Long> localStoredVersion = projectVersion.getProjectLocalVersion(A_TEST_PROJECT_NAME);
+    assertThat(localStoredVersion).isEqualTo(Optional.of(storedVersion));
+
     assertThat(storedVersion).isGreaterThan((long) masterPlusOneCommit.getCommitTime());
 
     verify(verLogger).log(A_TEST_PROJECT_NAME_KEY, storedVersion, 0);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/RefFixture.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/RefFixture.java
index baba94b..1885d27 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/RefFixture.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/RefFixture.java
@@ -33,6 +33,8 @@
   static final ObjectId AN_OBJECT_ID_1 = new ObjectId(1, 2, 3, 4, 5);
   static final ObjectId AN_OBJECT_ID_2 = new ObjectId(1, 2, 3, 4, 6);
   static final ObjectId AN_OBJECT_ID_3 = new ObjectId(1, 2, 3, 4, 7);
+  static final ObjectId AN_OUTDATED_OBJECT_ID =
+      ObjectId.fromString("da37cb098dd7df4c8662ae8afc1acae5f7567775");
   static final String A_TEST_REF_NAME = "refs/heads/master";
   static final String A_REF_NAME_OF_A_PATCHSET = "refs/changes/01/1/1";