Collect metrics upon ref replication event

In a multi-site/HA setup metrics would only be collected
on the node receiving the receive-pack.

In case of an active/passive configuration, metrics won't
be collected at all on the passive nodes.

Collect metrics on *-replication-done events.

Bug: Issue 16278
Change-Id: Ie838afd9b9cf340c5551f3930b07cbcdc4f1622a
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoUpdateListener.java b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoUpdateListener.java
index e9e4d98..3ac4903 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoUpdateListener.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoUpdateListener.java
@@ -15,34 +15,56 @@
 package com.googlesource.gerrit.plugins.gitrepometrics;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+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.ProjectEvent;
+import com.google.gerrit.server.events.RefUpdatedEvent;
 import com.google.inject.Inject;
+import java.util.Objects;
 import java.util.concurrent.ExecutorService;
 
-public class GitRepoUpdateListener implements GitReferenceUpdatedListener {
+class GitRepoUpdateListener implements EventListener {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
   private final ExecutorService executor;
   private final UpdateGitMetricsTask.Factory updateGitMetricsTaskFactory;
   private final GitRepoMetricsCache gitRepoMetricsCache;
+  private final String instanceId;
 
   @Inject
-  GitRepoUpdateListener(
+  protected GitRepoUpdateListener(
+      @GerritInstanceId String instanceId,
       @UpdateGitMetricsExecutor ExecutorService executor,
       UpdateGitMetricsTask.Factory updateGitMetricsTaskFactory,
       GitRepoMetricsCache gitRepoMetricsCache) {
+    this.instanceId = instanceId;
     this.executor = executor;
     this.updateGitMetricsTaskFactory = updateGitMetricsTaskFactory;
     this.gitRepoMetricsCache = gitRepoMetricsCache;
   }
 
   @Override
-  public void onGitReferenceUpdated(Event event) {
-    String projectName = event.getProjectName();
-    logger.atFine().log("Got an update for project %s", projectName);
+  public void onEvent(Event event) {
+    if (event instanceof RefUpdatedEvent || isReplicationDoneEvent(event)) {
+      String projectName = ((ProjectEvent) event).getProjectNameKey().get();
+      logger.atFine().log(
+          String.format(
+              "Got %s event from %s. Might need to collect metrics for project %s",
+              event.type, event.instanceId, projectName));
 
-    if (gitRepoMetricsCache.shouldCollectStats(projectName)) {
-      UpdateGitMetricsTask updateGitMetricsTask = updateGitMetricsTaskFactory.create(projectName);
-      executor.execute(updateGitMetricsTask);
+      if (gitRepoMetricsCache.shouldCollectStats(projectName)) {
+        UpdateGitMetricsTask updateGitMetricsTask = updateGitMetricsTaskFactory.create(projectName);
+        executor.execute(updateGitMetricsTask);
+      }
     }
   }
+
+  private boolean isReplicationDoneEvent(Event event) {
+    // Check the name of the event instead of checking the class type
+    // to avoid importing pull and push replication plugin dependencies
+    // only for this check.
+    return event.type != null
+        && !Objects.equals(event.instanceId, instanceId)
+        && event.type.endsWith("-replication-done");
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/Module.java b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/Module.java
index 8428ad8..10c116a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/Module.java
@@ -14,9 +14,9 @@
 
 package com.googlesource.gerrit.plugins.gitrepometrics;
 
-import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.events.EventListener;
 import com.google.inject.Scopes;
 import com.googlesource.gerrit.plugins.gitrepometrics.collectors.FSMetricsCollector;
 import com.googlesource.gerrit.plugins.gitrepometrics.collectors.GitStatsMetricsCollector;
@@ -32,7 +32,8 @@
         .annotatedWith(UpdateGitMetricsExecutor.class)
         .toProvider(UpdateGitMetricsExecutorProvider.class);
     bind(GitRepoUpdateListener.class);
-    DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(GitRepoUpdateListener.class);
+    DynamicSet.bind(binder(), EventListener.class).to(GitRepoUpdateListener.class);
+
     DynamicSet.setOf(binder(), MetricsCollector.class);
     DynamicSet.bind(binder(), MetricsCollector.class).to(GitStatsMetricsCollector.class);
     DynamicSet.bind(binder(), MetricsCollector.class).to(FSMetricsCollector.class);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/GitUpdateListenerTest.java b/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/GitUpdateListenerTest.java
index a97adc2..095d8a6 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/GitUpdateListenerTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/GitUpdateListenerTest.java
@@ -18,15 +18,16 @@
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verifyNoInteractions;
 
 import com.codahale.metrics.MetricRegistry;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.annotations.PluginName;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.metrics.DisabledMetricMaker;
+import com.google.gerrit.server.data.RefUpdateAttribute;
+import com.google.gerrit.server.events.RefEvent;
+import com.google.gerrit.server.events.RefUpdatedEvent;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.testing.InMemoryModule;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
@@ -38,7 +39,6 @@
 import java.io.IOException;
 import java.util.Collections;
 import java.util.concurrent.ExecutorService;
-import org.eclipse.jgit.lib.Repository;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
@@ -47,23 +47,24 @@
   private final GitRepositoryManager repoManager = new InMemoryRepositoryManager();
   private final ExecutorService mockedExecutorService = mock(ExecutorService.class);
   private GitRepoUpdateListener gitRepoUpdateListener;
-  private final String testProject = "testProject";
-  private final Project.NameKey testProjectNameKey = Project.nameKey("testProject");
-  private Repository repository;
-  ArgumentCaptor<UpdateGitMetricsTask> valueCapture =
+  private final String enabledProject = "enabledProject";
+  private final Project.NameKey enabledProjectNameKey = Project.nameKey(enabledProject);
+
+  ArgumentCaptor<UpdateGitMetricsTask> updateGitMetricsTaskCaptor =
       ArgumentCaptor.forClass(UpdateGitMetricsTask.class);
   private GitRepoMetricsCache gitRepoMetricsCache;
-
   private final String disabledProject = "disabledProject";
   private final Project.NameKey disabledProjectNameKey = Project.nameKey(disabledProject);
-  private Repository disabledRepository;
+  private final String producerInstanceId = "producerInstanceId";
+  private final String consumerInstanceId = "consumerInstanceId";
+  private final String refReplicationDoneType = "ref-replication-done";
 
   @Inject private UpdateGitMetricsTask.Factory updateGitMetricsTaskFactory;
 
   @Before
   public void setupRepo() throws IOException {
     ConfigSetupUtils configSetupUtils =
-        new ConfigSetupUtils(Collections.singletonList(testProject));
+        new ConfigSetupUtils(Collections.singletonList(enabledProject));
     gitRepoMetricsCache =
         new GitRepoMetricsCache(
             new DynamicSet<>(),
@@ -87,73 +88,105 @@
     injector.injectMembers(this);
 
     reset(mockedExecutorService);
+    doNothing().when(mockedExecutorService).execute(updateGitMetricsTaskCaptor.capture());
+    repoManager.createRepository(enabledProjectNameKey);
+    repoManager.createRepository(disabledProjectNameKey);
+
     gitRepoUpdateListener =
         new GitRepoUpdateListener(
-            mockedExecutorService, updateGitMetricsTaskFactory, gitRepoMetricsCache);
-    repository = repoManager.createRepository(testProjectNameKey);
-    disabledRepository = repoManager.createRepository(disabledProjectNameKey);
+            producerInstanceId,
+            mockedExecutorService,
+            updateGitMetricsTaskFactory,
+            gitRepoMetricsCache);
   }
 
   @Test
-  public void shouldUpdateMetricsIfProjectIsEnabled() {
-    doNothing().when(mockedExecutorService).execute(valueCapture.capture());
-    gitRepoUpdateListener.onGitReferenceUpdated(new TestEvent(testProject));
-    UpdateGitMetricsTask expectedUpdateGitMetricsTask =
-        updateGitMetricsTaskFactory.create(testProject);
-    assertThat(valueCapture.getValue().toString())
-        .isEqualTo(expectedUpdateGitMetricsTask.toString());
+  public void shouldUpdateMetricsIfProjectIsEnabledOnRefUpdated() {
+    gitRepoUpdateListener.onEvent(getRefUpdatedEvent(enabledProject));
+    assertMetricsAreUpdated();
   }
 
-  public static class TestEvent implements GitReferenceUpdatedListener.Event {
+  @Test
+  public void shouldNotUpdateMetricsIfProjectIsDisabledOnRefUpdated() {
+    gitRepoUpdateListener.onEvent(getRefUpdatedEvent(disabledProject));
+    assertMetricsAreNotUpdated();
+  }
+
+  @Test
+  public void shouldUpdateMetricsIfProjectIsEnabledOnRefReplicationDone() {
+    gitRepoUpdateListener.onEvent(
+        getRefReplicationEvent(refReplicationDoneType, enabledProject, consumerInstanceId));
+    assertMetricsAreUpdated();
+  }
+
+  @Test
+  public void shouldNotUpdateMetricsIfProjectIsDisabledOnReplicationDone() {
+    gitRepoUpdateListener.onEvent(
+        getRefReplicationEvent(refReplicationDoneType, disabledProject, consumerInstanceId));
+    assertMetricsAreNotUpdated();
+  }
+
+  @Test
+  public void shouldNotUpdateMetricsOnUnknownEvent() {
+    gitRepoUpdateListener.onEvent(
+        getRefReplicationEvent("any-event", enabledProject, consumerInstanceId));
+    assertMetricsAreNotUpdated();
+  }
+
+  @Test
+  public void shouldNotUpdateMetricsOnRefReplicationDoneFromSameNode() {
+    gitRepoUpdateListener.onEvent(
+        getRefReplicationEvent(refReplicationDoneType, enabledProject, producerInstanceId));
+    assertMetricsAreNotUpdated();
+  }
+
+  private RefUpdatedEvent getRefUpdatedEvent(String projectName) {
+    RefUpdatedEvent refUpdatedEvent = new RefUpdatedEvent();
+    refUpdatedEvent.refUpdate =
+        () -> {
+          RefUpdateAttribute attributes = new RefUpdateAttribute();
+          attributes.project = projectName;
+          attributes.refName = "refs/for/master";
+          return attributes;
+        };
+    return refUpdatedEvent;
+  }
+
+  private ReplicationTestEvent getRefReplicationEvent(
+      String type, String projectName, String instanceId) {
+    ReplicationTestEvent event = new ReplicationTestEvent(type, projectName);
+    event.instanceId = instanceId;
+    return event;
+  }
+
+  private static class ReplicationTestEvent extends RefEvent {
     private final String projectName;
 
-    protected TestEvent(String projectName) {
+    private ReplicationTestEvent(String type, String projectName) {
+      super(type);
       this.projectName = projectName;
     }
 
     @Override
-    public String getProjectName() {
-      return projectName;
-    }
-
-    @Override
-    public NotifyHandling getNotify() {
-      return null;
+    public Project.NameKey getProjectNameKey() {
+      return Project.NameKey.parse(projectName);
     }
 
     @Override
     public String getRefName() {
-      return null;
+      return "refs/for/test";
     }
+  }
 
-    @Override
-    public String getOldObjectId() {
-      return null;
-    }
+  private void assertMetricsAreUpdated() {
+    UpdateGitMetricsTask expectedUpdateGitMetricsTask =
+        updateGitMetricsTaskFactory.create(enabledProject);
+    assertThat(updateGitMetricsTaskCaptor.getValue().toString())
+        .isEqualTo(expectedUpdateGitMetricsTask.toString());
+  }
 
-    @Override
-    public String getNewObjectId() {
-      return null;
-    }
-
-    @Override
-    public boolean isCreate() {
-      return false;
-    }
-
-    @Override
-    public boolean isDelete() {
-      return false;
-    }
-
-    @Override
-    public boolean isNonFastForward() {
-      return false;
-    }
-
-    @Override
-    public AccountInfo getUpdater() {
-      return null;
-    }
+  private void assertMetricsAreNotUpdated() {
+    updateGitMetricsTaskFactory.create(enabledProject);
+    verifyNoInteractions(mockedExecutorService);
   }
 }