Allow the installation of push/fetch replication filters

Define explicit configuration settings in multisite.config
for installing the push/fetch replication filter modules.

The installation as Gerrit libModule would not work because of
the need to use a child injector that has all the proper fields
and optional injections resolved at the plugin startup phase.

Adapt the multi-site plugin documentation to reflect the new
settings.

Also avoid installing the Router/Broker modules when the
only functionality needed is the replication filtering, which
is likely to be the case for Gerrit replicas.

Change-Id: Ic90adbe6dda515965aa684c0ba638a2da68b373c
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 69bd62f..afdb41c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/Module.java
@@ -37,6 +37,7 @@
 
   @Override
   protected void configure() {
+    boolean brokerRouterNeeded = false;
 
     Collection<Message> validationErrors = config.validate();
     if (!validationErrors.isEmpty()) {
@@ -52,16 +53,20 @@
 
     if (config.cache().synchronize()) {
       install(new CacheModule());
+      brokerRouterNeeded = true;
     }
     if (config.event().synchronize()) {
       install(new EventModule(config));
+      brokerRouterNeeded = true;
     }
     if (config.index().synchronize()) {
       install(new IndexModule());
+      brokerRouterNeeded = true;
     }
 
-    install(new BrokerModule());
-
-    install(new RouterModule());
+    if (brokerRouterNeeded) {
+      install(new BrokerModule());
+      install(new RouterModule());
+    }
   }
 }
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 66d91a6..c650691 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/PluginModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/PluginModule.java
@@ -15,11 +15,16 @@
 package com.googlesource.gerrit.plugins.multisite;
 
 import com.gerritforge.gerrit.globalrefdb.validation.ProjectDeletedSharedDbCleanup;
+import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.events.ProjectDeletedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.git.WorkQueue;
+import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.ProvisionException;
 import com.google.inject.Scopes;
 import com.googlesource.gerrit.plugins.multisite.broker.BrokerApiWrapper;
 import com.googlesource.gerrit.plugins.multisite.consumer.MultiSiteConsumerRunner;
