Handle project versions upon project deletions

Project deletions were not handled by the multi-site plugin, possibly
causing false misalignments to be reported in the project_version_log
as well as the plugins_multi_site_multi_site_subscriber_subscriber_replication_status_sec_behind
metric.

Handle ProjectDeletionReplicationSucceededEvent to identify project
deletions:

- Log project deletion in the project_version_log, as such:

  { "project": "foo", "status": "DELETED" }

- Stop considering the project when publishing subscriber metrics

Bug: Issue 13598
Change-Id: I7259f520edc17b88bffce3e3e52a9ddfd476ed7f
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/Log4jProjectVersionLogger.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/Log4jProjectVersionLogger.java
index c2c4b46..23c720a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/Log4jProjectVersionLogger.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/Log4jProjectVersionLogger.java
@@ -45,4 +45,9 @@
       verLog.info("{ \"project\":\"{}\", \"version\":{} }", projectName, currentVersion);
     }
   }
+
+  @Override
+  public void logDeleted(Project.NameKey projectName) {
+    verLog.info("{ \"project\":\"{}\", \"status\":\"DELETED\" }", projectName);
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/ProjectVersionLogger.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/ProjectVersionLogger.java
index 6ababb6..2ee2c13 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/ProjectVersionLogger.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/ProjectVersionLogger.java
@@ -19,4 +19,6 @@
 public interface ProjectVersionLogger {
 
   public void log(Project.NameKey projectName, long currentVersion, long replicationLag);
+
+  public void logDeleted(Project.NameKey projectName);
 }
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 e23a3c7..791c464 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
@@ -15,6 +15,7 @@
 package com.googlesource.gerrit.plugins.multisite.consumer;
 
 import com.gerritforge.gerrit.eventbroker.EventMessage;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.metrics.Counter1;
@@ -27,6 +28,7 @@
 import com.googlesource.gerrit.plugins.multisite.MultiSiteMetrics;
 import com.googlesource.gerrit.plugins.multisite.ProjectVersionLogger;
 import com.googlesource.gerrit.plugins.multisite.validation.ProjectVersionRefUpdate;
+import com.googlesource.gerrit.plugins.replication.events.ProjectDeletionReplicationSucceededEvent;
 import com.googlesource.gerrit.plugins.replication.events.RefReplicatedEvent;
 import com.googlesource.gerrit.plugins.replication.events.RefReplicationDoneEvent;
 import com.googlesource.gerrit.plugins.replication.events.ReplicationScheduledEvent;
@@ -113,6 +115,21 @@
     } else if (event instanceof RefUpdatedEvent) {
       RefUpdatedEvent updated = (RefUpdatedEvent) event;
       updateReplicationLagMetrics(updated.getProjectNameKey(), updated.getRefName());
+    } else if (event instanceof ProjectDeletionReplicationSucceededEvent) {
+      ProjectDeletionReplicationSucceededEvent projectDeletion =
+          (ProjectDeletionReplicationSucceededEvent) event;
+      removeProjectFromReplicationLagMetrics(projectDeletion.getProjectNameKey());
+    }
+  }
+
+  private void removeProjectFromReplicationLagMetrics(Project.NameKey projectName) {
+    Optional<Long> localVersion = projectVersionRefUpdate.getProjectLocalVersion(projectName.get());
+
+    if (!localVersion.isPresent() && localVersionPerProject.containsKey(projectName.get())) {
+      replicationStatusPerProject.remove(projectName.get());
+      localVersionPerProject.remove(projectName.get());
+      verLogger.logDeleted(projectName);
+      logger.atFine().log("Removed project '%s' from replication lag metrics", projectName);
     }
   }
 
@@ -138,4 +155,14 @@
           projectName, localVersion.isPresent() ? "remote" : "local");
     }
   }
+
+  @VisibleForTesting
+  Long getReplicationStatus(String projectName) {
+    return replicationStatusPerProject.get(projectName);
+  }
+
+  @VisibleForTesting
+  Long getLocalVersion(String projectName) {
+    return localVersionPerProject.get(projectName);
+  }
 }
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 8aa043d..ef7552a 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
@@ -14,7 +14,9 @@
 
 package com.googlesource.gerrit.plugins.multisite.consumer;
 
+import static com.google.common.truth.Truth.assertThat;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
 import com.gerritforge.gerrit.eventbroker.EventMessage;
@@ -27,8 +29,11 @@
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.googlesource.gerrit.plugins.multisite.ProjectVersionLogger;
 import com.googlesource.gerrit.plugins.multisite.validation.ProjectVersionRefUpdate;
+import com.googlesource.gerrit.plugins.replication.events.ProjectDeletionReplicationSucceededEvent;
+import java.net.URISyntaxException;
 import java.util.Optional;
 import java.util.UUID;
+import org.eclipse.jgit.transport.URIish;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -86,6 +91,93 @@
     verify(verLogger).log(A_TEST_PROJECT_NAME_KEY, globalRefDbVersion.get(), replicationLag);
   }
 
