Make index synchronization configurable per index type

Allow configuring which index types should be synchronized across sites
in a multi-primary setup. Supported types are: change, account, group,
and project.

The `index.synchronize` setting accepts a list of types:
  - `change-index`
  - `account-index`
  - `group-index`
  - `project-index`

To disable synchronization entirely set to `false` and
to enable all set to `true`.

This enables finer-grained control on pull-replication and
multi-site indexing mechanism.

For example it will be possible to disable redundant change
indexing retries on fetch-ref-replicated when using pull-replication
in a multi-primary Gerrit setup.

Bug: Issue 425113249
Change-Id: I15fbf51e9814364cf624f251544164ec340ac225
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/Configuration.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/Configuration.java
index 7d76807..846192f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/Configuration.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/Configuration.java
@@ -29,14 +29,27 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import com.google.inject.spi.Message;
+import com.googlesource.gerrit.plugins.multisite.forwarder.ForwardedIndexAccountHandler;
+import com.googlesource.gerrit.plugins.multisite.forwarder.ForwardedIndexChangeHandler;
+import com.googlesource.gerrit.plugins.multisite.forwarder.ForwardedIndexGroupHandler;
+import com.googlesource.gerrit.plugins.multisite.forwarder.ForwardedIndexProjectHandler;
+import com.googlesource.gerrit.plugins.multisite.forwarder.ForwardedIndexingHandler;
+import com.googlesource.gerrit.plugins.multisite.forwarder.events.AccountIndexEvent;
+import com.googlesource.gerrit.plugins.multisite.forwarder.events.ChangeIndexEvent;
+import com.googlesource.gerrit.plugins.multisite.forwarder.events.GroupIndexEvent;
+import com.googlesource.gerrit.plugins.multisite.forwarder.events.IndexEvent;
+import com.googlesource.gerrit.plugins.multisite.forwarder.events.ProjectIndexEvent;
 import java.io.IOException;
 import java.time.Duration;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Random;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
@@ -366,11 +379,12 @@
     }
   }
 