@@ -28,13 +33,25 @@
 import com.googlesource.gerrit.plugins.multisite.forwarder.broker.BrokerForwarderModule;
 
 public class PluginModule extends LifecycleModule {
+  private static final FluentLogger log = FluentLogger.forEnclosingClass();
+  private static final String[] FILTER_MODULES_CLASS_NAMES =
+      new String[] {
+        /* Class names are defined as String for avoiding this class failing to load
+         * if either replication or pull-replication plugins are missing.
+         */
+        "com.googlesource.gerrit.plugins.multisite.validation.PullReplicationFilterModule",
+        "com.googlesource.gerrit.plugins.multisite.validation.PushReplicationFilterModule"
+      };
+
   private final Configuration config;
   private final WorkQueue workQueue;
+  private final Injector parentInjector;
 
   @Inject
-  public PluginModule(Configuration config, WorkQueue workQueue) {
+  public PluginModule(Configuration config, WorkQueue workQueue, Injector parentInjector) {
     this.config = config;
     this.workQueue = workQueue;
+    this.parentInjector = parentInjector;
   }
 
   @Override
@@ -57,5 +74,39 @@
       DynamicSet.bind(binder(), ProjectDeletedListener.class)
           .to(ProjectDeletedSharedDbCleanup.class);
     }
+
+    detectFilterModules()
+        .forEach(
+            mod -> {
+              install(mod);
+              log.atInfo().log(
+                  "Replication filter module %s installed successfully",
+                  mod.getClass().getSimpleName());
+            });
+  }
+
+  private Iterable<AbstractModule> detectFilterModules() {
+    ImmutableList.Builder<AbstractModule> filterModulesBuilder = ImmutableList.builder();
+
+    for (String filterClassName : FILTER_MODULES_CLASS_NAMES) {
+      try {
+        @SuppressWarnings("unchecked")
+        Class<AbstractModule> filterClass = (Class<AbstractModule>) Class.forName(filterClassName);
+
+        AbstractModule filterModule = parentInjector.getInstance(filterClass);
+        // Check if the filterModule would be valid for creating a child Guice Injector
+        parentInjector.createChildInjector(filterModule);
+
+        filterModulesBuilder.add(filterModule);
+      } catch (NoClassDefFoundError | ClassNotFoundException e) {
+        log.atFine().withCause(e).log(
+            "Not loading %s because of missing the associated replication plugin", filterClassName);
+      } catch (Exception e) {
+        throw new ProvisionException(
+            "Unable to instantiate replication filter " + filterClassName, e);
+      }
+    }
+
+    return filterModulesBuilder.build();
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationModule.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationModule.java
index f6c5b01..92a3db7 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationModule.java
@@ -29,14 +29,11 @@
 import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedRefEnforcement;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Scopes;
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Names;
 import com.googlesource.gerrit.plugins.multisite.Configuration;
-import com.googlesource.gerrit.plugins.replication.ReplicationExtensionPointModule;
-import com.googlesource.gerrit.plugins.replication.ReplicationPushFilter;
 
 public class ValidationModule extends FactoryModule {
   private final Configuration cfg;
@@ -47,8 +44,6 @@
 
   @Override
   protected void configure() {
-    install(new ReplicationExtensionPointModule());
-
     bind(SharedRefLogger.class).to(Log4jSharedRefLogger.class);
     factory(LockWrapper.Factory.class);
 
@@ -66,8 +61,6 @@
                 ProjectVersionRefUpdate.MULTI_SITE_VERSIONING_REF,
                 ProjectVersionRefUpdate.MULTI_SITE_VERSIONING_VALUE_REF));
     bind(GitRepositoryManager.class).to(SharedRefDbGitRepositoryManager.class);
-    DynamicItem.bind(binder(), ReplicationPushFilter.class)
-        .to(MultisiteReplicationPushFilter.class);
 
     if (cfg.getSharedRefDbConfiguration().getSharedRefDb().getEnforcementRules().isEmpty()) {
       bind(SharedRefEnforcement.class).to(DefaultSharedRefEnforcement.class).in(Scopes.SINGLETON);
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 5842100..e7681a5 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -3,8 +3,10 @@
 =========================
 
 The @PLUGIN@ plugin must be installed as a library module in the
-`$GERRIT_SITE/lib` folder of all the instances. Configuration should
-be specified in the `$site_path/etc/@PLUGIN@.config` file.
+`$GERRIT_SITE/lib` folder of all the instances and linked to
+`$GERRIT_SITE/plugins`, which enable to use it as both libModule
+and plugin.
+Configuration should be specified in the `$site_path/etc/@PLUGIN@.config` file.
 
 ## Configuration parameters
 
@@ -134,3 +136,50 @@
     the project `foo/bar`, but no other project.
 
     By default, all projects are matched.
+
+## Replication filters
+
+The @PLUGIN@ plugin is also responsible for filtering out replication events that may
+risk to create a split-brain situation.
+It integrates the push and pull replication filtering extension points for validating
+the refs to be replicated and dropping some of them.
+
+**Replication plugin**
+
+When using the Gerrit core replication plugin, also known as push-replication, link the
+`replication.jar` to the `$GERRIT_SITE/lib` directory and add the following libModule
+to `gerrit.config`:
+
+```
+[gerrit]
+  installModule = com.googlesource.gerrit.plugins.replication.ReplicationExtensionPointModule
+```
+
+The above configuration would be automatically detected by the @PLUGIN@ plugin which would then
+install the PushReplicationFilterModule for filtering outgoing replication refs based
+on their global-refdb status:
+
+- Outgoing replication of refs that are NOT up-to-date with the global-refdb will be
+  discarded, because they may cause split-brain on the remote replication endpoints.
+
+- All other refs will be pushed as normal to the remote replication ends.
+
+**Pull-replication plugin**
+
+When using the [pull-replication](https://gerrit.googlesource.com/plugins/pull-replication)
+plugin, link the `pull-replication.jar` to the `$GERRIT_SITE/lib` directory and add the following
+two libModules to `gerrit.config`:
+
+```
+[gerrit]
+        installModule = com.googlesource.gerrit.plugins.replication.pull.ReplicationExtensionPointModule
+```
+
+The above configuration would be automatically detected by the @PLUGIN@ plugin which would then
+install the PullReplicationFilterModule for filtering incoming fetch replication refs based
+on their global-refdb status.
+
+- Incoming replication of refs that locally are already up-to-date with the global-refdb will be
+  discarded, because they would not add anything more to the current status of the local refs.
+
+- All other refs will be fetched as normal from the replication sources.