Merge branch 'stable-3.3' into stable-3.4

* stable-3.3:
  Introduce persistent cache for replication-status

Change-Id: I8e3baa9a7d68bced6a056dbec35b0ea6035347b4
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 81a0d8d..20aaef6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/PluginModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/PluginModule.java
@@ -22,6 +22,7 @@
 import com.google.inject.Scopes;
 import com.googlesource.gerrit.plugins.multisite.broker.BrokerApiWrapper;
 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.forwarder.broker.BrokerForwarderModule;
 
@@ -41,6 +42,7 @@
     install(new BrokerForwarderModule());
     listener().to(MultiSiteConsumerRunner.class);
 
+    install(new ReplicationStatusModule());
     if (config.getSharedRefDbConfiguration().getSharedRefDb().isEnabled()) {
       listener().to(PluginStartup.class);
       DynamicSet.bind(binder(), ProjectDeletedListener.class)
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 bcecb3e..2f38720 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
@@ -15,10 +15,18 @@
 package com.googlesource.gerrit.plugins.multisite.consumer;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.cache.Cache;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.cache.serialize.JavaCacheSerializer;
+import com.google.gerrit.server.cache.serialize.StringCacheSerializer;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
+import com.google.inject.Module;
 import com.google.inject.Singleton;
+import com.google.inject.name.Named;
 import com.googlesource.gerrit.plugins.multisite.ProjectVersionLogger;
 import com.googlesource.gerrit.plugins.multisite.validation.ProjectVersionRefUpdate;
 import java.util.Collection;
@@ -27,22 +35,44 @@
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 @Singleton
-public class ReplicationStatus {
+public class ReplicationStatus implements LifecycleListener {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final Map<String, Long> replicationStatusPerProject = new HashMap<>();
+  static final String REPLICATION_STATUS_CACHE = "replication_status";
+
+  public static Module cacheModule() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        persist(REPLICATION_STATUS_CACHE, String.class, Long.class)
+            .version(1)
+            .keySerializer(StringCacheSerializer.INSTANCE)
+            .valueSerializer(new JavaCacheSerializer<>());
+      }
+    };
+  }
+
   private final Map<String, Long> localVersionPerProject = new HashMap<>();
+  private final Cache<String, Long> cache;
   private final ProjectVersionRefUpdate projectVersionRefUpdate;
   private final ProjectVersionLogger verLogger;
