Test cache invalidation behaviour inside a Gerrit daemon

All existing tests for cache forwarding were not plugging
the real Gerrit caches. A change happened in the project cache
in stable-3.3 broke the cache invalidation without breaking
the tests.

Perform an effective E2E cache invalidation test from
a plugin integration test environment, so that if any of
the cache keys are changing, the test would allow to
identify the breakage and the associated cache involved.

The first cache included in the E2E test is 'projects'.
Other caches are going to be covered in the follow-up
changes.

Bug: Issue 14255
Change-Id: I94db246f6310f898ee673dc1a9bad31ffedeef83
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedCacheEvictionHandlerIT.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedCacheEvictionHandlerIT.java
new file mode 100644
index 0000000..fb2a50b
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedCacheEvictionHandlerIT.java
@@ -0,0 +1,132 @@
+// 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.forwarder;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.gerritforge.gerrit.globalrefdb.validation.SharedRefDbConfiguration;
+import com.google.common.cache.RemovalNotification;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.server.cache.CacheRemovalListener;
+import com.google.gerrit.server.events.EventGson;
+import com.google.gerrit.server.project.ProjectCacheImpl;
+import com.google.gson.Gson;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.multisite.cache.CacheModule;
+import com.googlesource.gerrit.plugins.multisite.forwarder.events.CacheEvictionEvent;
+import com.googlesource.gerrit.plugins.multisite.forwarder.router.CacheEvictionEventRouter;
+import com.googlesource.gerrit.plugins.multisite.forwarder.router.RouterModule;
+import com.googlesource.gerrit.plugins.multisite.index.IndexModule;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Config;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+@TestPlugin(
+    name = "multi-site",
+    sysModule =
+        "com.googlesource.gerrit.plugins.multisite.forwarder.ForwardedCacheEvictionHandlerIT$TestModule")
+public class ForwardedCacheEvictionHandlerIT extends LightweightPluginDaemonTest {
+  private static final Duration CACHE_EVICTIONS_WAIT_TIMEOUT = Duration.ofMinutes(1);
+
+  @SuppressWarnings("rawtypes")
+  @Inject
+  private DynamicSet<CacheRemovalListener> cacheRemovalListeners;
+
+  @Inject private CacheEvictionEventRouter objectUnderTest;
+  @Inject @EventGson private Gson gson;
+  private CacheEvictionsTracker<?, ?> evictionsCacheTracker;
+  private RegistrationHandle cacheEvictionRegistrationHandle;
+
+  public static class TestModule extends AbstractModule {
+    @Override
+    protected void configure() {
+      install(new ForwarderModule());
+      install(new CacheModule());
+      install(new RouterModule());
+      install(new IndexModule());
+      SharedRefDbConfiguration sharedRefDbConfig =
+          new SharedRefDbConfiguration(new Config(), "multi-site");
+      bind(SharedRefDbConfiguration.class).toInstance(sharedRefDbConfig);
+    }
+  }
+
+  public static class CacheEvictionsTracker<K, V> implements CacheRemovalListener<K, V> {
+    private final Map<String, Set<Object>> trackedEvictions;
+    private final CountDownLatch allExpectedEvictionsArrived;
+
+    public CacheEvictionsTracker(int numExpectedEvictions) {
+      allExpectedEvictionsArrived = new CountDownLatch(numExpectedEvictions);
+      trackedEvictions = Maps.newHashMap();
+    }
+
+    public Set<Object> trackedEvictionsFor(String cacheName) {
+      return trackedEvictions.getOrDefault(cacheName, Collections.emptySet());
+    }
+
+    public void waitForExpectedEvictions() throws InterruptedException {
+      allExpectedEvictionsArrived.await(
+          CACHE_EVICTIONS_WAIT_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public void onRemoval(
+        String pluginName, String cacheName, RemovalNotification<K, V> notification) {
+      trackedEvictions.compute(
+          cacheName,
+          (k, v) -> {
+            if (v == null) {
+              return Sets.newHashSet(notification.getKey());
+            }
+            v.add(notification.getKey());
+            return v;
+          });
+      allExpectedEvictionsArrived.countDown();
+    }
+  }
+
+  @Before
+  public void startTrackingCacheEvictions() {
+    evictionsCacheTracker = new CacheEvictionsTracker<>(1);
+    cacheEvictionRegistrationHandle = cacheRemovalListeners.add("gerrit", evictionsCacheTracker);
+  }
+
+  @After
+  public void stopTrackingCacheEvictions() {
+    cacheEvictionRegistrationHandle.remove();
+  }
+
+  @Test
+  public void shouldEvictProjectCache() throws Exception {
+    objectUnderTest.route(
+        new CacheEvictionEvent(ProjectCacheImpl.CACHE_NAME, gson.toJson(project)));
+    evictionsCacheTracker.waitForExpectedEvictions();
+
+    assertThat(evictionsCacheTracker.trackedEvictionsFor(ProjectCacheImpl.CACHE_NAME))
+        .contains(project);
+  }
+}