+  @Test
+  public void
+      shouldLogUponProjectDeletionSuccessWhenLocalVersionDoesNotExistAndSubscriberMetricsExist()
+          throws Exception {
+    long nowSecs = System.currentTimeMillis() / 1000;
+    long replicationLagSecs = 60;
+    Optional<Long> globalRefDbVersion = Optional.of(nowSecs);
+    when(projectVersionRefUpdate.getProjectRemoteVersion(A_TEST_PROJECT_NAME))
+        .thenReturn(globalRefDbVersion.map(ts -> ts + replicationLagSecs));
+    when(projectVersionRefUpdate.getProjectLocalVersion(A_TEST_PROJECT_NAME))
+        .thenReturn(globalRefDbVersion);
+
+    EventMessage refUpdateEventMessage = new EventMessage(msgHeader, newRefUpdateEvent());
+    metrics.updateReplicationStatusMetrics(refUpdateEventMessage);
+
+    assertThat(metrics.getReplicationStatus(A_TEST_PROJECT_NAME)).isEqualTo(replicationLagSecs);
+    assertThat(metrics.getLocalVersion(A_TEST_PROJECT_NAME)).isEqualTo(nowSecs);
+
+    when(projectVersionRefUpdate.getProjectLocalVersion(A_TEST_PROJECT_NAME))
+        .thenReturn(Optional.empty());
+
+    EventMessage projectDeleteEventMessage = new EventMessage(msgHeader, projectDeletionSuccess());
+    metrics.updateReplicationStatusMetrics(projectDeleteEventMessage);
+
+    verify(verLogger).logDeleted(A_TEST_PROJECT_NAME_KEY);
+  }
+
+  @Test
+  public void shouldNotLogUponProjectDeletionSuccessWhenSubscriberMetricsDoNotExist()
+      throws Exception {
+    EventMessage eventMessage = new EventMessage(msgHeader, projectDeletionSuccess());
+    when(projectVersionRefUpdate.getProjectLocalVersion(A_TEST_PROJECT_NAME))
+        .thenReturn(Optional.empty());
+
+    assertThat(metrics.getReplicationStatus(A_TEST_PROJECT_NAME)).isNull();
+    assertThat(metrics.getLocalVersion(A_TEST_PROJECT_NAME)).isNull();
+
+    metrics.updateReplicationStatusMetrics(eventMessage);
+
+    verifyZeroInteractions(verLogger);
+  }
+
+  @Test
+  public void shouldNotLogUponProjectDeletionSuccessWhenLocalVersionStillExists() throws Exception {
+    EventMessage eventMessage = new EventMessage(msgHeader, projectDeletionSuccess());
+    Optional<Long> anyRefVersionValue = Optional.of(System.currentTimeMillis() / 1000);
+    when(projectVersionRefUpdate.getProjectLocalVersion(A_TEST_PROJECT_NAME))
+        .thenReturn(anyRefVersionValue);
+
+    metrics.updateReplicationStatusMetrics(eventMessage);
+
+    verifyZeroInteractions(verLogger);
+  }
+
+  @Test
+  public void shouldRemoveProjectMetricsUponProjectDeletionSuccess() throws Exception {
+    long nowSecs = System.currentTimeMillis() / 1000;
+    long replicationLagSecs = 60;
+    Optional<Long> globalRefDbVersion = Optional.of(nowSecs);
+    when(projectVersionRefUpdate.getProjectRemoteVersion(A_TEST_PROJECT_NAME))
+        .thenReturn(globalRefDbVersion.map(ts -> ts + replicationLagSecs));
+    when(projectVersionRefUpdate.getProjectLocalVersion(A_TEST_PROJECT_NAME))
+        .thenReturn(globalRefDbVersion);
+
+    EventMessage eventMessage = new EventMessage(msgHeader, newRefUpdateEvent());
+
+    metrics.updateReplicationStatusMetrics(eventMessage);
+
+    assertThat(metrics.getReplicationStatus(A_TEST_PROJECT_NAME)).isEqualTo(replicationLagSecs);
+    assertThat(metrics.getLocalVersion(A_TEST_PROJECT_NAME)).isEqualTo(nowSecs);
+
+    when(projectVersionRefUpdate.getProjectLocalVersion(A_TEST_PROJECT_NAME))
+        .thenReturn(Optional.empty());
+    EventMessage projectDeleteEvent = new EventMessage(msgHeader, projectDeletionSuccess());
+
+    metrics.updateReplicationStatusMetrics(projectDeleteEvent);
+
+    assertThat(metrics.getReplicationStatus(A_TEST_PROJECT_NAME)).isNull();
+    assertThat(metrics.getLocalVersion(A_TEST_PROJECT_NAME)).isNull();
+  }
+
+  private ProjectDeletionReplicationSucceededEvent projectDeletionSuccess()
+      throws URISyntaxException {
+    return new ProjectDeletionReplicationSucceededEvent(
+        A_TEST_PROJECT_NAME, new URIish("git://target"));
+  }
+
   private RefUpdatedEvent newRefUpdateEvent() {
     RefUpdateAttribute refUpdate = new RefUpdateAttribute();
     refUpdate.project = A_TEST_PROJECT_NAME;