+  private final ProjectCache projectCache;
 
   @Inject
   public ReplicationStatus(
-      ProjectVersionRefUpdate projectVersionRefUpdate, ProjectVersionLogger verLogger) {
+      @Named(REPLICATION_STATUS_CACHE) Cache<String, Long> cache,
+      ProjectVersionRefUpdate projectVersionRefUpdate,
+      ProjectVersionLogger verLogger,
+      ProjectCache projectCache) {
+    this.cache = cache;
     this.projectVersionRefUpdate = projectVersionRefUpdate;
     this.verLogger = verLogger;
+    this.projectCache = projectCache;
   }
 
   public Long getMaxLag() {
@@ -92,6 +122,7 @@
     Optional<Long> localVersion = projectVersionRefUpdate.getProjectLocalVersion(projectName.get());
 
     if (!localVersion.isPresent() && localVersionPerProject.containsKey(projectName.get())) {
+      cache.invalidate(projectName.get());
       replicationStatusPerProject.remove(projectName.get());
       localVersionPerProject.remove(projectName.get());
       verLogger.logDeleted(projectName);
@@ -101,6 +132,7 @@
 
   @VisibleForTesting
   public void doUpdateLag(Project.NameKey projectName, Long lag) {
+    cache.put(projectName.get(), lag);
     replicationStatusPerProject.put(projectName.get(), lag);
   }
 
@@ -113,4 +145,18 @@
   Long getLocalVersion(String projectName) {
     return localVersionPerProject.get(projectName);
   }
+
+  @Override
+  public void start() {
+    loadAllFromCache();
+  }
+
+  @Override
+  public void stop() {}
+
+  private void loadAllFromCache() {
+    Set<String> cachedProjects =
+        projectCache.all().stream().map(Project.NameKey::get).collect(Collectors.toSet());
+    replicationStatusPerProject.putAll(cache.getAllPresent(cachedProjects));
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/consumer/ReplicationStatusModule.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/consumer/ReplicationStatusModule.java
new file mode 100644
index 0000000..e954bc4
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/consumer/ReplicationStatusModule.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2021 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.consumer;
+
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.inject.Scopes;
+
+public class ReplicationStatusModule extends LifecycleModule {
+  @Override
+  protected void configure() {
+    bind(ReplicationStatus.class).in(Scopes.SINGLETON);
+    install(ReplicationStatus.cacheModule());
+    listener().to(ReplicationStatus.class);
+  }
+}
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
new file mode 100644
index 0000000..beb93dd
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/consumer/ReplicationStatusTest.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2021 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.consumer;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.project.ProjectCache;
+import com.googlesource.gerrit.plugins.multisite.ProjectVersionLogger;
+import com.googlesource.gerrit.plugins.multisite.validation.ProjectVersionRefUpdate;
+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 ReplicationStatusTest {
+
+  @Mock private ProjectVersionLogger verLogger;
+  @Mock private ProjectCache projectCache;
+  @Mock private ProjectVersionRefUpdate projectVersionRefUpdate;
+  private ReplicationStatus objectUnderTest;
+  private Cache<String, Long> replicationStatusCache;
+
+  @Before
+  public void setup() throws Exception {
+    when(projectCache.all())
+        .thenReturn(
+            ImmutableSortedSet.of(Project.nameKey("projectA"), Project.nameKey("projectB")));
+    replicationStatusCache = CacheBuilder.newBuilder().build();
+    objectUnderTest =
+        new ReplicationStatus(
+            replicationStatusCache, projectVersionRefUpdate, verLogger, projectCache);
+  }
+
+  @Test
+  public void shouldPopulateLagsFromPersistedCacheOnStart() {
+    replicationStatusCache.put("projectA", 10L);
+    replicationStatusCache.put("projectB", 3L);
+
+    objectUnderTest.start();
+    assertThat(objectUnderTest.getMaxLag()).isEqualTo(10L);
+  }
+
+  @Test
+  public void shouldBeAbleToUpdatePersistedCacheValues() {
+    replicationStatusCache.put("projectA", 3L);
+
+    objectUnderTest.start();
+
+    objectUnderTest.doUpdateLag(Project.nameKey("projectA"), 20L);
+    assertThat(objectUnderTest.getMaxLag()).isEqualTo(20L);
+  }
+
+  @Test
+  public void shouldCombinePersistedProjectsWithNewEntries() {
+    replicationStatusCache.put("projectA", 3L);
+    objectUnderTest.start();
+
+    objectUnderTest.doUpdateLag(Project.nameKey("projectB"), 20L);
+
+    assertThat(objectUnderTest.getReplicationLags(2).keySet())
+        .containsExactly("projectA", "projectB");
+  }
+
+  @Test
+  public void shouldUpdatePersistedCacheWhenUpdatingLagValue() {
+    objectUnderTest.doUpdateLag(Project.nameKey("projectA"), 20L);
+
+    assertThat(replicationStatusCache.getIfPresent("projectA")).isEqualTo(20L);
+  }
+}
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 6ad2471..ab1ef80 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
@@ -20,11 +20,13 @@
 import static org.mockito.Mockito.when;
 
 import com.google.common.base.Suppliers;
+import com.google.common.cache.CacheBuilder;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.server.data.RefUpdateAttribute;
 import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.RefUpdatedEvent;
+import com.google.gerrit.server.project.ProjectCache;
 import com.googlesource.gerrit.plugins.multisite.ProjectVersionLogger;
 import com.googlesource.gerrit.plugins.multisite.validation.ProjectVersionRefUpdate;
 import com.googlesource.gerrit.plugins.replication.events.ProjectDeletionReplicationSucceededEvent;
@@ -45,13 +47,16 @@
 
   @Mock private MetricMaker metricMaker;
   @Mock private ProjectVersionLogger verLogger;
+  @Mock private ProjectCache projectCache;
   @Mock private ProjectVersionRefUpdate projectVersionRefUpdate;
   private SubscriberMetrics metrics;
   private ReplicationStatus replicationStatus;
 
   @Before
   public void setup() throws Exception {
-    replicationStatus = new ReplicationStatus(projectVersionRefUpdate, verLogger);
+    replicationStatus =
+        new ReplicationStatus(
+            CacheBuilder.newBuilder().build(), projectVersionRefUpdate, verLogger, projectCache);
     metrics = new SubscriberMetrics(metricMaker, replicationStatus);
   }
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/http/ReplicationStatusServletIT.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/http/ReplicationStatusServletIT.java
index 3b5f9a6..da02d02 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/http/ReplicationStatusServletIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/http/ReplicationStatusServletIT.java
@@ -31,6 +31,7 @@
 import com.googlesource.gerrit.plugins.multisite.ProjectVersionLogger;
 import com.googlesource.gerrit.plugins.multisite.cache.CacheModule;
 import com.googlesource.gerrit.plugins.multisite.consumer.ReplicationStatus;
+import com.googlesource.gerrit.plugins.multisite.consumer.ReplicationStatusModule;
 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;
@@ -57,6 +58,7 @@
       install(new CacheModule());
       install(new RouterModule());
       install(new IndexModule());
+      install(new ReplicationStatusModule());
       SharedRefDbConfiguration sharedRefDbConfig =
           new SharedRefDbConfiguration(new Config(), "multi-site");
       bind(SharedRefDbConfiguration.class).toInstance(sharedRefDbConfig);