Merge branch 'stable-2.16'

* stable-2.16:
  ProjectIndex: support project index propagation
  Upgrade bazlets to latest stable-2.16 to build with 2.16.4 API
  Upgrade bazlets to latest stable-2.15 to build with 2.15.9 API
  Upgrade bazlets to latest stable-2.15 to build with 2.15.8 API
  Upgrade bazlets to latest stable-2.14 to build with 2.14.18 API
  ForwardedIndexChangeHandler: Replace parameter writing with variable
  PeerInfoNotAvailableException: Remove this class as no longer used
  Forwarder tests: Convert useless members to local variable
  ForwardedIndexChangeHandlerTest: Remove unused gitRepoMgrMock
  ChangeChecker{Impl}: Remove redundant public modifier
  ForwardedIndexChangeHandler: Remove redundant local variable
  ChangeReindexRunnable: Replace statement lambda with expression
  IndexTs: Move local variable to used scope
  ForwardedIndexChangeHandler: Remove unused changeFinder parameter
  Catch all exceptions when indexTs fails
  Minimize use of ReviewDb when not needed
  Retry change reindex because of NFS access caching
  RestForwarder: Replace lambda with method reference
  Always use the stored timestamp when checking for updates
  Use always the last TS of the reindex across runs
  GroupReindexRunnable: Replace lambdas with method reference
  Support n nodes when using static strategy
  Upgrade bazlets to latest stable-2.16 to build with 2.16.2 API
  Upgrade wiremock to 2.20.0
  FileBasedWebSessionCacheTest: Pass test upon invalid key
  Make the index striped locks size configurable
  Bazel: Include eclipse-out directory in .bazelignore
  Add explanatory comment to empty BUILD file(s)
  Update mockito to 2.23.4
  Setup: Replace 'multiply' with 'repeat' in init step
  Setup: Add jgroups skipInterface configuration step
  Setup: Add jgroups protocolStack configuration step
  Configuration: Remove unnecessary usage of 'this'
  Setup: Remove duplication of default auto reindex value
  Setup: Add healthCheck configuration init step
  Setup: Add synchronize event configuration init step
  Setup: Add synchronize configuration step to forwarding sections
  Setup: Call proper method for null subsection cases
  Setup: Remove unused subsection method parameter
  Setup: Add cache pattern configuration init step
  Setup: Rename the number-to-string conversion methods
  Setup: Add autoReindex delay and pollInterval configuration steps
  Setup: Make the http section method name consistent
  Setup: Add init step for autoReindex configuration
  Add missing init step declaration in manifest
  Upgrade bazlets to latest stable-2.15 to build with 2.15.7 API
  FileBasedWebSessionCacheTest: Fix IncompatibleArgumentType error
  Upgrade bazlets to latest stable-2.14 to build with 2.14.17 API
  Harmonize how REST API handlers get a JSON body
  Upgrade bazlets to latest stable-2.15 to build with 2.15.6 API
  Upgrade bazlets to latest stable-2.14 to build with 2.14.16 API
  Align Eclipse compiler settings with core Gerrit's
  WORKSPACE: Make header line space indent consistent
  WORKSPACE: Replace custom local_path with template
  Upgrade wiremock to 2.19.0
  bazlets: Replace native.git_repository with skylark rule
  Harmonize external dependency names to use hyphen
  Upgrade mockito to 2.23.0
  Upgrade bazlets to latest stable-2.15 to build with 2.15.5 API
  Update bazlets to latest stable-2.14 to build with 2.14.15 API
  Update bazlets to latest stable-2.15 to build with 2.15.4 API
  Update bazlets to latest stable-2.14 to build with 2.14.14 API
  Migrate `tools/bazel.rc` to `.bazelrc`
  Update bazlets to latest stable-2.14 to build with 2.14.13 API
  Update bazlets to latest stable-2.14 to use 2.14.12 API
  RestForwarder: Avoid retrying non recoverable exceptions
  Configuration: Fix logging arguments should not require evaluation
  Configuration: Move constants to corresponding classes
  Auto-reindex changes, groups and accounts during startup
  Upgrade wiremock to 2.18.0
  Upgrade mockito to 2.21.0
  Update bazlets to latest stable-2.14 to use 2.14.11 API
  Upgrade bazlets to latest stable-2.15 to use 2.15.3 API

Change-Id: I29612d0bea4294a7ec3ea978fbb23a10d949090e
diff --git a/.bazelignore b/.bazelignore
new file mode 100644
index 0000000..30f1613
--- /dev/null
+++ b/.bazelignore
@@ -0,0 +1 @@
+eclipse-out
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
index c2a4ef5..40e022d 100644
--- a/.settings/org.eclipse.jdt.core.prefs
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -18,7 +18,7 @@
 org.eclipse.jdt.core.compiler.debug.sourceFile=generate
 org.eclipse.jdt.core.compiler.doc.comment.support=enabled
 org.eclipse.jdt.core.compiler.problem.APILeak=warning
-org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=ignore
 org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
 org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
 org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
@@ -35,7 +35,7 @@
 org.eclipse.jdt.core.compiler.problem.fieldHiding=warning
 org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
 org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
-org.eclipse.jdt.core.compiler.problem.forbiddenReference=error
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
 org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
 org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled
 org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
@@ -46,9 +46,9 @@
 org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsDeprecatedRef=enabled
 org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsNotVisibleRef=enabled
 org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsVisibility=private
-org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore
 org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
-org.eclipse.jdt.core.compiler.problem.missingDefaultCase=warning
+org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
 org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore
 org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=enabled
 org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning
@@ -77,15 +77,15 @@
 org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
 org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning
 org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning
-org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning
-org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore
 org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
 org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
 org.eclipse.jdt.core.compiler.problem.redundantNullCheck=warning
 org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=warning
 org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore
 org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
-org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=warning
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
 org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
 org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
 org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled
@@ -113,7 +113,7 @@
 org.eclipse.jdt.core.compiler.problem.unusedImport=warning
 org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
 org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
-org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore
 org.eclipse.jdt.core.compiler.problem.unusedParameter=warning
 org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
 org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
diff --git a/BUILD b/BUILD
index 7fec2ea..7d5fad8 100644
--- a/BUILD
+++ b/BUILD
@@ -13,6 +13,7 @@
         "Gerrit-PluginName: high-availability",
         "Gerrit-Module: com.ericsson.gerrit.plugins.highavailability.Module",
         "Gerrit-HttpModule: com.ericsson.gerrit.plugins.highavailability.HttpModule",
+        "Gerrit-InitStep: com.ericsson.gerrit.plugins.highavailability.Setup",
         "Implementation-Title: high-availability plugin",
         "Implementation-URL: https://gerrit-review.googlesource.com/#/admin/projects/plugins/high-availability",
     ],
diff --git a/WORKSPACE b/WORKSPACE
index b07eaf9..d9f52f3 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -3,8 +3,8 @@
 load("//:bazlets.bzl", "load_bazlets")
 
 load_bazlets(
-    commit = "d025e909c2e8a369712165309f599a2765005f2d",
-    #local_path = "/home/ehugare/workspaces/bazlets",
+    commit = "c15e64db2a87c0453427ff45f79d1ed3899b5d92",
+    #local_path = "/home/<user>/projects/bazlets",
 )
 
 # Snapshot Plugin API
diff --git a/bazlets.bzl b/bazlets.bzl
index f97b72c..f089af4 100644
--- a/bazlets.bzl
+++ b/bazlets.bzl
@@ -1,10 +1,12 @@
+load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
+
 NAME = "com_googlesource_gerrit_bazlets"
 
 def load_bazlets(
         commit,
         local_path = None):
     if not local_path:
