Merge branch 'stable-2.15'

* stable-2.15:
  Update bazlets to the latest stable-2.15 revision
  Include project name in the index REST API
  Update bazlets to latest revision on stable-2.14
  Make Wiremock return 204 for any request
  Execute test setup before loading the plugin
  Rename setup method to beforeAction
  Fix GroupIndexForwardingIT
  Automate the SonarQube analysis with bazel
  Use consistent name for Logger instances
  Update bazlets to latest revision on stable-2.15
  Update bazlets to latest revision on stable-2.14
  Format Java code with google-java-format version 1.5
  Change default value of cache and index threadPoolSize
  Change default value of http.retryInterval and maxTries
  Set the http.retryInterval to 100ms for tests
  Update bazlets to latest stable-2.15 to use 2.15.1 API
  Avoid KeyUtil internals for better encapsulation
  Update bazlets to use 2.15 release API
  Remove unneeded finals
  Don't propagate cache eviction for the project_list cache
  Remove unneeded configuration options from IT tests
  Use projects cache in cache eviction IT test
  Add all core caches to CachePattenMatcherTest
  Extract indexing out of REST forwarder
  Use evicted object key toString
  Do not refer to KeyUtils internals in tests
  Remove references to GwtOrm ID parsing
  Replace usage of *.{Id,UUID}.parse() method in test
  Remove unneeded setup of the encoder

Change-Id: I63874d83b898f35da0b167bebc73598e3ec095f1
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 92bad4c..0cd4701 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java
@@ -95,9 +95,9 @@
   static final String CLEANUP_INTERVAL_KEY = "cleanupInterval";
 
   static final int DEFAULT_TIMEOUT_MS = 5000;
-  static final int DEFAULT_MAX_TRIES = 5;
-  static final int DEFAULT_RETRY_INTERVAL = 1000;
-  static final int DEFAULT_THREAD_POOL_SIZE = 1;
+  static final int DEFAULT_MAX_TRIES = 360;
+  static final int DEFAULT_RETRY_INTERVAL = 10000;
+  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;
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheModule.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheModule.java
index 570f981..39ca9ff 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheModule.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheModule.java
@@ -14,6 +14,8 @@
 
 package com.ericsson.gerrit.plugins.highavailability.cache;
 
+import com.google.gerrit.extensions.events.NewProjectCreatedListener;
+import com.google.gerrit.extensions.events.ProjectDeletedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.cache.CacheRemovalListener;
@@ -26,5 +28,7 @@
     bind(Executor.class).annotatedWith(CacheExecutor.class).toProvider(CacheExecutorProvider.class);
     listener().to(CacheExecutorProvider.class);
     DynamicSet.bind(binder(), CacheRemovalListener.class).to(CacheEvictionHandler.class);
+    DynamicSet.bind(binder(), NewProjectCreatedListener.class).to(ProjectListUpdateHandler.class);
+    DynamicSet.bind(binder(), ProjectDeletedListener.class).to(ProjectListUpdateHandler.class);
   }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CachePatternMatcher.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CachePatternMatcher.java
index 63a79fe..41a2b13 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CachePatternMatcher.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CachePatternMatcher.java
@@ -31,7 +31,7 @@
           "^groups.*",
           "ldap_groups",
           "ldap_usernames",
