Introduce persistent cache for replication-status
ReplicationStatus keeps track of the replication lags associated to
projects in a multi-site set-up, so that it is possible to understand
how much behind a particular project is falling due to replication.
This information however is lost upon gerrit restart.
Persist replication status information by using a persistent cache to
populate in-memory replication-status at startup.
Bug: Issue 15310
Change-Id: I86c40684439ec5a457866f56f4cb9157c4af0092
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 45e66a6..c024e67 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() {
@@ -90,6 +120,21 @@
@VisibleForTesting
public void doUpdateLag(Project.NameKey projectName, Long lag) {
+ cache.put(projectName.get(), lag);
replicationStatusPerProject.put(projectName.get(), lag);
}
+
+ @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 0c2f750..f648f6b 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 com.gerritforge.gerrit.eventbroker.EventMessage;
import com.gerritforge.gerrit.globalrefdb.validation.SharedRefDatabaseWrapper;
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.RefUpdatedEvent;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.project.ProjectCache;
import com.googlesource.gerrit.plugins.multisite.ProjectVersionLogger;
import com.googlesource.gerrit.plugins.multisite.validation.ProjectVersionRefUpdate;
import java.util.Optional;
@@ -45,6 +47,7 @@
@Mock private GitReferenceUpdated gitReferenceUpdated;
@Mock private MetricMaker metricMaker;
@Mock private ProjectVersionLogger verLogger;
+ @Mock private ProjectCache projectCache;
@Mock private ProjectVersionRefUpdate projectVersionRefUpdate;
private SubscriberMetrics metrics;
private EventMessage.Header msgHeader;
@@ -54,7 +57,12 @@
msgHeader = new EventMessage.Header(UUID.randomUUID(), UUID.randomUUID());
metrics =
new SubscriberMetrics(
- metricMaker, new ReplicationStatus(projectVersionRefUpdate, verLogger));
+ metricMaker,
+ new ReplicationStatus(
+ CacheBuilder.newBuilder().build(),
+ projectVersionRefUpdate,
+ verLogger,
+ projectCache));
}
@Test
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);