-        native.git_repository(
+        git_repository(
             name = NAME,
             remote = "https://gerrit.googlesource.com/bazlets",
             commit = commit,
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
index f3249eb..bca2271 100644
--- a/external_plugin_deps.bzl
+++ b/external_plugin_deps.bzl
@@ -3,33 +3,33 @@
 def external_plugin_deps():
     maven_jar(
         name = "wiremock",
-        artifact = "com.github.tomakehurst:wiremock-standalone:2.8.0",
-        sha1 = "b4d91aca283a86b447d3906deac6e1509c3a94c5",
+        artifact = "com.github.tomakehurst:wiremock-standalone:2.20.0",
+        sha1 = "26e5d42ca7af2a9d4b97129595edd0192ad9bd30",
     )
 
     maven_jar(
         name = "mockito",
-        artifact = "org.mockito:mockito-core:2.15.0",
-        sha1 = "b84bfbbc29cd22c9529409627af6ea2897f4fa85",
+        artifact = "org.mockito:mockito-core:2.23.4",
+        sha1 = "a35b6f8ffcfa786771eac7d7d903429e790fdf3f",
         deps = [
-            "@byte_buddy//jar",
-            "@byte_buddy_agent//jar",
+            "@byte-buddy//jar",
+            "@byte-buddy-agent//jar",
             "@objenesis//jar",
         ],
     )
 
-    BYTE_BUDDY_VER = "1.7.9"
+    BYTE_BUDDY_VERSION = "1.9.3"
 
     maven_jar(
-        name = "byte_buddy",
-        artifact = "net.bytebuddy:byte-buddy:" + BYTE_BUDDY_VER,
-        sha1 = "51218a01a882c04d0aba8c028179cce488bbcb58",
+        name = "byte-buddy",
+        artifact = "net.bytebuddy:byte-buddy:" + BYTE_BUDDY_VERSION,
+        sha1 = "f32e510b239620852fc9a2387fac41fd053d6a4d",
     )
 
     maven_jar(
-        name = "byte_buddy_agent",
-        artifact = "net.bytebuddy:byte-buddy-agent:" + BYTE_BUDDY_VER,
-        sha1 = "a6c65f9da7f467ee1f02ff2841ffd3155aee2fc9",
+        name = "byte-buddy-agent",
+        artifact = "net.bytebuddy:byte-buddy-agent:" + BYTE_BUDDY_VERSION,
+        sha1 = "f5b78c16cf4060664d80b6ca32d80dca4bd3d264",
     )
 
     maven_jar(
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java
index 9313656..edd96d7 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java
@@ -20,6 +20,7 @@
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.server.config.ConfigUtil;
@@ -32,7 +33,11 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -41,72 +46,19 @@
 public class Configuration {
   private static final Logger log = LoggerFactory.getLogger(Configuration.class);
 
-  // main section
-  static final String MAIN_SECTION = "main";
-  static final String SHARED_DIRECTORY_KEY = "sharedDirectory";
-  static final String DEFAULT_SHARED_DIRECTORY = "shared";
-
-  // peerInfo section
+  // common parameter to peerInfo section
   static final String PEER_INFO_SECTION = "peerInfo";
-  static final String STATIC_SUBSECTION = PeerInfoStrategy.STATIC.name().toLowerCase();
-  static final String JGROUPS_SUBSECTION = PeerInfoStrategy.JGROUPS.name().toLowerCase();
-  static final String URL_KEY = "url";
-  static final String STRATEGY_KEY = "strategy";
-  static final String MY_URL_KEY = "myUrl";
-
-  // jgroups section
-  static final String JGROUPS_SECTION = "jgroups";
-  static final String SKIP_INTERFACE_KEY = "skipInterface";
-  static final String CLUSTER_NAME_KEY = "clusterName";
-  static final String PROTOCOL_STACK_KEY = "protocolStack";
-
-  // http section
-  static final String HTTP_SECTION = "http";
-  static final String USER_KEY = "user";
-  static final String PASSWORD_KEY = "password";
-  static final String CONNECTION_TIMEOUT_KEY = "connectionTimeout";
-  static final String SOCKET_TIMEOUT_KEY = "socketTimeout";
-  static final String MAX_TRIES_KEY = "maxTries";
-  static final String RETRY_INTERVAL_KEY = "retryInterval";
-
-  // cache section
-  static final String CACHE_SECTION = "cache";
-  static final String PATTERN_KEY = "pattern";
-
-  // event section
-  static final String EVENT_SECTION = "event";
-
-  // index section
-  static final String INDEX_SECTION = "index";
 
   // common parameters to cache and index sections
   static final String THREAD_POOL_SIZE_KEY = "threadPoolSize";
-
-  // common parameters to cache, event index and websession sections
-  static final String SYNCHRONIZE_KEY = "synchronize";
-
-  // health check section
-  static final String HEALTH_CHECK_SECTION = "healthCheck";
-  static final String ENABLE_KEY = "enable";
-  static final boolean DEFAULT_HEALTH_CHECK_ENABLED = true;
-
-  // websession section
-  static final String WEBSESSION_SECTION = "websession";
-  static final String CLEANUP_INTERVAL_KEY = "cleanupInterval";
-
-  static final int DEFAULT_TIMEOUT_MS = 5000;
-  static final int DEFAULT_MAX_TRIES = 360;
-  static final int DEFAULT_RETRY_INTERVAL = 10000;
+  static final int DEFAULT_INDEX_MAX_TRIES = 2;
+  static final int DEFAULT_INDEX_RETRY_INTERVAL = 30000;
   static final int DEFAULT_THREAD_POOL_SIZE = 4;
-  static final String DEFAULT_CLEANUP_INTERVAL = "24 hours";
-  static final long DEFAULT_CLEANUP_INTERVAL_MS = HOURS.toMillis(24);
-  static final boolean DEFAULT_SYNCHRONIZE = true;
-  static final PeerInfoStrategy DEFAULT_PEER_INFO_STRATEGY = PeerInfoStrategy.STATIC;
-  static final ImmutableList<String> DEFAULT_SKIP_INTERFACE_LIST =
-      ImmutableList.of("lo*", "utun*", "awdl*");
-  static final String DEFAULT_CLUSTER_NAME = "GerritHA";
+  static final String NUM_STRIPED_LOCKS = "numStripedLocks";
+  static final int DEFAULT_NUM_STRIPED_LOCKS = 10;
 
   private final Main main;
+  private final AutoReindex autoReindex;
   private final PeerInfo peerInfo;
   private final JGroups jgroups;
   private final Http http;
@@ -128,6 +80,7 @@
       PluginConfigFactory pluginConfigFactory, @PluginName String pluginName, SitePaths site) {
     Config cfg = pluginConfigFactory.getGlobalPluginConfig(pluginName);
     main = new Main(site, cfg);
+    autoReindex = new AutoReindex(cfg);
     peerInfo = new PeerInfo(cfg);
     switch (peerInfo.strategy()) {
       case STATIC:
@@ -152,6 +105,10 @@
     return main;
   }
 
+  public AutoReindex autoReindex() {
+    return autoReindex;
+  }
+
   public PeerInfo peerInfo() {
     return peerInfo;
   }
@@ -202,12 +159,11 @@
     }
   }
 
-  @Nullable
-  private static String trimTrailingSlash(@Nullable String in) {
-    return in == null ? null : CharMatcher.is('/').trimTrailingFrom(in);
-  }
-
   public static class Main {
+    static final String MAIN_SECTION = "main";
+    static final String SHARED_DIRECTORY_KEY = "sharedDirectory";
+    static final String DEFAULT_SHARED_DIRECTORY = "shared";
+
     private final Path sharedDirectory;
 
     private Main(SitePaths site, Config cfg) {
@@ -228,12 +184,59 @@
     }
   }
 
+  public static class AutoReindex {
+
+    static final String AUTO_REINDEX_SECTION = "autoReindex";
+    static final String ENABLED = "enabled";
+    static final String DELAY = "delay";
+    static final String POLL_INTERVAL = "pollInterval";
+    static final boolean DEFAULT_AUTO_REINDEX = false;
+    static final long DEFAULT_DELAY = 10L;
+    static final long DEFAULT_POLL_INTERVAL = 0L;
+
+    private final boolean enabled;
+    private final long delaySec;
+    private final long pollSec;
+
+    public AutoReindex(Config cfg) {
+      enabled = cfg.getBoolean(AUTO_REINDEX_SECTION, ENABLED, DEFAULT_AUTO_REINDEX);
+      delaySec =
+          ConfigUtil.getTimeUnit(
+              cfg, AUTO_REINDEX_SECTION, null, DELAY, DEFAULT_DELAY, TimeUnit.SECONDS);
+      pollSec =
+          ConfigUtil.getTimeUnit(
+              cfg,
+              AUTO_REINDEX_SECTION,
+              null,
+              POLL_INTERVAL,
+              DEFAULT_POLL_INTERVAL,
+              TimeUnit.SECONDS);
+    }
+
+    public boolean enabled() {
+      return enabled;
+    }
+
+    public long delaySec() {
+      return delaySec;
+    }
+
+    public long pollSec() {
+      return pollSec;
+    }
+  }
+
   public static class PeerInfo {
+    static final PeerInfoStrategy DEFAULT_PEER_INFO_STRATEGY = PeerInfoStrategy.STATIC;
+    static final String STRATEGY_KEY = "strategy";
+
     private final PeerInfoStrategy strategy;
 
     private PeerInfo(Config cfg) {
       strategy = cfg.getEnum(PEER_INFO_SECTION, null, STRATEGY_KEY, DEFAULT_PEER_INFO_STRATEGY);
-      log.debug("Strategy: {}", strategy.name());
+      if (log.isDebugEnabled()) {
+        log.debug("Strategy: {}", strategy.name());
+      }
     }
 
     public PeerInfoStrategy strategy() {
@@ -241,22 +244,31 @@
     }
   }
 
-  public class PeerInfoStatic {
-    private final String url;
+  public static class PeerInfoStatic {
+    static final String STATIC_SUBSECTION = PeerInfoStrategy.STATIC.name().toLowerCase();
+    static final String URL_KEY = "url";
+
+    private final Set<String> urls;
 
     private PeerInfoStatic(Config cfg) {
-      url =
-          trimTrailingSlash(
-              Strings.nullToEmpty(cfg.getString(PEER_INFO_SECTION, STATIC_SUBSECTION, URL_KEY)));
-      log.debug("Url: {}", url);
+      urls =
+          Arrays.stream(cfg.getStringList(PEER_INFO_SECTION, STATIC_SUBSECTION, URL_KEY))
+              .filter(Objects::nonNull)
+              .filter(s -> !s.isEmpty())
+              .map(s -> CharMatcher.is('/').trimTrailingFrom(s))
+              .collect(Collectors.toSet());
+      log.debug("Urls: {}", urls);
     }
 
-    public String url() {
-      return url;
+    public Set<String> urls() {
+      return ImmutableSet.copyOf(urls);
     }
   }
 
   public static class PeerInfoJGroups {
+    static final String JGROUPS_SUBSECTION = PeerInfoStrategy.JGROUPS.name().toLowerCase();
+    static final String MY_URL_KEY = "myUrl";
+
     private final String myUrl;
 
     private PeerInfoJGroups(Config cfg) {
@@ -267,9 +279,22 @@
     public String myUrl() {
       return myUrl;
     }
+
+    @Nullable
+    private static String trimTrailingSlash(@Nullable String in) {
+      return in == null ? in : CharMatcher.is('/').trimTrailingFrom(in);
+    }
   }
 
   public static class JGroups {
+    static final String JGROUPS_SECTION = "jgroups";
+    static final String SKIP_INTERFACE_KEY = "skipInterface";
+    static final String CLUSTER_NAME_KEY = "clusterName";
+    static final String PROTOCOL_STACK_KEY = "protocolStack";
+    static final ImmutableList<String> DEFAULT_SKIP_INTERFACE_LIST =
+        ImmutableList.of("lo*", "utun*", "awdl*");
+    static final String DEFAULT_CLUSTER_NAME = "GerritHA";
+
     private final ImmutableList<String> skipInterface;
     private final String clusterName;
     private final Optional<Path> protocolStack;
@@ -311,6 +336,18 @@
   }
 
   public static class Http {
+    static final String HTTP_SECTION = "http";
+    static final String USER_KEY = "user";
+    static final String PASSWORD_KEY = "password";
+    static final String CONNECTION_TIMEOUT_KEY = "connectionTimeout";
+    static final String SOCKET_TIMEOUT_KEY = "socketTimeout";
+    static final String MAX_TRIES_KEY = "maxTries";
+    static final String RETRY_INTERVAL_KEY = "retryInterval";
+
+    static final int DEFAULT_TIMEOUT_MS = 5000;
+    static final int DEFAULT_MAX_TRIES = 360;
+    static final int DEFAULT_RETRY_INTERVAL = 10000;
+
     private final String user;
     private final String password;
     private final int connectionTimeout;
@@ -354,6 +391,9 @@
 
   /** Common parameters to cache, event, index and websession */
   public abstract static class Forwarding {
+    static final boolean DEFAULT_SYNCHRONIZE = true;
+    static final String SYNCHRONIZE_KEY = "synchronize";
+
     private final boolean synchronize;
 
     private Forwarding(Config cfg, String section) {
@@ -377,6 +417,9 @@
   }
 
   public static class Cache extends Forwarding {
+    static final String CACHE_SECTION = "cache";
+    static final String PATTERN_KEY = "pattern";
+
     private final int threadPoolSize;
     private final List<String> patterns;
 
@@ -396,30 +439,60 @@
   }
 
   public static class Event extends Forwarding {
+    static final String EVENT_SECTION = "event";
+
     private Event(Config cfg) {
       super(cfg, EVENT_SECTION);
     }
   }
 
   public static class Index extends Forwarding {
+    static final String INDEX_SECTION = "index";
+    static final String MAX_TRIES_KEY = "maxTries";
+    static final String RETRY_INTERVAL_KEY = "retryInterval";
+
     private final int threadPoolSize;
+    private final int retryInterval;
+    private final int maxTries;
+
+    private final int numStripedLocks;
 
     private Index(Config cfg) {
       super(cfg, INDEX_SECTION);
       threadPoolSize = getInt(cfg, INDEX_SECTION, THREAD_POOL_SIZE_KEY, DEFAULT_THREAD_POOL_SIZE);
+      numStripedLocks = getInt(cfg, INDEX_SECTION, NUM_STRIPED_LOCKS, DEFAULT_NUM_STRIPED_LOCKS);
+      retryInterval = getInt(cfg, INDEX_SECTION, RETRY_INTERVAL_KEY, DEFAULT_INDEX_RETRY_INTERVAL);
+      maxTries = getInt(cfg, INDEX_SECTION, MAX_TRIES_KEY, DEFAULT_INDEX_MAX_TRIES);
     }
 
     public int threadPoolSize() {
       return threadPoolSize;
     }
+
+    public int numStripedLocks() {
+      return numStripedLocks;
+    }
+
+    public int retryInterval() {
+      return retryInterval;
+    }
+
+    public int maxTries() {
+      return maxTries;
+    }
   }
 
   public static class Websession extends Forwarding {
+    static final String WEBSESSION_SECTION = "websession";
+    static final String CLEANUP_INTERVAL_KEY = "cleanupInterval";
+    static final String DEFAULT_CLEANUP_INTERVAL = "24 hours";
+    static final long DEFAULT_CLEANUP_INTERVAL_MS = HOURS.toMillis(24);
+
     private final long cleanupInterval;
 
     private Websession(Config cfg) {
       super(cfg, WEBSESSION_SECTION);
-      this.cleanupInterval =
+      cleanupInterval =
           ConfigUtil.getTimeUnit(
               Strings.nullToEmpty(cfg.getString(WEBSESSION_SECTION, null, CLEANUP_INTERVAL_KEY)),
               DEFAULT_CLEANUP_INTERVAL_MS,
@@ -432,6 +505,10 @@
   }
 
   public static class HealthCheck {
+    static final String HEALTH_CHECK_SECTION = "healthCheck";
+    static final String ENABLE_KEY = "enable";
+    static final boolean DEFAULT_HEALTH_CHECK_ENABLED = true;
+
     private final boolean enabled;
 
     private HealthCheck(Config cfg) {
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/ExecutorProvider.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/ExecutorProvider.java
index 597034d..3d3212b 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/ExecutorProvider.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/ExecutorProvider.java
@@ -17,11 +17,11 @@
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.inject.Provider;
-import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ScheduledExecutorService;
 
-public abstract class ExecutorProvider implements Provider<Executor>, LifecycleListener {
-  private ExecutorService executor;
+public abstract class ExecutorProvider
+    implements Provider<ScheduledExecutorService>, LifecycleListener {
+  private ScheduledExecutorService executor;
 
   protected ExecutorProvider(WorkQueue workQueue, int threadPoolSize, String threadNamePrefix) {
     executor = workQueue.createQueue(threadPoolSize, threadNamePrefix);
@@ -39,7 +39,7 @@
   }
 
   @Override
-  public Executor get() {
+  public ScheduledExecutorService get() {
     return executor;
   }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Module.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Module.java
index f9155da..744fac9 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Module.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Module.java
@@ -14,6 +14,7 @@
 
 package com.ericsson.gerrit.plugins.highavailability;
 
+import com.ericsson.gerrit.plugins.highavailability.autoreindex.AutoReindexModule;
 import com.ericsson.gerrit.plugins.highavailability.cache.CacheModule;
 import com.ericsson.gerrit.plugins.highavailability.event.EventModule;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwarderModule;
@@ -50,6 +51,9 @@
     if (config.index().synchronize()) {
       install(new IndexModule());
     }
+    if (config.autoReindex().enabled()) {
+      install(new AutoReindexModule());
+    }
     install(new PeerInfoModule(config.peerInfo().strategy()));
   }
 
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Setup.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Setup.java
index a8241cb..86c757e 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Setup.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Setup.java
@@ -14,33 +14,49 @@
 
 package com.ericsson.gerrit.plugins.highavailability;
 
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.CACHE_SECTION;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.CLEANUP_INTERVAL_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.CLUSTER_NAME_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.CONNECTION_TIMEOUT_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_CLEANUP_INTERVAL;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_CLUSTER_NAME;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_MAX_TRIES;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_RETRY_INTERVAL;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_SHARED_DIRECTORY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.AutoReindex.AUTO_REINDEX_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.AutoReindex.DEFAULT_AUTO_REINDEX;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.AutoReindex.DEFAULT_DELAY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.AutoReindex.DEFAULT_POLL_INTERVAL;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.AutoReindex.DELAY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.AutoReindex.ENABLED;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.AutoReindex.POLL_INTERVAL;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Cache.CACHE_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Cache.PATTERN_KEY;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_THREAD_POOL_SIZE;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_TIMEOUT_MS;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.HTTP_SECTION;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.INDEX_SECTION;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.JGROUPS_SUBSECTION;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.MAIN_SECTION;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.MAX_TRIES_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.PASSWORD_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Event.EVENT_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Forwarding.DEFAULT_SYNCHRONIZE;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Forwarding.SYNCHRONIZE_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.HealthCheck.DEFAULT_HEALTH_CHECK_ENABLED;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.HealthCheck.ENABLE_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.HealthCheck.HEALTH_CHECK_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Http.CONNECTION_TIMEOUT_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Http.DEFAULT_MAX_TRIES;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Http.DEFAULT_RETRY_INTERVAL;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Http.DEFAULT_TIMEOUT_MS;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Http.HTTP_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Http.MAX_TRIES_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Http.PASSWORD_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Http.RETRY_INTERVAL_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Http.SOCKET_TIMEOUT_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Http.USER_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Index.INDEX_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.JGroups.CLUSTER_NAME_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.JGroups.DEFAULT_CLUSTER_NAME;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.JGroups.PROTOCOL_STACK_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.JGroups.SKIP_INTERFACE_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Main.DEFAULT_SHARED_DIRECTORY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Main.MAIN_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Main.SHARED_DIRECTORY_KEY;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.PEER_INFO_SECTION;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.RETRY_INTERVAL_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.SHARED_DIRECTORY_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.SOCKET_TIMEOUT_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.STATIC_SUBSECTION;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.STRATEGY_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.PeerInfo.STRATEGY_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.PeerInfoJGroups.JGROUPS_SUBSECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.PeerInfoStatic.STATIC_SUBSECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.PeerInfoStatic.URL_KEY;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.THREAD_POOL_SIZE_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.URL_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.USER_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.WEBSESSION_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Websession.CLEANUP_INTERVAL_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Websession.DEFAULT_CLEANUP_INTERVAL;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Websession.WEBSESSION_SECTION;
 
 import com.ericsson.gerrit.plugins.highavailability.Configuration.PeerInfoStrategy;
 import com.google.common.base.Strings;
@@ -94,10 +110,13 @@
       Path pluginConfigFile = site.etc_dir.resolve(pluginName + ".config");
       config = new FileBasedConfig(pluginConfigFile.toFile(), FS.DETECTED);
       config.load();
-      configureHttp();
+      configureAutoReindexSection();
+      configureHttpSection();
       configureCacheSection();
+      configureEventSection();
       configureIndexSection();
       configureWebsessionsSection();
+      configureHealthCheckSection();
       if (!createHAReplicaSite(config)) {
         configureMainSection();
         configurePeerInfoSection();
@@ -107,6 +126,25 @@
     }
   }
 
+  private void configureAutoReindexSection() {
+    ui.header("AutoReindex section");
+    Boolean autoReindex =
+        promptAndSetBoolean("Auto reindex", AUTO_REINDEX_SECTION, ENABLED, DEFAULT_AUTO_REINDEX);
+    config.setBoolean(AUTO_REINDEX_SECTION, null, ENABLED, autoReindex);
+
+    String delay =
+        promptAndSetString("Delay", AUTO_REINDEX_SECTION, DELAY, numberToString(DEFAULT_DELAY));
+    config.setLong(AUTO_REINDEX_SECTION, null, DELAY, Long.valueOf(delay));
+
+    String pollInterval =
+        promptAndSetString(
+            "Poll interval",
+            AUTO_REINDEX_SECTION,
+            POLL_INTERVAL,
+            numberToString(DEFAULT_POLL_INTERVAL));
+    config.setLong(AUTO_REINDEX_SECTION, null, POLL_INTERVAL, Long.valueOf(pollInterval));
+  }
+
   private void configureMainSection() {
     ui.header("Main section");
     String sharedDirDefault = ui.isBatch() ? DEFAULT_SHARED_DIRECTORY : null;
@@ -126,7 +164,8 @@
             PeerInfoStrategy.JGROUPS, EnumSet.allOf(PeerInfoStrategy.class), "Peer info strategy");
     config.setEnum(PEER_INFO_SECTION, null, STRATEGY_KEY, strategy);
     if (strategy == PeerInfoStrategy.STATIC) {
-      promptAndSetString("Peer URL", PEER_INFO_SECTION, STATIC_SUBSECTION, URL_KEY, null);
+      promptAndSetString(
+          titleWithNote("Peer URL", "urls"), PEER_INFO_SECTION, STATIC_SUBSECTION, URL_KEY, null);
     } else {
       promptAndSetString(
           "JGroups cluster name",
@@ -134,10 +173,22 @@
           JGROUPS_SUBSECTION,
           CLUSTER_NAME_KEY,
           DEFAULT_CLUSTER_NAME);
+      promptAndSetString(
+          "Protocol stack (optional)",
+          PEER_INFO_SECTION,
+          JGROUPS_SUBSECTION,
+          PROTOCOL_STACK_KEY,
+          null);
+      promptAndSetString(
+          titleForOptionalWithNote("Skip interface", "interfaces"),
+          PEER_INFO_SECTION,
+          JGROUPS_SUBSECTION,
+          SKIP_INTERFACE_KEY,
+          null);
     }
   }
 
-  private void configureHttp() {
+  private void configureHttpSection() {
     ui.header("Http section");
     promptAndSetString("User", HTTP_SECTION, USER_KEY, null);
     promptAndSetString("Password", HTTP_SECTION, PASSWORD_KEY, null);
@@ -145,39 +196,82 @@
         "Max number of tries to forward to remote peer",
         HTTP_SECTION,
         MAX_TRIES_KEY,
-        str(DEFAULT_MAX_TRIES));
+        numberToString(DEFAULT_MAX_TRIES));
     promptAndSetString(
-        "Retry interval [ms]", HTTP_SECTION, RETRY_INTERVAL_KEY, str(DEFAULT_RETRY_INTERVAL));
+        "Retry interval [ms]",
+        HTTP_SECTION,
+        RETRY_INTERVAL_KEY,
+        numberToString(DEFAULT_RETRY_INTERVAL));
     promptAndSetString(
-        "Connection timeout [ms]", HTTP_SECTION, CONNECTION_TIMEOUT_KEY, str(DEFAULT_TIMEOUT_MS));
+        "Connection timeout [ms]",
+        HTTP_SECTION,
+        CONNECTION_TIMEOUT_KEY,
+        numberToString(DEFAULT_TIMEOUT_MS));
     promptAndSetString(
-        "Socket timeout [ms]", HTTP_SECTION, SOCKET_TIMEOUT_KEY, str(DEFAULT_TIMEOUT_MS));
+        "Socket timeout [ms]",
+        HTTP_SECTION,
+        SOCKET_TIMEOUT_KEY,
+        numberToString(DEFAULT_TIMEOUT_MS));
   }
 
   private void configureCacheSection() {
     ui.header("Cache section");
+    promptAndSetSynchronize("Cache", CACHE_SECTION);
     promptAndSetString(
         "Cache thread pool size",
         CACHE_SECTION,
         THREAD_POOL_SIZE_KEY,
-        str(DEFAULT_THREAD_POOL_SIZE));
+        numberToString(DEFAULT_THREAD_POOL_SIZE));
+    promptAndSetString(
+        titleForOptionalWithNote("Cache pattern", "patterns"), CACHE_SECTION, PATTERN_KEY, null);
+  }
+
+  private void configureEventSection() {
+    ui.header("Event section");
+    promptAndSetSynchronize("Event", EVENT_SECTION);
   }
 
   private void configureIndexSection() {
     ui.header("Index section");
+    promptAndSetSynchronize("Index", INDEX_SECTION);
     promptAndSetString(
         "Index thread pool size",
         INDEX_SECTION,
         THREAD_POOL_SIZE_KEY,
-        str(DEFAULT_THREAD_POOL_SIZE));
+        numberToString(DEFAULT_THREAD_POOL_SIZE));
   }
 
   private void configureWebsessionsSection() {
     ui.header("Websession section");
+    promptAndSetSynchronize("Websession", WEBSESSION_SECTION);
     promptAndSetString(
         "Cleanup interval", WEBSESSION_SECTION, CLEANUP_INTERVAL_KEY, DEFAULT_CLEANUP_INTERVAL);
   }
 
+  private void configureHealthCheckSection() {
+    ui.header("HealthCheck section");
+    Boolean healthCheck =
+        promptAndSetBoolean(
+            "Health check", HEALTH_CHECK_SECTION, ENABLE_KEY, DEFAULT_HEALTH_CHECK_ENABLED);
+    config.setBoolean(HEALTH_CHECK_SECTION, null, ENABLE_KEY, healthCheck);
+  }
+
+  private void promptAndSetSynchronize(String sectionTitle, String section) {
+    String titleSuffix = ": synchronize?";
+    String title = sectionTitle + titleSuffix;
+    promptAndSetBoolean(title, section, SYNCHRONIZE_KEY, DEFAULT_SYNCHRONIZE);
+  }
+
+  private Boolean promptAndSetBoolean(
+      String title, String section, String name, Boolean defaultValue) {
+    Boolean oldValue = config.getBoolean(section, null, name, defaultValue);
+    Boolean newValue = Boolean.parseBoolean(ui.readString(String.valueOf(oldValue), title));
+    if (!Objects.equals(oldValue, newValue)) {
+      config.setBoolean(section, null, name, newValue);
+    }
+    return newValue;
+  }
+
   private String promptAndSetString(
       String title, String section, String name, String defaultValue) {
     return promptAndSetString(title, section, null, name, defaultValue);
@@ -197,8 +291,20 @@
     return newValue;
   }
 
-  private static String str(int n) {
-    return Integer.toString(n);
+  private static String titleForOptionalWithNote(String prefix, String suffix) {
+    return titleWithNote(prefix + " (optional)", suffix);
+  }
+
+  private static String titleWithNote(String prefix, String suffix) {
+    return prefix + "; manually repeat this line to configure more " + suffix;
+  }
+
+  private static String numberToString(int number) {
+    return Integer.toString(number);
+  }
+
+  private static String numberToString(long number) {
+    return Long.toString(number);
   }
 
   private boolean createHAReplicaSite(FileBasedConfig pluginConfig)
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/SetupLocalHAReplica.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/SetupLocalHAReplica.java
index ddbd994..b5c9730 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/SetupLocalHAReplica.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/SetupLocalHAReplica.java
@@ -14,14 +14,14 @@
 
 package com.ericsson.gerrit.plugins.highavailability;
 
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.CLUSTER_NAME_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_CLUSTER_NAME;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_SHARED_DIRECTORY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.JGROUPS_SUBSECTION;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.MAIN_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.JGroups.CLUSTER_NAME_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.JGroups.DEFAULT_CLUSTER_NAME;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Main.DEFAULT_SHARED_DIRECTORY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Main.MAIN_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Main.SHARED_DIRECTORY_KEY;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.PEER_INFO_SECTION;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.SHARED_DIRECTORY_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.STRATEGY_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.PeerInfo.STRATEGY_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.PeerInfoJGroups.JGROUPS_SUBSECTION;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.FileUtil;
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AccountReindexRunnable.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AccountReindexRunnable.java
new file mode 100644
index 0000000..da52555
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AccountReindexRunnable.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2018 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.ericsson.gerrit.plugins.highavailability.autoreindex;
+
+import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexAccountHandler;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexingHandler.Operation;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.AbstractIndexRestApiServlet;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.Accounts;
+import com.google.gerrit.server.util.OneOffRequestContext;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Optional;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AccountReindexRunnable extends ReindexRunnable<AccountState> {
+  private static final Logger log = LoggerFactory.getLogger(AccountReindexRunnable.class);
+
+  private final ForwardedIndexAccountHandler accountIdx;
+
+  private final Accounts accounts;
+
+  @Inject
+  public AccountReindexRunnable(
+      ForwardedIndexAccountHandler accountIdx,
+      IndexTs indexTs,
+      OneOffRequestContext ctx,
+      Accounts accounts) {
+    super(AbstractIndexRestApiServlet.IndexName.ACCOUNT, indexTs, ctx);
+    this.accountIdx = accountIdx;
+    this.accounts = accounts;
+  }
+
+  @Override
+  protected Iterable<AccountState> fetchItems(ReviewDb db) throws Exception {
+    return accounts.all();
+  }
+
+  @Override
+  protected Optional<Timestamp> indexIfNeeded(ReviewDb db, AccountState as, Timestamp sinceTs) {
+    try {
+      Account a = as.getAccount();
+      Timestamp accountTs = a.getRegisteredOn();
+      if (accountTs.after(sinceTs)) {
+        log.info("Index {}/{}/{}/{}", a.getId(), a.getFullName(), a.getPreferredEmail(), accountTs);
+        accountIdx.index(a.getId(), Operation.INDEX, Optional.empty());
+        return Optional.of(accountTs);
+      }
+    } catch (IOException | OrmException e) {
+      log.error("Reindex failed", e);
+    }
+    return Optional.empty();
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AutoReindexModule.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AutoReindexModule.java
new file mode 100644
index 0000000..d4871a7
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AutoReindexModule.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2018 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.ericsson.gerrit.plugins.highavailability.autoreindex;
+
+import com.google.gerrit.extensions.events.AccountIndexedListener;
+import com.google.gerrit.extensions.events.ChangeIndexedListener;
+import com.google.gerrit.extensions.events.GroupIndexedListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.inject.AbstractModule;
+
+public class AutoReindexModule extends AbstractModule {
+
+  @Override
+  protected void configure() {
+    DynamicSet.bind(binder(), LifecycleListener.class).to(AutoReindexScheduler.class);
+    DynamicSet.bind(binder(), ChangeIndexedListener.class).to(IndexTs.class);
+    DynamicSet.bind(binder(), AccountIndexedListener.class).to(IndexTs.class);
+    DynamicSet.bind(binder(), GroupIndexedListener.class).to(IndexTs.class);
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AutoReindexScheduler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AutoReindexScheduler.java
new file mode 100644
index 0000000..1f7477a
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AutoReindexScheduler.java
@@ -0,0 +1,87 @@
+// Copyright (C) 2018 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.ericsson.gerrit.plugins.highavailability.autoreindex;
+
+import com.ericsson.gerrit.plugins.highavailability.Configuration;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class AutoReindexScheduler implements LifecycleListener {
+  private static final Logger log = LoggerFactory.getLogger(AutoReindexScheduler.class);
+  private final Configuration.AutoReindex cfg;
+  private final ChangeReindexRunnable changeReindex;
+  private final AccountReindexRunnable accountReindex;
+  private final GroupReindexRunnable groupReindex;
+  private final ProjectReindexRunnable projectReindex;
+  private final ScheduledExecutorService executor;
+  private final List<Future<?>> futureTasks = new ArrayList<>();
+
+  @Inject
+  public AutoReindexScheduler(
+      Configuration cfg,
+      WorkQueue workQueue,
+      ChangeReindexRunnable changeReindex,
+      AccountReindexRunnable accountReindex,
+      GroupReindexRunnable groupReindex,
+      ProjectReindexRunnable projectReindex) {
+    this.cfg = cfg.autoReindex();
+    this.changeReindex = changeReindex;
+    this.accountReindex = accountReindex;
+    this.groupReindex = groupReindex;
+    this.projectReindex = projectReindex;
+    this.executor = workQueue.createQueue(1, "HighAvailability-AutoReindex");
+  }
+
+  @Override
+  public void start() {
+    if (cfg.pollSec() > 0) {
+      log.info("Scheduling auto-reindex after {}s and every {}s", cfg.delaySec(), cfg.pollSec());
+      futureTasks.add(
+          executor.scheduleAtFixedRate(
+              changeReindex, cfg.delaySec(), cfg.pollSec(), TimeUnit.SECONDS));
+      futureTasks.add(
+          executor.scheduleAtFixedRate(
+              accountReindex, cfg.delaySec(), cfg.pollSec(), TimeUnit.SECONDS));
+      futureTasks.add(
+          executor.scheduleAtFixedRate(
+              groupReindex, cfg.delaySec(), cfg.pollSec(), TimeUnit.SECONDS));
+      futureTasks.add(
+          executor.scheduleAtFixedRate(
+              projectReindex, cfg.delaySec(), cfg.pollSec(), TimeUnit.SECONDS));
+    } else {
+      log.info("Scheduling auto-reindex after {}s", cfg.delaySec());
+      futureTasks.add(executor.schedule(changeReindex, cfg.delaySec(), TimeUnit.SECONDS));
+      futureTasks.add(executor.schedule(accountReindex, cfg.delaySec(), TimeUnit.SECONDS));
+      futureTasks.add(executor.schedule(groupReindex, cfg.delaySec(), TimeUnit.SECONDS));
+      futureTasks.add(executor.schedule(projectReindex, cfg.delaySec(), TimeUnit.SECONDS));
+    }
+  }
+
+  @Override
+  public void stop() {
+    futureTasks.forEach(t -> t.cancel(true));
+    executor.shutdown();
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ChangeReindexRunnable.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ChangeReindexRunnable.java
new file mode 100644
index 0000000..1f5b56e
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ChangeReindexRunnable.java
@@ -0,0 +1,111 @@
+// Copyright (C) 2018 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.ericsson.gerrit.plugins.highavailability.autoreindex;
+
+import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexChangeHandler;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexingHandler.Operation;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.AbstractIndexRestApiServlet;
+import com.google.common.collect.Streams;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeNotes.Factory.ChangeNotesResult;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.util.OneOffRequestContext;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Iterator;
+import java.util.Optional;
+import java.util.stream.Stream;
+import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ChangeReindexRunnable extends ReindexRunnable<Change> {
+  private static final Logger log = LoggerFactory.getLogger(ChangeReindexRunnable.class);
+
+  private final ForwardedIndexChangeHandler changeIdx;
+
+  private final ProjectCache projectCache;
+
+  private final GitRepositoryManager repoManager;
+
+  private final ChangeNotes.Factory notesFactory;
+
+  private static class StreamIterable implements Iterable<Change> {
+
+    private final Stream<Change> stream;
+
+    public StreamIterable(Stream<Change> stream) {
+      this.stream = stream;
+    }
+
+    @Override
+    public Iterator<Change> iterator() {
+      return stream.iterator();
+    }
+  }
+
+  @Inject
+  public ChangeReindexRunnable(
+      ForwardedIndexChangeHandler changeIdx,
+      IndexTs indexTs,
+      OneOffRequestContext ctx,
+      ProjectCache projectCache,
+      GitRepositoryManager repoManager,
+      ChangeNotes.Factory notesFactory) {
+    super(AbstractIndexRestApiServlet.IndexName.CHANGE, indexTs, ctx);
+    this.changeIdx = changeIdx;
+    this.projectCache = projectCache;
+    this.repoManager = repoManager;
+    this.notesFactory = notesFactory;
+  }
+
+  @Override
+  protected Iterable<Change> fetchItems(ReviewDb db) throws Exception {
+    Stream<Change> allChangesStream = Stream.empty();
+    Iterable<Project.NameKey> projects = projectCache.all();
+    for (Project.NameKey projectName : projects) {
+      try (Repository repo = repoManager.openRepository(projectName)) {
+        Stream<Change> projectChangesStream =
+            notesFactory
+                .scan(repo, db, projectName)
+                .map((ChangeNotesResult changeNotes) -> changeNotes.notes().getChange());
+        allChangesStream = Streams.concat(allChangesStream, projectChangesStream);
+      }
+    }
+    return new StreamIterable(allChangesStream);
+  }
+
+  @Override
+  protected Optional<Timestamp> indexIfNeeded(ReviewDb db, Change c, Timestamp sinceTs) {
+    try {
+      Timestamp changeTs = c.getLastUpdatedOn();
+      if (changeTs.after(sinceTs)) {
+        log.info(
+            "Index {}/{}/{} was updated after {}", c.getProject(), c.getId(), changeTs, sinceTs);
+        changeIdx.index(c.getProject() + "~" + c.getId(), Operation.INDEX, Optional.empty());
+        return Optional.of(changeTs);
+      }
+    } catch (OrmException | IOException e) {
+      log.error("Reindex failed", e);
+    }
+    return Optional.empty();
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/GroupReindexRunnable.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/GroupReindexRunnable.java
new file mode 100644
index 0000000..71a0280
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/GroupReindexRunnable.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2018 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.ericsson.gerrit.plugins.highavailability.autoreindex;
+
+import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.AbstractIndexRestApiServlet;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.group.db.Groups;
+import com.google.gerrit.server.util.OneOffRequestContext;
+import com.google.inject.Inject;
+import java.sql.Timestamp;
+import java.util.Optional;
+
+public class GroupReindexRunnable extends ReindexRunnable<GroupReference> {
+
+  private final Groups groups;
+
+  @Inject
+  public GroupReindexRunnable(IndexTs indexTs, OneOffRequestContext ctx, Groups groups) {
+    super(AbstractIndexRestApiServlet.IndexName.GROUP, indexTs, ctx);
+    this.groups = groups;
+  }
+
+  @Override
+  protected Iterable<GroupReference> fetchItems(ReviewDb db) throws Exception {
+    return groups.getAllGroupReferences()::iterator;
+  }
+
+  @Override
+  protected Optional<Timestamp> indexIfNeeded(ReviewDb db, GroupReference g, Timestamp sinceTs) {
+    return Optional.empty();
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/IndexTs.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/IndexTs.java
new file mode 100644
index 0000000..b36c309
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/IndexTs.java
@@ -0,0 +1,164 @@
+// Copyright (C) 2018 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.ericsson.gerrit.plugins.highavailability.autoreindex;
+
+import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.AbstractIndexRestApiServlet;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.AbstractIndexRestApiServlet.IndexName;
+import com.google.gerrit.extensions.annotations.PluginData;
+import com.google.gerrit.extensions.events.AccountIndexedListener;
+import com.google.gerrit.extensions.events.ChangeIndexedListener;
+import com.google.gerrit.extensions.events.GroupIndexedListener;
+import com.google.gerrit.extensions.events.ProjectIndexedListener;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.ChangeFinder;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Optional;
+import java.util.concurrent.ScheduledExecutorService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class IndexTs
+    implements ChangeIndexedListener,
+        AccountIndexedListener,
+        GroupIndexedListener,
+        ProjectIndexedListener {
+  private static final Logger log = LoggerFactory.getLogger(IndexTs.class);
+  private static final DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME;
+
+  private final Path dataDir;
+  private final ScheduledExecutorService exec;
+  private final FlusherRunner flusher;
+  private final SchemaFactory<ReviewDb> schemaFactory;
+  private final ChangeFinder changeFinder;
+
+  private volatile LocalDateTime changeTs;
+  private volatile LocalDateTime accountTs;
+  private volatile LocalDateTime groupTs;
+  private volatile LocalDateTime projectTs;
+
+  class FlusherRunner implements Runnable {
+
+    @Override
+    public void run() {
+      store(AbstractIndexRestApiServlet.IndexName.CHANGE, changeTs);
+      store(AbstractIndexRestApiServlet.IndexName.ACCOUNT, accountTs);
+      store(AbstractIndexRestApiServlet.IndexName.GROUP, groupTs);
+      store(AbstractIndexRestApiServlet.IndexName.PROJECT, projectTs);
+    }
+
+    private void store(AbstractIndexRestApiServlet.IndexName index, LocalDateTime latestTs) {
+      Optional<LocalDateTime> currTs = getUpdateTs(index);
+      if (!currTs.isPresent() || latestTs.isAfter(currTs.get())) {
+        Path indexTsFile = dataDir.resolve(index.name().toLowerCase());
+        try {
+          Files.write(indexTsFile, latestTs.format(formatter).getBytes(StandardCharsets.UTF_8));
+        } catch (IOException e) {
+          log.error("Unable to update last timestamp for index " + index, e);
+        }
+      }
+    }
+  }
+
+  @Inject
+  public IndexTs(
+      @PluginData Path dataDir,
+      WorkQueue queue,
+      SchemaFactory<ReviewDb> schemaFactory,
+      ChangeFinder changeFinder) {
+    this.dataDir = dataDir;
+    this.exec = queue.getDefaultQueue();
+    this.flusher = new FlusherRunner();
+    this.schemaFactory = schemaFactory;
+    this.changeFinder = changeFinder;
+  }
+
+  @Override
+  public void onProjectIndexed(String project) {
+    update(IndexName.PROJECT, LocalDateTime.now());
+  }
+
+  @Override
+  public void onGroupIndexed(String uuid) {
+    update(IndexName.GROUP, LocalDateTime.now());
+  }
+
+  @Override
+  public void onAccountIndexed(int id) {
+    update(IndexName.ACCOUNT, LocalDateTime.now());
+  }
+
+  @Override
+  public void onChangeIndexed(String projectName, int id) {
+    try (ReviewDb db = schemaFactory.open()) {
+      ChangeNotes changeNotes = changeFinder.findOne(projectName + "~" + id);
+      update(
+          IndexName.CHANGE,
+          changeNotes == null
+              ? LocalDateTime.now()
+              : changeNotes.getChange().getLastUpdatedOn().toLocalDateTime());
+    } catch (Exception e) {
+      log.warn("Unable to update the latest TS for change {}", e);
+    }
+  }
+
+  @Override
+  public void onChangeDeleted(int id) {
+    update(IndexName.CHANGE, LocalDateTime.now());
+  }
+
+  public Optional<LocalDateTime> getUpdateTs(AbstractIndexRestApiServlet.IndexName index) {
+    try {
+      Path indexTsFile = dataDir.resolve(index.name().toLowerCase());
+      if (indexTsFile.toFile().exists()) {
+        String tsString = Files.readAllLines(indexTsFile).get(0);
+        return Optional.of(LocalDateTime.parse(tsString, formatter));
+      }
+    } catch (Exception e) {
+      log.warn("Unable to read last timestamp for index {}", index, e);
+    }
+    return Optional.empty();
+  }
+
+  void update(AbstractIndexRestApiServlet.IndexName index, LocalDateTime dateTime) {
+    switch (index) {
+      case CHANGE:
+        changeTs = dateTime;
+        break;
+      case ACCOUNT:
+        accountTs = dateTime;
+        break;
+      case GROUP:
+        groupTs = dateTime;
+        break;
+      case PROJECT:
+        projectTs = dateTime;
+        break;
+      default:
+        throw new IllegalArgumentException("Unsupported index " + index);
+    }
+    exec.execute(flusher);
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ProjectReindexRunnable.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ProjectReindexRunnable.java
new file mode 100644
index 0000000..582227d
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ProjectReindexRunnable.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2018 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.ericsson.gerrit.plugins.highavailability.autoreindex;
+
+import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.AbstractIndexRestApiServlet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.util.OneOffRequestContext;
+import com.google.inject.Inject;
+import java.sql.Timestamp;
+import java.util.Optional;
+
+public class ProjectReindexRunnable extends ReindexRunnable<Project.NameKey> {
+
+  private final ProjectCache projectCache;
+
+  @Inject
+  public ProjectReindexRunnable(
+      IndexTs indexTs, OneOffRequestContext ctx, ProjectCache projectCache) {
+    super(AbstractIndexRestApiServlet.IndexName.PROJECT, indexTs, ctx);
+    this.projectCache = projectCache;
+  }
+
+  @Override
+  protected Iterable<Project.NameKey> fetchItems(ReviewDb db) {
+    return projectCache.all();
+  }
+
+  @Override
+  protected Optional<Timestamp> indexIfNeeded(ReviewDb db, Project.NameKey g, Timestamp sinceTs) {
+    return Optional.empty();
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ReindexRunnable.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ReindexRunnable.java
new file mode 100644
index 0000000..df3a0fd
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ReindexRunnable.java
@@ -0,0 +1,109 @@
+// Copyright (C) 2018 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.ericsson.gerrit.plugins.highavailability.autoreindex;
+
+import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.AbstractIndexRestApiServlet;
+import com.google.common.base.Stopwatch;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.OneOffRequestContext;
+import com.google.inject.Inject;
+import java.sql.Timestamp;
+import java.time.LocalDateTime;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+abstract class ReindexRunnable<T> implements Runnable {
+  private static final Logger log = LoggerFactory.getLogger(ReindexRunnable.class);
+
+  private final AbstractIndexRestApiServlet.IndexName itemName;
+  private final OneOffRequestContext ctx;
+  private final IndexTs indexTs;
+  private Timestamp newLastIndexTs;
+
+  @Inject
+  public ReindexRunnable(
+      AbstractIndexRestApiServlet.IndexName itemName, IndexTs indexTs, OneOffRequestContext ctx) {
+    this.itemName = itemName;
+    this.ctx = ctx;
+    this.indexTs = indexTs;
+  }
+
+  @Override
+  public void run() {
+    Optional<LocalDateTime> maybeIndexTs = indexTs.getUpdateTs(itemName);
+    String itemNameString = itemName.name().toLowerCase();
+    if (maybeIndexTs.isPresent()) {
+      newLastIndexTs = maxTimestamp(newLastIndexTs, Timestamp.valueOf(maybeIndexTs.get()));
+      log.debug("Scanning for all the {}s after {}", itemNameString, newLastIndexTs);
+      try (ManualRequestContext mctx = ctx.open();
+          ReviewDb db = mctx.getReviewDbProvider().get()) {
+        int count = 0;
+        int errors = 0;
+        Stopwatch stopwatch = Stopwatch.createStarted();
+        for (T c : fetchItems(db)) {
+          try {
+            Optional<Timestamp> itemTs = indexIfNeeded(db, c, newLastIndexTs);
+            if (itemTs.isPresent()) {
+              count++;
+              newLastIndexTs = maxTimestamp(newLastIndexTs, itemTs.get());
+            }
+          } catch (Exception e) {
+            log.error("Unable to reindex {} {}", itemNameString, c, e);
+            errors++;
+          }
+        }
+        long elapsedNanos = stopwatch.stop().elapsed(TimeUnit.NANOSECONDS);
+        if (count > 0) {
+          log.info(
+              "{} {}s reindexed in {} msec ({}/sec), {} failed",
+              count,
+              itemNameString,
+              elapsedNanos / 1000000L,
+              (count * 1000L) / (elapsedNanos / 1000000L),
+              errors);
+        } else if (errors > 0) {
+          log.info("{} {}s failed to reindex", errors, itemNameString);
+        } else {
+          log.debug("Scanning finished");
+        }
+        indexTs.update(itemName, newLastIndexTs.toLocalDateTime());
+      } catch (Exception e) {
+        log.error("Unable to scan " + itemNameString + "s", e);
+      }
+    }
+  }
+
+  private Timestamp maxTimestamp(Timestamp ts1, Timestamp ts2) {
+    if (ts1 == null) {
+      return ts2;
+    }
+
+    if (ts2 == null) {
+      return ts1;
+    }
+
+    if (ts1.after(ts2)) {
+      return ts1;
+    }
+    return ts2;
+  }
+
+  protected abstract Iterable<T> fetchItems(ReviewDb db) throws Exception;
+
+  protected abstract Optional<Timestamp> indexIfNeeded(ReviewDb db, T item, Timestamp sinceTs);
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexAccountHandler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexAccountHandler.java
index 42d2123..1d68e72 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexAccountHandler.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexAccountHandler.java
@@ -14,12 +14,14 @@
 
 package com.ericsson.gerrit.plugins.highavailability.forwarder;
 
+import com.ericsson.gerrit.plugins.highavailability.Configuration;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.util.Optional;
 
 /**
  * Index an account using {@link AccountIndexer}. This class is meant to be used on the receiving
@@ -32,18 +34,20 @@
   private final AccountIndexer indexer;
 
   @Inject
-  ForwardedIndexAccountHandler(AccountIndexer indexer) {
+  ForwardedIndexAccountHandler(AccountIndexer indexer, Configuration config) {
+    super(config.index());
     this.indexer = indexer;
   }
 
   @Override
-  protected void doIndex(Account.Id id) throws IOException, OrmException {
+  protected void doIndex(Account.Id id, Optional<IndexEvent> indexEvent)
+      throws IOException, OrmException {
     indexer.index(id);
     log.debug("Account {} successfully indexed", id);
   }
 
   @Override
-  protected void doDelete(Account.Id id) {
+  protected void doDelete(Account.Id id, Optional<IndexEvent> indexEvent) {
     throw new UnsupportedOperationException("Delete from account index not supported");
   }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandler.java
index f31ef81..d34959c 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandler.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandler.java
@@ -14,18 +14,27 @@
 
 package com.ericsson.gerrit.plugins.highavailability.forwarder;
 
+import com.ericsson.gerrit.plugins.highavailability.Configuration;
+import com.ericsson.gerrit.plugins.highavailability.Configuration.Index;
+import com.ericsson.gerrit.plugins.highavailability.index.ChangeChecker;
+import com.ericsson.gerrit.plugins.highavailability.index.ChangeCheckerImpl;
+import com.ericsson.gerrit.plugins.highavailability.index.ChangeDb;
+import com.ericsson.gerrit.plugins.highavailability.index.ForwardedIndexExecutor;
 import com.google.common.base.Splitter;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.change.ChangeFinder;
 import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.util.Optional;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Index a change using {@link ChangeIndexer}. This class is meant to be used on the receiving side
@@ -36,56 +45,131 @@
 @Singleton
 public class ForwardedIndexChangeHandler extends ForwardedIndexingHandler<String> {
   private final ChangeIndexer indexer;
-  private final SchemaFactory<ReviewDb> schemaFactory;
-  private final ChangeFinder changeFinder;
+  private final ChangeDb changeDb;
+  private final ScheduledExecutorService indexExecutor;
+  private final OneOffRequestContext oneOffCtx;
+  private final int retryInterval;
+  private final int maxTries;
+  private final ChangeCheckerImpl.Factory changeCheckerFactory;
 
   @Inject
   ForwardedIndexChangeHandler(
-      ChangeIndexer indexer, SchemaFactory<ReviewDb> schemaFactory, ChangeFinder changeFinder) {
+      ChangeIndexer indexer,
+      ChangeDb changeDb,
+      Configuration config,
+      @ForwardedIndexExecutor ScheduledExecutorService indexExecutor,
+      OneOffRequestContext oneOffCtx,
+      ChangeCheckerImpl.Factory changeCheckerFactory) {
+    super(config.index());
     this.indexer = indexer;
-    this.schemaFactory = schemaFactory;
-    this.changeFinder = changeFinder;
+    this.changeDb = changeDb;
+    this.indexExecutor = indexExecutor;
+    this.oneOffCtx = oneOffCtx;
+    this.changeCheckerFactory = changeCheckerFactory;
+
+    Index indexConfig = config.index();
+    this.retryInterval = indexConfig != null ? indexConfig.retryInterval() : 0;
+    this.maxTries = indexConfig != null ? indexConfig.maxTries() : 0;
   }
 
   @Override
-  protected void doIndex(String id) throws IOException, OrmException {
-    ChangeNotes change = null;
-    try (ReviewDb db = schemaFactory.open()) {
-      change = changeFinder.findOne(id);
-      if (change != null) {
-        change.reload();
-        indexer.index(db, change.getChange());
-        log.debug("Change {} successfully indexed", id);
+  protected void doIndex(String id, Optional<IndexEvent> indexEvent)
+      throws IOException, OrmException {
+    doIndex(id, indexEvent, 0);
+  }
+
+  private void doIndex(String id, Optional<IndexEvent> indexEvent, int retryCount)
+      throws IOException, OrmException {
+    try {
+      ChangeChecker checker = changeCheckerFactory.create(id);
+      Optional<ChangeNotes> changeNotes = checker.getChangeNotes();
+      if (changeNotes.isPresent()) {
+        ChangeNotes notes = changeNotes.get();
+        reindex(notes);
+
+        if (checker.isChangeUpToDate(indexEvent)) {
+          if (retryCount > 0) {
+            log.warn("Change {} has been eventually indexed after {} attempt(s)", id, retryCount);
+          } else {
+            log.debug("Change {} successfully indexed", id);
+          }
+        } else {
+          log.warn(
+              "Change {} seems too old compared to the event timestamp (event-Ts={} >> change-Ts={})",
+              id,
+              indexEvent,
+              checker);
+          rescheduleIndex(id, indexEvent, retryCount + 1);
+        }
+      } else {
+        indexer.delete(parseChangeId(id));
+        log.warn(
+            "Change {} could not be found in the local Git repository (eventTs={}), deleted from index",
+            id,
+            indexEvent);
       }
     } catch (Exception e) {
-      if (!isCausedByNoSuchChangeException(e)) {
-        throw e;
+      if (isCausedByNoSuchChangeException(e)) {
+        indexer.delete(parseChangeId(id));
+        log.warn("Error trying to index Change {}. Deleted from index", id, e);
+        return;
       }
-      log.debug("Change {} was deleted, aborting forwarded indexing the change.", id);
-    }
-    if (change == null) {
-      indexer.delete(parseChangeId(id));
-      log.debug("Change {} not found, deleted from index", id);
+
+      throw e;
     }
   }
 
+  private void reindex(ChangeNotes notes) throws IOException, OrmException {
+    try (ReviewDb db = changeDb.open()) {
+      notes.reload();
+      indexer.index(db, notes.getChange());
+    }
+  }
+
+  private void rescheduleIndex(String id, Optional<IndexEvent> indexEvent, int retryCount) {
+    if (retryCount > maxTries) {
+      log.error(
+          "Change {} could not be indexed after {} retries. Change index could be stale.",
+          id,
+          retryCount);
+      return;
+    }
+
+    log.warn(
+        "Retrying for the #{} time to index Change {} after {} msecs",
+        retryCount,
+        id,
+        retryInterval);
+    indexExecutor.schedule(
+        () -> {
+          try (ManualRequestContext ctx = oneOffCtx.open()) {
+            Context.setForwardedEvent(true);
+            doIndex(id, indexEvent, retryCount);
+          } catch (Exception e) {
+            log.warn("Change {} could not be indexed", id, e);
+          }
+        },
+        retryInterval,
+        TimeUnit.MILLISECONDS);
+  }
+
   @Override
-  protected void doDelete(String id) throws IOException {
+  protected void doDelete(String id, Optional<IndexEvent> indexEvent) throws IOException {
     indexer.delete(parseChangeId(id));
     log.debug("Change {} successfully deleted from index", id);
   }
 
   private static Change.Id parseChangeId(String id) {
-    Change.Id changeId = new Change.Id(Integer.parseInt(Splitter.on("~").splitToList(id).get(1)));
-    return changeId;
+    return new Change.Id(Integer.parseInt(Splitter.on("~").splitToList(id).get(1)));
   }
 
   private static boolean isCausedByNoSuchChangeException(Throwable throwable) {
-    while (throwable != null) {
-      if (throwable instanceof NoSuchChangeException) {
+    Throwable cause = throwable;
+    while (cause != null) {
+      if (cause instanceof NoSuchChangeException) {
         return true;
       }
-      throwable = throwable.getCause();
+      cause = cause.getCause();
     }
     return false;
   }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexGroupHandler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexGroupHandler.java
index c518484..cbb748b 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexGroupHandler.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexGroupHandler.java
@@ -14,12 +14,14 @@
 
 package com.ericsson.gerrit.plugins.highavailability.forwarder;
 
+import com.ericsson.gerrit.plugins.highavailability.Configuration;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.index.group.GroupIndexer;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.util.Optional;
 
 /**
  * Index a group using {@link GroupIndexer}. This class is meant to be used on the receiving side of
@@ -32,18 +34,20 @@
   private final GroupIndexer indexer;
 
   @Inject
-  ForwardedIndexGroupHandler(GroupIndexer indexer) {
+  ForwardedIndexGroupHandler(GroupIndexer indexer, Configuration config) {
+    super(config.index());
     this.indexer = indexer;
   }
 
   @Override
-  protected void doIndex(AccountGroup.UUID uuid) throws IOException, OrmException {
+  protected void doIndex(AccountGroup.UUID uuid, Optional<IndexEvent> indexEvent)
+      throws IOException, OrmException {
     indexer.index(uuid);
     log.debug("Group {} successfully indexed", uuid);
   }
 
   @Override
-  protected void doDelete(AccountGroup.UUID uuid) {
+  protected void doDelete(AccountGroup.UUID uuid, Optional<IndexEvent> indexEvent) {
     throw new UnsupportedOperationException("Delete from group index not supported");
   }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexProjectHandler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexProjectHandler.java
new file mode 100644
index 0000000..d690f5d
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexProjectHandler.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2018 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.ericsson.gerrit.plugins.highavailability.forwarder;
+
+import com.ericsson.gerrit.plugins.highavailability.Configuration;
+import com.google.gerrit.index.project.ProjectIndexer;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.Optional;
+
+/**
+ * Index a project using {@link ProjectIndexer}. This class is meant to be used on the receiving
+ * side of the {@link Forwarder} since it will prevent indexed project to be forwarded again causing
+ * an infinite forwarding loop between the 2 nodes. It will also make sure no concurrent indexing is
+ * done for the same project name.
+ */
+@Singleton
+public class ForwardedIndexProjectHandler extends ForwardedIndexingHandler<Project.NameKey> {
+  private final ProjectIndexer indexer;
+
+  @Inject
+  ForwardedIndexProjectHandler(ProjectIndexer indexer, Configuration config) {
+    super(config.index());
+    this.indexer = indexer;
+  }
+
+  @Override
+  protected void doIndex(Project.NameKey projectName, Optional<IndexEvent> indexEvent)
+      throws IOException {
+    indexer.index(projectName);
+    log.debug("Project {} successfully indexed", projectName);
+  }
+
+  @Override
+  protected void doDelete(Project.NameKey projectName, Optional<IndexEvent> indexEvent) {
+    throw new UnsupportedOperationException("Delete from project index not supported");
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexingHandler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexingHandler.java
index e64d97b..bfedf3f 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexingHandler.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexingHandler.java
@@ -14,9 +14,11 @@
 
 package com.ericsson.gerrit.plugins.highavailability.forwarder;
 
+import com.ericsson.gerrit.plugins.highavailability.Configuration;
 import com.google.common.util.concurrent.Striped;
 import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
+import java.util.Optional;
 import java.util.concurrent.locks.Lock;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -40,22 +42,29 @@
     }
   }
 
-  private final Striped<Lock> idLocks = Striped.lock(10);
+  private final Striped<Lock> idLocks;
 
-  protected abstract void doIndex(T id) throws IOException, OrmException;
+  protected abstract void doIndex(T id, Optional<IndexEvent> indexEvent)
+      throws IOException, OrmException;
 
-  protected abstract void doDelete(T id) throws IOException;
+  protected abstract void doDelete(T id, Optional<IndexEvent> indexEvent) throws IOException;
+
+  protected ForwardedIndexingHandler(Configuration.Index indexConfig) {
+    idLocks = Striped.lock(indexConfig.numStripedLocks());
+  }
 
   /**
    * Index an item in the local node, indexing will not be forwarded to the other node.
    *
    * @param id The id to index.
    * @param operation The operation to do; index or delete
+   * @param indexEvent The index event details.
    * @throws IOException If an error occur while indexing.
    * @throws OrmException If an error occur while retrieving a change related to the item to index
    */
-  public void index(T id, Operation operation) throws IOException, OrmException {
-    log.debug("{} {}", operation, id);
+  public void index(T id, Operation operation, Optional<IndexEvent> indexEvent)
+      throws IOException, OrmException {
+    log.debug("{} {} {}", operation, id, indexEvent);
     try {
       Context.setForwardedEvent(true);
       Lock idLock = idLocks.get(id);
@@ -63,10 +72,10 @@
       try {
         switch (operation) {
           case INDEX:
-            doIndex(id);
+            doIndex(id, indexEvent);
             break;
           case DELETE:
-            doDelete(id);
+            doDelete(id, indexEvent);
             break;
           default:
             log.error("unexpected operation: {}", operation);
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/Forwarder.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/Forwarder.java
index ab09b65..49cdc8b 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/Forwarder.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/Forwarder.java
@@ -23,34 +23,47 @@
    * Forward a account indexing event to the other master.
    *
    * @param accountId the account to index.
+   * @param indexEvent the details of the index event.
    * @return true if successful, otherwise false.
    */
-  boolean indexAccount(int accountId);
+  boolean indexAccount(int accountId, IndexEvent indexEvent);
 
   /**
    * Forward a change indexing event to the other master.
    *
    * @param projectName the project of the change to index.
    * @param changeId the change to index.
+   * @param indexEvent the details of the index event.
    * @return true if successful, otherwise false.
    */
-  boolean indexChange(String projectName, int changeId);
+  boolean indexChange(String projectName, int changeId, IndexEvent indexEvent);
 
   /**
    * Forward a delete change from index event to the other master.
    *
    * @param changeId the change to remove from the index.
+   * @param indexEvent the details of the index event.
    * @return rue if successful, otherwise false.
    */
-  boolean deleteChangeFromIndex(int changeId);
+  boolean deleteChangeFromIndex(int changeId, IndexEvent indexEvent);
 
   /**
    * Forward a group indexing event to the other master.
    *
    * @param uuid the group to index.
+   * @param indexEvent the details of the index event.
    * @return true if successful, otherwise false.
    */
-  boolean indexGroup(String uuid);
+  boolean indexGroup(String uuid, IndexEvent indexEvent);
+
+  /**
+   * Forward a project indexing event to the other master.
+   *
+   * @param projectName the project to index.
+   * @param indexEvent the details of the index event.
+   * @return true if successful, otherwise false.
+   */
+  boolean indexProject(String projectName, IndexEvent indexEvent);
 
   /**
    * Forward a stream event to the other master.
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/IndexEvent.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/IndexEvent.java
new file mode 100644
index 0000000..037c1c6
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/IndexEvent.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2018 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.ericsson.gerrit.plugins.highavailability.forwarder;
+
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+
+public class IndexEvent {
+  public long eventCreatedOn = System.currentTimeMillis() / 1000;
+  public String targetSha;
+
+  @Override
+  public String toString() {
+    return "IndexEvent@" + format(eventCreatedOn) + ((targetSha != null) ? "/" + targetSha : "");
+  }
+
+  public static String format(long eventTs) {
+    return LocalDateTime.ofEpochSecond(eventTs, 0, ZoneOffset.UTC)
+        .format(DateTimeFormatter.ISO_DATE_TIME);
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/AbstractIndexRestApiServlet.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/AbstractIndexRestApiServlet.java
index 7349ca5..d5de0c4 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/AbstractIndexRestApiServlet.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/AbstractIndexRestApiServlet.java
@@ -21,13 +21,20 @@
 
 import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexingHandler;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexingHandler.Operation;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.IndexEvent;
+import com.google.common.base.Charsets;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
 import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.Optional;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 public abstract class AbstractIndexRestApiServlet<T> extends AbstractRestApiServlet {
   private static final long serialVersionUID = -1L;
+  private static final Gson gson = new GsonBuilder().create();
 
   private final ForwardedIndexingHandler<T> forwardedIndexingHandler;
   private final IndexName indexName;
@@ -36,7 +43,8 @@
   public enum IndexName {
     CHANGE,
     ACCOUNT,
-    GROUP;
+    GROUP,
+    PROJECT;
 
     @Override
     public String toString() {
@@ -80,7 +88,7 @@
     String path = req.getRequestURI();
     T id = parse(path.substring(path.lastIndexOf('/') + 1));
     try {
-      forwardedIndexingHandler.index(id, operation);
+      forwardedIndexingHandler.index(id, operation, parseBody(req));
       rsp.setStatus(SC_NO_CONTENT);
     } catch (IOException e) {
       sendError(rsp, SC_CONFLICT, e.getMessage());
@@ -91,4 +99,14 @@
       log.debug(msg, e);
     }
   }
+
+  protected Optional<IndexEvent> parseBody(HttpServletRequest req) throws IOException {
+    String contentType = req.getContentType();
+    if (contentType != null && contentType.contains("application/json")) {
+      return Optional.ofNullable(
+          gson.fromJson(
+              new InputStreamReader(req.getInputStream(), Charsets.UTF_8), IndexEvent.class));
+    }
+    return Optional.empty();
+  }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpSession.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpSession.java
index d3fce97..f2ac080 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpSession.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpSession.java
@@ -15,52 +15,73 @@
 package com.ericsson.gerrit.plugins.highavailability.forwarder.rest;
 
 import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.HttpResponseHandler.HttpResult;
-import com.ericsson.gerrit.plugins.highavailability.peers.PeerInfo;
-import com.google.common.base.Strings;
+import com.google.common.base.Supplier;
 import com.google.common.net.MediaType;
+import com.google.gerrit.server.events.SupplierSerializer;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import java.io.IOException;
+import java.net.URI;
 import java.nio.charset.StandardCharsets;
-import java.util.Optional;
 import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.entity.StringEntity;
 import org.apache.http.impl.client.CloseableHttpClient;
 
 class HttpSession {
   private final CloseableHttpClient httpClient;
-  private final Provider<Optional<PeerInfo>> peerInfo;
+  private final Gson gson =
+      new GsonBuilder().registerTypeAdapter(Supplier.class, new SupplierSerializer()).create();
 
   @Inject
-  HttpSession(CloseableHttpClient httpClient, Provider<Optional<PeerInfo>> peerInfo) {
+  HttpSession(CloseableHttpClient httpClient) {
     this.httpClient = httpClient;
-    this.peerInfo = peerInfo;
   }
 
-  HttpResult post(String endpoint) throws IOException {
-    return post(endpoint, null);
+  HttpResult post(String uri) throws IOException {
+    return post(uri, null);
   }
 
-  HttpResult post(String endpoint, String content) throws IOException {
-    HttpPost post = new HttpPost(getPeerInfo().getDirectUrl() + endpoint);
-    if (!Strings.isNullOrEmpty(content)) {
-      post.addHeader("Content-Type", MediaType.JSON_UTF_8.toString());
-      post.setEntity(new StringEntity(content, StandardCharsets.UTF_8));
-    }
+  HttpResult post(String uri, Object content) throws IOException {
+    HttpPost post = new HttpPost(uri);
+    setContent(post, content);
     return httpClient.execute(post, new HttpResponseHandler());
   }
 
-  HttpResult delete(String endpoint) throws IOException {
-    return httpClient.execute(
-        new HttpDelete(getPeerInfo().getDirectUrl() + endpoint), new HttpResponseHandler());
+  HttpResult delete(String uri) throws IOException {
+    return delete(uri, null);
   }
 
-  private PeerInfo getPeerInfo() throws PeerInfoNotAvailableException {
-    PeerInfo info = peerInfo.get().orElse(null);
-    if (info == null) {
-      throw new PeerInfoNotAvailableException();
+  HttpResult delete(String uri, Object content) throws IOException {
+    HttpDeleteWithBody delete = new HttpDeleteWithBody(uri);
+    setContent(delete, content);
+    return httpClient.execute(delete, new HttpResponseHandler());
+  }
+
+  private void setContent(HttpEntityEnclosingRequestBase request, Object content) {
+    if (content != null) {
+      request.addHeader("Content-Type", MediaType.JSON_UTF_8.toString());
+      request.setEntity(new StringEntity(jsonEncode(content), StandardCharsets.UTF_8));
     }
-    return info;
+  }
+
+  private String jsonEncode(Object content) {
+    if (content instanceof String) {
+      return (String) content;
+    }
+    return gson.toJson(content);
+  }
+
+  private class HttpDeleteWithBody extends HttpEntityEnclosingRequestBase {
+    @Override
+    public String getMethod() {
+      return HttpDelete.METHOD_NAME;
+    }
+
+    private HttpDeleteWithBody(String uri) {
+      setURI(URI.create(uri));
+    }
   }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexProjectRestApiServlet.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexProjectRestApiServlet.java
new file mode 100644
index 0000000..cc87442
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexProjectRestApiServlet.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2018 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.ericsson.gerrit.plugins.highavailability.forwarder.rest;
+
+import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexProjectHandler;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+class IndexProjectRestApiServlet extends AbstractIndexRestApiServlet<Project.NameKey> {
+  private static final long serialVersionUID = -1L;
+
+  @Inject
+  IndexProjectRestApiServlet(ForwardedIndexProjectHandler handler) {
+    super(handler, IndexName.PROJECT);
+  }
+
+  @Override
+  Project.NameKey parse(String projectName) {
+    return new Project.NameKey(Url.decode(projectName));
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarder.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarder.java
index c2a0c6d..a6acbc0 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarder.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarder.java
@@ -17,73 +17,75 @@
 import com.ericsson.gerrit.plugins.highavailability.Configuration;
 import com.ericsson.gerrit.plugins.highavailability.cache.Constants;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.Forwarder;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.IndexEvent;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.HttpResponseHandler.HttpResult;
+import com.ericsson.gerrit.plugins.highavailability.peers.PeerInfo;
 import com.google.common.base.Joiner;
-import com.google.common.base.Supplier;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.server.events.Event;
-import com.google.gerrit.server.events.SupplierSerializer;
-import com.google.gson.GsonBuilder;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
 import javax.net.ssl.SSLException;
+import org.apache.http.HttpException;
+import org.apache.http.client.ClientProtocolException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 class RestForwarder implements Forwarder {
+  enum RequestMethod {
+    POST,
+    DELETE
+  }
+
   private static final Logger log = LoggerFactory.getLogger(RestForwarder.class);
 
   private final HttpSession httpSession;
   private final String pluginRelativePath;
   private final Configuration cfg;
+  private final Provider<Set<PeerInfo>> peerInfoProvider;
 
   @Inject
-  RestForwarder(HttpSession httpClient, @PluginName String pluginName, Configuration cfg) {
+  RestForwarder(
+      HttpSession httpClient,
+      @PluginName String pluginName,
+      Configuration cfg,
+      Provider<Set<PeerInfo>> peerInfoProvider) {
     this.httpSession = httpClient;
-    this.pluginRelativePath = Joiner.on("/").join("/plugins", pluginName);
+    this.pluginRelativePath = Joiner.on("/").join("plugins", pluginName);
     this.cfg = cfg;
+    this.peerInfoProvider = peerInfoProvider;
   }
 
   @Override
-  public boolean indexAccount(final int accountId) {
-    return new Request("index account", accountId) {
-      @Override
-      HttpResult send() throws IOException {
-        return httpSession.post(
-            Joiner.on("/").join(pluginRelativePath, "index/account", accountId));
-      }
-    }.execute();
+  public boolean indexAccount(final int accountId, IndexEvent event) {
+    return execute(RequestMethod.POST, "index account", "index/account", accountId, event);
   }
 
   @Override
-  public boolean indexChange(final String projectName, final int changeId) {
-    return new Request("index change", changeId) {
-      @Override
-      HttpResult send() throws IOException {
-        return httpSession.post(buildIndexEndpoint(projectName, changeId));
-      }
-    }.execute();
+  public boolean indexChange(String projectName, int changeId, IndexEvent event) {
+    return execute(
+        RequestMethod.POST,
+        "index change",
+        "index/change",
+        buildIndexEndpoint(projectName, changeId),
+        event);
   }
 
   @Override
-  public boolean deleteChangeFromIndex(final int changeId) {
-    return new Request("delete change", changeId) {
-      @Override
-      HttpResult send() throws IOException {
-        return httpSession.delete(buildIndexEndpoint(changeId));
-      }
-    }.execute();
+  public boolean deleteChangeFromIndex(final int changeId, IndexEvent event) {
+    return execute(
+        RequestMethod.DELETE, "delete change", "index/change", buildIndexEndpoint(changeId), event);
   }
 
   @Override
-  public boolean indexGroup(final String uuid) {
-    return new Request("index group", uuid) {
-      @Override
-      HttpResult send() throws IOException {
-        return httpSession.post(Joiner.on("/").join(pluginRelativePath, "index/group", uuid));
-      }
-    }.execute();
+  public boolean indexGroup(final String uuid, IndexEvent event) {
+    return execute(RequestMethod.POST, "index group", "index/group", uuid, event);
   }
 
   private String buildIndexEndpoint(int changeId) {
@@ -92,96 +94,135 @@
 
   private String buildIndexEndpoint(String projectName, int changeId) {
     String escapedProjectName = Url.encode(projectName);
-    return Joiner.on("/")
-        .join(pluginRelativePath, "index/change", escapedProjectName + '~' + changeId);
+    return escapedProjectName + '~' + changeId;
+  }
+
+  @Override
+  public boolean indexProject(String projectName, IndexEvent event) {
+    return execute(
+        RequestMethod.POST, "index project", "index/project", Url.encode(projectName), event);
   }
 
   @Override
   public boolean send(final Event event) {
-    return new Request("send event", event.type) {
-      @Override
-      HttpResult send() throws IOException {
-        String serializedEvent =
-            new GsonBuilder()
-                .registerTypeAdapter(Supplier.class, new SupplierSerializer())
-                .create()
-                .toJson(event);
-        return httpSession.post(Joiner.on("/").join(pluginRelativePath, "event"), serializedEvent);
-      }
-    }.execute();
+    return execute(RequestMethod.POST, "send event", "event", event.type, event);
   }
 
   @Override
   public boolean evict(final String cacheName, final Object key) {
-    return new Request("invalidate cache " + cacheName, key) {
-      @Override
-      HttpResult send() throws IOException {
-        String json = GsonParser.toJson(cacheName, key);
-        return httpSession.post(Joiner.on("/").join(pluginRelativePath, "cache", cacheName), json);
-      }
-    }.execute();
+    String json = GsonParser.toJson(cacheName, key);
+    return execute(RequestMethod.POST, "invalidate cache " + cacheName, "cache", cacheName, json);
   }
 
   @Override
   public boolean addToProjectList(String projectName) {
-    return new Request("Update project_list, add ", projectName) {
-      @Override
-      HttpResult send() throws IOException {
-        return httpSession.post(buildProjectListEndpoint(projectName));
-      }
-    }.execute();
+    return execute(
+        RequestMethod.POST,
+        "Update project_list, add ",
+        buildProjectListEndpoint(),
+        Url.encode(projectName));
   }
 
   @Override
   public boolean removeFromProjectList(String projectName) {
-    return new Request("Update project_list, remove ", projectName) {
-      @Override
-      HttpResult send() throws IOException {
-        return httpSession.delete(buildProjectListEndpoint(projectName));
-      }
-    }.execute();
+    return execute(
+        RequestMethod.DELETE,
+        "Update project_list, remove ",
+        buildProjectListEndpoint(),
+        Url.encode(projectName));
   }
 
-  private String buildProjectListEndpoint(String projectName) {
-    return Joiner.on("/")
-        .join(pluginRelativePath, "cache", Constants.PROJECT_LIST, Url.encode(projectName));
+  private static String buildProjectListEndpoint() {
+    return Joiner.on("/").join("cache", Constants.PROJECT_LIST);
+  }
+
+  private boolean execute(RequestMethod method, String action, String endpoint, Object id) {
+    return execute(method, action, endpoint, id, null);
+  }
+
+  private boolean execute(
+      RequestMethod method, String action, String endpoint, Object id, Object payload) {
+    List<CompletableFuture<Boolean>> futures =
+        peerInfoProvider.get().stream()
+            .map(peer -> createRequest(method, peer, action, endpoint, id, payload))
+            .map(request -> CompletableFuture.supplyAsync(request::execute))
+            .collect(Collectors.toList());
+    CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+    return futures.stream().allMatch(CompletableFuture::join);
+  }
+
+  private Request createRequest(
+      RequestMethod method,
+      PeerInfo peer,
+      String action,
+      String endpoint,
+      Object id,
+      Object payload) {
+    String destination = peer.getDirectUrl();
+    return new Request(action, id, destination) {
+      @Override
+      HttpResult send() throws IOException {
+        String request = Joiner.on("/").join(destination, pluginRelativePath, endpoint, id);
+        switch (method) {
+          case POST:
+            return httpSession.post(request, payload);
+          case DELETE:
+          default:
+            return httpSession.delete(request);
+        }
+      }
+    };
   }
 
   private abstract class Request {
     private final String action;
     private final Object key;
+    private final String destination;
+
     private int execCnt;
 
-    Request(String action, Object key) {
+    Request(String action, Object key, String destination) {
       this.action = action;
       this.key = key;
+      this.destination = destination;
     }
 
     boolean execute() {
-      log.debug("Executing {} {}", action, key);
+      log.debug("Executing {} {} towards {}", action, key, destination);
       for (; ; ) {
         try {
           execCnt++;
           tryOnce();
-          log.debug("{} {} OK", action, key);
+          log.debug("{} {} towards {} OK", action, key, destination);
           return true;
         } catch (ForwardingException e) {
           int maxTries = cfg.http().maxTries();
-          log.debug("Failed to {} {} [{}/{}]", action, key, execCnt, maxTries, e);
+          log.debug(
+              "Failed to {} {} on {} [{}/{}]", action, key, destination, execCnt, maxTries, e);
           if (!e.isRecoverable()) {
-            log.error("{} {} failed with unrecoverable error; giving up", action, key, e);
+            log.error(
+                "{} {} towards {} failed with unrecoverable error; giving up",
+                action,
+                key,
+                destination,
+                e);
             return false;
           }
           if (execCnt >= maxTries) {
-            log.error("Failed to {} {} after {} tries; giving up", action, key, maxTries);
+            log.error(
+                "Failed to {} {} on {} after {} tries; giving up",
+                action,
+                key,
+                destination,
+                maxTries);
             return false;
           }
 
-          log.debug("Retrying to {} {}", action, key);
+          log.debug("Retrying to {} {} on {}", action, key, destination);
           try {
             Thread.sleep(cfg.http().retryInterval());
           } catch (InterruptedException ie) {
-            log.error("{} {} was interrupted; giving up", action, key, ie);
+            log.error("{} {} towards {} was interrupted; giving up", action, key, destination, ie);
             Thread.currentThread().interrupt();
             return false;
           }
@@ -204,7 +245,10 @@
     abstract HttpResult send() throws IOException;
 
     boolean isRecoverable(IOException e) {
-      return !(e instanceof SSLException);
+      Throwable cause = e.getCause();
+      return !(e instanceof SSLException
+          || cause instanceof HttpException
+          || cause instanceof ClientProtocolException);
     }
   }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderServletModule.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderServletModule.java
index 9752e1a..354dcdb 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderServletModule.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderServletModule.java
@@ -22,6 +22,7 @@
     serveRegex("/index/account/\\d+$").with(IndexAccountRestApiServlet.class);
     serveRegex("/index/change/.*$").with(IndexChangeRestApiServlet.class);
     serveRegex("/index/group/\\w+$").with(IndexGroupRestApiServlet.class);
+    serveRegex("/index/project/.*$").with(IndexProjectRestApiServlet.class);
     serve("/event").with(EventRestApiServlet.class);
     serve("/cache/project_list/*").with(ProjectListApiServlet.class);
     serve("/cache/*").with(CacheRestApiServlet.class);
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeChecker.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeChecker.java
new file mode 100644
index 0000000..ce04589
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeChecker.java
@@ -0,0 +1,63 @@
+// Copyright (C) 2018 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.ericsson.gerrit.plugins.highavailability.index;
+
+import com.ericsson.gerrit.plugins.highavailability.forwarder.IndexEvent;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gwtorm.server.OrmException;
+import java.io.IOException;
+import java.util.Optional;
+
+/** Encapsulates the logic of verifying the up-to-date status of a change. */
+public interface ChangeChecker {
+
+  /**
+   * Return the Change notes read from ReviewDb or NoteDb.
+   *
+   * @return notes of the Change
+   * @throws OrmException if ReviewDb or NoteDb cannot be opened
+   */
+  Optional<ChangeNotes> getChangeNotes() throws OrmException;
+
+  /**
+   * Create a new index event POJO associated with the current Change.
+   *
+   * @return new IndexEvent
+   * @throws IOException if the current Change cannot read
+   * @throws OrmException if ReviewDb cannot be opened
+   */
+  Optional<IndexEvent> newIndexEvent() throws IOException, OrmException;
+
+  /**
+   * Check if the local Change is aligned with the indexEvent received.
+   *
+   * @param indexEvent indexing event
+   * @return true if the local Change is up-to-date, false otherwise.
+   * @throws IOException if an I/O error occurred while reading the local Change
+   * @throws OrmException if the local ReviewDb cannot be opened
+   */
+  boolean isChangeUpToDate(Optional<IndexEvent> indexEvent) throws IOException, OrmException;
+
+  /**
+   * Return the last computed up-to-date Change time-stamp.
+   *
+   * <p>Compute the up-to-date Change time-stamp when it is invoked for the very first time.
+   *
+   * @return the Change timestamp epoch in seconds
+   * @throws IOException if an I/O error occurred while reading the local Change
+   * @throws OrmException if the local ReviewDb cannot be opened
+   */
+  Optional<Long> getComputedChangeTs() throws IOException, OrmException;
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeCheckerImpl.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeCheckerImpl.java
new file mode 100644
index 0000000..75f3086
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeCheckerImpl.java
@@ -0,0 +1,170 @@
+// Copyright (C) 2018 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.ericsson.gerrit.plugins.highavailability.index;
+
+import com.ericsson.gerrit.plugins.highavailability.forwarder.IndexEvent;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.change.ChangeFinder;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.OneOffRequestContext;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Objects;
+import java.util.Optional;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ChangeCheckerImpl implements ChangeChecker {
+  private static final Logger log = LoggerFactory.getLogger(ChangeCheckerImpl.class);
+  private final GitRepositoryManager gitRepoMgr;
+  private final CommentsUtil commentsUtil;
+  private final ChangeDb changeDb;
+  private final OneOffRequestContext oneOffReqCtx;
+  private final String changeId;
+  private final ChangeFinder changeFinder;
+  private Optional<Long> computedChangeTs = Optional.empty();
+  private Optional<ChangeNotes> changeNotes = Optional.empty();
+
+  public interface Factory {
+    ChangeChecker create(String changeId);
+  }
+
+  @Inject
+  public ChangeCheckerImpl(
+      GitRepositoryManager gitRepoMgr,
+      CommentsUtil commentsUtil,
+      ChangeDb changeDb,
+      ChangeFinder changeFinder,
+      OneOffRequestContext oneOffReqCtx,
+      @Assisted String changeId) {
+    this.changeFinder = changeFinder;
+    this.gitRepoMgr = gitRepoMgr;
+    this.commentsUtil = commentsUtil;
+    this.changeDb = changeDb;
+    this.oneOffReqCtx = oneOffReqCtx;
+    this.changeId = changeId;
+  }
+
+  @Override
+  public Optional<IndexEvent> newIndexEvent() throws IOException, OrmException {
+    return getComputedChangeTs()
+        .map(
+            ts -> {
+              IndexEvent event = new IndexEvent();
+              event.eventCreatedOn = ts;
+              event.targetSha = getBranchTargetSha();
+              return event;
+            });
+  }
+
+  @Override
+  public Optional<ChangeNotes> getChangeNotes() throws OrmException {
+    try (ManualRequestContext ctx = oneOffReqCtx.open()) {
+      changeNotes = Optional.ofNullable(changeFinder.findOne(changeId));
+      return changeNotes;
+    }
+  }
+
+  @Override
+  public boolean isChangeUpToDate(Optional<IndexEvent> indexEvent)
+      throws IOException, OrmException {
+    getComputedChangeTs();
+    log.debug("Checking change {} against index event {}", this, indexEvent);
+    if (!computedChangeTs.isPresent()) {
+      log.warn("Unable to compute last updated ts for change {}", changeId);
+      return false;
+    }
+
+    if (indexEvent.isPresent() && indexEvent.get().targetSha == null) {
+      return indexEvent.map(e -> (computedChangeTs.get() >= e.eventCreatedOn)).orElse(true);
+    }
+
+    return indexEvent
+        .map(
+            e ->
+                (computedChangeTs.get() > e.eventCreatedOn)
+                    || (computedChangeTs.get() == e.eventCreatedOn)
+                        && (Objects.equals(getBranchTargetSha(), e.targetSha)))
+        .orElse(true);
+  }
+
+  @Override
+  public Optional<Long> getComputedChangeTs() throws IOException, OrmException {
+    if (!computedChangeTs.isPresent()) {
+      computedChangeTs = computeLastChangeTs();
+    }
+    return computedChangeTs;
+  }
+
+  @Override
+  public String toString() {
+    try {
+      return "change-id="
+          + changeId
+          + "@"
+          + getComputedChangeTs().map(IndexEvent::format)
+          + "/"
+          + getBranchTargetSha();
+    } catch (IOException | OrmException e) {
+      log.error("Unable to render change {}", changeId, e);
+      return "change-id=" + changeId;
+    }
+  }
+
+  private String getBranchTargetSha() {
+    try (Repository repo = gitRepoMgr.openRepository(changeNotes.get().getProjectName())) {
+      String refName = changeNotes.get().getChange().getDest().get();
+      Ref ref = repo.exactRef(refName);
+      if (ref == null) {
+        log.warn("Unable to find target ref {} for change {}", refName, changeId);
+        return null;
+      }
+      return ref.getTarget().getObjectId().getName();
+    } catch (IOException e) {
+      log.warn("Unable to resolve target branch SHA for change {}", changeId, e);
+      return null;
+    }
+  }
+
+  private Optional<Long> computeLastChangeTs() throws OrmException {
+    try (ReviewDb db = changeDb.open()) {
+      return getChangeNotes().map(notes -> getTsFromChangeAndDraftComments(db, notes));
+    }
+  }
+
+  private long getTsFromChangeAndDraftComments(ReviewDb db, ChangeNotes notes) {
+    Change change = notes.getChange();
+    Timestamp changeTs = change.getLastUpdatedOn();
+    try {
+      for (Comment comment : commentsUtil.draftByChange(db, changeNotes.get())) {
+        Timestamp commentTs = comment.writtenOn;
+        changeTs = commentTs.after(changeTs) ? commentTs : changeTs;
+      }
+    } catch (OrmException e) {
+      log.warn("Unable to access draft comments for change {}", change, e);
+    }
+    return changeTs.getTime() / 1000;
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeDb.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeDb.java
new file mode 100644
index 0000000..bef5363
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeDb.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2018 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.ericsson.gerrit.plugins.highavailability.index;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+public class ChangeDb {
+  private static final DisabledReviewDb disabledReviewDb = new DisabledReviewDb();
+
+  private final NotesMigration migration;
+  private final SchemaFactory<ReviewDb> schemaFactory;
+
+  @Inject
+  public ChangeDb(NotesMigration migration, SchemaFactory<ReviewDb> schemaFactory) {
+    this.migration = migration;
+    this.schemaFactory = schemaFactory;
+  }
+
+  public ReviewDb open() throws OrmException {
+    return migration.readChanges() ? disabledReviewDb : schemaFactory.open();
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/DisabledReviewDb.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/DisabledReviewDb.java
new file mode 100644
index 0000000..25ec6e8
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/DisabledReviewDb.java
@@ -0,0 +1,202 @@
+// Copyright (C) 2018 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.ericsson.gerrit.plugins.highavailability.index;
+
+import com.google.common.util.concurrent.CheckedFuture;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Id;
+import com.google.gerrit.reviewdb.server.ChangeAccess;
+import com.google.gerrit.reviewdb.server.ChangeMessageAccess;
+import com.google.gerrit.reviewdb.server.PatchLineCommentAccess;
+import com.google.gerrit.reviewdb.server.PatchSetAccess;
+import com.google.gerrit.reviewdb.server.PatchSetApprovalAccess;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.SchemaVersionAccess;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import com.google.gwtorm.server.StatementExecutor;
+import java.util.Map;
+
+/** ReviewDb that is disabled. */
+@SuppressWarnings("deprecation")
+public class DisabledReviewDb implements ReviewDb {
+  public static class Disabled extends RuntimeException {
+    private static final long serialVersionUID = 1L;
+
+    private Disabled() {
+      super("ReviewDb is disabled for changes");
+    }
+  }
+
+  public static class DisabledChangeAccess implements ChangeAccess {
+
+    @Override
+    public String getRelationName() {
+      throw new Disabled();
+    }
+
+    @Override
+    public int getRelationID() {
+      throw new Disabled();
+    }
+
+    @Override
+    public ResultSet<Change> iterateAllEntities() throws OrmException {
+      throw new Disabled();
+    }
+
+    @Override
+    public Id primaryKey(Change entity) {
+      throw new Disabled();
+    }
+
+    @Override
+    public Map<Id, Change> toMap(Iterable<Change> c) {
+      throw new Disabled();
+    }
+
+    @Override
+    public CheckedFuture<Change, OrmException> getAsync(Id key) {
+      throw new Disabled();
+    }
+
+    @Override
+    public ResultSet<Change> get(Iterable<Id> keys) throws OrmException {
+      throw new Disabled();
+    }
+
+    @Override
+    public void insert(Iterable<Change> instances) throws OrmException {
+      throw new Disabled();
+    }
+
+    @Override
+    public void update(Iterable<Change> instances) throws OrmException {
+      throw new Disabled();
+    }
+
+    @Override
+    public void upsert(Iterable<Change> instances) throws OrmException {
+      throw new Disabled();
+    }
+
+    @Override
+    public void deleteKeys(Iterable<Id> keys) throws OrmException {
+      throw new Disabled();
+    }
+
+    @Override
+    public void delete(Iterable<Change> instances) throws OrmException {
+      throw new Disabled();
+    }
+
+    @Override
+    public void beginTransaction(Id key) throws OrmException {
+      throw new Disabled();
+    }
+
+    @Override
+    public Change atomicUpdate(Id key, AtomicUpdate<Change> update) throws OrmException {
+      throw new Disabled();
+    }
+
+    @Override
+    public Change get(Id id) throws OrmException {
+      return null;
+    }
+
+    @Override
+    public ResultSet<Change> all() throws OrmException {
+      return null;
+    }
+  }
+
+  @Override
+  public void close() {
+    // Do nothing.
+  }
+
+  @Override
+  public void commit() {
+    throw new Disabled();
+  }
+
+  @Override
+  public void rollback() {
+    throw new Disabled();
+  }
+
+  @Override
+  public void updateSchema(StatementExecutor e) {
+    throw new Disabled();
+  }
+
+  @Override
+  public void pruneSchema(StatementExecutor e) {
+    throw new Disabled();
+  }
+
+  @Override
+  public Access<?, ?>[] allRelations() {
+    throw new Disabled();
+  }
+
+  @Override
+  public SchemaVersionAccess schemaVersion() {
+    throw new Disabled();
+  }
+
+  @Override
+  public ChangeAccess changes() {
+    return new DisabledChangeAccess();
+  }
+
+  @Override
+  public PatchSetApprovalAccess patchSetApprovals() {
+    throw new Disabled();
+  }
+
+  @Override
+  public ChangeMessageAccess changeMessages() {
+    throw new Disabled();
+  }
+
+  @Override
+  public PatchSetAccess patchSets() {
+    throw new Disabled();
+  }
+
+  @Override
+  public PatchLineCommentAccess patchComments() {
+    throw new Disabled();
+  }
+
+  @Override
+  public int nextAccountId() {
+    throw new Disabled();
+  }
+
+  @Override
+  public int nextAccountGroupId() {
+    throw new Disabled();
+  }
+
+  @Override
+  public int nextChangeId() {
+    throw new Disabled();
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/PeerInfoNotAvailableException.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ForwardedIndexExecutor.java
similarity index 61%
rename from src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/PeerInfoNotAvailableException.java
rename to src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ForwardedIndexExecutor.java
index df94f52..44c84dc 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/PeerInfoNotAvailableException.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ForwardedIndexExecutor.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2017 The Android Open Source Project
+// Copyright (C) 2018 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.
@@ -12,10 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.ericsson.gerrit.plugins.highavailability.forwarder.rest;
+package com.ericsson.gerrit.plugins.highavailability.index;
 
-import java.io.IOException;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-public class PeerInfoNotAvailableException extends IOException {
-  private static final long serialVersionUID = 1L;
-}
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface ForwardedIndexExecutor {}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ForwardedIndexExecutorProvider.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ForwardedIndexExecutorProvider.java
new file mode 100644
index 0000000..2112dbe
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ForwardedIndexExecutorProvider.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2018 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.ericsson.gerrit.plugins.highavailability.index;
+
+import com.ericsson.gerrit.plugins.highavailability.Configuration;
+import com.ericsson.gerrit.plugins.highavailability.ExecutorProvider;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+class ForwardedIndexExecutorProvider extends ExecutorProvider {
+
+  @Inject
+  ForwardedIndexExecutorProvider(WorkQueue workQueue, Configuration config) {
+    super(workQueue, config.index().threadPoolSize(), "Forwarded-Index-Event");
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/IndexEventHandler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/IndexEventHandler.java
index 175eaaf..e6a8b0c 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/IndexEventHandler.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/IndexEventHandler.java
@@ -16,30 +16,43 @@
 
 import com.ericsson.gerrit.plugins.highavailability.forwarder.Context;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.Forwarder;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.IndexEvent;
 import com.google.common.base.Objects;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.events.AccountIndexedListener;
 import com.google.gerrit.extensions.events.ChangeIndexedListener;
 import com.google.gerrit.extensions.events.GroupIndexedListener;
+import com.google.gerrit.extensions.events.ProjectIndexedListener;
 import com.google.inject.Inject;
 import java.util.Collections;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 class IndexEventHandler
-    implements ChangeIndexedListener, AccountIndexedListener, GroupIndexedListener {
+    implements ChangeIndexedListener,
+        AccountIndexedListener,
+        GroupIndexedListener,
+        ProjectIndexedListener {
+  private static final Logger log = LoggerFactory.getLogger(IndexEventHandler.class);
   private final Executor executor;
   private final Forwarder forwarder;
   private final String pluginName;
   private final Set<IndexTask> queuedTasks = Collections.newSetFromMap(new ConcurrentHashMap<>());
+  private final ChangeCheckerImpl.Factory changeChecker;
 
   @Inject
   IndexEventHandler(
-      @IndexExecutor Executor executor, @PluginName String pluginName, Forwarder forwarder) {
+      @IndexExecutor Executor executor,
+      @PluginName String pluginName,
+      Forwarder forwarder,
+      ChangeCheckerImpl.Factory changeChecker) {
     this.forwarder = forwarder;
     this.executor = executor;
     this.pluginName = pluginName;
+    this.changeChecker = changeChecker;
   }
 
   @Override
@@ -72,16 +85,46 @@
     }
   }
 
-  private void executeIndexChangeTask(String projectName, int id, boolean deleted) {
+  @Override
+  public void onProjectIndexed(String projectName) {
     if (!Context.isForwardedEvent()) {
-      IndexChangeTask task = new IndexChangeTask(projectName, id, deleted);
+      IndexProjectTask task = new IndexProjectTask(projectName);
       if (queuedTasks.add(task)) {
         executor.execute(task);
       }
     }
   }
 
+  private void executeIndexChangeTask(String projectName, int id, boolean deleted) {
+    if (!Context.isForwardedEvent()) {
+      ChangeChecker checker = changeChecker.create(projectName + "~" + id);
+      try {
+        checker
+            .newIndexEvent()
+            .map(event -> new IndexChangeTask(projectName, id, deleted, event))
+            .ifPresent(
+                task -> {
+                  if (queuedTasks.add(task)) {
+                    executor.execute(task);
+                  }
+                });
+      } catch (Exception e) {
+        log.warn("Unable to create task to handle change {}~{}", projectName, id, e);
+      }
+    }
+  }
+
   abstract class IndexTask implements Runnable {
+    protected final IndexEvent indexEvent;
+
+    IndexTask() {
+      indexEvent = new IndexEvent();
+    }
+
+    IndexTask(IndexEvent indexEvent) {
+      this.indexEvent = indexEvent;
+    }
+
     @Override
     public void run() {
       queuedTasks.remove(this);
@@ -96,7 +139,8 @@
     private final int changeId;
     private final String projectName;
 
-    IndexChangeTask(String projectName, int changeId, boolean deleted) {
+    IndexChangeTask(String projectName, int changeId, boolean deleted, IndexEvent indexEvent) {
+      super(indexEvent);
       this.projectName = projectName;
       this.changeId = changeId;
       this.deleted = deleted;
@@ -105,9 +149,9 @@
     @Override
     public void execute() {
       if (deleted) {
-        forwarder.deleteChangeFromIndex(changeId);
+        forwarder.deleteChangeFromIndex(changeId, indexEvent);
       } else {
-        forwarder.indexChange(projectName, changeId);
+        forwarder.indexChange(projectName, changeId, indexEvent);
       }
     }
 
@@ -140,7 +184,7 @@
 
     @Override
     public void execute() {
-      forwarder.indexAccount(accountId);
+      forwarder.indexAccount(accountId, indexEvent);
     }
 
     @Override
@@ -172,7 +216,7 @@
 
     @Override
     public void execute() {
-      forwarder.indexGroup(groupUUID);
+      forwarder.indexGroup(groupUUID, indexEvent);
     }
 
     @Override
@@ -194,4 +238,36 @@
       return String.format("[%s] Index group %s in target instance", pluginName, groupUUID);
     }
   }
+
+  class IndexProjectTask extends IndexTask {
+    private final String projectName;
+
+    IndexProjectTask(String projectName) {
+      this.projectName = projectName;
+    }
+
+    @Override
+    public void execute() {
+      forwarder.indexProject(projectName, indexEvent);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(IndexProjectTask.class, projectName);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (!(obj instanceof IndexProjectTask)) {
+        return false;
+      }
+      IndexProjectTask other = (IndexProjectTask) obj;
+      return projectName.equals(other.projectName);
+    }
+
+    @Override
+    public String toString() {
+      return String.format("[%s] Index project %s in target instance", pluginName, projectName);
+    }
+  }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/IndexModule.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/IndexModule.java
index f88a806..c17731f 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/IndexModule.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/IndexModule.java
@@ -17,18 +17,30 @@
 import com.google.gerrit.extensions.events.AccountIndexedListener;
 import com.google.gerrit.extensions.events.ChangeIndexedListener;
 import com.google.gerrit.extensions.events.GroupIndexedListener;
+import com.google.gerrit.extensions.events.ProjectIndexedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
 import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
 
 public class IndexModule extends LifecycleModule {
 
   @Override
   protected void configure() {
     bind(Executor.class).annotatedWith(IndexExecutor.class).toProvider(IndexExecutorProvider.class);
+    bind(ScheduledExecutorService.class)
+        .annotatedWith(ForwardedIndexExecutor.class)
+        .toProvider(ForwardedIndexExecutorProvider.class);
     listener().to(IndexExecutorProvider.class);
     DynamicSet.bind(binder(), ChangeIndexedListener.class).to(IndexEventHandler.class);
     DynamicSet.bind(binder(), AccountIndexedListener.class).to(IndexEventHandler.class);
     DynamicSet.bind(binder(), GroupIndexedListener.class).to(IndexEventHandler.class);
+    DynamicSet.bind(binder(), ProjectIndexedListener.class).to(IndexEventHandler.class);
+
+    install(
+        new FactoryModuleBuilder()
+            .implement(ChangeChecker.class, ChangeCheckerImpl.class)
+            .build(ChangeCheckerImpl.Factory.class));
   }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/PeerInfoModule.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/PeerInfoModule.java
index 577d66d..68707c2 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/PeerInfoModule.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/PeerInfoModule.java
@@ -18,7 +18,7 @@
 import com.ericsson.gerrit.plugins.highavailability.peers.jgroups.JGroupsPeerInfoProvider;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.inject.TypeLiteral;
-import java.util.Optional;
+import java.util.Set;
 
 public class PeerInfoModule extends LifecycleModule {
 
@@ -32,11 +32,10 @@
   protected void configure() {
     switch (strategy) {
       case STATIC:
-        bind(new TypeLiteral<Optional<PeerInfo>>() {})
-            .toProvider(PluginConfigPeerInfoProvider.class);
+        bind(new TypeLiteral<Set<PeerInfo>>() {}).toProvider(PluginConfigPeerInfoProvider.class);
         break;
       case JGROUPS:
-        bind(new TypeLiteral<Optional<PeerInfo>>() {}).toProvider(JGroupsPeerInfoProvider.class);
+        bind(new TypeLiteral<Set<PeerInfo>>() {}).toProvider(JGroupsPeerInfoProvider.class);
         listener().to(JGroupsPeerInfoProvider.class);
         break;
       default:
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/PluginConfigPeerInfoProvider.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/PluginConfigPeerInfoProvider.java
index f39eb6b..9233c79 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/PluginConfigPeerInfoProvider.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/PluginConfigPeerInfoProvider.java
@@ -18,20 +18,21 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 @Singleton
-public class PluginConfigPeerInfoProvider implements Provider<Optional<PeerInfo>> {
+public class PluginConfigPeerInfoProvider implements Provider<Set<PeerInfo>> {
 
-  private final Optional<PeerInfo> peerInfo;
+  private final Set<PeerInfo> peers;
 
   @Inject
   PluginConfigPeerInfoProvider(Configuration cfg) {
-    peerInfo = Optional.of(new PeerInfo(cfg.peerInfoStatic().url()));
+    peers = cfg.peerInfoStatic().urls().stream().map(PeerInfo::new).collect(Collectors.toSet());
   }
 
   @Override
-  public Optional<PeerInfo> get() {
-    return peerInfo;
+  public Set<PeerInfo> get() {
+    return peers;
   }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProvider.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProvider.java
index cf46ac3..596f86e 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProvider.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProvider.java
@@ -16,6 +16,7 @@
 
 import com.ericsson.gerrit.plugins.highavailability.Configuration;
 import com.ericsson.gerrit.plugins.highavailability.peers.PeerInfo;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -23,6 +24,7 @@
 import java.net.InetAddress;
 import java.nio.file.Path;
 import java.util.Optional;
+import java.util.Set;
 import org.jgroups.Address;
 import org.jgroups.JChannel;
 import org.jgroups.Message;
@@ -43,7 +45,7 @@
  */
 @Singleton
 public class JGroupsPeerInfoProvider extends ReceiverAdapter
-    implements Provider<Optional<PeerInfo>>, LifecycleListener {
+    implements Provider<Set<PeerInfo>>, LifecycleListener {
   private static final Logger log = LoggerFactory.getLogger(JGroupsPeerInfoProvider.class);
   private static final String JGROUPS_LOG_FACTORY_PROPERTY = "jgroups.logging.log_factory_class";
 
@@ -85,7 +87,6 @@
   @Override
   public void viewAccepted(View view) {
     log.info("viewAccepted(view: {}) called", view);
-
     synchronized (this) {
       if (view.getMembers().size() > 2) {
         log.warn(
@@ -164,8 +165,8 @@
   }
 
   @Override
-  public Optional<PeerInfo> get() {
-    return peerInfo;
+  public Set<PeerInfo> get() {
+    return peerInfo.isPresent() ? ImmutableSet.of(peerInfo.get()) : ImmutableSet.of();
   }
 
   @Override
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index 331dae5..b2040d5 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -1,3 +1,4 @@
+
 This plugin allows making Gerrit highly available by having redundant Gerrit
 masters.
 
@@ -7,10 +8,10 @@
 * sharing the git repositories using a shared file system (e.g. NFS)
 * behind a load balancer (e.g. HAProxy)
 
-Currently, the only mode supported is one primary (active) master and one backup
-(passive) master but eventually the plan is to support `n` active masters. In
+Currently, the mode supported is one primary (active) master and multiple backup
+(passive) masters but eventually the plan is to support `n` active masters. In
 the active/passive mode, the active master is handling all traffic while the
-passive is kept updated to be always ready to take over.
+passives are kept updated to be always ready to take over.
 
 Even if database and git repositories are shared by the masters, there are a few
 areas of concern in order to be able to switch traffic between masters in a
@@ -23,38 +24,38 @@
 * web sessions
 
 They need either to be shared or kept local to each master but synchronized.
-This plugin needs to be installed in both masters and will take care of sharing
+This plugin needs to be installed in all the masters and it will take care of sharing
 or synchronizing them.
 
 #### Caches
 Every time a cache eviction occurs in one of the masters, the eviction will be
-forwarded the other master so its caches do not contain stale entries.
+forwarded the other masters so their caches do not contain stale entries.
 
 #### Secondary indexes
 Every time the secondary index is modified in one of the masters, e.g., a change
-is added, updated or removed from the index, the other master's index is
+is added, updated or removed from the index, the others master's index are
 updated accordingly. This way, both indexes are kept synchronized.
 
 #### Stream events
 Every time a stream event occurs in one of the masters (see [more events info]
 (https://gerrit-review.googlesource.com/Documentation/cmd-stream-events.html#events)),
-the event is forwarded to the other master which re-plays it. This way, the
+the event is forwarded to the other masters which re-plays it. This way, the
 output of the stream-events command is the same, no matter which master a client
 is connected to.
 
 #### Web session
 The built-in Gerrit H2 based web session cache is replaced with a file based
-implementation that is shared amongst both masters.
+implementation that is shared amongst the masters.
 
 ## Setup
 
 Prerequisites:
 
-* Unique database server must be accessible from both masters
+* Unique database server must be accessible from all the masters
 * Git repositories must be located on a shared file system
 * A directory on a shared file system must be available for @PLUGIN@ to use
 
-For both masters:
+For the masters:
 
 * Configure database section in gerrit.config to use the shared database
 * Configure gerrit.basePath in gerrit.config to the shared repositories location
@@ -69,7 +70,7 @@
   sharedDirectory = /directory/accessible/from/both/masters
 
 [peerInfo "static"]
-  url = http://backupMasterHost:8081/
+  url = http://backupMasterHost1:8081/
 
 [http]
   user = username
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 12b00b9..04ccafc 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -1,7 +1,8 @@
+
 @PLUGIN@ Configuration
 =========================
 
-The @PLUGIN@ plugin must be installed on both instances and the following fields
+The @PLUGIN@ plugin must be installed on all the instances and the following fields
 should be specified in `$site_path/etc/@PLUGIN@.config` file:
 
 File '@PLUGIN@.config'
@@ -12,10 +13,13 @@
 ```
 [main]
   sharedDirectory = /directory/accessible/from/both/instances
+[autoReindex]
+  enabled = false
 [peerInfo]
   strategy = static
 [peerInfo "static"]
-  url = target_instance_url
+  url = first_target_instance_url
+  url = second_target_instance_url
 [http]
   user = username
   password = password
@@ -26,6 +30,8 @@
 ```
 [main]
   sharedDirectory = /directory/accessible/from/both/instances
+[autoReindex]
+  enabled = false
 [peerInfo]
   strategy = jgroups
 [peerInfo "jgroups"]
@@ -50,6 +56,32 @@
     directory is "/gerrit/root/shared/dir". When not specified, the default
     is "shared".
 
+```autoReindex.enabled```
+:   Enable the tracking of the latest change indexed under data/high-availability
+    for each of the indexes. At startup scans all the changes, accounts and groups
+    and reindex the ones that have been updated by other nodes while the server was down.
+    When not specified, the default is "false", that means no automatic tracking
+    and indexing at start.
+
+```autoReindex.delay```
+:   When autoReindex is enabled, indicates the delay aftere the plugin startup,
+    before triggering the conditional reindexing of all changes, accounts and groups.
+    Delay is expressed in Gerrit time values as in [websession.cleanupInterval](#websessioncleanupInterval).
+    When not specified, the default is "10 seconds".
+
+```autoReindex.pollInterval```
+:   When autoReindex is enabled, indicates the interval between the conditional
+    reindexing of all changes, accounts and groups.
+    Delay is expressed in Gerrit time values as in [websession.cleanupInterval](#websessioncleanupInterval).
+    When not specified, polling of conditional reindexing is disabled.
+
+```autoReindex.interval```
+:   Enable the tracking of the latest change indexed under data/high-availability
+    for each of the indexes. At startup scans all the changes, accounts and groups
+    and reindex the ones that have been updated by other nodes while the server was down.
+    When not specified, the default is "false", that means no automatic tracking
+    and indexing at start.
+
 ```peerInfo.strategy```
 :   Strategy to find other peers. Supported strategies are `static` or `jgroups`.
     Defaults to `static`.
@@ -63,7 +95,8 @@
 a member joins or leaves the cluster.
 
 ```peerInfo.static.url```
-:   Specify the URL for the peer instance.
+:   Specify the URL for the peer instance. If more than one peer instance is to be
+    configured, add as many url entries as necessary.
 
 ```peerInfo.jgroups.myUrl```
 :   The URL of this instance to be broadcast to other peers. If not specified, the
@@ -150,6 +183,10 @@
 :   Whether to synchronize stream events.
     Defaults to true.
 
+```index.numStripedLocks```
+:   Number of striped locks to use during reindexing.
+    Defaults to 10.
+
 ```index.synchronize```
 :   Whether to synchronize secondary indexes.
     Defaults to true.
@@ -158,6 +195,17 @@
 :   Maximum number of threads used to send index events to the target instance.
     Defaults to 4.
 
+```index.maxTries```
+:   Maximum number of times the plugin should attempt to reindex changes.
+    Setting this value to 0 will disable retries. After this number of failed tries,
+    an error is logged and the local index should be considered stale and needs
+    to be investigated and manually reindexed.
+    Defaults to 2.
+
+```index.retryInterval```
+:   The interval of time in milliseconds between the subsequent auto-retries.
+    Defaults to 30000 (30 seconds).
+
 ```websession.synchronize```
 :   Whether to synchronize web sessions.
     Defaults to true.
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/ConfigurationTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/ConfigurationTest.java
index bb4c360..63ab408 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/ConfigurationTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/ConfigurationTest.java
@@ -14,46 +14,48 @@
 
 package com.ericsson.gerrit.plugins.highavailability;
 
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.CACHE_SECTION;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.CLEANUP_INTERVAL_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.CLUSTER_NAME_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.CONNECTION_TIMEOUT_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_CLEANUP_INTERVAL_MS;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_CLUSTER_NAME;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_HEALTH_CHECK_ENABLED;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_MAX_TRIES;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_PEER_INFO_STRATEGY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_RETRY_INTERVAL;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_SHARED_DIRECTORY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_SKIP_INTERFACE_LIST;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_SYNCHRONIZE;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Cache.CACHE_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Cache.PATTERN_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_NUM_STRIPED_LOCKS;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_THREAD_POOL_SIZE;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_TIMEOUT_MS;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.ENABLE_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.EVENT_SECTION;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.HEALTH_CHECK_SECTION;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.HTTP_SECTION;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.INDEX_SECTION;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.JGROUPS_SECTION;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.JGROUPS_SUBSECTION;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.MAIN_SECTION;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.MAX_TRIES_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.MY_URL_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.PASSWORD_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.PATTERN_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Event.EVENT_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Forwarding.DEFAULT_SYNCHRONIZE;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Forwarding.SYNCHRONIZE_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.HealthCheck.DEFAULT_HEALTH_CHECK_ENABLED;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.HealthCheck.ENABLE_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.HealthCheck.HEALTH_CHECK_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Http.CONNECTION_TIMEOUT_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Http.DEFAULT_MAX_TRIES;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Http.DEFAULT_RETRY_INTERVAL;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Http.DEFAULT_TIMEOUT_MS;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Http.HTTP_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Http.MAX_TRIES_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Http.PASSWORD_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Http.RETRY_INTERVAL_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Http.SOCKET_TIMEOUT_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Http.USER_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Index.INDEX_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.JGroups.CLUSTER_NAME_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.JGroups.DEFAULT_CLUSTER_NAME;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.JGroups.DEFAULT_SKIP_INTERFACE_LIST;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.JGroups.JGROUPS_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.JGroups.PROTOCOL_STACK_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.JGroups.SKIP_INTERFACE_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Main.DEFAULT_SHARED_DIRECTORY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Main.MAIN_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Main.SHARED_DIRECTORY_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.NUM_STRIPED_LOCKS;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.PEER_INFO_SECTION;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.PROTOCOL_STACK_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.RETRY_INTERVAL_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.SHARED_DIRECTORY_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.SKIP_INTERFACE_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.SOCKET_TIMEOUT_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.STATIC_SUBSECTION;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.STRATEGY_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.SYNCHRONIZE_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.PeerInfo.DEFAULT_PEER_INFO_STRATEGY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.PeerInfo.STRATEGY_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.PeerInfoJGroups.JGROUPS_SUBSECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.PeerInfoJGroups.MY_URL_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.PeerInfoStatic.STATIC_SUBSECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.PeerInfoStatic.URL_KEY;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.THREAD_POOL_SIZE_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.URL_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.USER_KEY;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.WEBSESSION_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Websession.CLEANUP_INTERVAL_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Websession.DEFAULT_CLEANUP_INTERVAL_MS;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Websession.WEBSESSION_SECTION;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth8.assertThat;
 import static java.util.concurrent.TimeUnit.SECONDS;
@@ -67,6 +69,7 @@
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.List;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
 import org.junit.Test;
@@ -82,6 +85,7 @@
   private static final String PASS = "fakePass";
   private static final String USER = "fakeUser";
   private static final String URL = "http://fakeUrl";
+  private static final List<String> URLS = ImmutableList.of(URL, "http://anotherUrl/");
   private static final int TIMEOUT = 5000;
   private static final int MAX_TRIES = 5;
   private static final int RETRY_INTERVAL = 1000;
@@ -120,17 +124,12 @@
   }
 
   @Test
-  public void testGetUrl() throws Exception {
-    assertThat(getConfiguration().peerInfoStatic().url()).isEmpty();
+  public void testGetUrls() throws Exception {
+    assertThat(getConfiguration().peerInfoStatic().urls()).isEmpty();
 
-    globalPluginConfig.setString(PEER_INFO_SECTION, STATIC_SUBSECTION, URL_KEY, URL);
-    assertThat(getConfiguration().peerInfoStatic().url()).isEqualTo(URL);
-  }
-
-  @Test
-  public void testGetUrlIsDroppingTrailingSlash() throws Exception {
-    globalPluginConfig.setString(PEER_INFO_SECTION, STATIC_SUBSECTION, URL_KEY, URL + "/");
-    assertThat(getConfiguration().peerInfoStatic().url()).isEqualTo(URL);
+    globalPluginConfig.setStringList(PEER_INFO_SECTION, STATIC_SUBSECTION, URL_KEY, URLS);
+    assertThat(getConfiguration().peerInfoStatic().urls())
+        .containsAllIn(ImmutableList.of(URL, "http://anotherUrl"));
   }
 
   @Test
@@ -377,4 +376,12 @@
     globalPluginConfig.setBoolean(HEALTH_CHECK_SECTION, null, ENABLE_KEY, true);
     assertThat(getConfiguration().healthCheck().enabled()).isTrue();
   }
+
+  @Test
+  public void testGetIndexNumStripedLocks() throws Exception {
+    assertThat(getConfiguration().index().numStripedLocks()).isEqualTo(DEFAULT_NUM_STRIPED_LOCKS);
+
+    globalPluginConfig.setInt(INDEX_SECTION, null, NUM_STRIPED_LOCKS, 100);
+    assertThat(getConfiguration().index().numStripedLocks()).isEqualTo(100);
+  }
 }
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexAccountHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexAccountHandlerTest.java
index 0a96776..c2d3659 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexAccountHandlerTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexAccountHandlerTest.java
@@ -18,11 +18,14 @@
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
+import com.ericsson.gerrit.plugins.highavailability.Configuration;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexingHandler.Operation;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.index.account.AccountIndexer;
 import java.io.IOException;
+import java.util.Optional;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -37,18 +40,22 @@
 
   @Rule public ExpectedException exception = ExpectedException.none();
   @Mock private AccountIndexer indexerMock;
+  @Mock private Configuration configMock;
+  @Mock private Configuration.Index indexMock;
   private ForwardedIndexAccountHandler handler;
   private Account.Id id;
 
   @Before
   public void setUp() throws Exception {
-    handler = new ForwardedIndexAccountHandler(indexerMock);
+    when(configMock.index()).thenReturn(indexMock);
+    when(indexMock.numStripedLocks()).thenReturn(10);
+    handler = new ForwardedIndexAccountHandler(indexerMock, configMock);
     id = new Account.Id(123);
   }
 
   @Test
   public void testSuccessfulIndexing() throws Exception {
-    handler.index(id, Operation.INDEX);
+    handler.index(id, Operation.INDEX, Optional.empty());
     verify(indexerMock).index(id);
   }
 
@@ -56,7 +63,7 @@
   public void deleteIsNotSupported() throws Exception {
     exception.expect(UnsupportedOperationException.class);
     exception.expectMessage("Delete from account index not supported");
-    handler.index(id, Operation.DELETE);
+    handler.index(id, Operation.DELETE, Optional.empty());
   }
 
   @Test
@@ -73,7 +80,7 @@
         .index(id);
 
     assertThat(Context.isForwardedEvent()).isFalse();
-    handler.index(id, Operation.INDEX);
+    handler.index(id, Operation.INDEX, Optional.empty());
     assertThat(Context.isForwardedEvent()).isFalse();
 
     verify(indexerMock).index(id);
@@ -92,7 +99,7 @@
 
     assertThat(Context.isForwardedEvent()).isFalse();
     try {
-      handler.index(id, Operation.INDEX);
+      handler.index(id, Operation.INDEX, Optional.empty());
       fail("should have thrown an IOException");
     } catch (IOException e) {
       assertThat(e.getMessage()).isEqualTo("someMessage");
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandlerTest.java
index 63db939..6bfc58b 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandlerTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandlerTest.java
@@ -23,17 +23,21 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import com.ericsson.gerrit.plugins.highavailability.Configuration;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexingHandler.Operation;
+import com.ericsson.gerrit.plugins.highavailability.index.ChangeChecker;
+import com.ericsson.gerrit.plugins.highavailability.index.ChangeCheckerImpl;
+import com.ericsson.gerrit.plugins.highavailability.index.ChangeDb;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.change.ChangeFinder;
 import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import java.io.IOException;
+import java.util.Optional;
+import java.util.concurrent.ScheduledExecutorService;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -55,78 +59,88 @@
   private static final boolean DO_NOT_THROW_ORM_EXCEPTION = false;
   private static final boolean THROW_IO_EXCEPTION = true;
   private static final boolean THROW_ORM_EXCEPTION = true;
+  private static final boolean CHANGE_UP_TO_DATE = true;
+  private static final boolean CHANGE_OUTDATED = false;
 
   @Rule public ExpectedException exception = ExpectedException.none();
   @Mock private ChangeIndexer indexerMock;
-  @Mock private SchemaFactory<ReviewDb> schemaFactoryMock;
+  @Mock private ChangeDb changeDbMock;
   @Mock private ReviewDb dbMock;
-  @Mock private ChangeFinder changeFinderMock;
   @Mock private ChangeNotes changeNotes;
+  @Mock private Configuration configMock;
+  @Mock private Configuration.Index indexMock;
+  @Mock private ScheduledExecutorService indexExecutorMock;
+  @Mock private OneOffRequestContext ctxMock;
+  @Mock private ChangeCheckerImpl.Factory changeCheckerFactoryMock;
+  @Mock private ChangeChecker changeCheckerAbsentMock;
+  @Mock private ChangeChecker changeCheckerPresentMock;
   private ForwardedIndexChangeHandler handler;
   private Change.Id id;
-  private Change change;
 
   @Before
   public void setUp() throws Exception {
-    when(schemaFactoryMock.open()).thenReturn(dbMock);
+    when(changeDbMock.open()).thenReturn(dbMock);
     id = new Change.Id(TEST_CHANGE_NUMBER);
-    change = new Change(null, id, null, null, TimeUtil.nowTs());
+    Change change = new Change(null, id, null, null, TimeUtil.nowTs());
     when(changeNotes.getChange()).thenReturn(change);
-    handler = new ForwardedIndexChangeHandler(indexerMock, schemaFactoryMock, changeFinderMock);
+    when(configMock.index()).thenReturn(indexMock);
+    when(indexMock.numStripedLocks()).thenReturn(10);
+    when(changeCheckerFactoryMock.create(any())).thenReturn(changeCheckerAbsentMock);
+    handler =
+        new ForwardedIndexChangeHandler(
+            indexerMock,
+            changeDbMock,
+            configMock,
+            indexExecutorMock,
+            ctxMock,
+            changeCheckerFactoryMock);
   }
 
   @Test
-  public void changeIsIndexed() throws Exception {
-    setupChangeAccessRelatedMocks(CHANGE_EXISTS);
-    handler.index(TEST_CHANGE_ID, Operation.INDEX);
+  public void changeIsIndexedWhenUpToDate() throws Exception {
+    setupChangeAccessRelatedMocks(CHANGE_EXISTS, CHANGE_UP_TO_DATE);
+    handler.index(TEST_CHANGE_ID, Operation.INDEX, Optional.empty());
+    verify(indexerMock, times(1)).index(any(ReviewDb.class), any(Change.class));
+  }
+
+  @Test
+  public void changeIsStillIndexedEvenWhenOutdated() throws Exception {
+    setupChangeAccessRelatedMocks(CHANGE_EXISTS, CHANGE_OUTDATED);
+    handler.index(TEST_CHANGE_ID, Operation.INDEX, Optional.of(new IndexEvent()));
     verify(indexerMock, times(1)).index(any(ReviewDb.class), any(Change.class));
   }
 
   @Test
   public void changeIsDeletedFromIndex() throws Exception {
-    handler.index(TEST_CHANGE_ID, Operation.DELETE);
+    handler.index(TEST_CHANGE_ID, Operation.DELETE, Optional.empty());
     verify(indexerMock, times(1)).delete(id);
   }
 
   @Test
   public void changeToIndexDoesNotExist() throws Exception {
-    setupChangeAccessRelatedMocks(CHANGE_DOES_NOT_EXIST);
-    handler.index(TEST_CHANGE_ID, Operation.INDEX);
+    setupChangeAccessRelatedMocks(CHANGE_DOES_NOT_EXIST, CHANGE_OUTDATED);
+    handler.index(TEST_CHANGE_ID, Operation.INDEX, Optional.empty());
     verify(indexerMock, times(1)).delete(id);
   }
 
   @Test
   public void schemaThrowsExceptionWhenLookingUpForChange() throws Exception {
-    setupChangeAccessRelatedMocks(CHANGE_EXISTS, THROW_ORM_EXCEPTION);
+    setupChangeAccessRelatedMocks(CHANGE_EXISTS, THROW_ORM_EXCEPTION, CHANGE_UP_TO_DATE);
     exception.expect(OrmException.class);
-    handler.index(TEST_CHANGE_ID, Operation.INDEX);
-  }
-
-  @Test
-  public void indexerThrowsNoSuchChangeExceptionTryingToPostChange() throws Exception {
-    doThrow(new NoSuchChangeException(id)).when(schemaFactoryMock).open();
-    handler.index(TEST_CHANGE_ID, Operation.INDEX);
-    verify(indexerMock, times(1)).delete(id);
-  }
-
-  @Test
-  public void indexerThrowsNestedNoSuchChangeExceptionTryingToPostChange() throws Exception {
-    OrmException e = new OrmException("test", new NoSuchChangeException(id));
-    doThrow(e).when(schemaFactoryMock).open();
-    handler.index(TEST_CHANGE_ID, Operation.INDEX);
-    verify(indexerMock, times(1)).delete(id);
+    handler.index(TEST_CHANGE_ID, Operation.INDEX, Optional.empty());
   }
 
   @Test
   public void indexerThrowsIOExceptionTryingToIndexChange() throws Exception {
-    setupChangeAccessRelatedMocks(CHANGE_EXISTS, DO_NOT_THROW_ORM_EXCEPTION, THROW_IO_EXCEPTION);
+    setupChangeAccessRelatedMocks(
+        CHANGE_EXISTS, DO_NOT_THROW_ORM_EXCEPTION, THROW_IO_EXCEPTION, CHANGE_UP_TO_DATE);
     exception.expect(IOException.class);
-    handler.index(TEST_CHANGE_ID, Operation.INDEX);
+    handler.index(TEST_CHANGE_ID, Operation.INDEX, Optional.empty());
   }
 
   @Test
   public void shouldSetAndUnsetForwardedContext() throws Exception {
-    setupChangeAccessRelatedMocks(CHANGE_EXISTS);
+    setupChangeAccessRelatedMocks(CHANGE_EXISTS, CHANGE_UP_TO_DATE);
     // this doAnswer is to allow to assert that context is set to forwarded
     // while cache eviction is called.
     doAnswer(
@@ -139,7 +153,7 @@
         .index(any(ReviewDb.class), any(Change.class));
 
     assertThat(Context.isForwardedEvent()).isFalse();
-    handler.index(TEST_CHANGE_ID, Operation.INDEX);
+    handler.index(TEST_CHANGE_ID, Operation.INDEX, Optional.empty());
     assertThat(Context.isForwardedEvent()).isFalse();
 
     verify(indexerMock, times(1)).index(any(ReviewDb.class), any(Change.class));
@@ -147,7 +161,7 @@
 
   @Test
   public void shouldSetAndUnsetForwardedContextEvenIfExceptionIsThrown() throws Exception {
-    setupChangeAccessRelatedMocks(CHANGE_EXISTS);
+    setupChangeAccessRelatedMocks(CHANGE_EXISTS, CHANGE_UP_TO_DATE);
     doAnswer(
             (Answer<Void>)
                 invocation -> {
@@ -159,7 +173,7 @@
 
     assertThat(Context.isForwardedEvent()).isFalse();
     try {
-      handler.index(TEST_CHANGE_ID, Operation.INDEX);
+      handler.index(TEST_CHANGE_ID, Operation.INDEX, Optional.empty());
       fail("should have thrown an IOException");
     } catch (IOException e) {
       assertThat(e.getMessage()).isEqualTo("someMessage");
@@ -169,33 +183,38 @@
     verify(indexerMock, times(1)).index(any(ReviewDb.class), any(Change.class));
   }
 
-  private void setupChangeAccessRelatedMocks(boolean changeExist) throws Exception {
+  private void setupChangeAccessRelatedMocks(boolean changeExist, boolean changeUpToDate)
+      throws Exception {
     setupChangeAccessRelatedMocks(
-        changeExist, DO_NOT_THROW_ORM_EXCEPTION, DO_NOT_THROW_IO_EXCEPTION);
-  }
-
-  private void setupChangeAccessRelatedMocks(boolean changeExist, boolean ormException)
-      throws OrmException, IOException {
-    setupChangeAccessRelatedMocks(changeExist, ormException, DO_NOT_THROW_IO_EXCEPTION);
+        changeExist, DO_NOT_THROW_ORM_EXCEPTION, DO_NOT_THROW_IO_EXCEPTION, changeUpToDate);
   }
 
   private void setupChangeAccessRelatedMocks(
-      boolean changeExists, boolean ormException, boolean ioException)
+      boolean changeExist, boolean ormException, boolean changeUpToDate)
+      throws OrmException, IOException {
+    setupChangeAccessRelatedMocks(
+        changeExist, ormException, DO_NOT_THROW_IO_EXCEPTION, changeUpToDate);
+  }
+
+  private void setupChangeAccessRelatedMocks(
+      boolean changeExists, boolean ormException, boolean ioException, boolean changeIsUpToDate)
       throws OrmException, IOException {
     if (ormException) {
-      doThrow(new OrmException("")).when(schemaFactoryMock).open();
+      doThrow(new OrmException("")).when(changeDbMock).open();
     } else {
-      when(schemaFactoryMock.open()).thenReturn(dbMock);
-      if (changeExists) {
-        when(changeFinderMock.findOne(TEST_CHANGE_ID)).thenReturn(changeNotes);
-        if (ioException) {
-          doThrow(new IOException("io-error"))
-              .when(indexerMock)
-              .index(any(ReviewDb.class), any(Change.class));
-        }
-      } else {
-        when(changeFinderMock.findOne(TEST_CHANGE_ID)).thenReturn(null);
+      when(changeDbMock.open()).thenReturn(dbMock);
+    }
+
+    if (changeExists) {
+      when(changeCheckerFactoryMock.create(TEST_CHANGE_ID)).thenReturn(changeCheckerPresentMock);
+      when(changeCheckerPresentMock.getChangeNotes()).thenReturn(Optional.of(changeNotes));
+      if (ioException) {
+        doThrow(new IOException("io-error"))
+            .when(indexerMock)
+            .index(any(ReviewDb.class), any(Change.class));
       }
     }
+
+    when(changeCheckerPresentMock.isChangeUpToDate(any())).thenReturn(changeIsUpToDate);
   }
 }
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexGroupHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexGroupHandlerTest.java
index d2387d9..ab55b73 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexGroupHandlerTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexGroupHandlerTest.java
@@ -18,11 +18,14 @@
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
+import com.ericsson.gerrit.plugins.highavailability.Configuration;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexingHandler.Operation;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.index.group.GroupIndexer;
 import java.io.IOException;
+import java.util.Optional;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -37,18 +40,22 @@
 
   @Rule public ExpectedException exception = ExpectedException.none();
   @Mock private GroupIndexer indexerMock;
+  @Mock private Configuration configMock;
+  @Mock private Configuration.Index indexMock;
   private ForwardedIndexGroupHandler handler;
   private AccountGroup.UUID uuid;
 
   @Before
   public void setUp() throws Exception {
-    handler = new ForwardedIndexGroupHandler(indexerMock);
+    when(configMock.index()).thenReturn(indexMock);
+    when(indexMock.numStripedLocks()).thenReturn(10);
+    handler = new ForwardedIndexGroupHandler(indexerMock, configMock);
     uuid = new AccountGroup.UUID("123");
   }
 
   @Test
   public void testSuccessfulIndexing() throws Exception {
-    handler.index(uuid, Operation.INDEX);
+    handler.index(uuid, Operation.INDEX, Optional.empty());
     verify(indexerMock).index(uuid);
   }
 
@@ -56,7 +63,7 @@
   public void deleteIsNotSupported() throws Exception {
     exception.expect(UnsupportedOperationException.class);
     exception.expectMessage("Delete from group index not supported");
-    handler.index(uuid, Operation.DELETE);
+    handler.index(uuid, Operation.DELETE, Optional.empty());
   }
 
   @Test
@@ -73,7 +80,7 @@
         .index(uuid);
 
     assertThat(Context.isForwardedEvent()).isFalse();
-    handler.index(uuid, Operation.INDEX);
+    handler.index(uuid, Operation.INDEX, Optional.empty());
     assertThat(Context.isForwardedEvent()).isFalse();
 
     verify(indexerMock).index(uuid);
@@ -92,7 +99,7 @@
 
     assertThat(Context.isForwardedEvent()).isFalse();
     try {
-      handler.index(uuid, Operation.INDEX);
+      handler.index(uuid, Operation.INDEX, Optional.empty());
       fail("should have thrown an IOException");
     } catch (IOException e) {
       assertThat(e.getMessage()).isEqualTo("someMessage");
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexProjectHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexProjectHandlerTest.java
new file mode 100644
index 0000000..ea64d71
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexProjectHandlerTest.java
@@ -0,0 +1,111 @@
+// Copyright (C) 2018 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.ericsson.gerrit.plugins.highavailability.forwarder;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.ericsson.gerrit.plugins.highavailability.Configuration;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexingHandler.Operation;
+import com.google.gerrit.index.project.ProjectIndexer;
+import com.google.gerrit.reviewdb.client.Project;
+import java.io.IOException;
+import java.util.Optional;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ForwardedIndexProjectHandlerTest {
+
+  @Rule public ExpectedException exception = ExpectedException.none();
+  @Mock private ProjectIndexer indexerMock;
+  @Mock private Configuration configMock;
+  @Mock private Configuration.Index indexMock;
+  private ForwardedIndexProjectHandler handler;
+  private Project.NameKey nameKey;
+
+  @Before
+  public void setUp() {
+    when(configMock.index()).thenReturn(indexMock);
+    when(indexMock.numStripedLocks()).thenReturn(10);
+    handler = new ForwardedIndexProjectHandler(indexerMock, configMock);
+    nameKey = new Project.NameKey("project/name");
+  }
+
+  @Test
+  public void testSuccessfulIndexing() throws Exception {
+    handler.index(nameKey, Operation.INDEX, Optional.empty());
+    verify(indexerMock).index(nameKey);
+  }
+
+  @Test
+  public void deleteIsNotSupported() throws Exception {
+    exception.expect(UnsupportedOperationException.class);
+    exception.expectMessage("Delete from project index not supported");
+    handler.index(nameKey, Operation.DELETE, Optional.empty());
+  }
+
+  @Test
+  public void shouldSetAndUnsetForwardedContext() throws Exception {
+    // this doAnswer is to allow to assert that context is set to forwarded
+    // while cache eviction is called.
+    doAnswer(
+            (Answer<Void>)
+                invocation -> {
+                  assertThat(Context.isForwardedEvent()).isTrue();
+                  return null;
+                })
+        .when(indexerMock)
+        .index(nameKey);
+
+    assertThat(Context.isForwardedEvent()).isFalse();
+    handler.index(nameKey, Operation.INDEX, Optional.empty());
+    assertThat(Context.isForwardedEvent()).isFalse();
+
+    verify(indexerMock).index(nameKey);
+  }
+
+  @Test
+  public void shouldSetAndUnsetForwardedContextEvenIfExceptionIsThrown() throws Exception {
+    doAnswer(
+            (Answer<Void>)
+                invocation -> {
+                  assertThat(Context.isForwardedEvent()).isTrue();
+                  throw new IOException("someMessage");
+                })
+        .when(indexerMock)
+        .index(nameKey);
+
+    assertThat(Context.isForwardedEvent()).isFalse();
+    try {
+      handler.index(nameKey, Operation.INDEX, Optional.empty());
+      fail("should have thrown an IOException");
+    } catch (IOException e) {
+      assertThat(e.getMessage()).isEqualTo("someMessage");
+    }
+    assertThat(Context.isForwardedEvent()).isFalse();
+
+    verify(indexerMock).index(nameKey);
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpSessionTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpSessionTest.java
index b6eba3d..5e0d4c9 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpSessionTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpSessionTest.java
@@ -15,35 +15,27 @@
 package com.ericsson.gerrit.plugins.highavailability.forwarder.rest;
 
 import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
-import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor;
-import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
 import static com.github.tomakehurst.wiremock.client.WireMock.delete;
 import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
-import static com.github.tomakehurst.wiremock.client.WireMock.exactly;
 import static com.github.tomakehurst.wiremock.client.WireMock.post;
 import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
-import static com.github.tomakehurst.wiremock.client.WireMock.verify;
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.fail;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import com.ericsson.gerrit.plugins.highavailability.Configuration;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.HttpResponseHandler.HttpResult;
-import com.ericsson.gerrit.plugins.highavailability.peers.PeerInfo;
 import com.github.tomakehurst.wiremock.http.Fault;
 import com.github.tomakehurst.wiremock.junit.WireMockRule;
 import com.github.tomakehurst.wiremock.stubbing.Scenario;
-import com.google.inject.util.Providers;
-import java.io.IOException;
 import java.net.SocketTimeoutException;
-import java.util.Optional;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.Answers;
 
 public class HttpSessionTest {
+
   private static final int MAX_TRIES = 3;
   private static final int RETRY_INTERVAL = 250;
   private static final int TIMEOUT = 500;
@@ -64,12 +56,13 @@
 
   @Rule public WireMockRule wireMockRule = new WireMockRule(0);
 
-  private Configuration configMock;
+  private String uri;
 
   @Before
   public void setUp() throws Exception {
     String url = "http://localhost:" + wireMockRule.port();
-    configMock = mock(Configuration.class, Answers.RETURNS_DEEP_STUBS);
+    uri = url + ENDPOINT;
+    Configuration configMock = mock(Configuration.class, Answers.RETURNS_DEEP_STUBS);
     when(configMock.http().user()).thenReturn("user");
     when(configMock.http().password()).thenReturn("pass");
     when(configMock.http().maxTries()).thenReturn(MAX_TRIES);
@@ -77,11 +70,7 @@
     when(configMock.http().socketTimeout()).thenReturn(TIMEOUT);
     when(configMock.http().retryInterval()).thenReturn(RETRY_INTERVAL);
 
-    PeerInfo peerInfo = mock(PeerInfo.class);
-    when(peerInfo.getDirectUrl()).thenReturn(url);
-    httpSession =
-        new HttpSession(
-            new HttpClientProvider(configMock).get(), Providers.of(Optional.of(peerInfo)));
+    httpSession = new HttpSession(new HttpClientProvider(configMock).get());
   }
 
   @Test
@@ -89,7 +78,7 @@
     wireMockRule.givenThat(
         post(urlEqualTo(ENDPOINT)).willReturn(aResponse().withStatus(NO_CONTENT)));
 
-    assertThat(httpSession.post(ENDPOINT).isSuccessful()).isTrue();
+    assertThat(httpSession.post(uri).isSuccessful()).isTrue();
   }
 
   @Test
@@ -98,7 +87,7 @@
         post(urlEqualTo(ENDPOINT))
             .withRequestBody(equalTo(BODY))
             .willReturn(aResponse().withStatus(NO_CONTENT)));
-    assertThat(httpSession.post(ENDPOINT, BODY).isSuccessful()).isTrue();
+    assertThat(httpSession.post(uri, BODY).isSuccessful()).isTrue();
   }
 
   @Test
@@ -106,7 +95,7 @@
     wireMockRule.givenThat(
         delete(urlEqualTo(ENDPOINT)).willReturn(aResponse().withStatus(NO_CONTENT)));
 
-    assertThat(httpSession.delete(ENDPOINT).isSuccessful()).isTrue();
+    assertThat(httpSession.delete(uri).isSuccessful()).isTrue();
   }
 
   @Test
@@ -116,7 +105,7 @@
         post(urlEqualTo(ENDPOINT))
             .willReturn(aResponse().withStatus(UNAUTHORIZED).withBody(expected)));
 
-    HttpResult result = httpSession.post(ENDPOINT);
+    HttpResult result = httpSession.post(uri);
     assertThat(result.isSuccessful()).isFalse();
     assertThat(result.getMessage()).isEqualTo(expected);
   }
@@ -128,7 +117,7 @@
         post(urlEqualTo(ENDPOINT))
             .willReturn(aResponse().withStatus(NOT_FOUND).withBody(expected)));
 
-    HttpResult result = httpSession.post(ENDPOINT);
+    HttpResult result = httpSession.post(uri);
     assertThat(result.isSuccessful()).isFalse();
     assertThat(result.getMessage()).isEqualTo(expected);
   }
@@ -139,7 +128,7 @@
         post(urlEqualTo(ENDPOINT))
             .willReturn(aResponse().withStatus(ERROR).withBody(ERROR_MESSAGE)));
 
-    HttpResult result = httpSession.post(ENDPOINT);
+    HttpResult result = httpSession.post(uri);
     assertThat(result.isSuccessful()).isFalse();
     assertThat(result.getMessage()).isEqualTo(ERROR_MESSAGE);
   }
@@ -170,7 +159,7 @@
             .whenScenarioStateIs(THIRD_TRY)
             .willReturn(aResponse().withFixedDelay(TIMEOUT)));
 
-    httpSession.post(ENDPOINT);
+    httpSession.post(uri);
   }
 
   @Test
@@ -179,19 +168,6 @@
         post(urlEqualTo(ENDPOINT))
             .willReturn(aResponse().withFault(Fault.MALFORMED_RESPONSE_CHUNK)));
 
-    assertThat(httpSession.post(ENDPOINT).isSuccessful()).isFalse();
-  }
-
-  @Test
-  public void testNoRequestWhenPeerInfoUnknown() throws IOException {
-    httpSession =
-        new HttpSession(new HttpClientProvider(configMock).get(), Providers.of(Optional.empty()));
-    try {
-      httpSession.post(ENDPOINT);
-      fail("Expected PeerInfoNotAvailableException");
-    } catch (PeerInfoNotAvailableException e) {
-      // good
-    }
-    verify(exactly(0), anyRequestedFor(anyUrl()));
+    assertThat(httpSession.post(uri).isSuccessful()).isFalse();
   }
 }
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexAccountRestApiServletTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexAccountRestApiServletTest.java
index c41a4a9..4622a17 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexAccountRestApiServletTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexAccountRestApiServletTest.java
@@ -17,6 +17,8 @@
 import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
 import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED;
 import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -57,7 +59,7 @@
   @Test
   public void accountIsIndexed() throws Exception {
     servlet.doPost(requestMock, responseMock);
-    verify(handlerMock, times(1)).index(id, Operation.INDEX);
+    verify(handlerMock, times(1)).index(eq(id), eq(Operation.INDEX), any());
     verify(responseMock).setStatus(SC_NO_CONTENT);
   }
 
@@ -69,14 +71,14 @@
 
   @Test
   public void indexerThrowsIOExceptionTryingToIndexAccount() throws Exception {
-    doThrow(new IOException(IO_ERROR)).when(handlerMock).index(id, Operation.INDEX);
+    doThrow(new IOException(IO_ERROR)).when(handlerMock).index(eq(id), eq(Operation.INDEX), any());
     servlet.doPost(requestMock, responseMock);
     verify(responseMock).sendError(SC_CONFLICT, IO_ERROR);
   }
 
   @Test
   public void sendErrorThrowsIOException() throws Exception {
-    doThrow(new IOException(IO_ERROR)).when(handlerMock).index(id, Operation.INDEX);
+    doThrow(new IOException(IO_ERROR)).when(handlerMock).index(eq(id), eq(Operation.INDEX), any());
     doThrow(new IOException("someError")).when(responseMock).sendError(SC_CONFLICT, IO_ERROR);
     servlet.doPost(requestMock, responseMock);
     verify(responseMock).sendError(SC_CONFLICT, IO_ERROR);
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexChangeRestApiServletTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexChangeRestApiServletTest.java
index 7fccc21..c1fa765 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexChangeRestApiServletTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexChangeRestApiServletTest.java
@@ -17,6 +17,8 @@
 import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
 import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -58,34 +60,40 @@
   @Test
   public void changeIsIndexed() throws Exception {
     servlet.doPost(requestMock, responseMock);
-    verify(handlerMock, times(1)).index(CHANGE_ID, Operation.INDEX);
+    verify(handlerMock, times(1)).index(eq(CHANGE_ID), eq(Operation.INDEX), any());
     verify(responseMock).setStatus(SC_NO_CONTENT);
   }
 
   @Test
   public void changeIsDeletedFromIndex() throws Exception {
     servlet.doDelete(requestMock, responseMock);
-    verify(handlerMock, times(1)).index(CHANGE_ID, Operation.DELETE);
+    verify(handlerMock, times(1)).index(eq(CHANGE_ID), eq(Operation.DELETE), any());
     verify(responseMock).setStatus(SC_NO_CONTENT);
   }
 
   @Test
   public void indexerThrowsIOExceptionTryingToIndexChange() throws Exception {
-    doThrow(new IOException(IO_ERROR)).when(handlerMock).index(CHANGE_ID, Operation.INDEX);
+    doThrow(new IOException(IO_ERROR))
+        .when(handlerMock)
+        .index(eq(CHANGE_ID), eq(Operation.INDEX), any());
     servlet.doPost(requestMock, responseMock);
     verify(responseMock).sendError(SC_CONFLICT, IO_ERROR);
   }
 
   @Test
   public void indexerThrowsOrmExceptionTryingToIndexChange() throws Exception {
-    doThrow(new OrmException("some message")).when(handlerMock).index(CHANGE_ID, Operation.INDEX);
+    doThrow(new OrmException("some message"))
+        .when(handlerMock)
+        .index(eq(CHANGE_ID), eq(Operation.INDEX), any());
     servlet.doPost(requestMock, responseMock);
     verify(responseMock).sendError(SC_NOT_FOUND, "Error trying to find change");
   }
 
   @Test
   public void sendErrorThrowsIOException() throws Exception {
-    doThrow(new IOException(IO_ERROR)).when(handlerMock).index(CHANGE_ID, Operation.INDEX);
+    doThrow(new IOException(IO_ERROR))
+        .when(handlerMock)
+        .index(eq(CHANGE_ID), eq(Operation.INDEX), any());
     doThrow(new IOException("someError")).when(responseMock).sendError(SC_CONFLICT, IO_ERROR);
     servlet.doPost(requestMock, responseMock);
     verify(responseMock).sendError(SC_CONFLICT, IO_ERROR);
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexGroupRestApiServletTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexGroupRestApiServletTest.java
index f03727e..6291142 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexGroupRestApiServletTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexGroupRestApiServletTest.java
@@ -17,6 +17,8 @@
 import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
 import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED;
 import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -56,7 +58,7 @@
   @Test
   public void groupIsIndexed() throws Exception {
     servlet.doPost(requestMock, responseMock);
-    verify(handlerMock, times(1)).index(uuid, Operation.INDEX);
+    verify(handlerMock, times(1)).index(eq(uuid), eq(Operation.INDEX), any());
     verify(responseMock).setStatus(SC_NO_CONTENT);
   }
 
@@ -68,14 +70,18 @@
 
   @Test
   public void indexerThrowsIOExceptionTryingToIndexGroup() throws Exception {
-    doThrow(new IOException(IO_ERROR)).when(handlerMock).index(uuid, Operation.INDEX);
+    doThrow(new IOException(IO_ERROR))
+        .when(handlerMock)
+        .index(eq(uuid), eq(Operation.INDEX), any());
     servlet.doPost(requestMock, responseMock);
     verify(responseMock).sendError(SC_CONFLICT, IO_ERROR);
   }
 
   @Test
   public void sendErrorThrowsIOException() throws Exception {
-    doThrow(new IOException(IO_ERROR)).when(handlerMock).index(uuid, Operation.INDEX);
+    doThrow(new IOException(IO_ERROR))
+        .when(handlerMock)
+        .index(eq(uuid), eq(Operation.INDEX), any());
     doThrow(new IOException("someError")).when(responseMock).sendError(SC_CONFLICT, IO_ERROR);
     servlet.doPost(requestMock, responseMock);
     verify(responseMock).sendError(SC_CONFLICT, IO_ERROR);
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexProjectRestApiServletTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexProjectRestApiServletTest.java
new file mode 100644
index 0000000..83420b9
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexProjectRestApiServletTest.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2018 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.ericsson.gerrit.plugins.highavailability.forwarder.rest;
+
+import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
+import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED;
+import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexProjectHandler;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexingHandler.Operation;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.Project;
+import java.io.IOException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+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 IndexProjectRestApiServletTest {
+  private static final String IO_ERROR = "io-error";
+  private static final String PROJECT_NAME = "test/project";
+
+  @Mock private ForwardedIndexProjectHandler handlerMock;
+  @Mock private HttpServletRequest requestMock;
+  @Mock private HttpServletResponse responseMock;
+
+  private Project.NameKey nameKey;
+  private IndexProjectRestApiServlet servlet;
+
+  @Before
+  public void setUpMocks() {
+    servlet = new IndexProjectRestApiServlet(handlerMock);
+    nameKey = new Project.NameKey(PROJECT_NAME);
+    when(requestMock.getRequestURI())
+        .thenReturn("http://gerrit.com/index/project/" + Url.encode(nameKey.get()));
+  }
+
+  @Test
+  public void projectIsIndexed() throws Exception {
+    servlet.doPost(requestMock, responseMock);
+    verify(handlerMock, times(1)).index(eq(nameKey), eq(Operation.INDEX), any());
+    verify(responseMock).setStatus(SC_NO_CONTENT);
+  }
+
+  @Test
+  public void cannotDeleteProject() throws Exception {
+    servlet.doDelete(requestMock, responseMock);
+    verify(responseMock).sendError(SC_METHOD_NOT_ALLOWED, "cannot delete project from index");
+  }
+
+  @Test
+  public void indexerThrowsIOExceptionTryingToIndexProject() throws Exception {
+    doThrow(new IOException(IO_ERROR))
+        .when(handlerMock)
+        .index(eq(nameKey), eq(Operation.INDEX), any());
+    servlet.doPost(requestMock, responseMock);
+    verify(responseMock).sendError(SC_CONFLICT, IO_ERROR);
+  }
+
+  @Test
+  public void sendErrorThrowsIOException() throws Exception {
+    doThrow(new IOException(IO_ERROR))
+        .when(handlerMock)
+        .index(eq(nameKey), eq(Operation.INDEX), any());
+    doThrow(new IOException("someError")).when(responseMock).sendError(SC_CONFLICT, IO_ERROR);
+    servlet.doPost(requestMock, responseMock);
+    verify(responseMock).sendError(SC_CONFLICT, IO_ERROR);
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderTest.java
index f49f2a3..c5b52a0 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderTest.java
@@ -15,30 +15,38 @@
 package com.ericsson.gerrit.plugins.highavailability.forwarder.rest;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import com.ericsson.gerrit.plugins.highavailability.Configuration;
 import com.ericsson.gerrit.plugins.highavailability.cache.Constants;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.IndexEvent;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.HttpResponseHandler.HttpResult;
+import com.ericsson.gerrit.plugins.highavailability.peers.PeerInfo;
 import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.events.Event;
 import com.google.gson.GsonBuilder;
+import com.google.inject.Provider;
 import java.io.IOException;
+import java.util.Set;
 import javax.net.ssl.SSLException;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Answers;
 
 public class RestForwarderTest {
+  private static final String URL = "http://fake.com";
   private static final String PLUGIN_NAME = "high-availability";
   private static final String EMPTY_MSG = "";
   private static final String ERROR = "Error";
-  private static final String PLUGINS = "/plugins";
+  private static final String PLUGINS = "plugins";
   private static final String PROJECT_TO_ADD = "projectToAdd";
   private static final String PROJECT_TO_DELETE = "projectToDelete";
   private static final String SUCCESS = "Success";
@@ -51,128 +59,139 @@
   private static final String PROJECT_NAME_URL_END = "test%2Fproject";
   private static final String INDEX_CHANGE_ENDPOINT =
       Joiner.on("/")
-          .join(PLUGINS, PLUGIN_NAME, "index/change", PROJECT_NAME_URL_END + "~" + CHANGE_NUMBER);
+          .join(
+              URL,
+              PLUGINS,
+              PLUGIN_NAME,
+              "index/change",
+              PROJECT_NAME_URL_END + "~" + CHANGE_NUMBER);
   private static final String DELETE_CHANGE_ENDPOINT =
-      Joiner.on("/").join("/plugins", PLUGIN_NAME, "index/change", "~" + CHANGE_NUMBER);
+      Joiner.on("/").join(URL, PLUGINS, PLUGIN_NAME, "index/change", "~" + CHANGE_NUMBER);
   private static final int ACCOUNT_NUMBER = 2;
   private static final String INDEX_ACCOUNT_ENDPOINT =
-      Joiner.on("/").join(PLUGINS, PLUGIN_NAME, "index/account", ACCOUNT_NUMBER);
+      Joiner.on("/").join(URL, PLUGINS, PLUGIN_NAME, "index/account", ACCOUNT_NUMBER);
   private static final String UUID = "we235jdf92nfj2351";
   private static final String INDEX_GROUP_ENDPOINT =
-      Joiner.on("/").join(PLUGINS, PLUGIN_NAME, "index/group", UUID);
+      Joiner.on("/").join(URL, PLUGINS, PLUGIN_NAME, "index/group", UUID);
 
   // Event
-  private static final String EVENT_ENDPOINT = Joiner.on("/").join(PLUGINS, PLUGIN_NAME, "event");
   private static Event event = new Event("test-event") {};
-  private static String eventJson = new GsonBuilder().create().toJson(event);
+  private static final String EVENT_ENDPOINT =
+      Joiner.on("/").join(URL, PLUGINS, PLUGIN_NAME, "event", event.type);
 
   private RestForwarder forwarder;
   private HttpSession httpSessionMock;
 
+  @SuppressWarnings("unchecked")
   @Before
   public void setUp() {
     httpSessionMock = mock(HttpSession.class);
     Configuration configMock = mock(Configuration.class, Answers.RETURNS_DEEP_STUBS);
     when(configMock.http().maxTries()).thenReturn(3);
     when(configMock.http().retryInterval()).thenReturn(10);
-    forwarder = new RestForwarder(httpSessionMock, PLUGIN_NAME, configMock);
+    Provider<Set<PeerInfo>> peersMock = mock(Provider.class);
+    when(peersMock.get()).thenReturn(ImmutableSet.of(new PeerInfo(URL)));
+    forwarder =
+        new RestForwarder(
+            httpSessionMock, PLUGIN_NAME, configMock, peersMock); // TODO: Create provider
   }
 
   @Test
   public void testIndexAccountOK() throws Exception {
-    when(httpSessionMock.post(INDEX_ACCOUNT_ENDPOINT))
+    when(httpSessionMock.post(eq(INDEX_ACCOUNT_ENDPOINT), any()))
         .thenReturn(new HttpResult(SUCCESSFUL, EMPTY_MSG));
-    assertThat(forwarder.indexAccount(ACCOUNT_NUMBER)).isTrue();
+    assertThat(forwarder.indexAccount(ACCOUNT_NUMBER, new IndexEvent())).isTrue();
   }
 
   @Test
   public void testIndexAccountFailed() throws Exception {
-    when(httpSessionMock.post(INDEX_ACCOUNT_ENDPOINT))
+    when(httpSessionMock.post(eq(INDEX_ACCOUNT_ENDPOINT), any()))
         .thenReturn(new HttpResult(FAILED, EMPTY_MSG));
-    assertThat(forwarder.indexAccount(ACCOUNT_NUMBER)).isFalse();
+    assertThat(forwarder.indexAccount(ACCOUNT_NUMBER, new IndexEvent())).isFalse();
   }
 
   @Test
   public void testIndexAccountThrowsException() throws Exception {
-    doThrow(new IOException()).when(httpSessionMock).post(INDEX_ACCOUNT_ENDPOINT);
-    assertThat(forwarder.indexAccount(ACCOUNT_NUMBER)).isFalse();
+    doThrow(new IOException()).when(httpSessionMock).post(eq(INDEX_ACCOUNT_ENDPOINT), any());
+    assertThat(forwarder.indexAccount(ACCOUNT_NUMBER, new IndexEvent())).isFalse();
   }
 
   @Test
   public void testIndexGroupOK() throws Exception {
-    when(httpSessionMock.post(INDEX_GROUP_ENDPOINT))
+    when(httpSessionMock.post(eq(INDEX_GROUP_ENDPOINT), any()))
         .thenReturn(new HttpResult(SUCCESSFUL, EMPTY_MSG));
-    assertThat(forwarder.indexGroup(UUID)).isTrue();
+    assertThat(forwarder.indexGroup(UUID, new IndexEvent())).isTrue();
   }
 
   @Test
   public void testIndexGroupFailed() throws Exception {
-    when(httpSessionMock.post(INDEX_GROUP_ENDPOINT)).thenReturn(new HttpResult(FAILED, EMPTY_MSG));
-    assertThat(forwarder.indexGroup(UUID)).isFalse();
+    when(httpSessionMock.post(eq(INDEX_GROUP_ENDPOINT), any()))
+        .thenReturn(new HttpResult(FAILED, EMPTY_MSG));
+    assertThat(forwarder.indexGroup(UUID, new IndexEvent())).isFalse();
   }
 
   @Test
   public void testIndexGroupThrowsException() throws Exception {
-    doThrow(new IOException()).when(httpSessionMock).post(INDEX_GROUP_ENDPOINT);
-    assertThat(forwarder.indexGroup(UUID)).isFalse();
+    doThrow(new IOException()).when(httpSessionMock).post(eq(INDEX_GROUP_ENDPOINT), any());
+    assertThat(forwarder.indexGroup(UUID, new IndexEvent())).isFalse();
   }
 
   @Test
   public void testIndexChangeOK() throws Exception {
-    when(httpSessionMock.post(INDEX_CHANGE_ENDPOINT))
+    when(httpSessionMock.post(eq(INDEX_CHANGE_ENDPOINT), any()))
         .thenReturn(new HttpResult(SUCCESSFUL, EMPTY_MSG));
-    assertThat(forwarder.indexChange(PROJECT_NAME, CHANGE_NUMBER)).isTrue();
+    assertThat(forwarder.indexChange(PROJECT_NAME, CHANGE_NUMBER, new IndexEvent())).isTrue();
   }
 
   @Test
   public void testIndexChangeFailed() throws Exception {
-    when(httpSessionMock.post(INDEX_CHANGE_ENDPOINT)).thenReturn(new HttpResult(FAILED, EMPTY_MSG));
-    assertThat(forwarder.indexChange(PROJECT_NAME, CHANGE_NUMBER)).isFalse();
+    when(httpSessionMock.post(eq(INDEX_CHANGE_ENDPOINT), any()))
+        .thenReturn(new HttpResult(FAILED, EMPTY_MSG));
+    assertThat(forwarder.indexChange(PROJECT_NAME, CHANGE_NUMBER, new IndexEvent())).isFalse();
   }
 
   @Test
   public void testIndexChangeThrowsException() throws Exception {
-    doThrow(new IOException()).when(httpSessionMock).post(INDEX_CHANGE_ENDPOINT);
-    assertThat(forwarder.indexChange(PROJECT_NAME, CHANGE_NUMBER)).isFalse();
+    doThrow(new IOException()).when(httpSessionMock).post(eq(INDEX_CHANGE_ENDPOINT), any());
+    assertThat(forwarder.indexChange(PROJECT_NAME, CHANGE_NUMBER, new IndexEvent())).isFalse();
   }
 
   @Test
   public void testChangeDeletedFromIndexOK() throws Exception {
-    when(httpSessionMock.delete(DELETE_CHANGE_ENDPOINT))
+    when(httpSessionMock.delete(eq(DELETE_CHANGE_ENDPOINT)))
         .thenReturn(new HttpResult(SUCCESSFUL, EMPTY_MSG));
-    assertThat(forwarder.deleteChangeFromIndex(CHANGE_NUMBER)).isTrue();
+    assertThat(forwarder.deleteChangeFromIndex(CHANGE_NUMBER, new IndexEvent())).isTrue();
   }
 
   @Test
   public void testChangeDeletedFromIndexFailed() throws Exception {
-    when(httpSessionMock.delete(DELETE_CHANGE_ENDPOINT))
+    when(httpSessionMock.delete(eq(DELETE_CHANGE_ENDPOINT)))
         .thenReturn(new HttpResult(FAILED, EMPTY_MSG));
-    assertThat(forwarder.deleteChangeFromIndex(CHANGE_NUMBER)).isFalse();
+    assertThat(forwarder.deleteChangeFromIndex(CHANGE_NUMBER, new IndexEvent())).isFalse();
   }
 
   @Test
   public void testChangeDeletedFromThrowsException() throws Exception {
-    doThrow(new IOException()).when(httpSessionMock).delete(DELETE_CHANGE_ENDPOINT);
-    assertThat(forwarder.deleteChangeFromIndex(CHANGE_NUMBER)).isFalse();
+    doThrow(new IOException()).when(httpSessionMock).delete(eq(DELETE_CHANGE_ENDPOINT));
+    assertThat(forwarder.deleteChangeFromIndex(CHANGE_NUMBER, new IndexEvent())).isFalse();
   }
 
   @Test
   public void testEventSentOK() throws Exception {
-    when(httpSessionMock.post(EVENT_ENDPOINT, eventJson))
+    when(httpSessionMock.post(EVENT_ENDPOINT, event))
         .thenReturn(new HttpResult(SUCCESSFUL, EMPTY_MSG));
     assertThat(forwarder.send(event)).isTrue();
   }
 
   @Test
   public void testEventSentFailed() throws Exception {
-    when(httpSessionMock.post(EVENT_ENDPOINT, eventJson))
-        .thenReturn(new HttpResult(FAILED, EMPTY_MSG));
+    when(httpSessionMock.post(EVENT_ENDPOINT, event)).thenReturn(new HttpResult(FAILED, EMPTY_MSG));
     assertThat(forwarder.send(event)).isFalse();
   }
 
   @Test
   public void testEventSentThrowsException() throws Exception {
-    doThrow(new IOException()).when(httpSessionMock).post(EVENT_ENDPOINT, eventJson);
+    doThrow(new IOException()).when(httpSessionMock).post(EVENT_ENDPOINT, event);
     assertThat(forwarder.send(event)).isFalse();
   }
 
@@ -198,8 +217,8 @@
   public void testEvictGroupsOK() throws Exception {
     AccountGroup.Id key = new AccountGroup.Id(123);
     String keyJson = new GsonBuilder().create().toJson(key);
-    when(httpSessionMock.post(buildCacheEndpoint(Constants.GROUPS), keyJson))
-        .thenReturn(new HttpResult(SUCCESSFUL, EMPTY_MSG));
+    String endpoint = buildCacheEndpoint(Constants.GROUPS);
+    when(httpSessionMock.post(endpoint, keyJson)).thenReturn(new HttpResult(SUCCESSFUL, EMPTY_MSG));
     assertThat(forwarder.evict(Constants.GROUPS, key)).isTrue();
   }
 
@@ -241,13 +260,13 @@
   }
 
   private static String buildCacheEndpoint(String name) {
-    return Joiner.on("/").join(PLUGINS, PLUGIN_NAME, "cache", name);
+    return Joiner.on("/").join(URL, PLUGINS, PLUGIN_NAME, "cache", name);
   }
 
   @Test
   public void testAddToProjectListOK() throws Exception {
     String projectName = PROJECT_TO_ADD;
-    when(httpSessionMock.post(buildProjectListCacheEndpoint(projectName)))
+    when(httpSessionMock.post(buildProjectListCacheEndpoint(projectName), null))
         .thenReturn(new HttpResult(SUCCESSFUL, EMPTY_MSG));
     assertThat(forwarder.addToProjectList(projectName)).isTrue();
   }
@@ -255,7 +274,7 @@
   @Test
   public void testAddToProjectListFailed() throws Exception {
     String projectName = PROJECT_TO_ADD;
-    when(httpSessionMock.post(buildProjectListCacheEndpoint(projectName)))
+    when(httpSessionMock.post(buildProjectListCacheEndpoint(projectName), null))
         .thenReturn(new HttpResult(FAILED, EMPTY_MSG));
     assertThat(forwarder.addToProjectList(projectName)).isFalse();
   }
@@ -265,7 +284,7 @@
     String projectName = PROJECT_TO_ADD;
     doThrow(new IOException())
         .when(httpSessionMock)
-        .post(buildProjectListCacheEndpoint(projectName));
+        .post(buildProjectListCacheEndpoint(projectName), null);
     assertThat(forwarder.addToProjectList(projectName)).isFalse();
   }
 
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/IndexEventHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/IndexEventHandlerTest.java
index 39d7128..3ec979c 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/IndexEventHandlerTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/IndexEventHandlerTest.java
@@ -15,13 +15,17 @@
 package com.ericsson.gerrit.plugins.highavailability.index;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
 
 import com.ericsson.gerrit.plugins.highavailability.forwarder.Context;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.Forwarder;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.IndexEvent;
 import com.ericsson.gerrit.plugins.highavailability.index.IndexEventHandler.IndexAccountTask;
 import com.ericsson.gerrit.plugins.highavailability.index.IndexEventHandler.IndexChangeTask;
 import com.ericsson.gerrit.plugins.highavailability.index.IndexEventHandler.IndexGroupTask;
@@ -29,6 +33,7 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
+import java.util.Optional;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import org.junit.Before;
 import org.junit.Test;
@@ -47,41 +52,46 @@
 
   private IndexEventHandler indexEventHandler;
   @Mock private Forwarder forwarder;
+  @Mock private ChangeCheckerImpl.Factory changeCheckerFactoryMock;
+  @Mock private ChangeChecker changeCheckerMock;
   private Change.Id changeId;
   private Account.Id accountId;
   private AccountGroup.UUID accountGroupUUID;
 
   @Before
-  public void setUpMocks() {
+  public void setUpMocks() throws Exception {
     changeId = new Change.Id(CHANGE_ID);
     accountId = new Account.Id(ACCOUNT_ID);
     accountGroupUUID = new AccountGroup.UUID(UUID);
+    when(changeCheckerFactoryMock.create(any())).thenReturn(changeCheckerMock);
+    when(changeCheckerMock.newIndexEvent()).thenReturn(Optional.of(new IndexEvent()));
     indexEventHandler =
-        new IndexEventHandler(MoreExecutors.directExecutor(), PLUGIN_NAME, forwarder);
+        new IndexEventHandler(
+            MoreExecutors.directExecutor(), PLUGIN_NAME, forwarder, changeCheckerFactoryMock);
   }
 
   @Test
   public void shouldIndexInRemoteOnChangeIndexedEvent() throws Exception {
     indexEventHandler.onChangeIndexed(PROJECT_NAME, changeId.get());
-    verify(forwarder).indexChange(PROJECT_NAME, CHANGE_ID);
+    verify(forwarder).indexChange(eq(PROJECT_NAME), eq(CHANGE_ID), any());
   }
 
   @Test
   public void shouldIndexInRemoteOnAccountIndexedEvent() throws Exception {
     indexEventHandler.onAccountIndexed(accountId.get());
-    verify(forwarder).indexAccount(ACCOUNT_ID);
+    verify(forwarder).indexAccount(eq(ACCOUNT_ID), any());
   }
 
   @Test
   public void shouldDeleteFromIndexInRemoteOnChangeDeletedEvent() throws Exception {
     indexEventHandler.onChangeDeleted(changeId.get());
-    verify(forwarder).deleteChangeFromIndex(CHANGE_ID);
+    verify(forwarder).deleteChangeFromIndex(eq(CHANGE_ID), any());
   }
 
   @Test
   public void shouldIndexInRemoteOnGroupIndexedEvent() throws Exception {
     indexEventHandler.onGroupIndexed(accountGroupUUID.get());
-    verify(forwarder).indexGroup(UUID);
+    verify(forwarder).indexGroup(eq(UUID), any());
   }
 
   @Test
@@ -114,17 +124,19 @@
   @Test
   public void duplicateChangeEventOfAQueuedEventShouldGetDiscarded() {
     ScheduledThreadPoolExecutor poolMock = mock(ScheduledThreadPoolExecutor.class);
-    indexEventHandler = new IndexEventHandler(poolMock, PLUGIN_NAME, forwarder);
+    indexEventHandler =
+        new IndexEventHandler(poolMock, PLUGIN_NAME, forwarder, changeCheckerFactoryMock);
     indexEventHandler.onChangeIndexed(PROJECT_NAME, changeId.get());
     indexEventHandler.onChangeIndexed(PROJECT_NAME, changeId.get());
     verify(poolMock, times(1))
-        .execute(indexEventHandler.new IndexChangeTask(PROJECT_NAME, CHANGE_ID, false));
+        .execute(indexEventHandler.new IndexChangeTask(PROJECT_NAME, CHANGE_ID, false, null));
   }
 
   @Test
   public void duplicateAccountEventOfAQueuedEventShouldGetDiscarded() {
     ScheduledThreadPoolExecutor poolMock = mock(ScheduledThreadPoolExecutor.class);
-    indexEventHandler = new IndexEventHandler(poolMock, PLUGIN_NAME, forwarder);
+    indexEventHandler =
+        new IndexEventHandler(poolMock, PLUGIN_NAME, forwarder, changeCheckerFactoryMock);
     indexEventHandler.onAccountIndexed(accountId.get());
     indexEventHandler.onAccountIndexed(accountId.get());
     verify(poolMock, times(1)).execute(indexEventHandler.new IndexAccountTask(ACCOUNT_ID));
@@ -133,7 +145,8 @@
   @Test
   public void duplicateGroupEventOfAQueuedEventShouldGetDiscarded() {
     ScheduledThreadPoolExecutor poolMock = mock(ScheduledThreadPoolExecutor.class);
-    indexEventHandler = new IndexEventHandler(poolMock, PLUGIN_NAME, forwarder);
+    indexEventHandler =
+        new IndexEventHandler(poolMock, PLUGIN_NAME, forwarder, changeCheckerFactoryMock);
     indexEventHandler.onGroupIndexed(accountGroupUUID.get());
     indexEventHandler.onGroupIndexed(accountGroupUUID.get());
     verify(poolMock, times(1)).execute(indexEventHandler.new IndexGroupTask(UUID));
@@ -141,7 +154,8 @@
 
   @Test
   public void testIndexChangeTaskToString() throws Exception {
-    IndexChangeTask task = indexEventHandler.new IndexChangeTask(PROJECT_NAME, CHANGE_ID, false);
+    IndexChangeTask task =
+        indexEventHandler.new IndexChangeTask(PROJECT_NAME, CHANGE_ID, false, null);
     assertThat(task.toString())
         .isEqualTo(
             String.format("[%s] Index change %s in target instance", PLUGIN_NAME, CHANGE_ID));
@@ -164,29 +178,31 @@
 
   @Test
   public void testIndexChangeTaskHashCodeAndEquals() {
-    IndexChangeTask task = indexEventHandler.new IndexChangeTask(PROJECT_NAME, CHANGE_ID, false);
+    IndexChangeTask task =
+        indexEventHandler.new IndexChangeTask(PROJECT_NAME, CHANGE_ID, false, null);
 
     IndexChangeTask sameTask = task;
     assertThat(task.equals(sameTask)).isTrue();
     assertThat(task.hashCode()).isEqualTo(sameTask.hashCode());
 
     IndexChangeTask identicalTask =
-        indexEventHandler.new IndexChangeTask(PROJECT_NAME, CHANGE_ID, false);
+        indexEventHandler.new IndexChangeTask(PROJECT_NAME, CHANGE_ID, false, null);
     assertThat(task.equals(identicalTask)).isTrue();
     assertThat(task.hashCode()).isEqualTo(identicalTask.hashCode());
 
     assertThat(task.equals(null)).isFalse();
     assertThat(
-            task.equals(indexEventHandler.new IndexChangeTask(PROJECT_NAME, CHANGE_ID + 1, false)))
+            task.equals(
+                indexEventHandler.new IndexChangeTask(PROJECT_NAME, CHANGE_ID + 1, false, null)))
         .isFalse();
     assertThat(task.hashCode()).isNotEqualTo("test".hashCode());
 
     IndexChangeTask differentChangeIdTask =
-        indexEventHandler.new IndexChangeTask(PROJECT_NAME, 123, false);
+        indexEventHandler.new IndexChangeTask(PROJECT_NAME, 123, false, null);
     assertThat(task.equals(differentChangeIdTask)).isFalse();
     assertThat(task.hashCode()).isNotEqualTo(differentChangeIdTask.hashCode());
 
-    IndexChangeTask removeTask = indexEventHandler.new IndexChangeTask("", CHANGE_ID, true);
+    IndexChangeTask removeTask = indexEventHandler.new IndexChangeTask("", CHANGE_ID, true, null);
     assertThat(task.equals(removeTask)).isFalse();
     assertThat(task.hashCode()).isNotEqualTo(removeTask.hashCode());
   }
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/ProjectIndexForwardingIT.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/ProjectIndexForwardingIT.java
new file mode 100644
index 0000000..951e4f6
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/ProjectIndexForwardingIT.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2018 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.ericsson.gerrit.plugins.highavailability.index;
+
+import com.google.gerrit.extensions.restapi.Url;
+
+public class ProjectIndexForwardingIT extends AbstractIndexForwardingIT {
+  private String someProjectName;
+
+  @Override
+  public void beforeAction() throws Exception {
+    someProjectName = gApi.projects().create("someProject").get().name;
+  }
+
+  @Override
+  public String getExpectedRequest() {
+    return "/plugins/high-availability/index/project/" + Url.encode(someProjectName);
+  }
+
+  @Override
+  public void doAction() throws Exception {
+    gApi.projects().name(someProjectName).index(false);
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/websession/file/FileBasedWebSessionCacheTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/websession/file/FileBasedWebSessionCacheTest.java
index bccf300..d29bb99 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/websession/file/FileBasedWebSessionCacheTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/websession/file/FileBasedWebSessionCacheTest.java
@@ -127,7 +127,7 @@
   public void getIfPresentInvalidKeyTest() throws Exception {
     loadKeyToCacheDir(INVALID_KEY);
     Path path = websessionDir.resolve(INVALID_KEY);
-    assertThat(cache.getIfPresent((Object) path)).isNull();
+    assertThat(cache.getIfPresent(path.toString())).isNull();
   }
 
   @Test
diff --git a/tools/BUILD b/tools/BUILD
index e69de29..c5ed0b7 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -0,0 +1 @@
+# Empty file required by Bazel
diff --git a/tools/bzl/BUILD b/tools/bzl/BUILD
index e69de29..c5ed0b7 100644
--- a/tools/bzl/BUILD
+++ b/tools/bzl/BUILD
@@ -0,0 +1 @@
+# Empty file required by Bazel