-  public static class Index extends Forwarding {
+  public static class Index {
     static final String INDEX_SECTION = "index";
     static final String MAX_TRIES_KEY = "maxTries";
     static final String RETRY_INTERVAL_KEY = "retryInterval";
     static final String SYNCHRONIZE_FORCED_KEY = "synchronizeForced";
+    static final String SYNCHRONIZE_KEY = "synchronize";
     static final boolean DEFAULT_SYNCHRONIZE_FORCED = true;
 
     private final int threadPoolSize;
@@ -378,10 +392,39 @@
     private final int maxTries;
 
     private final int numStripedLocks;
+    private final Map<String, Class<? extends ForwardedIndexingHandler<?, ? extends IndexEvent>>>
+        synchronize;
     private final boolean synchronizeForced;
 
+    private static final Map<
+            String, Class<? extends ForwardedIndexingHandler<?, ? extends IndexEvent>>>
+        SYNCHRONIZE_ALL_EVENTS_HANDLERS =
+            Map.of(
+                ChangeIndexEvent.TYPE, ForwardedIndexChangeHandler.class,
+                GroupIndexEvent.TYPE, ForwardedIndexGroupHandler.class,
+                AccountIndexEvent.TYPE, ForwardedIndexAccountHandler.class,
+                ProjectIndexEvent.TYPE, ForwardedIndexProjectHandler.class);
+
+    public Map<String, Class<? extends ForwardedIndexingHandler<?, ? extends IndexEvent>>>
+        getSynchronizeIndex(Supplier<Config> cfg) {
+      Set<String> syncIndexTypes =
+          Arrays.stream(cfg.get().getStringList(INDEX_SECTION, null, SYNCHRONIZE_KEY))
+              .map(String::toLowerCase)
+              .collect(Collectors.toSet());
+
+      if (syncIndexTypes.contains("false")) {
+        return Map.of();
+      }
+
+      Map<String, Class<? extends ForwardedIndexingHandler<?, ? extends IndexEvent>>> synchIndex =
+          SYNCHRONIZE_ALL_EVENTS_HANDLERS.entrySet().stream()
+              .filter(entry -> syncIndexTypes.contains(entry.getKey()))
+              .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+
+      return synchIndex.isEmpty() ? SYNCHRONIZE_ALL_EVENTS_HANDLERS : synchIndex;
+    }
+
     private Index(Supplier<Config> cfg) {
-      super(cfg, INDEX_SECTION);
       threadPoolSize =
           getInt(cfg, INDEX_SECTION, null, THREAD_POOL_SIZE_KEY, DEFAULT_THREAD_POOL_SIZE);
       retryInterval =
@@ -391,6 +434,7 @@
           getInt(cfg, INDEX_SECTION, null, NUM_STRIPED_LOCKS, DEFAULT_NUM_STRIPED_LOCKS);
       synchronizeForced =
           getBoolean(cfg, INDEX_SECTION, null, SYNCHRONIZE_FORCED_KEY, DEFAULT_SYNCHRONIZE_FORCED);
+      synchronize = getSynchronizeIndex(cfg);
     }
 
     public int threadPoolSize() {
@@ -412,6 +456,11 @@
     public boolean synchronizeForced() {
       return synchronizeForced;
     }
+
+    public Map<String, Class<? extends ForwardedIndexingHandler<?, ? extends IndexEvent>>>
+        synchronize() {
+      return synchronize;
+    }
   }
 
   public static class Broker {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/Module.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/Module.java
index c161a6d..0e5ae15 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/Module.java
@@ -54,14 +54,14 @@
     if (config.event().synchronize()) {
       brokerRouterNeeded = true;
     }
-    if (config.index().synchronize()) {
+    if (!config.index().synchronize().isEmpty()) {
       install(new IndexModule());
       brokerRouterNeeded = true;
     }
 
     if (brokerRouterNeeded) {
       install(new BrokerModule());
-      install(new RouterModule());
+      install(new RouterModule(config.index()));
     }
   }
 }
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 c77b057..dd0b98d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/PluginModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/PluginModule.java
@@ -53,7 +53,7 @@
 
   @Override
   protected void configure() {
-    if (config.index().synchronize()
+    if (!config.index().synchronize().isEmpty()
         || config.cache().synchronize()
         || config.event().synchronize()) {
       install(new EventModule(config));
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/router/RouterModule.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/router/RouterModule.java
index 1353e30..b5af428 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/router/RouterModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/router/RouterModule.java
@@ -19,21 +19,22 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.events.EventListener;
+import com.google.inject.Inject;
 import com.google.inject.Scopes;
 import com.google.inject.TypeLiteral;
-import com.googlesource.gerrit.plugins.multisite.forwarder.ForwardedIndexAccountHandler;
-import com.googlesource.gerrit.plugins.multisite.forwarder.ForwardedIndexChangeHandler;
-import com.googlesource.gerrit.plugins.multisite.forwarder.ForwardedIndexGroupHandler;
-import com.googlesource.gerrit.plugins.multisite.forwarder.ForwardedIndexProjectHandler;
+import com.googlesource.gerrit.plugins.multisite.Configuration;
 import com.googlesource.gerrit.plugins.multisite.forwarder.ForwardedIndexingHandler;
-import com.googlesource.gerrit.plugins.multisite.forwarder.events.AccountIndexEvent;
-import com.googlesource.gerrit.plugins.multisite.forwarder.events.ChangeIndexEvent;
-import com.googlesource.gerrit.plugins.multisite.forwarder.events.GroupIndexEvent;
 import com.googlesource.gerrit.plugins.multisite.forwarder.events.IndexEvent;
-import com.googlesource.gerrit.plugins.multisite.forwarder.events.ProjectIndexEvent;
 
 public class RouterModule extends LifecycleModule {
 
+  private final Configuration.Index indexConfig;
+
+  @Inject
+  public RouterModule(Configuration.Index indexConfig) {
+    this.indexConfig = indexConfig;
+  }
+
   public static final TypeLiteral<ForwardedIndexingHandler<?, ? extends IndexEvent>> INDEX_HANDLER =
       new TypeLiteral<>() {};
 
@@ -44,18 +45,10 @@
     DynamicSet.bind(binder(), EventListener.class).to(IndexEventRouter.class);
     DynamicMap.mapOf(binder(), INDEX_HANDLER);
 
-    bind(INDEX_HANDLER)
-        .annotatedWith(Exports.named(ChangeIndexEvent.TYPE))
-        .to(ForwardedIndexChangeHandler.class);
-    bind(INDEX_HANDLER)
-        .annotatedWith(Exports.named(GroupIndexEvent.TYPE))
-        .to(ForwardedIndexGroupHandler.class);
-    bind(INDEX_HANDLER)
-        .annotatedWith(Exports.named(ProjectIndexEvent.TYPE))
-        .to(ForwardedIndexProjectHandler.class);
-    bind(INDEX_HANDLER)
-        .annotatedWith(Exports.named(AccountIndexEvent.TYPE))
-        .to(ForwardedIndexAccountHandler.class);
+    indexConfig
+        .synchronize()
+        .forEach(
+            (type, handler) -> bind(INDEX_HANDLER).annotatedWith(Exports.named(type)).to(handler));
 
     bind(CacheEvictionEventRouter.class).in(Scopes.SINGLETON);
     bind(ProjectListUpdateRouter.class).in(Scopes.SINGLETON);
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 75fc55b..95cb2ca 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -42,10 +42,27 @@
     Defaults to 10
 
 ```index.synchronize```
-:   Whether to synchronize secondary indexes. Set to false when using multi-site
-    on Gerrit replicas that do not have an index, or when using an external
-    service such as ElasticSearch.
-    Defaults to true.
+:   Controls which types of index events should be synchronized across sites.
+    It supports the following values:
+
+    - `true`: shortcut for including all index types
+    - `false`: disable index synchronization
+    - A list of specific index types to synchronize:
+      - `change-index`
+      - `account-index`
+      - `group-index`
+      - `project-index`
+
+    Example to synchronize only change and project indexes:
+    ```
+    [index]
+      synchronize = change-index
+      synchronize = project-index
+    ```
+
+> NOTE: All unknown index types are ignored.
+
+Defaults: all index types are enabled.
 
 ```index.synchronizeForced```
 :   Whether to synchronize forced index events. E.g. on-line reindex
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/ConfigurationTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/ConfigurationTest.java
index 3a24773..4f0d373 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/ConfigurationTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/ConfigurationTest.java
@@ -65,16 +65,16 @@
 
   @Test
   public void testGetIndexSynchronize() throws Exception {
-    assertThat(getConfiguration().index().synchronize()).isEqualTo(DEFAULT_SYNCHRONIZE);
+    assertThat(getConfiguration().index().synchronize()).isNotEmpty();
 
     globalPluginConfig.setBoolean(INDEX_SECTION, null, SYNCHRONIZE_KEY, false);
-    assertThat(getConfiguration().index().synchronize()).isFalse();
+    assertThat(getConfiguration().index().synchronize()).isEmpty();
 
     globalPluginConfig.setBoolean(INDEX_SECTION, null, SYNCHRONIZE_KEY, true);
-    assertThat(getConfiguration().index().synchronize()).isTrue();
+    assertThat(getConfiguration().index().synchronize()).isNotEmpty();
 
     globalPluginConfig.setString(INDEX_SECTION, null, SYNCHRONIZE_KEY, INVALID_BOOLEAN);
-    assertThat(getConfiguration().index().synchronize()).isTrue();
+    assertThat(getConfiguration().index().synchronize()).isNotEmpty();
   }
 
   @Test
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
index 6ca1a7a..3233baf 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedCacheEvictionHandlerIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/forwarder/ForwardedCacheEvictionHandlerIT.java
@@ -35,6 +35,7 @@
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.multisite.Configuration;
 import com.googlesource.gerrit.plugins.multisite.ExecutorProvider;
 import com.googlesource.gerrit.plugins.multisite.cache.CacheModule;
 import com.googlesource.gerrit.plugins.multisite.forwarder.events.CacheEvictionEvent;
@@ -72,11 +73,13 @@
   private RegistrationHandle cacheEvictionRegistrationHandle;
 
   public static class TestModule extends AbstractModule {
+    @Inject Configuration config;
+
     @Override
     protected void configure() {
       install(new ForwarderModule());
       install(new CacheModule(TestForwardingExecutorProvider.class));
-      install(new RouterModule());
+      install(new RouterModule(config.index()));
       install(new IndexModule());
       SharedRefDbConfiguration sharedRefDbConfig =
           new SharedRefDbConfiguration(new Config(), "multi-site");
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 80c3d8a..f11ee26 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
@@ -63,12 +63,13 @@
 
   public static class TestModule extends AbstractModule {
     @Inject WorkQueue workQueue;
+    @Inject Configuration config;
 
     @Override
     protected void configure() {
       install(new ForwarderModule());
       install(new CacheModule());
-      install(new RouterModule());
+      install(new RouterModule(config.index()));
       install(new IndexModule());
       install(new ReplicationStatusModule(workQueue));
       SharedRefDbConfiguration sharedRefDbConfig =