-          "^project.*",
+          "projects",
           "sshkeys",
           "web_sessions");
 
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/ProjectListUpdateHandler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/ProjectListUpdateHandler.java
new file mode 100644
index 0000000..5514538
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/ProjectListUpdateHandler.java
@@ -0,0 +1,85 @@
+// 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.cache;
+
+import com.ericsson.gerrit.plugins.highavailability.forwarder.Context;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.Forwarder;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.events.NewProjectCreatedListener;
+import com.google.gerrit.extensions.events.ProjectDeletedListener;
+import com.google.gerrit.extensions.events.ProjectEvent;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.concurrent.Executor;
+
+@Singleton
+public class ProjectListUpdateHandler implements NewProjectCreatedListener, ProjectDeletedListener {
+
+  private final Forwarder forwarder;
+  private final Executor executor;
+  private final String pluginName;
+
+  @Inject
+  public ProjectListUpdateHandler(
+      Forwarder forwarder, @CacheExecutor Executor executor, @PluginName String pluginName) {
+    this.forwarder = forwarder;
+    this.executor = executor;
+    this.pluginName = pluginName;
+  }
+
+  @Override
+  public void onNewProjectCreated(
+      com.google.gerrit.extensions.events.NewProjectCreatedListener.Event event) {
+    process(event, false);
+  }
+
+  @Override
+  public void onProjectDeleted(
+      com.google.gerrit.extensions.events.ProjectDeletedListener.Event event) {
+    process(event, true);
+  }
+
+  private void process(ProjectEvent event, boolean delete) {
+    if (!Context.isForwardedEvent()) {
+      executor.execute(new ProjectListUpdateTask(event.getProjectName(), delete));
+    }
+  }
+
+  class ProjectListUpdateTask implements Runnable {
+    private final String projectName;
+    private final boolean delete;
+
+    ProjectListUpdateTask(String projectName, boolean delete) {
+      this.projectName = projectName;
+      this.delete = delete;
+    }
+
+    @Override
+    public void run() {
+      if (delete) {
+        forwarder.removeFromProjectList(projectName);
+      } else {
+        forwarder.addToProjectList(projectName);
+      }
+    }
+
+    @Override
+    public String toString() {
+      return String.format(
+          "[%s] Update project list in target instance: %s '%s'",
+          pluginName, (delete ? "remove" : "add"), projectName);
+    }
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedCacheEvictionHandler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedCacheEvictionHandler.java
index 66a455a..3cb7c43 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedCacheEvictionHandler.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedCacheEvictionHandler.java
@@ -29,7 +29,7 @@
  */
 @Singleton
 public class ForwardedCacheEvictionHandler {
-  private static final Logger logger = LoggerFactory.getLogger(ForwardedCacheEvictionHandler.class);
+  private static final Logger log = LoggerFactory.getLogger(ForwardedCacheEvictionHandler.class);
 
   private final DynamicMap<Cache<?, ?>> cacheMap;
 
@@ -55,10 +55,10 @@
       if (Constants.PROJECT_LIST.equals(entry.getCacheName())) {
         // One key is holding the list of projects
         cache.invalidateAll();
-        logger.debug("Invalidated cache {}", entry.getCacheName());
+        log.debug("Invalidated cache {}", entry.getCacheName());
       } else {
         cache.invalidate(entry.getKey());
-        logger.debug("Invalidated cache {}[{}]", entry.getCacheName(), entry.getKey());
+        log.debug("Invalidated cache {}[{}]", entry.getCacheName(), entry.getKey());
       }
     } finally {
       Context.unsetForwardedEvent();
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedEventHandler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedEventHandler.java
index 4044673..b19db9a 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedEventHandler.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedEventHandler.java
@@ -30,7 +30,7 @@
  */
 @Singleton
 public class ForwardedEventHandler {
-  private static final Logger logger = LoggerFactory.getLogger(ForwardedEventHandler.class);
+  private static final Logger log = LoggerFactory.getLogger(ForwardedEventHandler.class);
 
   private final EventDispatcher dispatcher;
 
@@ -48,7 +48,7 @@
   public void dispatch(Event event) throws OrmException, PermissionBackendException {
     try {
       Context.setForwardedEvent(true);
-      logger.debug("dispatching event {}", event.getType());
+      log.debug("dispatching event {}", event.getType());
       dispatcher.postEvent(event);
     } finally {
       Context.unsetForwardedEvent();
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
new file mode 100644
index 0000000..f296e58
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexAccountHandler.java
@@ -0,0 +1,44 @@
+// 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.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;
+
+/**
+ * Index an account using {@link AccountIndexer}. This class is meant to be used on the receiving
+ * side of the {@link Forwarder} since it will prevent indexed account 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 account id
+ */
+@Singleton
+public class ForwardedIndexAccountHandler extends ForwardedIndexingHandler<Account.Id> {
+  private final AccountIndexer indexer;
+
+  @Inject
+  ForwardedIndexAccountHandler(AccountIndexer indexer) {
+    this.indexer = indexer;
+  }
+
+  @Override
+  protected void doIndex(Account.Id id) throws IOException, OrmException {
+    indexer.index(id);
+    log.debug("Account {} successfully indexed", id);
+  }
+}
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
new file mode 100644
index 0000000..c65813b
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandler.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;
+
+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.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.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+
+/**
+ * Index a change using {@link ChangeIndexer}. This class is meant to be used on the receiving side
+ * of the {@link Forwarder} since it will prevent indexed change 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 change id
+ */
+@Singleton
+public class ForwardedIndexChangeHandler extends ForwardedIndexingHandler<String> {
+  private final ChangeIndexer indexer;
+  private final SchemaFactory<ReviewDb> schemaFactory;
+  private final ChangeFinder changeFinder;
+
+  @Inject
+  ForwardedIndexChangeHandler(
+      ChangeIndexer indexer, SchemaFactory<ReviewDb> schemaFactory, ChangeFinder changeFinder) {
+    this.indexer = indexer;
+    this.schemaFactory = schemaFactory;
+    this.changeFinder = changeFinder;
+  }
+
+  @Override
+  protected void doIndex(String id) throws IOException, OrmException {
+    ChangeNotes change = null;
+    try (ReviewDb db = schemaFactory.open()) {
+      change = changeFinder.findOne(id);
+      if (change != null) {
+        indexer.index(db, change.getChange());
+        log.debug("Change {} successfully indexed", id);
+      }
+    } catch (Exception e) {
+      if (!isCausedByNoSuchChangeException(e)) {
+        throw e;
+      }
+      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);
+    }
+  }
+
+  @Override
+  protected void doDelete(String id) throws IOException {
+    indexer.delete(parseChangeId(id));
+    log.debug("Change {} successfully deleted from index", id);
+  }
+
+  private Change.Id parseChangeId(String id) {
+    Change.Id changeId = new Change.Id(Integer.parseInt(Splitter.on("~").splitToList(id).get(1)));
+    return changeId;
+  }
+
+  private boolean isCausedByNoSuchChangeException(Throwable throwable) {
+    while (throwable != null) {
+      if (throwable instanceof NoSuchChangeException) {
+        return true;
+      }
+      throwable = throwable.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
new file mode 100644
index 0000000..52485b2
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexGroupHandler.java
@@ -0,0 +1,44 @@
+// 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.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;
+
+/**
+ * Index a group using {@link GroupIndexer}. This class is meant to be used on the receiving side of
+ * the {@link Forwarder} since it will prevent indexed group 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 group uuid
+ */
+@Singleton
+public class ForwardedIndexGroupHandler extends ForwardedIndexingHandler<AccountGroup.UUID> {
+  private final GroupIndexer indexer;
+
+  @Inject
+  ForwardedIndexGroupHandler(GroupIndexer indexer) {
+    this.indexer = indexer;
+  }
+
+  @Override
+  protected void doIndex(AccountGroup.UUID uuid) throws IOException, OrmException {
+    indexer.index(uuid);
+    log.debug("Group {} successfully indexed", uuid);
+  }
+}
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
new file mode 100644
index 0000000..db1aa8f
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexingHandler.java
@@ -0,0 +1,81 @@
+// 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.google.common.util.concurrent.Striped;
+import com.google.gwtorm.server.OrmException;
+import java.io.IOException;
+import java.util.concurrent.locks.Lock;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Base class to handle forwarded indexing. This class is meant to be extended by classes used on
+ * the receiving side of the {@link Forwarder} since it will prevent indexing 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 id.
+ */
+public abstract class ForwardedIndexingHandler<T> {
+  protected final Logger log = LoggerFactory.getLogger(getClass());
+
+  public enum Operation {
+    INDEX,
+    DELETE;
+
+    @Override
+    public String toString() {
+      return name().toLowerCase();
+    }
+  }
+
+  private final Striped<Lock> idLocks = Striped.lock(10);
+
+  protected abstract void doIndex(T id) throws IOException, OrmException;
+
+  protected void doDelete(T id) throws IOException {
+    throw new UnsupportedOperationException("Delete from index not supported");
+  }
+
+  /**
+   * 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
+   * @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);
+    try {
+      Context.setForwardedEvent(true);
+      Lock idLock = idLocks.get(id);
+      idLock.lock();
+      try {
+        switch (operation) {
+          case INDEX:
+            doIndex(id);
+            break;
+          case DELETE:
+            doDelete(id);
+            break;
+        }
+      } finally {
+        idLock.unlock();
+      }
+    } finally {
+      Context.unsetForwardedEvent();
+    }
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedProjectListUpdateHandler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedProjectListUpdateHandler.java
new file mode 100644
index 0000000..094c3ec
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedProjectListUpdateHandler.java
@@ -0,0 +1,64 @@
+// 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.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Update project list cache. This class is meant to be used on the receiving side of the {@link
+ * Forwarder} since it will prevent project list updates to be forwarded again causing an infinite
+ * forwarding loop between the 2 nodes.
+ */
+@Singleton
+public class ForwardedProjectListUpdateHandler {
+  private static final Logger log =
+      LoggerFactory.getLogger(ForwardedProjectListUpdateHandler.class);
+
+  private final ProjectCache projectCache;
+
+  @Inject
+  ForwardedProjectListUpdateHandler(ProjectCache projectCache) {
+    this.projectCache = projectCache;
+  }
+
+  /**
+   * Update the project list, update will not be forwarded to the other node
+   *
+   * @param projectName the name of the project to add or remove.
+   * @param remove true to remove, false to add project.
+   * @throws IOException
+   */
+  public void update(String projectName, boolean remove) throws IOException {
+    Project.NameKey projectKey = new Project.NameKey(projectName);
+    try {
+      Context.setForwardedEvent(true);
+      if (remove) {
+        projectCache.remove(projectKey);
+        log.debug("Removed {} from project list", projectName);
+      } else {
+        projectCache.onCreateProject(projectKey);
+        log.debug("Added {} to project list", projectName);
+      }
+    } finally {
+      Context.unsetForwardedEvent();
+    }
+  }
+}
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 651f609..6a96d6a 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
@@ -30,10 +30,11 @@
   /**
    * Forward a change indexing event to the other master.
    *
+   * @param projectName the project of the change to index.
    * @param changeId the change to index.
    * @return true if successful, otherwise false.
    */
-  boolean indexChange(int changeId);
+  boolean indexChange(String projectName, int changeId);
 
   /**
    * Forward a delete change from index event to the other master.
@@ -67,4 +68,20 @@
    * @return true if successful, otherwise false.
    */
   boolean evict(String cacheName, Object key);
+
+  /**
+   * Forward an addition to the project list cache to the other master.
+   *
+   * @param projectName the name of the project to add to the project list cache
+   * @return true if successful, otherwise false.
+   */
+  boolean addToProjectList(String projectName);
+
+  /**
+   * Forward a removal from the project list cache to the other master.
+   *
+   * @param projectName the name of the project to remove from the project list cache
+   * @return true if successful, otherwise false.
+   */
+  boolean removeFromProjectList(String projectName);
 }
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 56cc176..7349ca5 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
@@ -19,30 +19,19 @@
 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
 import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
 
-import com.ericsson.gerrit.plugins.highavailability.forwarder.Context;
-import com.google.common.util.concurrent.Striped;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexingHandler;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexingHandler.Operation;
 import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
-import java.util.concurrent.locks.Lock;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 public abstract class AbstractIndexRestApiServlet<T> extends AbstractRestApiServlet {
   private static final long serialVersionUID = -1L;
 
+  private final ForwardedIndexingHandler<T> forwardedIndexingHandler;
   private final IndexName indexName;
   private final boolean allowDelete;
-  private final Striped<Lock> idLocks;
-
-  enum Operation {
-    INDEX,
-    DELETE;
-
-    @Override
-    public String toString() {
-      return name().toLowerCase();
-    }
-  }
 
   public enum IndexName {
     CHANGE,
@@ -57,16 +46,18 @@
 
   abstract T parse(String id);
 
-  abstract void index(T id, Operation operation) throws IOException, OrmException;
-
-  AbstractIndexRestApiServlet(IndexName indexName, boolean allowDelete) {
+  AbstractIndexRestApiServlet(
+      ForwardedIndexingHandler<T> forwardedIndexingHandler,
+      IndexName indexName,
+      boolean allowDelete) {
+    this.forwardedIndexingHandler = forwardedIndexingHandler;
     this.indexName = indexName;
     this.allowDelete = allowDelete;
-    this.idLocks = Striped.lock(10);
   }
 
-  AbstractIndexRestApiServlet(IndexName indexName) {
-    this(indexName, false);
+  AbstractIndexRestApiServlet(
+      ForwardedIndexingHandler<T> forwardedIndexingHandler, IndexName indexName) {
+    this(forwardedIndexingHandler, indexName, false);
   }
 
   @Override
@@ -86,28 +77,18 @@
 
   private void process(HttpServletRequest req, HttpServletResponse rsp, Operation operation) {
     setHeaders(rsp);
-    String path = req.getPathInfo();
+    String path = req.getRequestURI();
     T id = parse(path.substring(path.lastIndexOf('/') + 1));
-    logger.debug("{} {} {}", operation, indexName, id);
     try {
-      Context.setForwardedEvent(true);
-      Lock idLock = idLocks.get(id);
-      idLock.lock();
-      try {
-        index(id, operation);
-      } finally {
-        idLock.unlock();
-      }
+      forwardedIndexingHandler.index(id, operation);
       rsp.setStatus(SC_NO_CONTENT);
     } catch (IOException e) {
       sendError(rsp, SC_CONFLICT, e.getMessage());
-      logger.error("Unable to update {} index", indexName, e);
+      log.error("Unable to update {} index", indexName, e);
     } catch (OrmException e) {
-      String msg = String.format("Error trying to find %s \n", indexName);
+      String msg = String.format("Error trying to find %s", indexName);
       sendError(rsp, SC_NOT_FOUND, msg);
-      logger.debug(msg, e);
-    } finally {
-      Context.unsetForwardedEvent();
+      log.debug(msg, e);
     }
   }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/AbstractRestApiServlet.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/AbstractRestApiServlet.java
index cff54cd..054f640 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/AbstractRestApiServlet.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/AbstractRestApiServlet.java
@@ -24,7 +24,7 @@
 
 public abstract class AbstractRestApiServlet extends HttpServlet {
   private static final long serialVersionUID = 1L;
-  protected final Logger logger = LoggerFactory.getLogger(getClass());
+  protected final Logger log = LoggerFactory.getLogger(getClass());
 
   protected static void setHeaders(HttpServletResponse rsp) {
     rsp.setContentType("text/plain");
@@ -35,7 +35,7 @@
     try {
       rsp.sendError(statusCode, message);
     } catch (IOException e) {
-      logger.error("Failed to send error messsage: {}", e.getMessage(), e);
+      log.error("Failed to send error messsage: {}", e.getMessage(), e);
     }
   }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/CacheRestApiServlet.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/CacheRestApiServlet.java
index d82b8e2..dd7e324 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/CacheRestApiServlet.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/CacheRestApiServlet.java
@@ -51,10 +51,10 @@
           CacheEntry.from(cacheName, GsonParser.fromJson(cacheName, json)));
       rsp.setStatus(SC_NO_CONTENT);
     } catch (CacheNotFoundException e) {
-      logger.error("Failed to process eviction request: {}", e.getMessage());
+      log.error("Failed to process eviction request: {}", e.getMessage());
       sendError(rsp, SC_BAD_REQUEST, e.getMessage());
     } catch (IOException e) {
-      logger.error("Failed to process eviction request: {}", e.getMessage(), e);
+      log.error("Failed to process eviction request: {}", e.getMessage(), e);
       sendError(rsp, SC_BAD_REQUEST, e.getMessage());
     }
   }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/EventRestApiServlet.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/EventRestApiServlet.java
index 5b4caa8..849e60f 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/EventRestApiServlet.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/EventRestApiServlet.java
@@ -60,10 +60,10 @@
       forwardedEventHandler.dispatch(getEventFromRequest(req));
       rsp.setStatus(SC_NO_CONTENT);
     } catch (OrmException e) {
-      logger.debug("Error trying to find a change ", e);
+      log.debug("Error trying to find a change ", e);
       sendError(rsp, SC_NOT_FOUND, "Change not found\n");
     } catch (IOException | PermissionBackendException e) {
-      logger.error("Unable to re-trigger event", e);
+      log.error("Unable to re-trigger event", e);
       sendError(rsp, SC_BAD_REQUEST, e.getMessage());
     }
   }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexAccountRestApiServlet.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexAccountRestApiServlet.java
index 4822886..cf22ec0 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexAccountRestApiServlet.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexAccountRestApiServlet.java
@@ -14,32 +14,22 @@
 
 package com.ericsson.gerrit.plugins.highavailability.forwarder.rest;
 
+import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexAccountHandler;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.io.IOException;
 
 @Singleton
 class IndexAccountRestApiServlet extends AbstractIndexRestApiServlet<Account.Id> {
   private static final long serialVersionUID = -1L;
 
-  private final AccountIndexer indexer;
-
   @Inject
-  IndexAccountRestApiServlet(AccountIndexer indexer) {
-    super(IndexName.ACCOUNT);
-    this.indexer = indexer;
+  IndexAccountRestApiServlet(ForwardedIndexAccountHandler handler) {
+    super(handler, IndexName.ACCOUNT);
   }
 
   @Override
   Account.Id parse(String id) {
-    return Account.Id.tryParse(id).get();
-  }
-
-  @Override
-  void index(Account.Id id, Operation operation) throws IOException {
-    indexer.index(id);
-    logger.debug("Account {} successfully indexed", id);
+    return new Account.Id(Integer.parseInt(id));
   }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexChangeRestApiServlet.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexChangeRestApiServlet.java
index f786dc6..d155215 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexChangeRestApiServlet.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexChangeRestApiServlet.java
@@ -14,71 +14,22 @@
 
 package com.ericsson.gerrit.plugins.highavailability.forwarder.rest;
 
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.index.change.ChangeIndexer;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexChangeHandler;
+import com.google.gerrit.extensions.restapi.Url;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.io.IOException;
 
 @Singleton
-class IndexChangeRestApiServlet extends AbstractIndexRestApiServlet<Change.Id> {
+class IndexChangeRestApiServlet extends AbstractIndexRestApiServlet<String> {
   private static final long serialVersionUID = -1L;
 
-  private final ChangeIndexer indexer;
-  private final SchemaFactory<ReviewDb> schemaFactory;
-
   @Inject
-  IndexChangeRestApiServlet(ChangeIndexer indexer, SchemaFactory<ReviewDb> schemaFactory) {
-    super(IndexName.CHANGE, true);
-    this.indexer = indexer;
-    this.schemaFactory = schemaFactory;
+  IndexChangeRestApiServlet(ForwardedIndexChangeHandler handler) {
+    super(handler, IndexName.CHANGE, true);
   }
 
   @Override
-  Change.Id parse(String id) {
-    return Change.Id.parse(id);
-  }
-
-  @Override
-  void index(Change.Id id, Operation operation) throws IOException, OrmException {
-    switch (operation) {
-      case INDEX:
-        Change change = null;
-        try (ReviewDb db = schemaFactory.open()) {
-          change = db.changes().get(id);
-          if (change != null) {
-            indexer.index(db, change);
-            logger.debug("Change {} successfully indexed", id);
-          }
-        } catch (Exception e) {
-          if (!isCausedByNoSuchChangeException(e)) {
-            throw e;
-          }
-          logger.debug("Change {} was deleted, aborting forwarded indexing the change.", id.get());
-        }
-        if (change == null) {
-          indexer.delete(id);
-          logger.debug("Change {} not found, deleted from index", id);
-        }
-        break;
-      case DELETE:
-        indexer.delete(id);
-        logger.debug("Change {} successfully deleted from index", id);
-        break;
-    }
-  }
-
-  private boolean isCausedByNoSuchChangeException(Throwable throwable) {
-    while (throwable != null) {
-      if (throwable instanceof NoSuchChangeException) {
-        return true;
-      }
-      throwable = throwable.getCause();
-    }
-    return false;
+  String parse(String id) {
+    return Url.decode(id);
   }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexGroupRestApiServlet.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexGroupRestApiServlet.java
index 1cfb606..80e51fb 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexGroupRestApiServlet.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexGroupRestApiServlet.java
@@ -14,32 +14,22 @@
 
 package com.ericsson.gerrit.plugins.highavailability.forwarder.rest;
 
+import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexGroupHandler;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.index.group.GroupIndexer;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.io.IOException;
 
 @Singleton
 class IndexGroupRestApiServlet extends AbstractIndexRestApiServlet<AccountGroup.UUID> {
   private static final long serialVersionUID = -1L;
 
-  private final GroupIndexer indexer;
-
   @Inject
-  IndexGroupRestApiServlet(GroupIndexer indexer) {
-    super(IndexName.GROUP);
-    this.indexer = indexer;
+  IndexGroupRestApiServlet(ForwardedIndexGroupHandler handler) {
+    super(handler, IndexName.GROUP);
   }
 
   @Override
   AccountGroup.UUID parse(String id) {
-    return AccountGroup.UUID.parse(id);
-  }
-
-  @Override
-  void index(AccountGroup.UUID uuid, Operation operation) throws IOException {
-    indexer.index(uuid);
-    logger.debug("Group {} successfully indexed", uuid);
+    return new AccountGroup.UUID(id);
   }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/ProjectListApiServlet.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/ProjectListApiServlet.java
new file mode 100644
index 0000000..28e6a37
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/ProjectListApiServlet.java
@@ -0,0 +1,60 @@
+// 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_BAD_REQUEST;
+import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
+
+import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedProjectListUpdateHandler;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+class ProjectListApiServlet extends AbstractRestApiServlet {
+  private static final long serialVersionUID = -1L;
+
+  private final ForwardedProjectListUpdateHandler forwardedProjectListUpdateHandler;
+
+  @Inject
+  ProjectListApiServlet(ForwardedProjectListUpdateHandler forwardedProjectListUpdateHandler) {
+    this.forwardedProjectListUpdateHandler = forwardedProjectListUpdateHandler;
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest req, HttpServletResponse rsp) {
+    process(req, rsp, false);
+  }
+
+  @Override
+  protected void doDelete(HttpServletRequest req, HttpServletResponse rsp) {
+    process(req, rsp, true);
+  }
+
+  private void process(HttpServletRequest req, HttpServletResponse rsp, boolean delete) {
+    setHeaders(rsp);
+    String path = req.getPathInfo();
+    String projectName = path.substring(path.lastIndexOf('/') + 1);
+    try {
+      forwardedProjectListUpdateHandler.update(projectName, delete);
+      rsp.setStatus(SC_NO_CONTENT);
+    } catch (IOException e) {
+      log.error("Unable to update project list", e);
+      sendError(rsp, SC_BAD_REQUEST, e.getMessage());
+    }
+  }
+}
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 34b7013..53ca073 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
@@ -15,11 +15,13 @@
 package com.ericsson.gerrit.plugins.highavailability.forwarder.rest;
 
 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.rest.HttpResponseHandler.HttpResult;
 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;
@@ -45,7 +47,7 @@
 
   @Override
   public boolean indexAccount(final int accountId) {
-    return new Request("index account " + accountId) {
+    return new Request("index account", accountId) {
       @Override
       HttpResult send() throws IOException {
         return httpSession.post(
@@ -55,18 +57,18 @@
   }
 
   @Override
-  public boolean indexChange(final int changeId) {
-    return new Request("index change " + changeId) {
+  public boolean indexChange(final String projectName, final int changeId) {
+    return new Request("index change", changeId) {
       @Override
       HttpResult send() throws IOException {
-        return httpSession.post(buildIndexEndpoint(changeId));
+        return httpSession.post(buildIndexEndpoint(projectName, changeId));
       }
     }.execute();
   }
 
   @Override
   public boolean deleteChangeFromIndex(final int changeId) {
-    return new Request("delete change " + changeId + " from index") {
+    return new Request("delete change", changeId) {
       @Override
       HttpResult send() throws IOException {
         return httpSession.delete(buildIndexEndpoint(changeId));
@@ -76,7 +78,7 @@
 
   @Override
   public boolean indexGroup(final String uuid) {
-    return new Request("index group " + uuid) {
+    return new Request("index group", uuid) {
       @Override
       HttpResult send() throws IOException {
         return httpSession.post(Joiner.on("/").join(pluginRelativePath, "index/group", uuid));
@@ -85,12 +87,18 @@
   }
 
   private String buildIndexEndpoint(int changeId) {
-    return Joiner.on("/").join(pluginRelativePath, "index/change", changeId);
+    return buildIndexEndpoint("", changeId);
+  }
+
+  private String buildIndexEndpoint(String projectName, int changeId) {
+    String escapedProjectName = Url.encode(projectName);
+    return Joiner.on("/")
+        .join(pluginRelativePath, "index/change", escapedProjectName + '~' + changeId);
   }
 
   @Override
   public boolean send(final Event event) {
-    return new Request("send event " + event.type) {
+    return new Request("send event", event.type) {
       @Override
       HttpResult send() throws IOException {
         String serializedEvent =
@@ -105,7 +113,7 @@
 
   @Override
   public boolean evict(final String cacheName, final Object key) {
-    return new Request("invalidate cache " + cacheName + "[" + key + "]") {
+    return new Request("invalidate cache " + cacheName, key) {
       @Override
       HttpResult send() throws IOException {
         String json = GsonParser.toJson(cacheName, key);
@@ -114,39 +122,65 @@
     }.execute();
   }
 
+  @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();
+  }
+
+  @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();
+  }
+
+  private String buildProjectListEndpoint(String projectName) {
+    return Joiner.on("/").join(pluginRelativePath, "cache", Constants.PROJECT_LIST, projectName);
+  }
+
   private abstract class Request {
-    private final String name;
+    private final String action;
+    private final Object key;
     private int execCnt;
 
-    Request(String name) {
-      this.name = name;
+    Request(String action, Object key) {
+      this.action = action;
+      this.key = key;
     }
 
     boolean execute() {
-      log.debug(name);
+      log.debug("Executing {} {}", action, key);
       for (; ; ) {
         try {
           execCnt++;
           tryOnce();
-          log.debug("{} OK", name);
+          log.debug("{} {} OK", action, key);
           return true;
         } catch (ForwardingException e) {
           int maxTries = cfg.http().maxTries();
-          log.debug("Failed to {} [{}/{}]", name, execCnt, maxTries, e);
+          log.debug("Failed to {} {} [{}/{}]", action, key, execCnt, maxTries, e);
           if (!e.isRecoverable()) {
-            log.error("{} failed with unrecoverable error; giving up", name);
+            log.error("{} {} failed with unrecoverable error; giving up", action, key, e);
             return false;
           }
           if (execCnt >= maxTries) {
-            log.error("Failed to {} after {} tries; giving up", name, maxTries);
+            log.error("Failed to {} {} after {} tries; giving up", action, key, maxTries);
             return false;
           }
 
-          log.debug("Retrying to {}", name);
+          log.debug("Retrying to {} {}", action, key);
           try {
             Thread.sleep(cfg.http().retryInterval());
           } catch (InterruptedException ie) {
-            log.error("{} was interrupted; giving up", name, ie);
+            log.error("{} {} was interrupted; giving up", action, key, ie);
             Thread.currentThread().interrupt();
             return false;
           }
@@ -158,7 +192,8 @@
       try {
         HttpResult result = send();
         if (!result.isSuccessful()) {
-          throw new ForwardingException(true, "Unable to " + name + ": " + result.getMessage());
+          throw new ForwardingException(
+              true, String.format("Unable to %s %s : %s", action, key, result.getMessage()));
         }
       } catch (IOException e) {
         throw new ForwardingException(isRecoverable(e), e.getMessage(), e);
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 d5027d1..56b3f65 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
@@ -20,9 +20,10 @@
   @Override
   protected void configureServlets() {
     serveRegex("/index/account/\\d+$").with(IndexAccountRestApiServlet.class);
-    serveRegex("/index/change/\\d+$").with(IndexChangeRestApiServlet.class);
+    serveRegex("/index/change/.*$").with(IndexChangeRestApiServlet.class);
     serveRegex("/index/group/\\w+$").with(IndexGroupRestApiServlet.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/IndexEventHandler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/IndexEventHandler.java
index 7813dd0..d503a81 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
@@ -54,13 +54,13 @@
   }
 
   @Override
-  public void onChangeIndexed(int id) {
-    executeIndexChangeTask(id, false);
+  public void onChangeIndexed(String projectName, int id) {
+    executeIndexChangeTask(projectName, id, false);
   }
 
   @Override
   public void onChangeDeleted(int id) {
-    executeIndexChangeTask(id, true);
+    executeIndexChangeTask("", id, true);
   }
 
   @Override
@@ -73,9 +73,9 @@
     }
   }
 
-  private void executeIndexChangeTask(int id, boolean deleted) {
+  private void executeIndexChangeTask(String projectName, int id, boolean deleted) {
     if (!Context.isForwardedEvent()) {
-      IndexChangeTask task = new IndexChangeTask(id, deleted);
+      IndexChangeTask task = new IndexChangeTask(projectName, id, deleted);
       if (queuedTasks.add(task)) {
         executor.execute(task);
       }
@@ -95,8 +95,10 @@
   class IndexChangeTask extends IndexTask {
     private final boolean deleted;
     private final int changeId;
+    private final String projectName;
 
-    IndexChangeTask(int changeId, boolean deleted) {
+    IndexChangeTask(String projectName, int changeId, boolean deleted) {
+      this.projectName = projectName;
       this.changeId = changeId;
       this.deleted = deleted;
     }
@@ -106,7 +108,7 @@
       if (deleted) {
         forwarder.deleteChangeFromIndex(changeId);
       } else {
-        forwarder.indexChange(changeId);
+        forwarder.indexChange(projectName, changeId);
       }
     }
 
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/websession/file/FileBasedWebSessionCacheCleaner.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/websession/file/FileBasedWebSessionCacheCleaner.java
index 68ffd17..14134e2 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/websession/file/FileBasedWebSessionCacheCleaner.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/websession/file/FileBasedWebSessionCacheCleaner.java
@@ -66,7 +66,7 @@
 }
 
 class CleanupTask implements Runnable {
-  private static final Logger logger = LoggerFactory.getLogger(CleanupTask.class);
+  private static final Logger log = LoggerFactory.getLogger(CleanupTask.class);
   private final FileBasedWebsessionCache fileBasedWebSessionCache;
   private final String pluginName;
 
@@ -78,9 +78,9 @@
 
   @Override
   public void run() {
-    logger.info("Cleaning up expired file based websessions...");
+    log.info("Cleaning up expired file based websessions...");
     fileBasedWebSessionCache.cleanUp();
-    logger.info("Cleaning up expired file based websessions...Done");
+    log.info("Cleaning up expired file based websessions...Done");
   }
 
   @Override
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index ef6dc7c..12b00b9 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -121,12 +121,15 @@
 ```http.maxTries```
 :   Maximum number of times the plugin should attempt when calling a REST API in
     the target instance. Setting this value to 0 will disable retries. When not
-    specified, the default value is 5. After this number of failed tries, an
+    specified, the default value is 360. After this number of failed tries, an
     error is logged.
 
 ```http.retryInterval```
 :   The interval of time in milliseconds between the subsequent auto-retries.
-    When not specified, the default value is set to 1000ms.
+    When not specified, the default value is set to 10000ms.
+
+NOTE: the default settings for `http.timeout` and `http.maxTries` ensure that
+the plugin will keep retrying to forward a message for one hour.
 
 ```cache.synchronize```
 :   Whether to synchronize cache evictions.
@@ -134,7 +137,7 @@
 
 ```cache.threadPoolSize```
 :   Maximum number of threads used to send cache evictions to the target instance.
-    Defaults to 1.
+    Defaults to 4.
 
 ```cache.pattern```
 :   Pattern to match names of custom caches for which evictions should be
@@ -153,7 +156,7 @@
 
 ```index.threadPoolSize```
 :   Maximum number of threads used to send index events to the target instance.
-    Defaults to 1.
+    Defaults to 4.
 
 ```websession.synchronize```
 :   Whether to synchronize web sessions.
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheEvictionIT.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheEvictionIT.java
index 0e39c28..28a6d18 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheEvictionIT.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheEvictionIT.java
@@ -15,8 +15,9 @@
 package com.ericsson.gerrit.plugins.highavailability.cache;
 
 import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.any;
+import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
 import static com.github.tomakehurst.wiremock.client.WireMock.givenThat;
-import static com.github.tomakehurst.wiremock.client.WireMock.post;
 import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
 import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
 import static com.github.tomakehurst.wiremock.client.WireMock.verify;
@@ -47,25 +48,20 @@
   private static final int PORT = 18888;
   private static final String URL = "http://localhost:" + PORT;
 
-  @Rule public WireMockRule wireMockRule = new WireMockRule(options().port(PORT), false);
+  @Rule public WireMockRule wireMockRule = new WireMockRule(options().port(PORT));
+
+  @Override
+  public void setUpTestPlugin() throws Exception {
+    givenThat(any(anyUrl()).willReturn(aResponse().withStatus(HttpStatus.SC_NO_CONTENT)));
+    super.setUpTestPlugin();
+  }
 
   @Test
   @UseLocalDisk
-  @GlobalPluginConfig(
-    pluginName = "high-availability",
-    name = "peerInfo.strategy",
-    value = "static"
-  )
   @GlobalPluginConfig(pluginName = "high-availability", name = "peerInfo.static.url", value = URL)
-  @GlobalPluginConfig(pluginName = "high-availability", name = "http.user", value = "admin")
-  @GlobalPluginConfig(pluginName = "high-availability", name = "cache.threadPoolSize", value = "10")
-  @GlobalPluginConfig(
-    pluginName = "high-availability",
-    name = "main.sharedDirectory",
-    value = "directory"
-  )
+  @GlobalPluginConfig(pluginName = "high-availability", name = "http.retryInterval", value = "100")
   public void flushAndSendPost() throws Exception {
-    final String flushRequest = "/plugins/high-availability/cache/" + Constants.PROJECT_LIST;
+    final String flushRequest = "/plugins/high-availability/cache/" + Constants.PROJECTS;
     final CountDownLatch expectedRequestLatch = new CountDownLatch(1);
     wireMockRule.addMockServiceRequestListener(
         (request, response) -> {
@@ -73,11 +69,8 @@
             expectedRequestLatch.countDown();
           }
         });
-    givenThat(
-        post(urlEqualTo(flushRequest))
-            .willReturn(aResponse().withStatus(HttpStatus.SC_NO_CONTENT)));
 
-    adminSshSession.exec("gerrit flush-caches --cache " + Constants.PROJECT_LIST);
+    adminSshSession.exec("gerrit flush-caches --cache " + Constants.PROJECTS);
     assertThat(expectedRequestLatch.await(5, TimeUnit.SECONDS)).isTrue();
     verify(postRequestedFor(urlEqualTo(flushRequest)));
   }
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/cache/CachePattenMatcherTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/cache/CachePattenMatcherTest.java
index 1bbf7b5..35a6ba3 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/cache/CachePattenMatcherTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/cache/CachePattenMatcherTest.java
@@ -14,7 +14,7 @@
 
 package com.ericsson.gerrit.plugins.highavailability.cache;
 
-import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static org.mockito.Answers.RETURNS_DEEP_STUBS;
 import static org.mockito.Mockito.when;
 
@@ -37,16 +37,45 @@
     CachePatternMatcher matcher = new CachePatternMatcher(configurationMock);
     for (String cache :
         ImmutableList.of(
+            "accounts",
             "accounts_byemail",
+            "accounts_byname",
+            "groups",
+            "groups_byinclude",
+            "groups_byname",
+            "groups_byuuid",
+            "groups_external",
+            "groups_members",
             "ldap_groups",
-            "project_list",
+            "ldap_usernames",
+            "projects",
+            "sshkeys",
             "my_cache_a",
             "my_cache_b",
             "other")) {
-      assertThat(matcher.matches(cache)).isTrue();
+      assertWithMessage(cache + " should match").that(matcher.matches(cache)).isTrue();
     }
-    for (String cache : ImmutableList.of("ldap_groups_by_include", "foo")) {
-      assertThat(matcher.matches(cache)).isFalse();
+    for (String cache :
+        ImmutableList.of(
+            "adv_bases",
+            "change_kind",
+            "change_notes",
+            "changes",
+            "conflicts",
+            "diff",
+            "diff_intraline",
+            "diff_summary",
+            "git_tags",
+            "ldap_group_existence",
+            "ldap_groups_byinclude",
+            "mergeability",
+            "oauth_tokens",
+            "permission_sort",
+            "project_list",
+            "plugin_resources",
+            "static_content",
+            "foo")) {
+      assertWithMessage(cache + " should not match").that(matcher.matches(cache)).isFalse();
     }
   }
 }
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/cache/ProjectListIT.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/cache/ProjectListIT.java
new file mode 100644
index 0000000..4f056bf
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/cache/ProjectListIT.java
@@ -0,0 +1,77 @@
+// 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.cache;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.any;
+import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
+import static com.github.tomakehurst.wiremock.client.WireMock.givenThat;
+import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.verify;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import com.google.gerrit.acceptance.GlobalPluginConfig;
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import org.apache.http.HttpStatus;
+import org.junit.Rule;
+import org.junit.Test;
+
+@NoHttpd
+@TestPlugin(
+  name = "high-availability",
+  sysModule = "com.ericsson.gerrit.plugins.highavailability.Module",
+  httpModule = "com.ericsson.gerrit.plugins.highavailability.HttpModule"
+)
+public class ProjectListIT extends LightweightPluginDaemonTest {
+  private static final int PORT = 18888;
+  private static final String URL = "http://localhost:" + PORT;
+
+  @Rule public WireMockRule wireMockRule = new WireMockRule(options().port(PORT));
+
+  @Override
+  public void setUpTestPlugin() throws Exception {
+    givenThat(any(anyUrl()).willReturn(aResponse().withStatus(HttpStatus.SC_NO_CONTENT)));
+    super.setUpTestPlugin();
+  }
+
+  @Test
+  @UseLocalDisk
+  @GlobalPluginConfig(pluginName = "high-availability", name = "peerInfo.static.url", value = URL)
+  @GlobalPluginConfig(pluginName = "high-availability", name = "http.retryInterval", value = "100")
+  public void addToProjectListAreForwarded() throws Exception {
+    String createdProject = "someProject";
+    String expectedRequest =
+        "/plugins/high-availability/cache/" + Constants.PROJECT_LIST + "/" + createdProject;
+    CountDownLatch expectedRequestLatch = new CountDownLatch(1);
+    wireMockRule.addMockServiceRequestListener(
+        (request, response) -> {
+          if (request.getAbsoluteUrl().contains(expectedRequest)) {
+            expectedRequestLatch.countDown();
+          }
+        });
+
+    adminRestSession.put("/projects/" + createdProject).assertCreated();
+    assertThat(expectedRequestLatch.await(5, TimeUnit.SECONDS)).isTrue();
+    verify(postRequestedFor(urlEqualTo(expectedRequest)));
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/cache/ProjectListUpdateHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/cache/ProjectListUpdateHandlerTest.java
new file mode 100644
index 0000000..f6865ec
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/cache/ProjectListUpdateHandlerTest.java
@@ -0,0 +1,90 @@
+// 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.cache;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import com.ericsson.gerrit.plugins.highavailability.cache.ProjectListUpdateHandler.ProjectListUpdateTask;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.Context;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.Forwarder;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.extensions.events.NewProjectCreatedListener;
+import com.google.gerrit.extensions.events.ProjectDeletedListener;
+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 ProjectListUpdateHandlerTest {
+  private static final String PLUGIN_NAME = "high-availability";
+
+  private ProjectListUpdateHandler handler;
+
+  @Mock private Forwarder forwarder;
+
+  @Before
+  public void setUp() {
+    handler = new ProjectListUpdateHandler(forwarder, MoreExecutors.directExecutor(), PLUGIN_NAME);
+  }
+
+  @Test
+  public void shouldForwardAddedProject() throws Exception {
+    String projectName = "projectToAdd";
+    NewProjectCreatedListener.Event event = mock(NewProjectCreatedListener.Event.class);
+    when(event.getProjectName()).thenReturn(projectName);
+    handler.onNewProjectCreated(event);
+    verify(forwarder).addToProjectList(projectName);
+  }
+
+  @Test
+  public void shouldForwardDeletedProject() throws Exception {
+    String projectName = "projectToDelete";
+    ProjectDeletedListener.Event event = mock(ProjectDeletedListener.Event.class);
+    when(event.getProjectName()).thenReturn(projectName);
+    handler.onProjectDeleted(event);
+    verify(forwarder).removeFromProjectList(projectName);
+  }
+
+  @Test
+  public void shouldNotForwardIfAlreadyForwardedEvent() throws Exception {
+    Context.setForwardedEvent(true);
+    handler.onNewProjectCreated(mock(NewProjectCreatedListener.Event.class));
+    handler.onProjectDeleted(mock(ProjectDeletedListener.Event.class));
+    Context.unsetForwardedEvent();
+    verifyZeroInteractions(forwarder);
+  }
+
+  @Test
+  public void testProjectUpdateTaskToString() throws Exception {
+    String projectName = "someProjectName";
+    ProjectListUpdateTask task = handler.new ProjectListUpdateTask(projectName, false);
+    assertThat(task.toString())
+        .isEqualTo(
+            String.format(
+                "[%s] Update project list in target instance: add '%s'", PLUGIN_NAME, projectName));
+
+    task = handler.new ProjectListUpdateTask(projectName, true);
+    assertThat(task.toString())
+        .isEqualTo(
+            String.format(
+                "[%s] Update project list in target instance: remove '%s'",
+                PLUGIN_NAME, projectName));
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedCacheEvictionHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedCacheEvictionHandlerTest.java
index a9bb5be..3426b05 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedCacheEvictionHandlerTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedCacheEvictionHandlerTest.java
@@ -78,8 +78,8 @@
     CacheEntry entry = new CacheEntry(Constants.GERRIT, Constants.ACCOUNTS, new Account.Id(456));
     doReturn(cacheMock).when(cacheMapMock).get(entry.getPluginName(), entry.getCacheName());
 
-    //this doAnswer is to allow to assert that context is set to forwarded
-    //while cache eviction is called.
+    // this doAnswer is to allow to assert that context is set to forwarded
+    // while cache eviction is called.
     doAnswer(
             (Answer<Void>)
                 invocation -> {
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedEventHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedEventHandlerTest.java
index 814f4a5..9d0b73c 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedEventHandlerTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedEventHandlerTest.java
@@ -54,8 +54,8 @@
   @Test
   public void shouldSetAndUnsetForwardedContext() throws Exception {
     Event event = new ProjectCreatedEvent();
-    //this doAnswer is to allow to assert that context is set to forwarded
-    //while cache eviction is called.
+    // this doAnswer is to allow to assert that context is set to forwarded
+    // while cache eviction is called.
     doAnswer(
             (Answer<Void>)
                 invocation -> {
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
new file mode 100644
index 0000000..74a515d
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexAccountHandlerTest.java
@@ -0,0 +1,104 @@
+// 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 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 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 ForwardedIndexAccountHandlerTest {
+
+  @Rule public ExpectedException exception = ExpectedException.none();
+  @Mock private AccountIndexer indexerMock;
+  private ForwardedIndexAccountHandler handler;
+  private Account.Id id;
+
+  @Before
+  public void setUp() throws Exception {
+    handler = new ForwardedIndexAccountHandler(indexerMock);
+    id = new Account.Id(123);
+  }
+
+  @Test
+  public void testSuccessfulIndexing() throws Exception {
+    handler.index(id, Operation.INDEX);
+    verify(indexerMock).index(id);
+  }
+
+  @Test
+  public void deleteIsNotSupported() throws Exception {
+    exception.expect(UnsupportedOperationException.class);
+    exception.expectMessage("Delete from index not supported");
+    handler.index(id, Operation.DELETE);
+  }
+
+  @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(id);
+
+    assertThat(Context.isForwardedEvent()).isFalse();
+    handler.index(id, Operation.INDEX);
+    assertThat(Context.isForwardedEvent()).isFalse();
+
+    verify(indexerMock).index(id);
+  }
+
+  @Test
+  public void shouldSetAndUnsetForwardedContextEvenIfExceptionIsThrown() throws Exception {
+    doAnswer(
+            (Answer<Void>)
+                invocation -> {
+                  assertThat(Context.isForwardedEvent()).isTrue();
+                  throw new IOException("someMessage");
+                })
+        .when(indexerMock)
+        .index(id);
+
+    assertThat(Context.isForwardedEvent()).isFalse();
+    try {
+      handler.index(id, Operation.INDEX);
+      fail("should have thrown an IOException");
+    } catch (IOException e) {
+      assertThat(e.getMessage()).isEqualTo("someMessage");
+    }
+    assertThat(Context.isForwardedEvent()).isFalse();
+
+    verify(indexerMock).index(id);
+  }
+}
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
new file mode 100644
index 0000000..000bfdb
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandlerTest.java
@@ -0,0 +1,201 @@
+// 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.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+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.ForwardedIndexingHandler.Operation;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.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.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import java.io.IOException;
+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 ForwardedIndexChangeHandlerTest {
+
+  private static final int TEST_CHANGE_NUMBER = 123;
+  private static String TEST_PROJECT = "test/project";
+  private static String TEST_CHANGE_ID = TEST_PROJECT + "~" + TEST_CHANGE_NUMBER;
+  private static final boolean CHANGE_EXISTS = true;
+  private static final boolean CHANGE_DOES_NOT_EXIST = false;
+  private static final boolean DO_NOT_THROW_IO_EXCEPTION = false;
+  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;
+
+  @Rule public ExpectedException exception = ExpectedException.none();
+  @Mock private ChangeIndexer indexerMock;
+  @Mock private SchemaFactory<ReviewDb> schemaFactoryMock;
+  @Mock private ReviewDb dbMock;
+  @Mock private ChangeFinder changeFinderMock;
+  @Mock private ChangeNotes changeNotes;
+  private ForwardedIndexChangeHandler handler;
+  private Change.Id id;
+  private Change change;
+
+  @Before
+  public void setUp() throws Exception {
+    when(schemaFactoryMock.open()).thenReturn(dbMock);
+    id = new Change.Id(TEST_CHANGE_NUMBER);
+    change = new Change(null, id, null, null, TimeUtil.nowTs());
+    when(changeNotes.getChange()).thenReturn(change);
+    handler = new ForwardedIndexChangeHandler(indexerMock, schemaFactoryMock, changeFinderMock);
+  }
+
+  @Test
+  public void changeIsIndexed() throws Exception {
+    setupChangeAccessRelatedMocks(CHANGE_EXISTS);
+    handler.index(TEST_CHANGE_ID, Operation.INDEX);
+    verify(indexerMock, times(1)).index(any(ReviewDb.class), any(Change.class));
+  }
+
+  @Test
+  public void changeIsDeletedFromIndex() throws Exception {
+    handler.index(TEST_CHANGE_ID, Operation.DELETE);
+    verify(indexerMock, times(1)).delete(id);
+  }
+
+  @Test
+  public void changeToIndexDoesNotExist() throws Exception {
+    setupChangeAccessRelatedMocks(CHANGE_DOES_NOT_EXIST);
+    handler.index(TEST_CHANGE_ID, Operation.INDEX);
+    verify(indexerMock, times(1)).delete(id);
+  }
+
+  @Test
+  public void schemaThrowsExceptionWhenLookingUpForChange() throws Exception {
+    setupChangeAccessRelatedMocks(CHANGE_EXISTS, THROW_ORM_EXCEPTION);
+    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);
+  }
+
+  @Test
+  public void indexerThrowsIOExceptionTryingToIndexChange() throws Exception {
+    setupChangeAccessRelatedMocks(CHANGE_EXISTS, DO_NOT_THROW_ORM_EXCEPTION, THROW_IO_EXCEPTION);
+    exception.expect(IOException.class);
+    handler.index(TEST_CHANGE_ID, Operation.INDEX);
+  }
+
+  @Test
+  public void shouldSetAndUnsetForwardedContext() throws Exception {
+    setupChangeAccessRelatedMocks(CHANGE_EXISTS);
+    // 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(any(ReviewDb.class), any(Change.class));
+
+    assertThat(Context.isForwardedEvent()).isFalse();
+    handler.index(TEST_CHANGE_ID, Operation.INDEX);
+    assertThat(Context.isForwardedEvent()).isFalse();
+
+    verify(indexerMock, times(1)).index(any(ReviewDb.class), any(Change.class));
+  }
+
+  @Test
+  public void shouldSetAndUnsetForwardedContextEvenIfExceptionIsThrown() throws Exception {
+    setupChangeAccessRelatedMocks(CHANGE_EXISTS);
+    doAnswer(
+            (Answer<Void>)
+                invocation -> {
+                  assertThat(Context.isForwardedEvent()).isTrue();
+                  throw new IOException("someMessage");
+                })
+        .when(indexerMock)
+        .index(any(ReviewDb.class), any(Change.class));
+
+    assertThat(Context.isForwardedEvent()).isFalse();
+    try {
+      handler.index(TEST_CHANGE_ID, Operation.INDEX);
+      fail("should have thrown an IOException");
+    } catch (IOException e) {
+      assertThat(e.getMessage()).isEqualTo("someMessage");
+    }
+    assertThat(Context.isForwardedEvent()).isFalse();
+
+    verify(indexerMock, times(1)).index(any(ReviewDb.class), any(Change.class));
+  }
+
+  private void setupChangeAccessRelatedMocks(boolean changeExist) 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);
+  }
+
+  private void setupChangeAccessRelatedMocks(
+      boolean changeExists, boolean ormException, boolean ioException)
+      throws OrmException, IOException {
+    if (ormException) {
+      doThrow(new OrmException("")).when(schemaFactoryMock).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);
+      }
+    }
+  }
+}
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
new file mode 100644
index 0000000..b963a5b
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexGroupHandlerTest.java
@@ -0,0 +1,104 @@
+// 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 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 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 ForwardedIndexGroupHandlerTest {
+
+  @Rule public ExpectedException exception = ExpectedException.none();
+  @Mock private GroupIndexer indexerMock;
+  private ForwardedIndexGroupHandler handler;
+  private AccountGroup.UUID uuid;
+
+  @Before
+  public void setUp() throws Exception {
+    handler = new ForwardedIndexGroupHandler(indexerMock);
+    uuid = new AccountGroup.UUID("123");
+  }
+
+  @Test
+  public void testSuccessfulIndexing() throws Exception {
+    handler.index(uuid, Operation.INDEX);
+    verify(indexerMock).index(uuid);
+  }
+
+  @Test
+  public void deleteIsNotSupported() throws Exception {
+    exception.expect(UnsupportedOperationException.class);
+    exception.expectMessage("Delete from index not supported");
+    handler.index(uuid, Operation.DELETE);
+  }
+
+  @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(uuid);
+
+    assertThat(Context.isForwardedEvent()).isFalse();
+    handler.index(uuid, Operation.INDEX);
+    assertThat(Context.isForwardedEvent()).isFalse();
+
+    verify(indexerMock).index(uuid);
+  }
+
+  @Test
+  public void shouldSetAndUnsetForwardedContextEvenIfExceptionIsThrown() throws Exception {
+    doAnswer(
+            (Answer<Void>)
+                invocation -> {
+                  assertThat(Context.isForwardedEvent()).isTrue();
+                  throw new IOException("someMessage");
+                })
+        .when(indexerMock)
+        .index(uuid);
+
+    assertThat(Context.isForwardedEvent()).isFalse();
+    try {
+      handler.index(uuid, Operation.INDEX);
+      fail("should have thrown an IOException");
+    } catch (IOException e) {
+      assertThat(e.getMessage()).isEqualTo("someMessage");
+    }
+    assertThat(Context.isForwardedEvent()).isFalse();
+
+    verify(indexerMock).index(uuid);
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedProjectListUpdateHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedProjectListUpdateHandlerTest.java
new file mode 100644
index 0000000..2e263dd
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedProjectListUpdateHandlerTest.java
@@ -0,0 +1,145 @@
+// 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 com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.project.ProjectCache;
+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 ForwardedProjectListUpdateHandlerTest {
+
+  private static final String PROJECT_NAME = "someProject";
+  private static final Project.NameKey PROJECT_KEY = new Project.NameKey(PROJECT_NAME);
+  @Rule public ExpectedException exception = ExpectedException.none();
+  @Mock private ProjectCache projectCacheMock;
+  private ForwardedProjectListUpdateHandler handler;
+
+  @Before
+  public void setUp() throws Exception {
+    handler = new ForwardedProjectListUpdateHandler(projectCacheMock);
+  }
+
+  @Test
+  public void testSuccessfulAdd() throws Exception {
+    handler.update(PROJECT_NAME, false);
+    verify(projectCacheMock).onCreateProject(PROJECT_KEY);
+  };
+
+  @Test
+  public void testSuccessfulRemove() throws Exception {
+    handler.update(PROJECT_NAME, true);
+    verify(projectCacheMock).remove(PROJECT_KEY);
+  };
+
+  @Test
+  public void shouldSetAndUnsetForwardedContextOnAdd() 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(projectCacheMock)
+        .onCreateProject(PROJECT_KEY);
+
+    assertThat(Context.isForwardedEvent()).isFalse();
+    handler.update(PROJECT_NAME, false);
+    assertThat(Context.isForwardedEvent()).isFalse();
+
+    verify(projectCacheMock).onCreateProject(PROJECT_KEY);
+  }
+
+  @Test
+  public void shouldSetAndUnsetForwardedContextOnRemove() 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(projectCacheMock)
+        .remove(PROJECT_KEY);
+
+    assertThat(Context.isForwardedEvent()).isFalse();
+    handler.update(PROJECT_NAME, true);
+    assertThat(Context.isForwardedEvent()).isFalse();
+
+    verify(projectCacheMock).remove(PROJECT_KEY);
+  }
+
+  @Test
+  public void shouldSetAndUnsetForwardedContextEvenIfExceptionIsThrownOnAdd() throws Exception {
+    doAnswer(
+            (Answer<Void>)
+                invocation -> {
+                  assertThat(Context.isForwardedEvent()).isTrue();
+                  throw new RuntimeException("someMessage");
+                })
+        .when(projectCacheMock)
+        .onCreateProject(PROJECT_KEY);
+
+    assertThat(Context.isForwardedEvent()).isFalse();
+    try {
+      handler.update(PROJECT_NAME, false);
+      fail("should have thrown a RuntimeException");
+    } catch (RuntimeException e) {
+      assertThat(e.getMessage()).isEqualTo("someMessage");
+    }
+    assertThat(Context.isForwardedEvent()).isFalse();
+
+    verify(projectCacheMock).onCreateProject(PROJECT_KEY);
+  }
+
+  @Test
+  public void shouldSetAndUnsetForwardedContextEvenIfExceptionIsThrownOnRemove() throws Exception {
+    doAnswer(
+            (Answer<Void>)
+                invocation -> {
+                  assertThat(Context.isForwardedEvent()).isTrue();
+                  throw new RuntimeException("someMessage");
+                })
+        .when(projectCacheMock)
+        .remove(PROJECT_KEY);
+
+    assertThat(Context.isForwardedEvent()).isFalse();
+    try {
+      handler.update(PROJECT_NAME, true);
+      ;
+      fail("should have thrown a RuntimeException");
+    } catch (RuntimeException e) {
+      assertThat(e.getMessage()).isEqualTo("someMessage");
+    }
+    assertThat(Context.isForwardedEvent()).isFalse();
+
+    verify(projectCacheMock).remove(PROJECT_KEY);
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/EventRestApiServletTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/EventRestApiServletTest.java
index 3774059..feda4a4 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/EventRestApiServletTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/EventRestApiServletTest.java
@@ -29,9 +29,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.events.EventTypes;
 import com.google.gerrit.server.events.RefEvent;
-import com.google.gwtorm.client.KeyUtil;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.StandardKeyEncoder;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.StringReader;
@@ -56,7 +54,6 @@
   @BeforeClass
   public static void setup() {
     EventTypes.register(RefReplicationDoneEvent.TYPE, RefReplicationDoneEvent.class);
-    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
   }
 
   @Before
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 16f7f21..fbfbfcf 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
@@ -22,15 +22,13 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexAccountHandler;
+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 com.google.gwtorm.client.KeyUtil;
-import com.google.gwtorm.server.StandardKeyEncoder;
 import java.io.IOException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -38,31 +36,27 @@
 
 @RunWith(MockitoJUnitRunner.class)
 public class IndexAccountRestApiServletTest {
-  private static final String ACCOUNT_NUMBER = "1";
+  private static final int ACCOUNT_NUMBER = 1;
 
-  @Mock private AccountIndexer indexerMock;
+  @Mock private ForwardedIndexAccountHandler handlerMock;
   @Mock private HttpServletRequest requestMock;
   @Mock private HttpServletResponse responseMock;
 
   private Account.Id id;
   private IndexAccountRestApiServlet servlet;
 
-  @BeforeClass
-  public static void setup() {
-    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
-  }
-
   @Before
   public void setUpMocks() {
-    servlet = new IndexAccountRestApiServlet(indexerMock);
-    id = Account.Id.tryParse(ACCOUNT_NUMBER).get();
-    when(requestMock.getPathInfo()).thenReturn("/index/account/" + ACCOUNT_NUMBER);
+    servlet = new IndexAccountRestApiServlet(handlerMock);
+    id = new Account.Id(ACCOUNT_NUMBER);
+    when(requestMock.getRequestURI())
+        .thenReturn("http://gerrit.com/index/account/" + ACCOUNT_NUMBER);
   }
 
   @Test
   public void accountIsIndexed() throws Exception {
     servlet.doPost(requestMock, responseMock);
-    verify(indexerMock, times(1)).index(id);
+    verify(handlerMock, times(1)).index(id, Operation.INDEX);
     verify(responseMock).setStatus(SC_NO_CONTENT);
   }
 
@@ -74,14 +68,14 @@
 
   @Test
   public void indexerThrowsIOExceptionTryingToIndexAccount() throws Exception {
-    doThrow(new IOException("io-error")).when(indexerMock).index(id);
+    doThrow(new IOException("io-error")).when(handlerMock).index(id, Operation.INDEX);
     servlet.doPost(requestMock, responseMock);
     verify(responseMock).sendError(SC_CONFLICT, "io-error");
   }
 
   @Test
   public void sendErrorThrowsIOException() throws Exception {
-    doThrow(new IOException("io-error")).when(indexerMock).index(id);
+    doThrow(new IOException("io-error")).when(handlerMock).index(id, Operation.INDEX);
     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 4348e27..7e8e2c0 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
@@ -18,27 +18,17 @@
 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
 import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
 import static org.mockito.Mockito.doThrow;
-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.google.gerrit.common.TimeUtil;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.server.ChangeAccess;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.index.change.ChangeIndexer;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gwtorm.client.KeyUtil;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexChangeHandler;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexingHandler.Operation;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.gwtorm.server.StandardKeyEncoder;
 import java.io.IOException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -46,133 +36,57 @@
 
 @RunWith(MockitoJUnitRunner.class)
 public class IndexChangeRestApiServletTest {
-  private static final boolean CHANGE_EXISTS = true;
-  private static final boolean CHANGE_DOES_NOT_EXIST = false;
-  private static final boolean DO_NOT_THROW_IO_EXCEPTION = false;
-  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 String CHANGE_NUMBER = "1";
+  private static final int CHANGE_NUMBER = 1;
+  private static final String PROJECT_NAME = "test/project";
+  private static final String PROJECT_NAME_URL_ENC = "test%2Fproject";
+  private static final String CHANGE_ID = PROJECT_NAME + "~" + CHANGE_NUMBER;
 
-  @Mock private ChangeIndexer indexerMock;
-  @Mock private SchemaFactory<ReviewDb> schemaFactoryMock;
-  @Mock private ReviewDb dbMock;
+  @Mock private ForwardedIndexChangeHandler handlerMock;
   @Mock private HttpServletRequest requestMock;
   @Mock private HttpServletResponse responseMock;
-  private Change.Id id;
-  private Change change;
-  private IndexChangeRestApiServlet indexRestApiServlet;
 
-  @BeforeClass
-  public static void setup() {
-    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
-  }
+  private IndexChangeRestApiServlet servlet;
 
   @Before
   public void setUpMocks() {
-    indexRestApiServlet = new IndexChangeRestApiServlet(indexerMock, schemaFactoryMock);
-    id = Change.Id.parse(CHANGE_NUMBER);
-    when(requestMock.getPathInfo()).thenReturn("/index/change/" + CHANGE_NUMBER);
-    change = new Change(null, id, null, null, TimeUtil.nowTs());
+    servlet = new IndexChangeRestApiServlet(handlerMock);
+    when(requestMock.getRequestURI())
+        .thenReturn("http://gerrit.com/index/change/" + PROJECT_NAME_URL_ENC + "~" + CHANGE_NUMBER);
   }
 
   @Test
   public void changeIsIndexed() throws Exception {
-    setupPostMocks(CHANGE_EXISTS);
-    indexRestApiServlet.doPost(requestMock, responseMock);
-    verify(indexerMock, times(1)).index(dbMock, change);
+    servlet.doPost(requestMock, responseMock);
+    verify(handlerMock, times(1)).index(CHANGE_ID, Operation.INDEX);
     verify(responseMock).setStatus(SC_NO_CONTENT);
   }
 
   @Test
-  public void changeToIndexDoNotExist() throws Exception {
-    setupPostMocks(CHANGE_DOES_NOT_EXIST);
-    indexRestApiServlet.doPost(requestMock, responseMock);
-    verify(indexerMock, times(1)).delete(id);
-    verify(responseMock).setStatus(SC_NO_CONTENT);
-  }
-
-  @Test
-  public void schemaThrowsExceptionWhenLookingUpForChange() throws Exception {
-    setupPostMocks(CHANGE_EXISTS, THROW_ORM_EXCEPTION);
-    indexRestApiServlet.doPost(requestMock, responseMock);
-    verify(responseMock).sendError(SC_NOT_FOUND, "Error trying to find change \n");
-  }
-
-  @Test
-  public void indexerThrowsNoSuchChangeExceptionTryingToPostChange() throws Exception {
-    doThrow(new NoSuchChangeException(id)).when(schemaFactoryMock).open();
-    indexRestApiServlet.doPost(requestMock, responseMock);
-    verify(indexerMock, times(1)).delete(id);
-    verify(responseMock).setStatus(SC_NO_CONTENT);
-  }
-
-  @Test
-  public void indexerThrowsNestedNoSuchChangeExceptionTryingToPostChange() throws Exception {
-    OrmException e = new OrmException("test", new NoSuchChangeException(id));
-    doThrow(e).when(schemaFactoryMock).open();
-    indexRestApiServlet.doPost(requestMock, responseMock);
-    verify(indexerMock, times(1)).delete(id);
+  public void changeIsDeletedFromIndex() throws Exception {
+    servlet.doDelete(requestMock, responseMock);
+    verify(handlerMock, times(1)).index(CHANGE_ID, Operation.DELETE);
     verify(responseMock).setStatus(SC_NO_CONTENT);
   }
 
   @Test
   public void indexerThrowsIOExceptionTryingToIndexChange() throws Exception {
-    setupPostMocks(CHANGE_EXISTS, DO_NOT_THROW_ORM_EXCEPTION, THROW_IO_EXCEPTION);
-    indexRestApiServlet.doPost(requestMock, responseMock);
+    doThrow(new IOException("io-error")).when(handlerMock).index(CHANGE_ID, Operation.INDEX);
+    servlet.doPost(requestMock, responseMock);
     verify(responseMock).sendError(SC_CONFLICT, "io-error");
   }
 
   @Test
-  public void changeIsDeletedFromIndex() throws Exception {
-    indexRestApiServlet.doDelete(requestMock, responseMock);
-    verify(indexerMock, times(1)).delete(id);
-    verify(responseMock).setStatus(SC_NO_CONTENT);
-  }
-
-  @Test
-  public void indexerThrowsExceptionTryingToDeleteChange() throws Exception {
-    doThrow(new IOException("io-error")).when(indexerMock).delete(id);
-    indexRestApiServlet.doDelete(requestMock, responseMock);
-    verify(responseMock).sendError(SC_CONFLICT, "io-error");
+  public void indexerThrowsOrmExceptionTryingToIndexChange() throws Exception {
+    doThrow(new OrmException("some message")).when(handlerMock).index(CHANGE_ID, Operation.INDEX);
+    servlet.doPost(requestMock, responseMock);
+    verify(responseMock).sendError(SC_NOT_FOUND, "Error trying to find change");
   }
 
   @Test
   public void sendErrorThrowsIOException() throws Exception {
-    doThrow(new IOException("someError"))
-        .when(responseMock)
-        .sendError(SC_NOT_FOUND, "Error trying to find change \n");
-    setupPostMocks(CHANGE_EXISTS, THROW_ORM_EXCEPTION);
-    indexRestApiServlet.doPost(requestMock, responseMock);
-    verify(responseMock).sendError(SC_NOT_FOUND, "Error trying to find change \n");
-    verifyZeroInteractions(indexerMock);
-  }
-
-  private void setupPostMocks(boolean changeExist) throws Exception {
-    setupPostMocks(changeExist, DO_NOT_THROW_ORM_EXCEPTION, DO_NOT_THROW_IO_EXCEPTION);
-  }
-
-  private void setupPostMocks(boolean changeExist, boolean ormException)
-      throws OrmException, IOException {
-    setupPostMocks(changeExist, ormException, DO_NOT_THROW_IO_EXCEPTION);
-  }
-
-  private void setupPostMocks(boolean changeExist, boolean ormException, boolean ioException)
-      throws OrmException, IOException {
-    if (ormException) {
-      doThrow(new OrmException("")).when(schemaFactoryMock).open();
-    } else {
-      when(schemaFactoryMock.open()).thenReturn(dbMock);
-      ChangeAccess ca = mock(ChangeAccess.class);
-      when(dbMock.changes()).thenReturn(ca);
-      if (changeExist) {
-        when(ca.get(id)).thenReturn(change);
-        if (ioException) {
-          doThrow(new IOException("io-error")).when(indexerMock).index(dbMock, change);
-        }
-      } else {
-        when(ca.get(id)).thenReturn(null);
-      }
-    }
+    doThrow(new IOException("io-error")).when(handlerMock).index(CHANGE_ID, Operation.INDEX);
+    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 ffdd93e..278b157 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
@@ -22,15 +22,13 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexGroupHandler;
+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 com.google.gwtorm.client.KeyUtil;
-import com.google.gwtorm.server.StandardKeyEncoder;
 import java.io.IOException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -40,29 +38,24 @@
 public class IndexGroupRestApiServletTest {
   private static final String UUID = "we235jdf92nfj2351";
 
-  @Mock private GroupIndexer indexerMock;
+  @Mock private ForwardedIndexGroupHandler handlerMock;
   @Mock private HttpServletRequest requestMock;
   @Mock private HttpServletResponse responseMock;
 
   private AccountGroup.UUID uuid;
   private IndexGroupRestApiServlet servlet;
 
-  @BeforeClass
-  public static void setup() {
-    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
-  }
-
   @Before
   public void setUpMocks() {
-    servlet = new IndexGroupRestApiServlet(indexerMock);
-    uuid = AccountGroup.UUID.parse(UUID);
-    when(requestMock.getPathInfo()).thenReturn("/index/group/" + UUID);
+    servlet = new IndexGroupRestApiServlet(handlerMock);
+    uuid = new AccountGroup.UUID(UUID);
+    when(requestMock.getRequestURI()).thenReturn("http://gerrit.com/index/group/" + UUID);
   }
 
   @Test
   public void groupIsIndexed() throws Exception {
     servlet.doPost(requestMock, responseMock);
-    verify(indexerMock, times(1)).index(uuid);
+    verify(handlerMock, times(1)).index(uuid, Operation.INDEX);
     verify(responseMock).setStatus(SC_NO_CONTENT);
   }
 
@@ -74,14 +67,14 @@
 
   @Test
   public void indexerThrowsIOExceptionTryingToIndexGroup() throws Exception {
-    doThrow(new IOException("io-error")).when(indexerMock).index(uuid);
+    doThrow(new IOException("io-error")).when(handlerMock).index(uuid, Operation.INDEX);
     servlet.doPost(requestMock, responseMock);
     verify(responseMock).sendError(SC_CONFLICT, "io-error");
   }
 
   @Test
   public void sendErrorThrowsIOException() throws Exception {
-    doThrow(new IOException("io-error")).when(indexerMock).index(uuid);
+    doThrow(new IOException("io-error")).when(handlerMock).index(uuid, Operation.INDEX);
     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/ProjectListRestApiServletIT.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/ProjectListRestApiServletIT.java
new file mode 100644
index 0000000..65e2d5d
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/ProjectListRestApiServletIT.java
@@ -0,0 +1,53 @@
+// 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 com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.reviewdb.client.Project;
+import org.junit.Test;
+
+@NoHttpd
+@TestPlugin(
+  name = "high-availability",
+  sysModule = "com.ericsson.gerrit.plugins.highavailability.Module",
+  httpModule = "com.ericsson.gerrit.plugins.highavailability.HttpModule"
+)
+public class ProjectListRestApiServletIT extends LightweightPluginDaemonTest {
+  @Test
+  @UseLocalDisk
+  public void addProject() throws Exception {
+    Project.NameKey newProject = new Project.NameKey("someNewProject");
+    assertThat(projectCache.all()).doesNotContain(newProject);
+    adminRestSession
+        .post("/plugins/high-availability/cache/project_list/" + newProject.get())
+        .assertNoContent();
+    assertThat(projectCache.all()).contains(newProject);
+  }
+
+  @Test
+  @UseLocalDisk
+  public void removeProject() throws Exception {
+    assertThat(projectCache.all()).contains(project);
+    adminRestSession
+        .delete("/plugins/high-availability/cache/project_list/" + project.get())
+        .assertNoContent();
+    assertThat(projectCache.all()).doesNotContain(project);
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/ProjectListRestApiServletTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/ProjectListRestApiServletTest.java
new file mode 100644
index 0000000..fa0a3ed
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/ProjectListRestApiServletTest.java
@@ -0,0 +1,60 @@
+// 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_NO_CONTENT;
+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.ForwardedProjectListUpdateHandler;
+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 ProjectListRestApiServletTest {
+  private static final String PROJECT_NAME = "someProject";
+
+  @Mock private ForwardedProjectListUpdateHandler handlerMock;
+  @Mock private HttpServletRequest requestMock;
+  @Mock private HttpServletResponse responseMock;
+
+  private ProjectListApiServlet servlet;
+
+  @Before
+  public void setUpMocks() {
+    servlet = new ProjectListApiServlet(handlerMock);
+    when(requestMock.getPathInfo()).thenReturn("/cache/project_list/" + PROJECT_NAME);
+  }
+
+  @Test
+  public void addProject() throws Exception {
+    servlet.doPost(requestMock, responseMock);
+    verify(handlerMock, times(1)).update(PROJECT_NAME, false);
+    verify(responseMock).setStatus(SC_NO_CONTENT);
+  }
+
+  @Test
+  public void deleteProject() throws Exception {
+    servlet.doDelete(requestMock, responseMock);
+    verify(handlerMock, times(1)).update(PROJECT_NAME, true);
+    verify(responseMock).setStatus(SC_NO_CONTENT);
+  }
+}
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 d4efd03..d1805d3 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
@@ -28,12 +28,9 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.events.Event;
 import com.google.gson.GsonBuilder;
-import com.google.gwtorm.client.KeyUtil;
-import com.google.gwtorm.server.StandardKeyEncoder;
 import java.io.IOException;
 import javax.net.ssl.SSLException;
 import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.mockito.Answers;
 
@@ -43,10 +40,16 @@
   private static final boolean SUCCESSFUL = true;
   private static final boolean FAILED = false;
 
-  //Index
+  // Index
   private static final int CHANGE_NUMBER = 1;
+  private static final String PROJECT_NAME = "test/project";
+  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", CHANGE_NUMBER);
+      Joiner.on("/")
+          .join(
+              "/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);
   private static final int ACCOUNT_NUMBER = 2;
   private static final String INDEX_ACCOUNT_ENDPOINT =
       Joiner.on("/").join("/plugins", PLUGIN_NAME, "index/account", ACCOUNT_NUMBER);
@@ -54,7 +57,7 @@
   private static final String INDEX_GROUP_ENDPOINT =
       Joiner.on("/").join("/plugins", PLUGIN_NAME, "index/group", UUID);
 
-  //Event
+  // Event
   private static final String EVENT_ENDPOINT =
       Joiner.on("/").join("/plugins", PLUGIN_NAME, "event");
   private static Event event = new Event("test-event") {};
@@ -63,11 +66,6 @@
   private RestForwarder forwarder;
   private HttpSession httpSessionMock;
 
-  @BeforeClass
-  public static void setup() {
-    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
-  }
-
   @Before
   public void setUp() {
     httpSessionMock = mock(HttpSession.class);
@@ -120,38 +118,38 @@
   public void testIndexChangeOK() throws Exception {
     when(httpSessionMock.post(INDEX_CHANGE_ENDPOINT))
         .thenReturn(new HttpResult(SUCCESSFUL, EMPTY_MSG));
-    assertThat(forwarder.indexChange(CHANGE_NUMBER)).isTrue();
+    assertThat(forwarder.indexChange(PROJECT_NAME, CHANGE_NUMBER)).isTrue();
   }
 
   @Test
   public void testIndexChangeFailed() throws Exception {
     when(httpSessionMock.post(INDEX_CHANGE_ENDPOINT)).thenReturn(new HttpResult(FAILED, EMPTY_MSG));
-    assertThat(forwarder.indexChange(CHANGE_NUMBER)).isFalse();
+    assertThat(forwarder.indexChange(PROJECT_NAME, CHANGE_NUMBER)).isFalse();
   }
 
   @Test
   public void testIndexChangeThrowsException() throws Exception {
     doThrow(new IOException()).when(httpSessionMock).post(INDEX_CHANGE_ENDPOINT);
-    assertThat(forwarder.indexChange(CHANGE_NUMBER)).isFalse();
+    assertThat(forwarder.indexChange(PROJECT_NAME, CHANGE_NUMBER)).isFalse();
   }
 
   @Test
   public void testChangeDeletedFromIndexOK() throws Exception {
-    when(httpSessionMock.delete(INDEX_CHANGE_ENDPOINT))
+    when(httpSessionMock.delete(DELETE_CHANGE_ENDPOINT))
         .thenReturn(new HttpResult(SUCCESSFUL, EMPTY_MSG));
     assertThat(forwarder.deleteChangeFromIndex(CHANGE_NUMBER)).isTrue();
   }
 
   @Test
   public void testChangeDeletedFromIndexFailed() throws Exception {
-    when(httpSessionMock.delete(INDEX_CHANGE_ENDPOINT))
+    when(httpSessionMock.delete(DELETE_CHANGE_ENDPOINT))
         .thenReturn(new HttpResult(FAILED, EMPTY_MSG));
     assertThat(forwarder.deleteChangeFromIndex(CHANGE_NUMBER)).isFalse();
   }
 
   @Test
   public void testChangeDeletedFromThrowsException() throws Exception {
-    doThrow(new IOException()).when(httpSessionMock).delete(INDEX_CHANGE_ENDPOINT);
+    doThrow(new IOException()).when(httpSessionMock).delete(DELETE_CHANGE_ENDPOINT);
     assertThat(forwarder.deleteChangeFromIndex(CHANGE_NUMBER)).isFalse();
   }
 
@@ -186,7 +184,7 @@
 
   @Test
   public void testEvictAccountsOK() throws Exception {
-    Account.Id key = Account.Id.tryParse("123").get();
+    Account.Id key = new Account.Id(123);
     String keyJson = new GsonBuilder().create().toJson(key);
     when(httpSessionMock.post(buildCacheEndpoint(Constants.ACCOUNTS), keyJson))
         .thenReturn(new HttpResult(SUCCESSFUL, EMPTY_MSG));
@@ -195,7 +193,7 @@
 
   @Test
   public void testEvictGroupsOK() throws Exception {
-    AccountGroup.Id key = AccountGroup.Id.parse("123");
+    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));
@@ -204,7 +202,7 @@
 
   @Test
   public void testEvictGroupsByIncludeOK() throws Exception {
-    AccountGroup.UUID key = AccountGroup.UUID.parse("90b3042d9094a37985f3f9281391dbbe9a5addad");
+    AccountGroup.UUID key = new AccountGroup.UUID("90b3042d9094a37985f3f9281391dbbe9a5addad");
     String keyJson = new GsonBuilder().create().toJson(key);
     when(httpSessionMock.post(buildCacheEndpoint(Constants.GROUPS_BYINCLUDE), keyJson))
         .thenReturn(new HttpResult(SUCCESSFUL, EMPTY_MSG));
@@ -213,7 +211,7 @@
 
   @Test
   public void testEvictGroupsMembersOK() throws Exception {
-    AccountGroup.UUID key = AccountGroup.UUID.parse("90b3042d9094a37985f3f9281391dbbe9a5addad");
+    AccountGroup.UUID key = new AccountGroup.UUID("90b3042d9094a37985f3f9281391dbbe9a5addad");
     String keyJson = new GsonBuilder().create().toJson(key);
     when(httpSessionMock.post(buildCacheEndpoint(Constants.GROUPS_MEMBERS), keyJson))
         .thenReturn(new HttpResult(SUCCESSFUL, EMPTY_MSG));
@@ -221,15 +219,6 @@
   }
 
   @Test
-  public void testEvictProjectListOK() throws Exception {
-    String key = "all";
-    String keyJson = new GsonBuilder().create().toJson(key);
-    when(httpSessionMock.post(buildCacheEndpoint(Constants.PROJECT_LIST), keyJson))
-        .thenReturn(new HttpResult(SUCCESSFUL, EMPTY_MSG));
-    assertThat(forwarder.evict(Constants.PROJECT_LIST, key)).isTrue();
-  }
-
-  @Test
   public void testEvictCacheFailed() throws Exception {
     String key = "projectName";
     String keyJson = new GsonBuilder().create().toJson(key);
@@ -253,6 +242,60 @@
   }
 
   @Test
+  public void testAddToProjectListOK() throws Exception {
+    String projectName = "projectToAdd";
+    when(httpSessionMock.post(buildProjectListCacheEndpoint(projectName)))
+        .thenReturn(new HttpResult(SUCCESSFUL, EMPTY_MSG));
+    assertThat(forwarder.addToProjectList(projectName)).isTrue();
+  }
+
+  @Test
+  public void testAddToProjectListFailed() throws Exception {
+    String projectName = "projectToAdd";
+    when(httpSessionMock.post(buildProjectListCacheEndpoint(projectName)))
+        .thenReturn(new HttpResult(FAILED, EMPTY_MSG));
+    assertThat(forwarder.addToProjectList(projectName)).isFalse();
+  }
+
+  @Test
+  public void testAddToProjectListThrowsException() throws Exception {
+    String projectName = "projectToAdd";
+    doThrow(new IOException())
+        .when(httpSessionMock)
+        .post((buildProjectListCacheEndpoint(projectName)));
+    assertThat(forwarder.addToProjectList(projectName)).isFalse();
+  }
+
+  @Test
+  public void testRemoveFromProjectListOK() throws Exception {
+    String projectName = "projectToDelete";
+    when(httpSessionMock.delete(buildProjectListCacheEndpoint(projectName)))
+        .thenReturn(new HttpResult(SUCCESSFUL, EMPTY_MSG));
+    assertThat(forwarder.removeFromProjectList(projectName)).isTrue();
+  }
+
+  @Test
+  public void testRemoveToProjectListFailed() throws Exception {
+    String projectName = "projectToDelete";
+    when(httpSessionMock.delete(buildProjectListCacheEndpoint(projectName)))
+        .thenReturn(new HttpResult(FAILED, EMPTY_MSG));
+    assertThat(forwarder.removeFromProjectList(projectName)).isFalse();
+  }
+
+  @Test
+  public void testRemoveToProjectListThrowsException() throws Exception {
+    String projectName = "projectToDelete";
+    doThrow(new IOException())
+        .when(httpSessionMock)
+        .delete((buildProjectListCacheEndpoint(projectName)));
+    assertThat(forwarder.removeFromProjectList(projectName)).isFalse();
+  }
+
+  private String buildProjectListCacheEndpoint(String projectName) {
+    return Joiner.on("/").join(buildCacheEndpoint(Constants.PROJECT_LIST), projectName);
+  }
+
+  @Test
   public void testRetryOnErrorThenSuccess() throws IOException {
     when(httpSessionMock.post(anyString(), anyString()))
         .thenReturn(new HttpResult(false, "Error"))
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/AbstractIndexForwardingIT.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/AbstractIndexForwardingIT.java
index cdf7d51..2fc53e4 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/AbstractIndexForwardingIT.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/AbstractIndexForwardingIT.java
@@ -15,6 +15,8 @@
 package com.ericsson.gerrit.plugins.highavailability.index;
 
 import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.any;
+import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
 import static com.github.tomakehurst.wiremock.client.WireMock.givenThat;
 import static com.github.tomakehurst.wiremock.client.WireMock.post;
 import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
@@ -32,7 +34,6 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import org.apache.http.HttpStatus;
-import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
@@ -48,31 +49,22 @@
   private static final int PORT = 18889;
   private static final String URL = "http://localhost:" + PORT;
 
-  @Rule public WireMockRule wireMockRule = new WireMockRule(options().port(PORT), false);
+  @Rule public WireMockRule wireMockRule = new WireMockRule(options().port(PORT));
 
-  @Before
-  public void before() throws Exception {
-    setup();
+  @Override
+  public void setUpTestPlugin() throws Exception {
+    givenThat(any(anyUrl()).willReturn(aResponse().withStatus(HttpStatus.SC_NO_CONTENT)));
+    beforeAction();
+    super.setUpTestPlugin();
   }
 
   @Test
   @UseLocalDisk
-  @GlobalPluginConfig(
-    pluginName = "high-availability",
-    name = "peerInfo.strategy",
-    value = "static"
-  )
   @GlobalPluginConfig(pluginName = "high-availability", name = "peerInfo.static.url", value = URL)
-  @GlobalPluginConfig(pluginName = "high-availability", name = "http.user", value = "admin")
-  @GlobalPluginConfig(pluginName = "high-availability", name = "index.threadPoolSize", value = "10")
-  @GlobalPluginConfig(
-    pluginName = "high-availability",
-    name = "main.sharedDirectory",
-    value = "directory"
-  )
+  @GlobalPluginConfig(pluginName = "high-availability", name = "http.retryInterval", value = "100")
   public void testIndexForwarding() throws Exception {
-    final String expectedRequest = getExpectedRequest();
-    final CountDownLatch expectedRequestLatch = new CountDownLatch(1);
+    String expectedRequest = getExpectedRequest();
+    CountDownLatch expectedRequestLatch = new CountDownLatch(1);
     wireMockRule.addMockServiceRequestListener(
         (request, response) -> {
           if (request.getAbsoluteUrl().contains(expectedRequest)) {
@@ -88,7 +80,7 @@
   }
 
   /** Perform pre-test setup. */
-  protected abstract void setup() throws Exception;
+  protected abstract void beforeAction() throws Exception;
 
   /**
    * Get the URL on which a request is expected.
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/AccountIndexForwardingIT.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/AccountIndexForwardingIT.java
index f59c199..4b2462e 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/AccountIndexForwardingIT.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/AccountIndexForwardingIT.java
@@ -20,7 +20,7 @@
   private TestAccount testAccount;
 
   @Override
-  public void setup() throws Exception {
+  public void beforeAction() throws Exception {
     testAccount = accountCreator.create("someUser");
   }
 
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeIndexForwardingIT.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeIndexForwardingIT.java
index 3a00cad..f80f56f 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeIndexForwardingIT.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeIndexForwardingIT.java
@@ -18,13 +18,13 @@
   private int changeId;
 
   @Override
-  public void setup() throws Exception {
+  public void beforeAction() throws Exception {
     changeId = createChange().getChange().getId().get();
   }
 
   @Override
   public String getExpectedRequest() {
-    return "/plugins/high-availability/index/change/" + changeId;
+    return "/plugins/high-availability/index/change/" + project.get() + "~" + changeId;
   }
 
   @Override
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/GroupIndexForwardingIT.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/GroupIndexForwardingIT.java
index aa3a706..74da06a 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/GroupIndexForwardingIT.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/GroupIndexForwardingIT.java
@@ -16,12 +16,10 @@
 
 public class GroupIndexForwardingIT extends AbstractIndexForwardingIT {
   private String someGroupId;
-  private String someOtherGroupId;
 
   @Override
-  public void setup() throws Exception {
+  public void beforeAction() throws Exception {
     someGroupId = gApi.groups().create("someGroup").get().id;
-    someOtherGroupId = gApi.groups().create("someOtherGroup").get().id;
   }
 
   @Override
@@ -31,6 +29,6 @@
 
   @Override
   public void doAction() throws Exception {
-    gApi.groups().id(someGroupId).addGroups(someOtherGroupId);
+    gApi.groups().id(someGroupId).index();
   }
 }
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 0bfbea8..1a60add 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
@@ -29,11 +29,8 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwtorm.client.KeyUtil;
-import com.google.gwtorm.server.StandardKeyEncoder;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -42,6 +39,7 @@
 @RunWith(MockitoJUnitRunner.class)
 public class IndexEventHandlerTest {
   private static final String PLUGIN_NAME = "high-availability";
+  private static final String PROJECT_NAME = "test/project";
   private static final int CHANGE_ID = 1;
   private static final int ACCOUNT_ID = 2;
   private static final String UUID = "3";
@@ -53,24 +51,19 @@
   private Account.Id accountId;
   private AccountGroup.UUID accountGroupUUID;
 
-  @BeforeClass
-  public static void setUp() {
-    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
-  }
-
   @Before
   public void setUpMocks() {
-    changeId = Change.Id.parse(Integer.toString(CHANGE_ID));
-    accountId = Account.Id.tryParse(Integer.toString(ACCOUNT_ID)).get();
-    accountGroupUUID = AccountGroup.UUID.parse(UUID);
+    changeId = new Change.Id(CHANGE_ID);
+    accountId = new Account.Id(ACCOUNT_ID);
+    accountGroupUUID = new AccountGroup.UUID(UUID);
     indexEventHandler =
         new IndexEventHandler(MoreExecutors.directExecutor(), PLUGIN_NAME, forwarder);
   }
 
   @Test
   public void shouldIndexInRemoteOnChangeIndexedEvent() throws Exception {
-    indexEventHandler.onChangeIndexed(changeId.get());
-    verify(forwarder).indexChange(CHANGE_ID);
+    indexEventHandler.onChangeIndexed(PROJECT_NAME, changeId.get());
+    verify(forwarder).indexChange(PROJECT_NAME, CHANGE_ID);
   }
 
   @Test
@@ -94,7 +87,7 @@
   @Test
   public void shouldNotCallRemoteWhenChangeEventIsForwarded() throws Exception {
     Context.setForwardedEvent(true);
-    indexEventHandler.onChangeIndexed(changeId.get());
+    indexEventHandler.onChangeIndexed(PROJECT_NAME, changeId.get());
     indexEventHandler.onChangeDeleted(changeId.get());
     Context.unsetForwardedEvent();
     verifyZeroInteractions(forwarder);
@@ -122,9 +115,10 @@
   public void duplicateChangeEventOfAQueuedEventShouldGetDiscarded() {
     ScheduledThreadPoolExecutor poolMock = mock(ScheduledThreadPoolExecutor.class);
     indexEventHandler = new IndexEventHandler(poolMock, PLUGIN_NAME, forwarder);
-    indexEventHandler.onChangeIndexed(changeId.get());
-    indexEventHandler.onChangeIndexed(changeId.get());
-    verify(poolMock, times(1)).execute(indexEventHandler.new IndexChangeTask(CHANGE_ID, false));
+    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));
   }
 
   @Test
@@ -147,7 +141,7 @@
 
   @Test
   public void testIndexChangeTaskToString() throws Exception {
-    IndexChangeTask task = indexEventHandler.new IndexChangeTask(CHANGE_ID, false);
+    IndexChangeTask task = indexEventHandler.new IndexChangeTask(PROJECT_NAME, CHANGE_ID, false);
     assertThat(task.toString())
         .isEqualTo(
             String.format("[%s] Index change %s in target instance", PLUGIN_NAME, CHANGE_ID));
@@ -170,25 +164,29 @@
 
   @Test
   public void testIndexChangeTaskHashCodeAndEquals() {
-    IndexChangeTask task = indexEventHandler.new IndexChangeTask(CHANGE_ID, false);
+    IndexChangeTask task = indexEventHandler.new IndexChangeTask(PROJECT_NAME, CHANGE_ID, false);
 
     IndexChangeTask sameTask = task;
     assertThat(task.equals(sameTask)).isTrue();
     assertThat(task.hashCode()).isEqualTo(sameTask.hashCode());
 
-    IndexChangeTask identicalTask = indexEventHandler.new IndexChangeTask(CHANGE_ID, false);
+    IndexChangeTask identicalTask =
+        indexEventHandler.new IndexChangeTask(PROJECT_NAME, CHANGE_ID, false);
     assertThat(task.equals(identicalTask)).isTrue();
     assertThat(task.hashCode()).isEqualTo(identicalTask.hashCode());
 
     assertThat(task.equals(null)).isFalse();
-    assertThat(task.equals(indexEventHandler.new IndexChangeTask(CHANGE_ID + 1, false))).isFalse();
+    assertThat(
+            task.equals(indexEventHandler.new IndexChangeTask(PROJECT_NAME, CHANGE_ID + 1, false)))
+        .isFalse();
     assertThat(task.hashCode()).isNotEqualTo("test".hashCode());
 
-    IndexChangeTask differentChangeIdTask = indexEventHandler.new IndexChangeTask(123, false);
+    IndexChangeTask differentChangeIdTask =
+        indexEventHandler.new IndexChangeTask(PROJECT_NAME, 123, false);
     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);
     assertThat(task.equals(removeTask)).isFalse();
     assertThat(task.hashCode()).isNotEqualTo(removeTask.hashCode());
   }
diff --git a/tools/sonar/sonar.sh b/tools/sonar/sonar.sh
new file mode 100755
index 0000000..39df185
--- /dev/null
+++ b/tools/sonar/sonar.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+# 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.
+`bazel query @com_googlesource_gerrit_bazlets//tools/sonar:sonar --output location | sed s/BUILD:.*//`sonar.py