Replication settings auto-reload and restart

New configuration setting in replication.config to
allow replication plugin to automatically detect,
reload and restart replication based on config
changes. Additional check is made on secure.config
as well in order to capture new credentials that
could have been potentially added to reach the
new replication targets.

Semantically equivalent to changing the configs and
restarting the plugin, but works automatically
without further human interaction.

NOTE: it is actually more lightweight than a full
plugin restart because besides the new replication
Destinations, everything else is kept (code,
injectors).

Change-Id: I8bbbaad8bd62f1d7efc58b45266446220641f106
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java
new file mode 100644
index 0000000..8bf2da4
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecorator.java
@@ -0,0 +1,129 @@
+// Copyright (C) 2013 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.replication;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.PluginUser;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Singleton;
+
+import com.googlesource.gerrit.plugins.replication.RemoteSiteUser;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.List;
+
+@Singleton
+public class AutoReloadConfigDecorator implements ReplicationConfig {
+  private static final Logger log = LoggerFactory
+      .getLogger(AutoReloadConfigDecorator.class);
+  private ReplicationFileBasedConfig currentConfig;
+  private long currentConfigTs;
+
+  private final Injector injector;
+  private final SitePaths site;
+  private final RemoteSiteUser.Factory remoteSiteUserFactory;
+  private final PluginUser pluginUser;
+  private final SchemaFactory<ReviewDb> db;
+  private final GitRepositoryManager gitRepositoryManager;
+  private final GroupBackend groupBackend;
+  private final WorkQueue workQueue;
+
+  @Inject
+  public AutoReloadConfigDecorator(Injector injector, SitePaths site,
+      RemoteSiteUser.Factory ruf, PluginUser pu, SchemaFactory<ReviewDb> db,
+      GitRepositoryManager grm, GroupBackend gb,
+      WorkQueue workQueue) throws ConfigInvalidException,
+      IOException {
+    this.injector = injector;
+    this.site = site;
+    this.remoteSiteUserFactory = ruf;
+    this.pluginUser = pu;
+    this.db = db;
+    this.gitRepositoryManager = grm;
+    this.groupBackend = gb;
+    this.currentConfig = loadConfig();
+    this.currentConfigTs = currentConfig.getCfgPath().lastModified();
+    this.workQueue = workQueue;
+  }
+
+  private ReplicationFileBasedConfig loadConfig()
+      throws ConfigInvalidException, IOException {
+    return new ReplicationFileBasedConfig(injector, site,
+        remoteSiteUserFactory, pluginUser, db, gitRepositoryManager,
+        groupBackend);
+  }
+
+  private synchronized boolean isAutoReload() {
+    return currentConfig.getConfig().getBoolean("gerrit", "autoReload", false);
+  }
+
+  @Override
+  public synchronized List<Destination> getDestinations() {
+    reloadIfNeeded();
+    return currentConfig.getDestinations();
+  }
+
+  private void reloadIfNeeded() {
+    if (isAutoReload()
+        && currentConfig.getCfgPath().lastModified() > currentConfigTs) {
+      try {
+        ReplicationFileBasedConfig newConfig = loadConfig();
+        newConfig.startup(workQueue);
+        int discarded = currentConfig.shutdown();
+
+        this.currentConfig = newConfig;
+        this.currentConfigTs = currentConfig.getCfgPath().lastModified();
+        log.info("Configuration reloaded: "
+            + currentConfig.getDestinations().size() + " destinations, "
+            + discarded + " replication events discarded");
+
+      } catch (Exception e) {
+        log.error(
+            "Cannot reload replication configuration: keeping existing settings",
+            e);
+        return;
+      }
+    }
+  }
+
+  @Override
+  public synchronized boolean isReplicateAllOnPluginStart() {
+    return currentConfig.isReplicateAllOnPluginStart();
+  }
+
+  @Override
+  public synchronized boolean isEmpty() {
+    return currentConfig.isEmpty();
+  }
+
+  @Override
+  public synchronized int shutdown() {
+    return currentConfig.shutdown();
+  }
+
+  @Override
+  public synchronized void startup(WorkQueue workQueue) {
+    currentConfig.startup(workQueue);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadSecureCredentialsFactoryDecorator.java b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadSecureCredentialsFactoryDecorator.java
new file mode 100644
index 0000000..9d6ce69
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/AutoReloadSecureCredentialsFactoryDecorator.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2013 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.replication;
+
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class AutoReloadSecureCredentialsFactoryDecorator implements
+    CredentialsFactory {
+  private static final Logger log = LoggerFactory
+      .getLogger(AutoReloadSecureCredentialsFactoryDecorator.class);
+
+  private final AtomicReference<SecureCredentialsFactory> secureCredentialsFactory;
+  private volatile long secureCredentialsFactoryLoadTs;
+  private final SitePaths site;
+  private ReplicationFileBasedConfig config;
+
+  @Inject
+  public AutoReloadSecureCredentialsFactoryDecorator(SitePaths site,
+      ReplicationFileBasedConfig config) throws ConfigInvalidException,
+      IOException {
+    this.site = site;
+    this.config = config;
+    this.secureCredentialsFactory =
+        new AtomicReference<SecureCredentialsFactory>(
+            new SecureCredentialsFactory(site));
+    this.secureCredentialsFactoryLoadTs = getSecureConfigLastEditTs();
+  }
+
+  private long getSecureConfigLastEditTs() {
+    FileBasedConfig cfg = new FileBasedConfig(site.secure_config, FS.DETECTED);
+    if (cfg.getFile().exists()) {
+      return cfg.getFile().lastModified();
+    } else {
+      return 0L;
+    }
+  }
+
+  @Override
+  public SecureCredentialsProvider create(String remoteName) {
+    if (needsReload()) {
+      try {
+        secureCredentialsFactory.compareAndSet(secureCredentialsFactory.get(),
+            new SecureCredentialsFactory(site));
+        secureCredentialsFactoryLoadTs = getSecureConfigLastEditTs();
+        log.info("secure.config reloaded as it was updated on the file system");
+      } catch (Exception e) {
+        log.error("Unexpected error while trying to reload "
+            + "secure.config: keeping existing credentials", e);
+      }
+    }
+
+    return secureCredentialsFactory.get().create(remoteName);
+  }
+
+
+  private boolean needsReload() {
+    return config.getConfig().getBoolean("gerrit", "autoReload", false) &&
+        getSecureConfigLastEditTs() != secureCredentialsFactoryLoadTs;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/CredentialsFactory.java b/src/main/java/com/googlesource/gerrit/plugins/replication/CredentialsFactory.java
new file mode 100644
index 0000000..9ce4a54
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/CredentialsFactory.java
@@ -0,0 +1,20 @@
+// Copyright (C) 2013 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.replication;
+
+interface CredentialsFactory {
+
+  SecureCredentialsProvider create(String remoteName);
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java b/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java
index 93f261e..ddcb933 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java
@@ -110,7 +110,7 @@
       final SchemaFactory<ReviewDb> s,
       final Destination p,
       final RemoteConfig c,
-      final SecureCredentialsFactory cpFactory,
+      final CredentialsFactory cpFactory,
       final TagCache tc,
       final PerThreadRequestScope.Scoper ts,
       final ChangeCache cc,
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfig.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfig.java
index 1d79750..1b971c3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfig.java
@@ -13,148 +13,20 @@
 // limitations under the License.
 package com.googlesource.gerrit.plugins.replication;
 
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.PluginUser;
-import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.Injector;
-import com.google.inject.Singleton;
+import com.google.gerrit.server.git.WorkQueue;
 
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.transport.RefSpec;
-import org.eclipse.jgit.transport.RemoteConfig;
-import org.eclipse.jgit.transport.URIish;
-import org.eclipse.jgit.util.FS;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.File;
-import java.io.IOException;
-import java.net.URISyntaxException;
-import java.util.Collections;
 import java.util.List;
-import java.util.Set;
 
-@Singleton
-public class ReplicationConfig {
-  static final Logger log = LoggerFactory.getLogger(ReplicationConfig.class);
-  private List<Destination> destinations;
-  private File cfgPath;
-  private boolean replicateAllOnPluginStart;
-  private Injector injector;
-  private final SchemaFactory<ReviewDb> database;
-  private final RemoteSiteUser.Factory replicationUserFactory;
-  private final PluginUser pluginUser;
-  private final GitRepositoryManager gitRepositoryManager;
-  private final GroupBackend groupBackend;
+public interface ReplicationConfig {
 
-  @Inject
-  public ReplicationConfig(final Injector injector, final SitePaths site,
-      final RemoteSiteUser.Factory ruf, final PluginUser pu,
-      final SchemaFactory<ReviewDb> db, final GitRepositoryManager grm,
-      final GroupBackend gb) throws ConfigInvalidException, IOException {
-    this.cfgPath = new File(site.etc_dir, "replication.config");
-    this.injector = injector;
-    this.replicationUserFactory = ruf;
-    this.pluginUser = pu;
-    this.database = db;
-    this.gitRepositoryManager = grm;
-    this.groupBackend = gb;
-    this.destinations = allDestinations(cfgPath);
-  }
+  List<Destination> getDestinations();
 
-  public List<Destination> getDestinations() {
-    return destinations;
-  }
+  boolean isReplicateAllOnPluginStart();
 
-  private List<Destination> allDestinations(File cfgPath)
-      throws ConfigInvalidException, IOException {
-    FileBasedConfig cfg = new FileBasedConfig(cfgPath, FS.DETECTED);
-    if (!cfg.getFile().exists()) {
-      log.warn("No " + cfg.getFile() + "; not replicating");
-      return Collections.emptyList();
-    }
-    if (cfg.getFile().length() == 0) {
-      log.info("Empty " + cfg.getFile() + "; not replicating");
-      return Collections.emptyList();
-    }
+  boolean isEmpty();
 
-    try {
-      cfg.load();
-    } catch (ConfigInvalidException e) {
-      throw new ConfigInvalidException(String.format(
-          "Config file %s is invalid: %s", cfg.getFile(), e.getMessage()), e);
-    } catch (IOException e) {
-      throw new IOException(String.format("Cannot read %s: %s", cfg.getFile(),
-          e.getMessage()), e);
-    }
+  int shutdown();
 
-    replicateAllOnPluginStart =
-        cfg.getBoolean("gerrit", "replicateOnStartup", true);
+  void startup(WorkQueue workQueue);
 
-    ImmutableList.Builder<Destination> dest = ImmutableList.builder();
-    for (RemoteConfig c : allRemotes(cfg)) {
-      if (c.getURIs().isEmpty()) {
-        continue;
-      }
-
-      // If destination for push is not set assume equal to source.
-      for (RefSpec ref : c.getPushRefSpecs()) {
-        if (ref.getDestination() == null) {
-          ref.setDestination(ref.getSource());
-        }
-      }
-
-      if (c.getPushRefSpecs().isEmpty()) {
-        c.addPushRefSpec(new RefSpec().setSourceDestination("refs/*", "refs/*")
-            .setForceUpdate(true));
-      }
-
-      Destination destination =
-          new Destination(injector, c, cfg, database, replicationUserFactory,
-              pluginUser, gitRepositoryManager, groupBackend);
-
-      if (!destination.isSingleProjectMatch()) {
-        for (URIish u : c.getURIs()) {
-          if (u.getPath() == null || !u.getPath().contains("${name}")) {
-            throw new ConfigInvalidException(String.format(
-                "remote.%s.url \"%s\" lacks ${name} placeholder in %s",
-                c.getName(), u, cfg.getFile()));
-          }
-        }
-      }
-
-      dest.add(destination);
-    }
-    return dest.build();
-  }
-
-  public boolean isReplicateAllOnPluginStart() {
-    return replicateAllOnPluginStart;
-  }
-
-  private static List<RemoteConfig> allRemotes(FileBasedConfig cfg)
-      throws ConfigInvalidException {
-    Set<String> names = cfg.getSubsections("remote");
-    List<RemoteConfig> result = Lists.newArrayListWithCapacity(names.size());
-    for (String name : names) {
-      try {
-        result.add(new RemoteConfig(cfg, name));
-      } catch (URISyntaxException e) {
-        throw new ConfigInvalidException(String.format(
-            "remote %s has invalid URL in %s", name, cfg.getFile()));
-      }
-    }
-    return result;
-  }
-
-  public boolean isEmpty() {
-    return destinations.isEmpty();
-  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java
new file mode 100644
index 0000000..60a3918
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java
@@ -0,0 +1,197 @@
+// Copyright (C) 2013 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.replication;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.PluginUser;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.transport.RemoteConfig;
+import org.eclipse.jgit.transport.URIish;
+import org.eclipse.jgit.util.FS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+@Singleton
+public class ReplicationFileBasedConfig implements ReplicationConfig {
+  static final Logger log = LoggerFactory.getLogger(ReplicationFileBasedConfig.class);
+  private List<Destination> destinations;
+  private File cfgPath;
+  private boolean replicateAllOnPluginStart;
+  private Injector injector;
+  private final SchemaFactory<ReviewDb> database;
+  private final RemoteSiteUser.Factory replicationUserFactory;
+  private final PluginUser pluginUser;
+  private final GitRepositoryManager gitRepositoryManager;
+  private final GroupBackend groupBackend;
+  private final FileBasedConfig config;
+
+  @Inject
+  public ReplicationFileBasedConfig(final Injector injector, final SitePaths site,
+      final RemoteSiteUser.Factory ruf, final PluginUser pu,
+      final SchemaFactory<ReviewDb> db, final GitRepositoryManager grm,
+      final GroupBackend gb) throws ConfigInvalidException, IOException {
+    this.cfgPath = new File(site.etc_dir, "replication.config");
+    this.injector = injector;
+    this.replicationUserFactory = ruf;
+    this.pluginUser = pu;
+    this.database = db;
+    this.gitRepositoryManager = grm;
+    this.groupBackend = gb;
+    this.config = new FileBasedConfig(cfgPath, FS.DETECTED);
+    this.destinations = allDestinations();
+  }
+
+  /* (non-Javadoc)
+   * @see com.googlesource.gerrit.plugins.replication.ReplicationConfig#getDestinations()
+   */
+  @Override
+  public List<Destination> getDestinations() {
+    return destinations;
+  }
+
+  private List<Destination> allDestinations()
+      throws ConfigInvalidException, IOException {
+    if (!config.getFile().exists()) {
+      log.warn("Config file " + config.getFile() + "does not exist; not replicating");
+      return Collections.emptyList();
+    }
+    if (config.getFile().length() == 0) {
+      log.info("Config file " + config.getFile() + " is empty; not replicating");
+      return Collections.emptyList();
+    }
+
+    try {
+      config.load();
+    } catch (ConfigInvalidException e) {
+      throw new ConfigInvalidException(String.format(
+          "Config file %s is invalid: %s", config.getFile(), e.getMessage()), e);
+    } catch (IOException e) {
+      throw new IOException(String.format("Cannot read %s: %s", config.getFile(),
+          e.getMessage()), e);
+    }
+
+    replicateAllOnPluginStart =
+        config.getBoolean("gerrit", "replicateOnStartup", true);
+
+    ImmutableList.Builder<Destination> dest = ImmutableList.builder();
+    for (RemoteConfig c : allRemotes(config)) {
+      if (c.getURIs().isEmpty()) {
+        continue;
+      }
+
+      // If destination for push is not set assume equal to source.
+      for (RefSpec ref : c.getPushRefSpecs()) {
+        if (ref.getDestination() == null) {
+          ref.setDestination(ref.getSource());
+        }
+      }
+
+      if (c.getPushRefSpecs().isEmpty()) {
+        c.addPushRefSpec(new RefSpec().setSourceDestination("refs/*", "refs/*")
+            .setForceUpdate(true));
+      }
+
+      Destination destination =
+          new Destination(injector, c, config, database, replicationUserFactory,
+              pluginUser, gitRepositoryManager, groupBackend);
+
+      if (!destination.isSingleProjectMatch()) {
+        for (URIish u : c.getURIs()) {
+          if (u.getPath() == null || !u.getPath().contains("${name}")) {
+            throw new ConfigInvalidException(String.format(
+                "remote.%s.url \"%s\" lacks ${name} placeholder in %s",
+                c.getName(), u, config.getFile()));
+          }
+        }
+      }
+
+      dest.add(destination);
+    }
+    return dest.build();
+  }
+
+  /* (non-Javadoc)
+   * @see com.googlesource.gerrit.plugins.replication.ReplicationConfig#isReplicateAllOnPluginStart()
+   */
+  @Override
+  public boolean isReplicateAllOnPluginStart() {
+    return replicateAllOnPluginStart;
+  }
+
+  private static List<RemoteConfig> allRemotes(FileBasedConfig cfg)
+      throws ConfigInvalidException {
+    Set<String> names = cfg.getSubsections("remote");
+    List<RemoteConfig> result = Lists.newArrayListWithCapacity(names.size());
+    for (String name : names) {
+      try {
+        result.add(new RemoteConfig(cfg, name));
+      } catch (URISyntaxException e) {
+        throw new ConfigInvalidException(String.format(
+            "remote %s has invalid URL in %s", name, cfg.getFile()));
+      }
+    }
+    return result;
+  }
+
+  /* (non-Javadoc)
+   * @see com.googlesource.gerrit.plugins.replication.ReplicationConfig#isEmpty()
+   */
+  @Override
+  public boolean isEmpty() {
+    return destinations.isEmpty();
+  }
+
+  File getCfgPath() {
+    return cfgPath;
+  }
+
+  public int shutdown() {
+    int discarded = 0;
+    for (Destination cfg : destinations) {
+      discarded += cfg.shutdown();
+    }
+    return discarded;
+  }
+
+  FileBasedConfig getConfig() {
+    return config;
+  }
+
+  @Override
+  public void startup(WorkQueue workQueue) {
+    for (Destination cfg : destinations) {
+      cfg.start(workQueue);
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java
index 4b3b730..8aa3248 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java
@@ -32,7 +32,6 @@
 class ReplicationModule extends AbstractModule {
   @Override
   protected void configure() {
-    bind(ReplicationConfig.class);
     bind(ReplicationQueue.class).in(Scopes.SINGLETON);
 
     DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
@@ -48,12 +47,15 @@
     bind(LifecycleListener.class)
       .annotatedWith(UniqueAnnotations.create())
       .to(OnStartStop.class);
-    bind(SecureCredentialsFactory.class).in(Scopes.SINGLETON);
+    bind(CredentialsFactory.class).to(
+        AutoReloadSecureCredentialsFactoryDecorator.class).in(Scopes.SINGLETON);
     bind(CapabilityDefinition.class)
       .annotatedWith(Exports.named(START_REPLICATION))
       .to(StartReplicationCapability.class);
 
     install(new FactoryModuleBuilder().build(PushAll.Factory.class));
     install(new FactoryModuleBuilder().build(RemoteSiteUser.Factory.class));
+
+    bind(ReplicationConfig.class).to(AutoReloadConfigDecorator.class);
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java
index 78725a3..651aba0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java
@@ -93,19 +93,14 @@
 
   @Override
   public void start() {
-    for (Destination cfg : config.getDestinations()) {
-      cfg.start(workQueue);
-    }
+    config.startup(workQueue);
     running = true;
   }
 
   @Override
   public void stop() {
     running = false;
-    int discarded = 0;
-    for (Destination cfg : config.getDestinations()) {
-      discarded += cfg.shutdown();
-    }
+    int discarded = config.shutdown();
     if (discarded > 0) {
       log.warn(String.format(
           "Cancelled %d replication events during shutdown",
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/SecureCredentialsFactory.java b/src/main/java/com/googlesource/gerrit/plugins/replication/SecureCredentialsFactory.java
index 7bb9831..68433f1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/SecureCredentialsFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/SecureCredentialsFactory.java
@@ -25,7 +25,7 @@
 import java.io.IOException;
 
 /** Looks up a remote's password in secure.config. */
-class SecureCredentialsFactory {
+class SecureCredentialsFactory implements CredentialsFactory {
   private final Config config;
 
   @Inject
@@ -51,7 +51,7 @@
     return cfg;
   }
 
-  SecureCredentialsProvider create(String remoteName) {
+  public SecureCredentialsProvider create(String remoteName) {
     String user = config.getString("remote", remoteName, "username");
     String pass = config.getString("remote", remoteName, "password");
     return new SecureCredentialsProvider(user, pass);
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index f40c75f..04f907c 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -67,6 +67,12 @@
 :	If true, replicates to all remotes on startup to ensure they
 	are in-sync with this server.  By default, true.
 
+gerrit.autoReload
+:	If true, automatically reloads replication destinations and settings
+	after `replication.config` file is updated, without the need to restart
+	the replication plugin. When the reload takes place, pending replication
+	events based on old settings are discarded. By default, false.
+
 remote.NAME.url
 :	Address of the remote server to push to.  Multiple URLs may be
 	specified within a single remote block, listing different