Merge branch 'stable-2.15' into stable-2.16

* stable-2.15:
  Adapt to the latest Gerrit on stable-2.14

Change-Id: I53aad6f93e01561340d2f8c05ec92b85770600fc
diff --git a/.bazelversion b/.bazelversion
index 9084fa2..fd2a018 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-1.1.0
+3.1.0
diff --git a/WORKSPACE b/WORKSPACE
index 61156a9..fd2a0c0 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -3,7 +3,7 @@
 load("//:bazlets.bzl", "load_bazlets")
 
 load_bazlets(
-    commit = "f53f51fb660552d0581aa0ba52c3836ed63d56a3",
+    commit = "c40247e30381ddc4a90ef99d749a9161ac4d064b",
     #local_path = "/home/<user>/projects/bazlets",
 )
 
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
index 2aecc68..12bf919 100644
--- a/external_plugin_deps.bzl
+++ b/external_plugin_deps.bzl
@@ -3,8 +3,8 @@
 def external_plugin_deps():
     maven_jar(
         name = "wiremock",
-        artifact = "com.github.tomakehurst:wiremock-standalone:2.25.1",
-        sha1 = "bf46d41a76c274522a8b88eb5b054edca630b089",
+        artifact = "com.github.tomakehurst:wiremock-standalone:2.26.3",
+        sha1 = "245c6efae2cbcb4e4f3457caf3d1c030cbaf2eb5",
     )
 
     maven_jar(
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java
index edd96d7..58a21b6 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java
@@ -21,6 +21,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.server.config.ConfigUtil;
@@ -39,12 +40,10 @@
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class Configuration {
-  private static final Logger log = LoggerFactory.getLogger(Configuration.class);
+  private static final FluentLogger log = FluentLogger.forEnclosingClass();
 
   // common parameter to peerInfo section
   static final String PEER_INFO_SECTION = "peerInfo";
@@ -153,8 +152,8 @@
     try {
       return cfg.getInt(section, name, defaultValue);
     } catch (IllegalArgumentException e) {
-      log.error("invalid value for {}; using default value {}", name, defaultValue);
-      log.debug("Failed to retrieve integer value: {}", e.getMessage(), e);
+      log.atSevere().log("invalid value for %s; using default value %d", name, defaultValue);
+      log.atFine().withCause(e).log("Failed to retrieve integer value");
       return defaultValue;
     }
   }
@@ -234,9 +233,7 @@
 
     private PeerInfo(Config cfg) {
       strategy = cfg.getEnum(PEER_INFO_SECTION, null, STRATEGY_KEY, DEFAULT_PEER_INFO_STRATEGY);
-      if (log.isDebugEnabled()) {
-        log.debug("Strategy: {}", strategy.name());
-      }
+      log.atFine().log("Strategy: %s", strategy.name());
     }
 
     public PeerInfoStrategy strategy() {
@@ -257,7 +254,7 @@
               .filter(s -> !s.isEmpty())
               .map(s -> CharMatcher.is('/').trimTrailingFrom(s))
               .collect(Collectors.toSet());
-      log.debug("Urls: {}", urls);
+      log.atFine().log("Urls: %s", urls);
     }
 
     public Set<String> urls() {
@@ -273,7 +270,7 @@
 
     private PeerInfoJGroups(Config cfg) {
       myUrl = trimTrailingSlash(cfg.getString(PEER_INFO_SECTION, JGROUPS_SUBSECTION, MY_URL_KEY));
-      log.debug("My Url: {}", myUrl);
+      log.atFine().log("My Url: %s", myUrl);
     }
 
     public String myUrl() {
@@ -302,12 +299,12 @@
     private JGroups(SitePaths site, Config cfg) {
       String[] skip = cfg.getStringList(JGROUPS_SECTION, null, SKIP_INTERFACE_KEY);
       skipInterface = skip.length == 0 ? DEFAULT_SKIP_INTERFACE_LIST : ImmutableList.copyOf(skip);
-      log.debug("Skip interface(s): {}", skipInterface);
+      log.atFine().log("Skip interface(s): %s", skipInterface);
       clusterName = getString(cfg, JGROUPS_SECTION, null, CLUSTER_NAME_KEY, DEFAULT_CLUSTER_NAME);
-      log.debug("Cluster name: {}", clusterName);
+      log.atFine().log("Cluster name: %s", clusterName);
       protocolStack = getProtocolStack(cfg, site);
-      log.debug(
-          "Protocol stack config {}",
+      log.atFine().log(
+          "Protocol stack config %s",
           protocolStack.isPresent() ? protocolStack.get() : "not configured, using default stack.");
     }
 
@@ -405,8 +402,8 @@
       try {
         return cfg.getBoolean(section, name, defaultValue);
       } catch (IllegalArgumentException e) {
-        log.error("invalid value for {}; using default value {}", name, defaultValue);
-        log.debug("Failed to retrieve boolean value: {}", e.getMessage(), e);
+        log.atSevere().log("invalid value for %s; using default value %s", name, defaultValue);
+        log.atFine().withCause(e).log("Failed to retrieve boolean value");
         return defaultValue;
       }
     }
@@ -450,12 +447,14 @@
     static final String INDEX_SECTION = "index";
     static final String MAX_TRIES_KEY = "maxTries";
     static final String RETRY_INTERVAL_KEY = "retryInterval";
+    static final String SYNCHRONIZE_FORCED_KEY = "synchronizeForced";
+    static final boolean DEFAULT_SYNCHRONIZE_FORCED = true;
 
     private final int threadPoolSize;
     private final int retryInterval;
     private final int maxTries;
-
     private final int numStripedLocks;
+    private final boolean synchronizeForced;
 
     private Index(Config cfg) {
       super(cfg, INDEX_SECTION);
@@ -463,6 +462,8 @@
       numStripedLocks = getInt(cfg, INDEX_SECTION, NUM_STRIPED_LOCKS, DEFAULT_NUM_STRIPED_LOCKS);
       retryInterval = getInt(cfg, INDEX_SECTION, RETRY_INTERVAL_KEY, DEFAULT_INDEX_RETRY_INTERVAL);
       maxTries = getInt(cfg, INDEX_SECTION, MAX_TRIES_KEY, DEFAULT_INDEX_MAX_TRIES);
+      synchronizeForced =
+          cfg.getBoolean(INDEX_SECTION, SYNCHRONIZE_FORCED_KEY, DEFAULT_SYNCHRONIZE_FORCED);
     }
 
     public int threadPoolSize() {
@@ -480,6 +481,10 @@
     public int maxTries() {
       return maxTries;
     }
+
+    public boolean synchronizeForced() {
+      return synchronizeForced;
+    }
   }
 
   public static class Websession extends Forwarding {
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Setup.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Setup.java
index 165b0dd..6635b80 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Setup.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Setup.java
@@ -105,9 +105,11 @@
     ui.message("\n");
     ui.header("%s Plugin", pluginName);
 
-    if (ui.yesno(true, "Configure %s", pluginName)) {
+    Path pluginConfigFile = site.etc_dir.resolve(pluginName + ".config");
+    boolean autoConfigure = !pluginConfigFile.toFile().exists();
+
+    if (ui.yesno(autoConfigure, "Configure %s", pluginName)) {
       ui.header("Configuring %s", pluginName);
-      Path pluginConfigFile = site.etc_dir.resolve(pluginName + ".config");
       config = new FileBasedConfig(pluginConfigFile.toFile(), FS.DETECTED);
       config.load();
       configureAutoReindexSection();
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AccountReindexRunnable.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AccountReindexRunnable.java
index 487f3da..de156a2 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AccountReindexRunnable.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AccountReindexRunnable.java
@@ -17,8 +17,10 @@
 import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexAccountHandler;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexingHandler.Operation;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.AbstractIndexRestApiServlet;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.Accounts;
 import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gwtorm.server.OrmException;
@@ -26,11 +28,9 @@
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.Optional;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
-public class AccountReindexRunnable extends ReindexRunnable<Account> {
-  private static final Logger log = LoggerFactory.getLogger(AccountReindexRunnable.class);
+public class AccountReindexRunnable extends ReindexRunnable<AccountState> {
+  private static final FluentLogger log = FluentLogger.forEnclosingClass();
 
   private final ForwardedIndexAccountHandler accountIdx;
 
@@ -48,21 +48,23 @@
   }
 
   @Override
-  protected Iterable<Account> fetchItems(ReviewDb db) throws Exception {
+  protected Iterable<AccountState> fetchItems(ReviewDb db) throws Exception {
     return accounts.all();
   }
 
   @Override
-  protected Optional<Timestamp> indexIfNeeded(ReviewDb db, Account a, Timestamp sinceTs) {
+  protected Optional<Timestamp> indexIfNeeded(ReviewDb db, AccountState as, Timestamp sinceTs) {
     try {
+      Account a = as.getAccount();
       Timestamp accountTs = a.getRegisteredOn();
       if (accountTs.after(sinceTs)) {
-        log.info("Index {}/{}/{}/{}", a.getId(), a.getFullName(), a.getPreferredEmail(), accountTs);
+        log.atInfo().log(
+            "Index %s/%s/%s/%s", a.getId(), a.getFullName(), a.getPreferredEmail(), accountTs);
         accountIdx.index(a.getId(), Operation.INDEX, Optional.empty());
         return Optional.of(accountTs);
       }
     } catch (IOException | OrmException e) {
-      log.error("Reindex failed", e);
+      log.atSevere().withCause(e).log("Reindex failed");
     }
     return Optional.empty();
   }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AutoReindexScheduler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AutoReindexScheduler.java
index c0d90d1..ef7ae86 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AutoReindexScheduler.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AutoReindexScheduler.java
@@ -15,6 +15,7 @@
 package com.ericsson.gerrit.plugins.highavailability.autoreindex;
 
 import com.ericsson.gerrit.plugins.highavailability.Configuration;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.inject.Inject;
@@ -24,16 +25,15 @@
 import java.util.concurrent.Future;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class AutoReindexScheduler implements LifecycleListener {
-  private static final Logger log = LoggerFactory.getLogger(AutoReindexScheduler.class);
+  private static final FluentLogger log = FluentLogger.forEnclosingClass();
   private final Configuration.AutoReindex cfg;
   private final ChangeReindexRunnable changeReindex;
   private final AccountReindexRunnable accountReindex;
   private final GroupReindexRunnable groupReindex;
+  private final ProjectReindexRunnable projectReindex;
   private final ScheduledExecutorService executor;
   private final List<Future<?>> futureTasks = new ArrayList<>();
 
@@ -43,18 +43,21 @@
       WorkQueue workQueue,
       ChangeReindexRunnable changeReindex,
       AccountReindexRunnable accountReindex,
-      GroupReindexRunnable groupReindex) {
+      GroupReindexRunnable groupReindex,
+      ProjectReindexRunnable projectReindex) {
     this.cfg = cfg.autoReindex();
     this.changeReindex = changeReindex;
     this.accountReindex = accountReindex;
     this.groupReindex = groupReindex;
+    this.projectReindex = projectReindex;
     this.executor = workQueue.createQueue(1, "HighAvailability-AutoReindex");
   }
 
   @Override
   public void start() {
     if (cfg.pollSec() > 0) {
-      log.info("Scheduling auto-reindex after {}s and every {}s", cfg.delaySec(), cfg.pollSec());
+      log.atInfo().log(
+          "Scheduling auto-reindex after %ds and every %ds", cfg.delaySec(), cfg.pollSec());
       futureTasks.add(
           executor.scheduleAtFixedRate(
               changeReindex, cfg.delaySec(), cfg.pollSec(), TimeUnit.SECONDS));
@@ -64,11 +67,15 @@
       futureTasks.add(
           executor.scheduleAtFixedRate(
               groupReindex, cfg.delaySec(), cfg.pollSec(), TimeUnit.SECONDS));
+      futureTasks.add(
+          executor.scheduleAtFixedRate(
+              projectReindex, cfg.delaySec(), cfg.pollSec(), TimeUnit.SECONDS));
     } else {
-      log.info("Scheduling auto-reindex after {}s", cfg.delaySec());
+      log.atInfo().log("Scheduling auto-reindex after %ds", cfg.delaySec());
       futureTasks.add(executor.schedule(changeReindex, cfg.delaySec(), TimeUnit.SECONDS));
       futureTasks.add(executor.schedule(accountReindex, cfg.delaySec(), TimeUnit.SECONDS));
       futureTasks.add(executor.schedule(groupReindex, cfg.delaySec(), TimeUnit.SECONDS));
+      futureTasks.add(executor.schedule(projectReindex, cfg.delaySec(), TimeUnit.SECONDS));
     }
   }
 
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ChangeReindexRunnable.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ChangeReindexRunnable.java
index 1f5b56e..67c8325 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ChangeReindexRunnable.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ChangeReindexRunnable.java
@@ -18,6 +18,7 @@
 import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexingHandler.Operation;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.AbstractIndexRestApiServlet;
 import com.google.common.collect.Streams;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -34,11 +35,9 @@
 import java.util.Optional;
 import java.util.stream.Stream;
 import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class ChangeReindexRunnable extends ReindexRunnable<Change> {
-  private static final Logger log = LoggerFactory.getLogger(ChangeReindexRunnable.class);
+  private static final FluentLogger log = FluentLogger.forEnclosingClass();
 
   private final ForwardedIndexChangeHandler changeIdx;
 
@@ -98,13 +97,13 @@
     try {
       Timestamp changeTs = c.getLastUpdatedOn();
       if (changeTs.after(sinceTs)) {
-        log.info(
-            "Index {}/{}/{} was updated after {}", c.getProject(), c.getId(), changeTs, sinceTs);
+        log.atInfo().log(
+            "Index %s/%s/%s was updated after %s", c.getProject(), c.getId(), changeTs, sinceTs);
         changeIdx.index(c.getProject() + "~" + c.getId(), Operation.INDEX, Optional.empty());
         return Optional.of(changeTs);
       }
     } catch (OrmException | IOException e) {
-      log.error("Reindex failed", e);
+      log.atSevere().withCause(e).log("Reindex failed");
     }
     return Optional.empty();
   }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/GroupReindexRunnable.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/GroupReindexRunnable.java
index 640457d..71a0280 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/GroupReindexRunnable.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/GroupReindexRunnable.java
@@ -14,74 +14,32 @@
 
 package com.ericsson.gerrit.plugins.highavailability.autoreindex;
 
-import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexGroupHandler;
-import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexingHandler.Operation;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.AbstractIndexRestApiServlet;
-import com.google.common.collect.Streams;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroup.Id;
-import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.group.db.Groups;
 import com.google.gerrit.server.util.OneOffRequestContext;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
-import java.io.IOException;
 import java.sql.Timestamp;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Objects;
 import java.util.Optional;
-import java.util.stream.Stream;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
-public class GroupReindexRunnable extends ReindexRunnable<AccountGroup> {
-  private static final Logger log = LoggerFactory.getLogger(GroupReindexRunnable.class);
+public class GroupReindexRunnable extends ReindexRunnable<GroupReference> {
 
-  private final ForwardedIndexGroupHandler indexer;
+  private final Groups groups;
 
   @Inject
-  public GroupReindexRunnable(
-      ForwardedIndexGroupHandler indexer, IndexTs indexTs, OneOffRequestContext ctx) {
+  public GroupReindexRunnable(IndexTs indexTs, OneOffRequestContext ctx, Groups groups) {
     super(AbstractIndexRestApiServlet.IndexName.GROUP, indexTs, ctx);
-    this.indexer = indexer;
+    this.groups = groups;
   }
 
   @Override
-  protected ResultSet<AccountGroup> fetchItems(ReviewDb db) throws OrmException {
-    return db.accountGroups().all();
+  protected Iterable<GroupReference> fetchItems(ReviewDb db) throws Exception {
+    return groups.getAllGroupReferences()::iterator;
   }
 
   @Override
-  protected Optional<Timestamp> indexIfNeeded(ReviewDb db, AccountGroup g, Timestamp sinceTs) {
-    try {
-      Id groupId = g.getId();
-      Stream<Timestamp> groupIdAudTs =
-          db.accountGroupByIdAud().byGroup(g.getId()).toList().stream()
-              .map(AccountGroupByIdAud::getRemovedOn)
-              .filter(Objects::nonNull);
-      List<AccountGroupMemberAudit> groupMembersAud =
-          db.accountGroupMembersAudit().byGroup(groupId).toList();
-      Stream<Timestamp> groupMemberAudAddedTs =
-          groupMembersAud.stream().map(ga -> ga.getKey().getAddedOn()).filter(Objects::nonNull);
-      Stream<Timestamp> groupMemberAudRemovedTs =
-          groupMembersAud.stream()
-              .map(AccountGroupMemberAudit::getRemovedOn)
-              .filter(Objects::nonNull);
-      Optional<Timestamp> groupLastTs =
-          Streams.concat(groupIdAudTs, groupMemberAudAddedTs, groupMemberAudRemovedTs)
-              .max(Comparator.naturalOrder());
-
-      if (groupLastTs.isPresent() && groupLastTs.get().after(sinceTs)) {
-        log.info("Index {}/{}/{}", g.getGroupUUID(), g.getName(), groupLastTs.get());
-        indexer.index(g.getGroupUUID(), Operation.INDEX, Optional.empty());
-        return groupLastTs;
-      }
-    } catch (OrmException | IOException e) {
-      log.error("Reindex failed", e);
-    }
+  protected Optional<Timestamp> indexIfNeeded(ReviewDb db, GroupReference g, Timestamp sinceTs) {
     return Optional.empty();
   }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/IndexTs.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/IndexTs.java
index 952fcd1..0e0602a 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/IndexTs.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/IndexTs.java
@@ -16,12 +16,15 @@
 
 import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.AbstractIndexRestApiServlet;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.AbstractIndexRestApiServlet.IndexName;
+import com.ericsson.gerrit.plugins.highavailability.index.CurrentRequestContext;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.annotations.PluginData;
 import com.google.gerrit.extensions.events.AccountIndexedListener;
 import com.google.gerrit.extensions.events.ChangeIndexedListener;
 import com.google.gerrit.extensions.events.GroupIndexedListener;
+import com.google.gerrit.extensions.events.ProjectIndexedListener;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ChangeFinder;
+import com.google.gerrit.server.change.ChangeFinder;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gwtorm.server.SchemaFactory;
@@ -35,13 +38,14 @@
 import java.time.format.DateTimeFormatter;
 import java.util.Optional;
 import java.util.concurrent.ScheduledExecutorService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class IndexTs
-    implements ChangeIndexedListener, AccountIndexedListener, GroupIndexedListener {
-  private static final Logger log = LoggerFactory.getLogger(IndexTs.class);
+    implements ChangeIndexedListener,
+        AccountIndexedListener,
+        GroupIndexedListener,
+        ProjectIndexedListener {
+  private static final FluentLogger log = FluentLogger.forEnclosingClass();
   private static final DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME;
 
   private final Path dataDir;
@@ -49,10 +53,12 @@
   private final FlusherRunner flusher;
   private final SchemaFactory<ReviewDb> schemaFactory;
   private final ChangeFinder changeFinder;
+  private final CurrentRequestContext currCtx;
 
   private volatile LocalDateTime changeTs;
   private volatile LocalDateTime accountTs;
   private volatile LocalDateTime groupTs;
+  private volatile LocalDateTime projectTs;
 
   class FlusherRunner implements Runnable {
 
@@ -61,6 +67,7 @@
       store(AbstractIndexRestApiServlet.IndexName.CHANGE, changeTs);
       store(AbstractIndexRestApiServlet.IndexName.ACCOUNT, accountTs);
       store(AbstractIndexRestApiServlet.IndexName.GROUP, groupTs);
+      store(AbstractIndexRestApiServlet.IndexName.PROJECT, projectTs);
     }
 
     private void store(AbstractIndexRestApiServlet.IndexName index, LocalDateTime latestTs) {
@@ -70,7 +77,7 @@
         try {
           Files.write(indexTsFile, latestTs.format(formatter).getBytes(StandardCharsets.UTF_8));
         } catch (IOException e) {
-          log.error("Unable to update last timestamp for index {}", index, e);
+          log.atSevere().withCause(e).log("Unable to update last timestamp for index %s", index);
         }
       }
     }
@@ -81,41 +88,51 @@
       @PluginData Path dataDir,
       WorkQueue queue,
       SchemaFactory<ReviewDb> schemaFactory,
-      ChangeFinder changeFinder) {
+      ChangeFinder changeFinder,
+      CurrentRequestContext currCtx) {
     this.dataDir = dataDir;
     this.exec = queue.getDefaultQueue();
     this.flusher = new FlusherRunner();
     this.schemaFactory = schemaFactory;
     this.changeFinder = changeFinder;
+    this.currCtx = currCtx;
+  }
+
+  @Override
+  public void onProjectIndexed(String project) {
+    currCtx.onlyWithContext((ctx) -> update(IndexName.PROJECT, LocalDateTime.now()));
   }
 
   @Override
   public void onGroupIndexed(String uuid) {
-    update(IndexName.GROUP, LocalDateTime.now());
+    currCtx.onlyWithContext((ctx) -> update(IndexName.GROUP, LocalDateTime.now()));
   }
 
   @Override
   public void onAccountIndexed(int id) {
-    update(IndexName.ACCOUNT, LocalDateTime.now());
+    currCtx.onlyWithContext((ctx) -> update(IndexName.ACCOUNT, LocalDateTime.now()));
   }
 
   @Override
   public void onChangeIndexed(String projectName, int id) {
-    try (ReviewDb db = schemaFactory.open()) {
-      ChangeNotes changeNotes = changeFinder.findOne(projectName + "~" + id);
-      update(
-          IndexName.CHANGE,
-          changeNotes == null
-              ? LocalDateTime.now()
-              : changeNotes.getChange().getLastUpdatedOn().toLocalDateTime());
-    } catch (Exception e) {
-      log.warn("Unable to update the latest TS for change {}", id, e);
-    }
+    currCtx.onlyWithContext(
+        (ctx) -> {
+          try (ReviewDb db = schemaFactory.open()) {
+            ChangeNotes changeNotes = changeFinder.findOne(projectName + "~" + id);
+            update(
+                IndexName.CHANGE,
+                changeNotes == null
+                    ? LocalDateTime.now()
+                    : changeNotes.getChange().getLastUpdatedOn().toLocalDateTime());
+          } catch (Exception e) {
+            log.atWarning().withCause(e).log("Unable to update the latest TS for change %d", id);
+          }
+        });
   }
 
   @Override
   public void onChangeDeleted(int id) {
-    update(IndexName.CHANGE, LocalDateTime.now());
+    currCtx.onlyWithContext((ctx) -> update(IndexName.CHANGE, LocalDateTime.now()));
   }
 
   public Optional<LocalDateTime> getUpdateTs(AbstractIndexRestApiServlet.IndexName index) {
@@ -126,7 +143,7 @@
         return Optional.of(LocalDateTime.parse(tsString, formatter));
       }
     } catch (Exception e) {
-      log.warn("Unable to read last timestamp for index {}", index, e);
+      log.atWarning().withCause(e).log("Unable to read last timestamp for index %s", index);
     }
     return Optional.empty();
   }
@@ -142,6 +159,9 @@
       case GROUP:
         groupTs = dateTime;
         break;
+      case PROJECT:
+        projectTs = dateTime;
+        break;
       default:
         throw new IllegalArgumentException("Unsupported index " + index);
     }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ProjectReindexRunnable.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ProjectReindexRunnable.java
new file mode 100644
index 0000000..582227d
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ProjectReindexRunnable.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.ericsson.gerrit.plugins.highavailability.autoreindex;
+
+import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.AbstractIndexRestApiServlet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.util.OneOffRequestContext;
+import com.google.inject.Inject;
+import java.sql.Timestamp;
+import java.util.Optional;
+
+public class ProjectReindexRunnable extends ReindexRunnable<Project.NameKey> {
+
+  private final ProjectCache projectCache;
+
+  @Inject
+  public ProjectReindexRunnable(
+      IndexTs indexTs, OneOffRequestContext ctx, ProjectCache projectCache) {
+    super(AbstractIndexRestApiServlet.IndexName.PROJECT, indexTs, ctx);
+    this.projectCache = projectCache;
+  }
+
+  @Override
+  protected Iterable<Project.NameKey> fetchItems(ReviewDb db) {
+    return projectCache.all();
+  }
+
+  @Override
+  protected Optional<Timestamp> indexIfNeeded(ReviewDb db, Project.NameKey g, Timestamp sinceTs) {
+    return Optional.empty();
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ReindexRunnable.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ReindexRunnable.java
index df3a0fd..7a5669e 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ReindexRunnable.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ReindexRunnable.java
@@ -16,6 +16,7 @@
 
 import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.AbstractIndexRestApiServlet;
 import com.google.common.base.Stopwatch;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.OneOffRequestContext;
@@ -24,11 +25,9 @@
 import java.time.LocalDateTime;
 import java.util.Optional;
 import java.util.concurrent.TimeUnit;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 abstract class ReindexRunnable<T> implements Runnable {
-  private static final Logger log = LoggerFactory.getLogger(ReindexRunnable.class);
+  private static final FluentLogger log = FluentLogger.forEnclosingClass();
 
   private final AbstractIndexRestApiServlet.IndexName itemName;
   private final OneOffRequestContext ctx;
@@ -49,7 +48,7 @@
     String itemNameString = itemName.name().toLowerCase();
     if (maybeIndexTs.isPresent()) {
       newLastIndexTs = maxTimestamp(newLastIndexTs, Timestamp.valueOf(maybeIndexTs.get()));
-      log.debug("Scanning for all the {}s after {}", itemNameString, newLastIndexTs);
+      log.atFine().log("Scanning for all the %ss after %s", itemNameString, newLastIndexTs);
       try (ManualRequestContext mctx = ctx.open();
           ReviewDb db = mctx.getReviewDbProvider().get()) {
         int count = 0;
@@ -63,27 +62,27 @@
               newLastIndexTs = maxTimestamp(newLastIndexTs, itemTs.get());
             }
           } catch (Exception e) {
-            log.error("Unable to reindex {} {}", itemNameString, c, e);
+            log.atSevere().withCause(e).log("Unable to reindex %s %s", itemNameString, c);
             errors++;
           }
         }
         long elapsedNanos = stopwatch.stop().elapsed(TimeUnit.NANOSECONDS);
         if (count > 0) {
-          log.info(
-              "{} {}s reindexed in {} msec ({}/sec), {} failed",
+          log.atInfo().log(
+              "%d %ss reindexed in %d msec (%d/sec), %d failed",
               count,
               itemNameString,
               elapsedNanos / 1000000L,
               (count * 1000L) / (elapsedNanos / 1000000L),
               errors);
         } else if (errors > 0) {
-          log.info("{} {}s failed to reindex", errors, itemNameString);
+          log.atInfo().log("%d %ss failed to reindex", errors, itemNameString);
         } else {
-          log.debug("Scanning finished");
+          log.atFine().log("Scanning finished");
         }
         indexTs.update(itemName, newLastIndexTs.toLocalDateTime());
       } catch (Exception e) {
-        log.error("Unable to scan " + itemNameString + "s", e);
+        log.atSevere().withCause(e).log("Unable to scan %ss", itemNameString);
       }
     }
   }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/event/EventHandler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/event/EventHandler.java
index f42d115..025115c 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/event/EventHandler.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/event/EventHandler.java
@@ -16,9 +16,9 @@
 
 import com.ericsson.gerrit.plugins.highavailability.forwarder.Context;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.Forwarder;
-import com.google.gerrit.common.EventListener;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.EventListener;
 import com.google.gerrit.server.events.ProjectEvent;
 import com.google.inject.Inject;
 import java.util.concurrent.Executor;
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/event/EventModule.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/event/EventModule.java
index a848c20..28a9e8e 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/event/EventModule.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/event/EventModule.java
@@ -14,9 +14,9 @@
 
 package com.ericsson.gerrit.plugins.highavailability.event;
 
-import com.google.gerrit.common.EventListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.events.EventListener;
 import java.util.concurrent.Executor;
 
 public class EventModule extends LifecycleModule {
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedAwareEventBroker.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedAwareEventBroker.java
index b3696fb..3d9fd60 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedAwareEventBroker.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedAwareEventBroker.java
@@ -14,14 +14,14 @@
 
 package com.ericsson.gerrit.plugins.highavailability.forwarder;
 
-import com.google.gerrit.common.EventBroker;
-import com.google.gerrit.common.EventListener;
-import com.google.gerrit.common.UserScopedEventListener;
-import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.EventBroker;
+import com.google.gerrit.server.events.EventListener;
+import com.google.gerrit.server.events.UserScopedEventListener;
 import com.google.gerrit.server.notedb.ChangeNotes.Factory;
 import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -30,8 +30,8 @@
 
   @Inject
   ForwardedAwareEventBroker(
-      DynamicSet<UserScopedEventListener> listeners,
-      DynamicSet<EventListener> unrestrictedListeners,
+      PluginSetContext<UserScopedEventListener> listeners,
+      PluginSetContext<EventListener> unrestrictedListeners,
       PermissionBackend permissionBackend,
       ProjectCache projectCache,
       Factory notesFactory,
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 c4c2219..27531b0 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
@@ -16,11 +16,10 @@
 
 import com.ericsson.gerrit.plugins.highavailability.cache.Constants;
 import com.google.common.cache.Cache;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Evict cache entries. This class is meant to be used on the receiving side of the {@link
@@ -29,7 +28,7 @@
  */
 @Singleton
 public class ForwardedCacheEvictionHandler {
-  private static final Logger log = LoggerFactory.getLogger(ForwardedCacheEvictionHandler.class);
+  private static final FluentLogger log = FluentLogger.forEnclosingClass();
 
   private final DynamicMap<Cache<?, ?>> cacheMap;
 
@@ -55,10 +54,10 @@
       if (Constants.PROJECT_LIST.equals(entry.getCacheName())) {
         // One key is holding the list of projects
         cache.invalidateAll();
-        log.debug("Invalidated cache {}", entry.getCacheName());
+        log.atFine().log("Invalidated cache %s", entry.getCacheName());
       } else {
         cache.invalidate(entry.getKey());
-        log.debug("Invalidated cache {}[{}]", entry.getCacheName(), entry.getKey());
+        log.atFine().log("Invalidated cache %s[%s]", 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 4e01abf..51db006 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
@@ -14,14 +14,13 @@
 
 package com.ericsson.gerrit.plugins.highavailability.forwarder;
 
-import com.google.gerrit.common.EventDispatcher;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.EventDispatcher;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Dispatch event to the {@link EventDispatcher}. This class is meant to be used on the receiving
@@ -30,7 +29,7 @@
  */
 @Singleton
 public class ForwardedEventHandler {
-  private static final Logger log = LoggerFactory.getLogger(ForwardedEventHandler.class);
+  private static final FluentLogger log = FluentLogger.forEnclosingClass();
 
   private final EventDispatcher dispatcher;
 
@@ -48,7 +47,7 @@
   public void dispatch(Event event) throws OrmException, PermissionBackendException {
     try {
       Context.setForwardedEvent(true);
-      log.debug("dispatching event {}", event.getType());
+      log.atFine().log("dispatching event %s", 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
index 649ffc8..ba60830 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexAccountHandler.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexAccountHandler.java
@@ -41,7 +41,7 @@
   @Override
   protected void doIndex(Account.Id id, Optional<IndexEvent> indexEvent) throws IOException {
     indexer.index(id);
-    log.debug("Account {} successfully indexed", id);
+    log.atFine().log("Account %s successfully indexed", id);
   }
 
   @Override
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandler.java
index d34959c..d7c7e88 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandler.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandler.java
@@ -89,29 +89,31 @@
 
         if (checker.isChangeUpToDate(indexEvent)) {
           if (retryCount > 0) {
-            log.warn("Change {} has been eventually indexed after {} attempt(s)", id, retryCount);
+            log.atWarning().log(
+                "Change %s has been eventually indexed after %d attempt(s)", id, retryCount);
           } else {
-            log.debug("Change {} successfully indexed", id);
+            log.atFine().log("Change %s successfully indexed", id);
           }
         } else {
-          log.warn(
-              "Change {} seems too old compared to the event timestamp (event-Ts={} >> change-Ts={})",
-              id,
-              indexEvent,
-              checker);
+          log.atWarning().log(
+              "Change %s seems too old compared to the event timestamp (event-Ts=%s >> change-Ts=%s)",
+              id, indexEvent, checker);
           rescheduleIndex(id, indexEvent, retryCount + 1);
         }
       } else {
-        indexer.delete(parseChangeId(id));
-        log.warn(
-            "Change {} could not be found in the local Git repository (eventTs={}), deleted from index",
-            id,
-            indexEvent);
+        log.atWarning().log(
+            "Change %s not present yet in local Git repository (event=%s) after %d attempt(s)",
+            id, indexEvent, retryCount);
+        if (!rescheduleIndex(id, indexEvent, retryCount + 1)) {
+          log.atSevere().log(
+              "Change %s could not be found in the local Git repository (event=%s)",
+              id, indexEvent);
+        }
       }
     } catch (Exception e) {
       if (isCausedByNoSuchChangeException(e)) {
         indexer.delete(parseChangeId(id));
-        log.warn("Error trying to index Change {}. Deleted from index", id, e);
+        log.atWarning().withCause(e).log("Error trying to index Change %s. Deleted from index", id);
         return;
       }
 
@@ -126,37 +128,35 @@
     }
   }
 
-  private void rescheduleIndex(String id, Optional<IndexEvent> indexEvent, int retryCount) {
+  private boolean rescheduleIndex(String id, Optional<IndexEvent> indexEvent, int retryCount) {
     if (retryCount > maxTries) {
-      log.error(
-          "Change {} could not be indexed after {} retries. Change index could be stale.",
-          id,
-          retryCount);
-      return;
+      log.atSevere().log(
+          "Change %s could not be indexed after %d retries. Change index could be stale.",
+          id, retryCount);
+      return false;
     }
 
-    log.warn(
-        "Retrying for the #{} time to index Change {} after {} msecs",
-        retryCount,
-        id,
-        retryInterval);
+    log.atWarning().log(
+        "Retrying for the #%d time to index Change %s after %d msecs",
+        retryCount, id, retryInterval);
     indexExecutor.schedule(
         () -> {
           try (ManualRequestContext ctx = oneOffCtx.open()) {
             Context.setForwardedEvent(true);
             doIndex(id, indexEvent, retryCount);
           } catch (Exception e) {
-            log.warn("Change {} could not be indexed", id, e);
+            log.atWarning().withCause(e).log("Change %s could not be indexed", id);
           }
         },
         retryInterval,
         TimeUnit.MILLISECONDS);
+    return true;
   }
 
   @Override
   protected void doDelete(String id, Optional<IndexEvent> indexEvent) throws IOException {
     indexer.delete(parseChangeId(id));
-    log.debug("Change {} successfully deleted from index", id);
+    log.atFine().log("Change %s successfully deleted from index", id);
   }
 
   private static Change.Id parseChangeId(String id) {
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexGroupHandler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexGroupHandler.java
index 7c6d723..fb4d2f2 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexGroupHandler.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexGroupHandler.java
@@ -42,7 +42,7 @@
   protected void doIndex(AccountGroup.UUID uuid, Optional<IndexEvent> indexEvent)
       throws IOException {
     indexer.index(uuid);
-    log.debug("Group {} successfully indexed", uuid);
+    log.atFine().log("Group %s successfully indexed", uuid);
   }
 
   @Override
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexProjectHandler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexProjectHandler.java
new file mode 100644
index 0000000..5ae4cc0
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexProjectHandler.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.ericsson.gerrit.plugins.highavailability.forwarder;
+
+import com.ericsson.gerrit.plugins.highavailability.Configuration;
+import com.google.gerrit.index.project.ProjectIndexer;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.Optional;
+
+/**
+ * Index a project using {@link ProjectIndexer}. This class is meant to be used on the receiving
+ * side of the {@link Forwarder} since it will prevent indexed project to be forwarded again causing
+ * an infinite forwarding loop between the 2 nodes. It will also make sure no concurrent indexing is
+ * done for the same project name.
+ */
+@Singleton
+public class ForwardedIndexProjectHandler extends ForwardedIndexingHandler<Project.NameKey> {
+  private final ProjectIndexer indexer;
+
+  @Inject
+  ForwardedIndexProjectHandler(ProjectIndexer indexer, Configuration config) {
+    super(config.index());
+    this.indexer = indexer;
+  }
+
+  @Override
+  protected void doIndex(Project.NameKey projectName, Optional<IndexEvent> indexEvent)
+      throws IOException {
+    indexer.index(projectName);
+    log.atFine().log("Project %s successfully indexed", projectName);
+  }
+
+  @Override
+  protected void doDelete(Project.NameKey projectName, Optional<IndexEvent> indexEvent) {
+    throw new UnsupportedOperationException("Delete from project index not supported");
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexingHandler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexingHandler.java
index bfedf3f..78e103d 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexingHandler.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexingHandler.java
@@ -15,13 +15,12 @@
 package com.ericsson.gerrit.plugins.highavailability.forwarder;
 
 import com.ericsson.gerrit.plugins.highavailability.Configuration;
+import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.Striped;
 import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 import java.util.Optional;
 import java.util.concurrent.locks.Lock;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Base class to handle forwarded indexing. This class is meant to be extended by classes used on
@@ -30,7 +29,7 @@
  * indexing is done for the same id.
  */
 public abstract class ForwardedIndexingHandler<T> {
-  protected final Logger log = LoggerFactory.getLogger(getClass());
+  protected static final FluentLogger log = FluentLogger.forEnclosingClass();
 
   public enum Operation {
     INDEX,
@@ -64,7 +63,7 @@
    */
   public void index(T id, Operation operation, Optional<IndexEvent> indexEvent)
       throws IOException, OrmException {
-    log.debug("{} {} {}", operation, id, indexEvent);
+    log.atFine().log("%s %s %s", operation, id, indexEvent);
     try {
       Context.setForwardedEvent(true);
       Lock idLock = idLocks.get(id);
@@ -78,7 +77,7 @@
             doDelete(id, indexEvent);
             break;
           default:
-            log.error("unexpected operation: {}", operation);
+            log.atSevere().log("unexpected operation: %s", operation);
             break;
         }
       } finally {
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
index 93e1186..84f4675 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedProjectListUpdateHandler.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedProjectListUpdateHandler.java
@@ -14,12 +14,12 @@
 
 package com.ericsson.gerrit.plugins.highavailability.forwarder;
 
+import com.google.common.flogger.FluentLogger;
 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 org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import java.io.IOException;
 
 /**
  * Update project list cache. This class is meant to be used on the receiving side of the {@link
@@ -28,8 +28,7 @@
  */
 @Singleton
 public class ForwardedProjectListUpdateHandler {
-  private static final Logger log =
-      LoggerFactory.getLogger(ForwardedProjectListUpdateHandler.class);
+  private static final FluentLogger log = FluentLogger.forEnclosingClass();
 
   private final ProjectCache projectCache;
 
@@ -43,17 +42,18 @@
    *
    * @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) {
+  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);
+        log.atFine().log("Removed %s from project list", projectName);
       } else {
         projectCache.onCreateProject(projectKey);
-        log.debug("Added {} to project list", projectName);
+        log.atFine().log("Added %s 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 b156563..49cdc8b 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/Forwarder.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/Forwarder.java
@@ -57,6 +57,15 @@
   boolean indexGroup(String uuid, IndexEvent indexEvent);
 
   /**
+   * Forward a project indexing event to the other master.
+   *
+   * @param projectName the project to index.
+   * @param indexEvent the details of the index event.
+   * @return true if successful, otherwise false.
+   */
+  boolean indexProject(String projectName, IndexEvent indexEvent);
+
+  /**
    * Forward a stream event to the other master.
    *
    * @param event the event to forward.
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwarderModule.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwarderModule.java
index 15fdcfd..99a820e 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwarderModule.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwarderModule.java
@@ -14,8 +14,8 @@
 
 package com.ericsson.gerrit.plugins.highavailability.forwarder;
 
-import com.google.gerrit.common.EventDispatcher;
 import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.server.events.EventDispatcher;
 import com.google.inject.AbstractModule;
 
 public class ForwarderModule extends AbstractModule {
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 8c429de..410aa23 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
@@ -43,7 +43,8 @@
   public enum IndexName {
     CHANGE,
     ACCOUNT,
-    GROUP;
+    GROUP,
+    PROJECT;
 
     @Override
     public String toString() {
@@ -91,11 +92,11 @@
       rsp.setStatus(SC_NO_CONTENT);
     } catch (IOException e) {
       sendError(rsp, SC_CONFLICT, e.getMessage());
-      log.error("Unable to update {} index", indexName, e);
+      log.atSevere().withCause(e).log("Unable to update %s index", indexName);
     } catch (OrmException e) {
       String msg = String.format("Error trying to find %s", indexName);
       sendError(rsp, SC_NOT_FOUND, msg);
-      log.debug(msg, e);
+      log.atFine().withCause(e).log(msg);
     }
   }
 
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 054f640..a76d8ea 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
@@ -16,15 +16,14 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.flogger.FluentLogger;
 import java.io.IOException;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletResponse;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public abstract class AbstractRestApiServlet extends HttpServlet {
   private static final long serialVersionUID = 1L;
-  protected final Logger log = LoggerFactory.getLogger(getClass());
+  protected static final FluentLogger log = FluentLogger.forEnclosingClass();
 
   protected static void setHeaders(HttpServletResponse rsp) {
     rsp.setContentType("text/plain");
@@ -35,7 +34,7 @@
     try {
       rsp.sendError(statusCode, message);
     } catch (IOException e) {
-      log.error("Failed to send error messsage: {}", e.getMessage(), e);
+      log.atSevere().withCause(e).log("Failed to send error messsage");
     }
   }
 }
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 fb49cf2..60e782f 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
@@ -54,10 +54,10 @@
           CacheEntry.from(cacheName, gson.fromJson(cacheName, json)));
       rsp.setStatus(SC_NO_CONTENT);
     } catch (CacheNotFoundException e) {
-      log.error("Failed to process eviction request: {}", e.getMessage());
+      log.atSevere().log("Failed to process eviction request: %s", e.getMessage());
       sendError(rsp, SC_BAD_REQUEST, e.getMessage());
     } catch (IOException e) {
-      log.error("Failed to process eviction request: {}", e.getMessage(), e);
+      log.atSevere().withCause(e).log("Failed to process eviction request");
       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 3da5081..61b714c 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
@@ -56,10 +56,10 @@
       forwardedEventHandler.dispatch(getEventFromRequest(req));
       rsp.setStatus(SC_NO_CONTENT);
     } catch (OrmException e) {
-      log.debug("Error trying to find a change ", e);
+      log.atFine().withCause(e).log("Error trying to find a change");
       sendError(rsp, SC_NOT_FOUND, "Change not found\n");
     } catch (IOException | PermissionBackendException e) {
-      log.error("Unable to re-trigger event", e);
+      log.atSevere().withCause(e).log("Unable to re-trigger event");
       sendError(rsp, SC_BAD_REQUEST, e.getMessage());
     }
   }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpClientProvider.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpClientProvider.java
index 26943c2..0510aed 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpClientProvider.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpClientProvider.java
@@ -15,6 +15,7 @@
 package com.ericsson.gerrit.plugins.highavailability.forwarder.rest;
 
 import com.ericsson.gerrit.plugins.highavailability.Configuration;
+import com.google.common.flogger.FluentLogger;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.security.KeyManagementException;
@@ -37,12 +38,10 @@
 import org.apache.http.impl.client.CloseableHttpClient;
 import org.apache.http.impl.client.HttpClients;
 import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Provides an HTTP client with SSL capabilities. */
 class HttpClientProvider implements Provider<CloseableHttpClient> {
-  private static final Logger log = LoggerFactory.getLogger(HttpClientProvider.class);
+  private static final FluentLogger log = FluentLogger.forEnclosingClass();
   private static final int CONNECTIONS_PER_ROUTE = 100;
   // Up to 2 target instances with the max number of connections per host:
   private static final int MAX_CONNECTIONS = 2 * CONNECTIONS_PER_ROUTE;
@@ -101,7 +100,7 @@
       context.init(null, trustAllCerts, null);
       return context;
     } catch (KeyManagementException | NoSuchAlgorithmException e) {
-      log.warn("Error building SSLContext object", e);
+      log.atWarning().withCause(e).log("Error building SSLContext object");
       return null;
     }
   }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpResponseHandler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpResponseHandler.java
index cfcd519..9a1e635 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpResponseHandler.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpResponseHandler.java
@@ -17,13 +17,12 @@
 import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
 
 import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.HttpResponseHandler.HttpResult;
+import com.google.common.flogger.FluentLogger;
 import java.io.IOException;
 import org.apache.http.HttpEntity;
 import org.apache.http.HttpResponse;
 import org.apache.http.client.ResponseHandler;
 import org.apache.http.util.EntityUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 class HttpResponseHandler implements ResponseHandler<HttpResult> {
 
@@ -45,7 +44,7 @@
     }
   }
 
-  private static final Logger log = LoggerFactory.getLogger(HttpResponseHandler.class);
+  private static final FluentLogger log = FluentLogger.forEnclosingClass();
 
   @Override
   public HttpResult handleResponse(HttpResponse response) {
@@ -63,7 +62,7 @@
       try {
         asString = EntityUtils.toString(entity);
       } catch (IOException e) {
-        log.error("Error parsing entity", e);
+        log.atSevere().withCause(e).log("Error parsing entity");
       }
     }
     return asString;
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexProjectRestApiServlet.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexProjectRestApiServlet.java
new file mode 100644
index 0000000..cc87442
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexProjectRestApiServlet.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.ericsson.gerrit.plugins.highavailability.forwarder.rest;
+
+import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexProjectHandler;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+class IndexProjectRestApiServlet extends AbstractIndexRestApiServlet<Project.NameKey> {
+  private static final long serialVersionUID = -1L;
+
+  @Inject
+  IndexProjectRestApiServlet(ForwardedIndexProjectHandler handler) {
+    super(handler, IndexName.PROJECT);
+  }
+
+  @Override
+  Project.NameKey parse(String projectName) {
+    return new Project.NameKey(Url.decode(projectName));
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/ProjectListApiServlet.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/ProjectListApiServlet.java
index e781a10..7a31ea0 100644
--- 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
@@ -14,12 +14,14 @@
 
 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.gerrit.extensions.restapi.Url;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import java.io.IOException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
@@ -48,7 +50,12 @@
     setHeaders(rsp);
     String requestURI = req.getRequestURI();
     String projectName = requestURI.substring(requestURI.lastIndexOf('/') + 1);
-    forwardedProjectListUpdateHandler.update(Url.decode(projectName), delete);
-    rsp.setStatus(SC_NO_CONTENT);
+    try {
+      forwardedProjectListUpdateHandler.update(Url.decode(projectName), delete);
+      rsp.setStatus(SC_NO_CONTENT);
+    } catch (IOException e) {
+      log.atSevere().withCause(e).log("Unable to update project list");
+      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 ef1b26d..fb3aba9 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
@@ -21,6 +21,7 @@
 import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.HttpResponseHandler.HttpResult;
 import com.ericsson.gerrit.plugins.highavailability.peers.PeerInfo;
 import com.google.common.base.Joiner;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.server.events.Event;
@@ -34,8 +35,6 @@
 import javax.net.ssl.SSLException;
 import org.apache.http.HttpException;
 import org.apache.http.client.ClientProtocolException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 class RestForwarder implements Forwarder {
   enum RequestMethod {
@@ -43,7 +42,7 @@
     DELETE
   }
 
-  private static final Logger log = LoggerFactory.getLogger(RestForwarder.class);
+  private static final FluentLogger log = FluentLogger.forEnclosingClass();
 
   private final HttpSession httpSession;
   private final String pluginRelativePath;
@@ -101,6 +100,12 @@
   }
 
   @Override
+  public boolean indexProject(String projectName, IndexEvent event) {
+    return execute(
+        RequestMethod.POST, "index project", "index/project", Url.encode(projectName), event);
+  }
+
+  @Override
   public boolean send(final Event event) {
     return execute(RequestMethod.POST, "send event", "event", event.type, event);
   }
@@ -185,41 +190,36 @@
     }
 
     boolean execute() {
-      log.debug("Executing {} {} towards {}", action, key, destination);
+      log.atFine().log("Executing %s %s towards %s", action, key, destination);
       for (; ; ) {
         try {
           execCnt++;
           tryOnce();
-          log.debug("{} {} towards {} OK", action, key, destination);
+          log.atFine().log("%s %s towards %s OK", action, key, destination);
           return true;
         } catch (ForwardingException e) {
           int maxTries = cfg.http().maxTries();
-          log.debug(
-              "Failed to {} {} on {} [{}/{}]", action, key, destination, execCnt, maxTries, e);
+          log.atFine().withCause(e).log(
+              "Failed to %s %s on %s [%d/%d]", action, key, destination, execCnt, maxTries);
           if (!e.isRecoverable()) {
-            log.error(
-                "{} {} towards {} failed with unrecoverable error; giving up",
-                action,
-                key,
-                destination,
-                e);
+            log.atSevere().withCause(e).log(
+                "%s %s towards %s failed with unrecoverable error; giving up",
+                action, key, destination);
             return false;
           }
           if (execCnt >= maxTries) {
-            log.error(
-                "Failed to {} {} on {} after {} tries; giving up",
-                action,
-                key,
-                destination,
-                maxTries);
+            log.atSevere().log(
+                "Failed to %s %s on %s after %d tries; giving up",
+                action, key, destination, maxTries);
             return false;
           }
 
-          log.debug("Retrying to {} {} on {}", action, key, destination);
+          log.atFine().log("Retrying to %s %s on %s", action, key, destination);
           try {
             Thread.sleep(cfg.http().retryInterval());
           } catch (InterruptedException ie) {
-            log.error("{} {} towards {} was interrupted; giving up", action, key, destination, ie);
+            log.atSevere().withCause(ie).log(
+                "%s %s towards %s was interrupted; giving up", action, key, destination);
             Thread.currentThread().interrupt();
             return false;
           }
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 184157d..589bbef 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderServletModule.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderServletModule.java
@@ -22,6 +22,7 @@
     serveRegex("/index/account/\\d+$").with(IndexAccountRestApiServlet.class);
     serveRegex("/index/change/.*$").with(IndexChangeRestApiServlet.class);
     serveRegex("/index/group/\\w+$").with(IndexGroupRestApiServlet.class);
+    serveRegex("/index/project/.*$").with(IndexProjectRestApiServlet.class);
     serve("/event/*").with(EventRestApiServlet.class);
     serve("/cache/project_list/*").with(ProjectListApiServlet.class);
     serve("/cache/*").with(CacheRestApiServlet.class);
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/health/HealthServlet.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/health/HealthServlet.java
index 6d98d83..0f8125e 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/health/HealthServlet.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/health/HealthServlet.java
@@ -20,6 +20,7 @@
 import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
 import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.annotations.PluginData;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -34,12 +35,10 @@
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class HealthServlet extends HttpServlet {
-  private static final Logger log = LoggerFactory.getLogger(HealthServlet.class);
+  private static final FluentLogger log = FluentLogger.forEnclosingClass();
   private static final long serialVersionUID = -1L;
 
   private final Provider<CurrentUser> currentUserProvider;
@@ -66,7 +65,7 @@
       setHealthy();
       rsp.setStatus(SC_NO_CONTENT);
     } catch (IOException e) {
-      log.error("Failed to set healthy", e);
+      log.atSevere().withCause(e).log("Failed to set healthy");
       sendError(rsp, SC_INTERNAL_SERVER_ERROR);
     }
   }
@@ -81,7 +80,7 @@
       setUnhealthy();
       rsp.setStatus(SC_NO_CONTENT);
     } catch (IOException e) {
-      log.error("Failed to set unhealthy", e);
+      log.atSevere().withCause(e).log("Failed to set unhealthy");
       sendError(rsp, SC_INTERNAL_SERVER_ERROR);
     }
   }
@@ -100,7 +99,7 @@
       rsp.sendError(statusCode);
     } catch (IOException e) {
       rsp.setStatus(SC_INTERNAL_SERVER_ERROR);
-      log.error("Failed to send error response", e);
+      log.atSevere().withCause(e).log("Failed to send error response");
     }
   }
 
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeCheckerImpl.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeCheckerImpl.java
index 9dca6dd..4cdf431 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeCheckerImpl.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeCheckerImpl.java
@@ -15,11 +15,12 @@
 package com.ericsson.gerrit.plugins.highavailability.index;
 
 import com.ericsson.gerrit.plugins.highavailability.forwarder.IndexEvent;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Comment;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ChangeFinder;
 import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.change.ChangeFinder;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.util.ManualRequestContext;
@@ -33,11 +34,9 @@
 import java.util.Optional;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class ChangeCheckerImpl implements ChangeChecker {
-  private static final Logger log = LoggerFactory.getLogger(ChangeCheckerImpl.class);
+  private static final FluentLogger log = FluentLogger.forEnclosingClass();
   private final GitRepositoryManager gitRepoMgr;
   private final CommentsUtil commentsUtil;
   private final ChangeDb changeDb;
@@ -91,9 +90,9 @@
   public boolean isChangeUpToDate(Optional<IndexEvent> indexEvent)
       throws IOException, OrmException {
     getComputedChangeTs();
-    log.debug("Checking change {} against index event {}", this, indexEvent);
+    log.atFine().log("Checking change %s against index event %s", this, indexEvent);
     if (!computedChangeTs.isPresent()) {
-      log.warn("Unable to compute last updated ts for change {}", changeId);
+      log.atWarning().log("Unable to compute last updated ts for change %s", changeId);
       return false;
     }
 
@@ -128,7 +127,7 @@
           + "/"
           + getBranchTargetSha();
     } catch (IOException | OrmException e) {
-      log.error("Unable to render change {}", changeId, e);
+      log.atSevere().withCause(e).log("Unable to render change %s", changeId);
       return "change-id=" + changeId;
     }
   }
@@ -138,12 +137,13 @@
       String refName = changeNotes.get().getChange().getDest().get();
       Ref ref = repo.exactRef(refName);
       if (ref == null) {
-        log.warn("Unable to find target ref {} for change {}", refName, changeId);
+        log.atWarning().log("Unable to find target ref %s for change %s", refName, changeId);
         return null;
       }
       return ref.getTarget().getObjectId().getName();
     } catch (IOException e) {
-      log.warn("Unable to resolve target branch SHA for change {}", changeId, e);
+      log.atWarning().withCause(e).log(
+          "Unable to resolve target branch SHA for change %s", changeId);
       return null;
     }
   }
@@ -163,7 +163,7 @@
         changeTs = commentTs.after(changeTs) ? commentTs : changeTs;
       }
     } catch (OrmException e) {
-      log.warn("Unable to access draft comments for change {}", change, e);
+      log.atWarning().withCause(e).log("Unable to access draft comments for change %s", change);
     }
     return changeTs.getTime() / 1000;
   }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/CurrentRequestContext.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/CurrentRequestContext.java
new file mode 100644
index 0000000..f8c6e31
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/CurrentRequestContext.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.ericsson.gerrit.plugins.highavailability.index;
+
+import com.ericsson.gerrit.plugins.highavailability.Configuration;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.OneOffRequestContext;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.function.Consumer;
+
+@Singleton
+public class CurrentRequestContext {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private ThreadLocalRequestContext threadLocalCtx;
+  private Configuration cfg;
+  private OneOffRequestContext oneOffCtx;
+
+  @Inject
+  public CurrentRequestContext(
+      ThreadLocalRequestContext threadLocalCtx, Configuration cfg, OneOffRequestContext oneOffCtx) {
+    this.threadLocalCtx = threadLocalCtx;
+    this.cfg = cfg;
+    this.oneOffCtx = oneOffCtx;
+  }
+
+  public void onlyWithContext(Consumer<RequestContext> body) {
+    RequestContext ctx = threadLocalCtx.getContext();
+    if (ctx == null && !cfg.index().synchronizeForced()) {
+      logger.atFine().log("No context, skipping event (index.synchronizeForced is false)");
+      return;
+    }
+
+    if (ctx == null) {
+      try (ManualRequestContext manualCtx = oneOffCtx.open()) {
+        body.accept(manualCtx);
+      } catch (OrmException e) {
+        logger.atSevere().withCause(e).log("Unable to open request context");
+      }
+    } else {
+      body.accept(ctx);
+    }
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/DisabledReviewDb.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/DisabledReviewDb.java
index 192ee8c..48c6ecb 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/DisabledReviewDb.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/DisabledReviewDb.java
@@ -16,13 +16,6 @@
 
 import com.google.common.util.concurrent.CheckedFuture;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Change.Id;
-import com.google.gerrit.reviewdb.server.AccountGroupAccess;
-import com.google.gerrit.reviewdb.server.AccountGroupByIdAccess;
-import com.google.gerrit.reviewdb.server.AccountGroupByIdAudAccess;
-import com.google.gerrit.reviewdb.server.AccountGroupMemberAccess;
-import com.google.gerrit.reviewdb.server.AccountGroupMemberAuditAccess;
-import com.google.gerrit.reviewdb.server.AccountGroupNameAccess;
 import com.google.gerrit.reviewdb.server.ChangeAccess;
 import com.google.gerrit.reviewdb.server.ChangeMessageAccess;
 import com.google.gerrit.reviewdb.server.PatchLineCommentAccess;
@@ -30,7 +23,6 @@
 import com.google.gerrit.reviewdb.server.PatchSetApprovalAccess;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.reviewdb.server.SchemaVersionAccess;
-import com.google.gerrit.reviewdb.server.SystemConfigAccess;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
@@ -67,22 +59,22 @@
     }
 
     @Override
-    public Id primaryKey(Change entity) {
+    public Change.Id primaryKey(Change entity) {
       throw new Disabled();
     }
 
     @Override
-    public Map<Id, Change> toMap(Iterable<Change> c) {
+    public Map<Change.Id, Change> toMap(Iterable<Change> c) {
       throw new Disabled();
     }
 
     @Override
-    public CheckedFuture<Change, OrmException> getAsync(Id key) {
+    public CheckedFuture<Change, OrmException> getAsync(Change.Id key) {
       throw new Disabled();
     }
 
     @Override
-    public ResultSet<Change> get(Iterable<Id> keys) throws OrmException {
+    public ResultSet<Change> get(Iterable<Change.Id> keys) throws OrmException {
       throw new Disabled();
     }
 
@@ -102,7 +94,7 @@
     }
 
     @Override
-    public void deleteKeys(Iterable<Id> keys) throws OrmException {
+    public void deleteKeys(Iterable<Change.Id> keys) throws OrmException {
       throw new Disabled();
     }
 
@@ -112,17 +104,17 @@
     }
 
     @Override
-    public void beginTransaction(Id key) throws OrmException {
+    public void beginTransaction(Change.Id key) throws OrmException {
       throw new Disabled();
     }
 
     @Override
-    public Change atomicUpdate(Id key, AtomicUpdate<Change> update) throws OrmException {
+    public Change atomicUpdate(Change.Id key, AtomicUpdate<Change> update) throws OrmException {
       throw new Disabled();
     }
 
     @Override
-    public Change get(Id id) throws OrmException {
+    public Change get(Change.Id id) throws OrmException {
       return null;
     }
 
@@ -168,31 +160,6 @@
   }
 
   @Override
-  public SystemConfigAccess systemConfig() {
-    throw new Disabled();
-  }
-
-  @Override
-  public AccountGroupAccess accountGroups() {
-    throw new Disabled();
-  }
-
-  @Override
-  public AccountGroupNameAccess accountGroupNames() {
-    throw new Disabled();
-  }
-
-  @Override
-  public AccountGroupMemberAccess accountGroupMembers() {
-    throw new Disabled();
-  }
-
-  @Override
-  public AccountGroupMemberAuditAccess accountGroupMembersAudit() {
-    throw new Disabled();
-  }
-
-  @Override
   public ChangeAccess changes() {
     return new DisabledChangeAccess();
   }
@@ -218,16 +185,6 @@
   }
 
   @Override
-  public AccountGroupByIdAccess accountGroupById() {
-    throw new Disabled();
-  }
-
-  @Override
-  public AccountGroupByIdAudAccess accountGroupByIdAud() {
-    throw new Disabled();
-  }
-
-  @Override
   public int nextAccountId() {
     throw new Disabled();
   }
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 3922f54..0697ad7 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
@@ -18,51 +18,65 @@
 import com.ericsson.gerrit.plugins.highavailability.forwarder.Forwarder;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.IndexEvent;
 import com.google.common.base.Objects;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.events.AccountIndexedListener;
 import com.google.gerrit.extensions.events.ChangeIndexedListener;
 import com.google.gerrit.extensions.events.GroupIndexedListener;
+import com.google.gerrit.extensions.events.ProjectIndexedListener;
 import com.google.inject.Inject;
 import java.util.Collections;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executor;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 class IndexEventHandler
-    implements ChangeIndexedListener, AccountIndexedListener, GroupIndexedListener {
-  private static final Logger log = LoggerFactory.getLogger(IndexEventHandler.class);
+    implements ChangeIndexedListener,
+        AccountIndexedListener,
+        GroupIndexedListener,
+        ProjectIndexedListener {
+  private static final FluentLogger log = FluentLogger.forEnclosingClass();
   private final Executor executor;
   private final Forwarder forwarder;
   private final String pluginName;
   private final Set<IndexTask> queuedTasks = Collections.newSetFromMap(new ConcurrentHashMap<>());
   private final ChangeCheckerImpl.Factory changeChecker;
+  private final CurrentRequestContext currCtx;
 
   @Inject
   IndexEventHandler(
       @IndexExecutor Executor executor,
       @PluginName String pluginName,
       Forwarder forwarder,
-      ChangeCheckerImpl.Factory changeChecker) {
+      ChangeCheckerImpl.Factory changeChecker,
+      CurrentRequestContext currCtx) {
     this.forwarder = forwarder;
     this.executor = executor;
     this.pluginName = pluginName;
     this.changeChecker = changeChecker;
+    this.currCtx = currCtx;
   }
 
   @Override
   public void onAccountIndexed(int id) {
-    if (!Context.isForwardedEvent()) {
-      IndexAccountTask task = new IndexAccountTask(id);
-      if (queuedTasks.add(task)) {
-        executor.execute(task);
-      }
-    }
+    currCtx.onlyWithContext(
+        (ctx) -> {
+          if (!Context.isForwardedEvent()) {
+            IndexAccountTask task = new IndexAccountTask(id);
+            if (queuedTasks.add(task)) {
+              executor.execute(task);
+            }
+          }
+        });
   }
 
   @Override
   public void onChangeIndexed(String projectName, int id) {
+    currCtx.onlyWithContext((ctx) -> executeIndexChangeTask(projectName, id));
+  }
+
+  private void executeIndexChangeTask(String projectName, int id) {
+
     if (!Context.isForwardedEvent()) {
       String changeId = projectName + "~" + id;
       try {
@@ -77,7 +91,7 @@
                   }
                 });
       } catch (Exception e) {
-        log.warn("Unable to create task to reindex change {}", changeId, e);
+        log.atWarning().withCause(e).log("Unable to create task to reindex change {}", changeId);
       }
     }
   }
@@ -93,6 +107,16 @@
   }
 
   @Override
+  public void onProjectIndexed(String projectName) {
+    if (!Context.isForwardedEvent()) {
+      IndexProjectTask task = new IndexProjectTask(projectName);
+      if (queuedTasks.add(task)) {
+        executor.execute(task);
+      }
+    }
+  }
+
+  @Override
   public void onGroupIndexed(String groupUUID) {
     if (!Context.isForwardedEvent()) {
       IndexGroupTask task = new IndexGroupTask(groupUUID);
@@ -253,4 +277,36 @@
       return String.format("[%s] Index group %s in target instance", pluginName, groupUUID);
     }
   }
+
+  class IndexProjectTask extends IndexTask {
+    private final String projectName;
+
+    IndexProjectTask(String projectName) {
+      this.projectName = projectName;
+    }
+
+    @Override
+    public void execute() {
+      forwarder.indexProject(projectName, indexEvent);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(IndexProjectTask.class, projectName);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (!(obj instanceof IndexProjectTask)) {
+        return false;
+      }
+      IndexProjectTask other = (IndexProjectTask) obj;
+      return projectName.equals(other.projectName);
+    }
+
+    @Override
+    public String toString() {
+      return String.format("[%s] Index project %s in target instance", pluginName, projectName);
+    }
+  }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/IndexModule.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/IndexModule.java
index ebf8fdf..c17731f 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/IndexModule.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/IndexModule.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.extensions.events.AccountIndexedListener;
 import com.google.gerrit.extensions.events.ChangeIndexedListener;
 import com.google.gerrit.extensions.events.GroupIndexedListener;
+import com.google.gerrit.extensions.events.ProjectIndexedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.inject.assistedinject.FactoryModuleBuilder;
@@ -35,6 +36,7 @@
     DynamicSet.bind(binder(), ChangeIndexedListener.class).to(IndexEventHandler.class);
     DynamicSet.bind(binder(), AccountIndexedListener.class).to(IndexEventHandler.class);
     DynamicSet.bind(binder(), GroupIndexedListener.class).to(IndexEventHandler.class);
+    DynamicSet.bind(binder(), ProjectIndexedListener.class).to(IndexEventHandler.class);
 
     install(
         new FactoryModuleBuilder()
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProvider.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProvider.java
index 596f86e..ce47390 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProvider.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProvider.java
@@ -17,6 +17,7 @@
 import com.ericsson.gerrit.plugins.highavailability.Configuration;
 import com.ericsson.gerrit.plugins.highavailability.peers.PeerInfo;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -30,8 +31,6 @@
 import org.jgroups.Message;
 import org.jgroups.ReceiverAdapter;
 import org.jgroups.View;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Provider which uses JGroups to find the peer gerrit instances. On startup every gerrit instance
@@ -46,7 +45,7 @@
 @Singleton
 public class JGroupsPeerInfoProvider extends ReceiverAdapter
     implements Provider<Set<PeerInfo>>, LifecycleListener {
-  private static final Logger log = LoggerFactory.getLogger(JGroupsPeerInfoProvider.class);
+  private static final FluentLogger log = FluentLogger.forEnclosingClass();
   private static final String JGROUPS_LOG_FACTORY_PROPERTY = "jgroups.logging.log_factory_class";
 
   static {
@@ -80,25 +79,25 @@
       peerAddress = msg.getSrc();
       String url = (String) msg.getObject();
       peerInfo = Optional.of(new PeerInfo(url));
-      log.info("receive(): Set new peerInfo: {}", url);
+      log.atInfo().log("receive(): Set new peerInfo: %s", url);
     }
   }
 
   @Override
   public void viewAccepted(View view) {
-    log.info("viewAccepted(view: {}) called", view);
+    log.atInfo().log("viewAccepted(view: %s) called", view);
     synchronized (this) {
       if (view.getMembers().size() > 2) {
-        log.warn(
-            "{} members joined the jgroups cluster {} ({}). "
-                + " Only two members are supported. Members: {}",
+        log.atWarning().log(
+            "%d members joined the jgroups cluster %s (%s). "
+                + " Only two members are supported. Members: %s",
             view.getMembers().size(),
             jgroupsConfig.clusterName(),
             channel.getName(),
             view.getMembers());
       }
       if (peerAddress != null && !view.getMembers().contains(peerAddress)) {
-        log.info("viewAccepted(): removed peerInfo");
+        log.atInfo().log("viewAccepted(): removed peerInfo");
         peerAddress = null;
         peerInfo = Optional.empty();
       }
@@ -108,11 +107,9 @@
         channel.send(new Message(null, myUrl));
       } catch (Exception e) {
         // channel communication caused an error. Can't do much about it.
-        log.error(
-            "Sending a message over channel {} to cluster {} failed",
-            channel.getName(),
-            jgroupsConfig.clusterName(),
-            e);
+        log.atSevere().withCause(e).log(
+            "Sending a message over channel %s to cluster %s failed",
+            channel.getName(), jgroupsConfig.clusterName());
       }
     }
   }
@@ -122,28 +119,25 @@
       channel = getChannel();
       Optional<InetAddress> address = finder.findAddress();
       if (address.isPresent()) {
-        log.debug("Protocol stack: " + channel.getProtocolStack());
+        log.atFine().log("Protocol stack: %s", channel.getProtocolStack());
         channel.getProtocolStack().getTransport().setBindAddress(address.get());
-        log.debug("Channel bound to {}", address.get());
+        log.atFine().log("Channel bound to %s", address.get());
       } else {
-        log.warn("Channel not bound: address not present");
+        log.atWarning().log("Channel not bound: address not present");
       }
       channel.setReceiver(this);
       channel.setDiscardOwnMessages(true);
       channel.connect(jgroupsConfig.clusterName());
-      log.info(
-          "Channel {} successfully joined jgroups cluster {}",
-          channel.getName(),
-          jgroupsConfig.clusterName());
+      log.atInfo().log(
+          "Channel %s successfully joined jgroups cluster %s",
+          channel.getName(), jgroupsConfig.clusterName());
     } catch (Exception e) {
       if (channel != null) {
-        log.error(
-            "joining cluster {} (channel {}) failed",
-            jgroupsConfig.clusterName(),
-            channel.getName(),
-            e);
+        log.atSevere().withCause(e).log(
+            "joining cluster %s (channel %s) failed",
+            jgroupsConfig.clusterName(), channel.getName());
       } else {
-        log.error("joining cluster {} failed", jgroupsConfig.clusterName(), e);
+        log.atSevere().withCause(e).log("joining cluster %s failed", jgroupsConfig.clusterName());
       }
     }
   }
@@ -156,10 +150,9 @@
       }
       return new JChannel();
     } catch (Exception e) {
-      log.error(
-          "Unable to create a channel with protocol stack: {}",
-          protocolStack.isPresent() ? protocolStack : "default",
-          e);
+      log.atSevere().withCause(e).log(
+          "Unable to create a channel with protocol stack: %s",
+          protocolStack.isPresent() ? protocolStack : "default");
       throw e;
     }
   }
@@ -177,10 +170,9 @@
   @Override
   public void stop() {
     if (channel != null) {
-      log.info(
-          "closing jgroups channel {} (cluster {})",
-          channel.getName(),
-          jgroupsConfig.clusterName());
+      log.atInfo().log(
+          "closing jgroups channel %s (cluster %s)",
+          channel.getName(), jgroupsConfig.clusterName());
       channel.close();
     }
     peerInfo = Optional.empty();
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/MyUrlProvider.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/MyUrlProvider.java
index adf7d2e..a257eb3 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/MyUrlProvider.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/MyUrlProvider.java
@@ -16,6 +16,7 @@
 
 import com.ericsson.gerrit.plugins.highavailability.Configuration;
 import com.google.common.base.CharMatcher;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -26,12 +27,10 @@
 import java.net.UnknownHostException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.transport.URIish;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 class MyUrlProvider implements Provider<String> {
-  private static final Logger log = LoggerFactory.getLogger(MyUrlProvider.class);
+  private static final FluentLogger log = FluentLogger.forEnclosingClass();
 
   private static final String HTTPD_SECTION = "httpd";
   private static final String LISTEN_URL_KEY = "listenUrl";
@@ -44,7 +43,7 @@
   MyUrlProvider(@GerritServerConfig Config srvConfig, Configuration pluginConfiguration) {
     String url = pluginConfiguration.peerInfoJGroups().myUrl();
     if (url == null) {
-      log.info("myUrl not configured; attempting to determine from {}", LISTEN_URL);
+      log.atInfo().log("myUrl not configured; attempting to determine from %s", LISTEN_URL);
       try {
         url = CharMatcher.is('/').trimTrailingFrom(getMyUrlFromListenUrl(srvConfig));
       } catch (MyUrlProviderException e) {
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 b9dc245..afc6568 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
@@ -18,6 +18,7 @@
 import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.ericsson.gerrit.plugins.highavailability.Configuration;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.git.WorkQueue;
@@ -25,8 +26,6 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.util.concurrent.ScheduledFuture;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 class FileBasedWebSessionCacheCleaner implements LifecycleListener {
@@ -37,7 +36,7 @@
   private ScheduledFuture<?> scheduledCleanupTask;
 
   static class CleanupTask implements Runnable {
-    private static final Logger log = LoggerFactory.getLogger(CleanupTask.class);
+    private static final FluentLogger log = FluentLogger.forEnclosingClass();
     private final FileBasedWebsessionCache fileBasedWebSessionCache;
     private final String pluginName;
 
@@ -49,9 +48,9 @@
 
     @Override
     public void run() {
-      log.info("Cleaning up expired file based websessions...");
+      log.atInfo().log("Cleaning up expired file based websessions...");
       fileBasedWebSessionCache.cleanUp();
-      log.info("Cleaning up expired file based websessions...Done");
+      log.atInfo().log("Cleaning up expired file based websessions...Done");
     }
 
     @Override
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/websession/file/FileBasedWebsessionCache.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/websession/file/FileBasedWebsessionCache.java
index b4c99b5..674de10 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/websession/file/FileBasedWebsessionCache.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/websession/file/FileBasedWebsessionCache.java
@@ -18,6 +18,7 @@
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheStats;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.httpd.WebSessionManager;
 import com.google.gerrit.httpd.WebSessionManager.Val;
@@ -44,8 +45,6 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.ExecutionException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
 public class FileBasedWebsessionCache implements Cache<String, WebSessionManager.Val> {
@@ -74,7 +73,7 @@
     }
   }
 
-  private static final Logger log = LoggerFactory.getLogger(FileBasedWebsessionCache.class);
+  private static final FluentLogger log = FluentLogger.forEnclosingClass();
 
   private final Path websessionsDir;
 
@@ -179,7 +178,7 @@
             StandardCopyOption.ATOMIC_MOVE);
       }
     } catch (IOException e) {
-      log.warn("Cannot put into cache {}", websessionsDir, e);
+      log.atWarning().withCause(e).log("Cannot put into cache %s", websessionsDir);
     }
   }
 
@@ -197,7 +196,7 @@
 
   @Override
   public CacheStats stats() {
-    log.warn("stats() unimplemented");
+    log.atWarning().log("stats() unimplemented");
     return null;
   }
 
@@ -207,15 +206,14 @@
           ObjectInputStream objStream = new ObjectInputStream(fileStream)) {
         return (Val) objStream.readObject();
       } catch (ClassNotFoundException e) {
-        log.warn(
-            "Entry {} in cache {} has an incompatible class and can't be"
+        log.atWarning().log(
+            "Entry %s in cache %s has an incompatible class and can't be"
                 + " deserialized. Invalidating entry.",
-            path,
-            websessionsDir);
-        log.debug(e.getMessage(), e);
+            path, websessionsDir);
+        log.atFine().withCause(e).log(e.getMessage());
         invalidate(path.getFileName().toString());
       } catch (IOException e) {
-        log.warn("Cannot read cache {}", websessionsDir, e);
+        log.atWarning().withCause(e).log("Cannot read cache %s", websessionsDir);
       }
     }
     return null;
@@ -225,7 +223,7 @@
     try {
       Files.deleteIfExists(path);
     } catch (IOException e) {
-      log.error("Error trying to delete {} from {}", path, websessionsDir, e);
+      log.atSevere().withCause(e).log("Error trying to delete %s from %s", path, websessionsDir);
     }
   }
 
@@ -236,7 +234,7 @@
         files.add(path);
       }
     } catch (IOException e) {
-      log.error("Cannot list files in cache {}", websessionsDir, e);
+      log.atSevere().withCause(e).log("Cannot list files in cache %s", websessionsDir);
     }
     return files;
   }
diff --git a/src/main/resources/Documentation/build.md b/src/main/resources/Documentation/build.md
index 38084d6..1d86dd0 100644
--- a/src/main/resources/Documentation/build.md
+++ b/src/main/resources/Documentation/build.md
@@ -71,6 +71,13 @@
   bazel-bin/plugins/@PLUGIN@/@PLUGIN@.jar
 ```
 
+To execute the tests run either one of:
+
+```
+  bazel test --test_tag_filters=@PLUGIN@ //...
+  bazel test plugins/@PLUGIN@:@PLUGIN@_tests
+```
+
 This project can be imported into the Eclipse IDE:
 Add the plugin name to the `CUSTOM_PLUGINS` and to the
 `CUSTOM_PLUGINS_TEST_DEPS` set in Gerrit core in
@@ -80,12 +87,6 @@
   ./tools/eclipse/project.py
 ```
 
-To execute the tests run:
-
-```
-  bazel test --test_tag_filters=@PLUGIN@ //...
-```
-
 How to build the Gerrit Plugin API is described in the [Gerrit
 documentation](../../../Documentation/dev-bazel.html#_extension_and_plugin_api_jar_files).
 
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index bb72501..e79cbdd 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -191,6 +191,11 @@
 :   Whether to synchronize secondary indexes.
     Defaults to true.
 
+```index.synchronizeForced```
+:   Whether to synchronize forced index events. E.g. on-line reindex
+    automatically triggered upon version upgrades.
+    Defaults to true.
+
 ```index.threadPoolSize```
 :   Maximum number of threads used to send index events to the target instance.
     Defaults to 4.
diff --git a/src/test/README.md b/src/test/README.md
new file mode 100644
index 0000000..c7a5225
--- /dev/null
+++ b/src/test/README.md
@@ -0,0 +1,8 @@
+# About this directory structure
+
+Refer to docker/README.md for more about these directory structures:
+
+```
+  ./resources/com
+  ./scala
+```
diff --git a/src/test/docker/README.md b/src/test/docker/README.md
index 68030ae..d97e929 100644
--- a/src/test/docker/README.md
+++ b/src/test/docker/README.md
@@ -39,6 +39,30 @@
   $ docker-compose up
 ```
 
+## How to test
+
+Consider the
+[instructions](https://gerrit-review.googlesource.com/Documentation/dev-e2e-tests.html)
+on how to use Gerrit core's Gatling framework, to run non-core test
+scenarios such as this plugin one below:
+
+```
+  $ sbt "gatling:testOnly com.ericsson.gerrit.plugins.highavailability.scenarios.CloneUsingHAGerrit2"
+```
+
+This is a scenario that can serve as an example for how to start
+testing an HA Gerrit system. That scenario tries to clone a project
+created on gerrit 1 (port 8081) but from gerrit 2 (on 8082). The
+scenario therefore expects Gerrit HA to have properly synchronized
+the new project from 1 to 2. That project gets deleted after, here
+using HA Gerrit straight (no specific port).
+
+Scenario scala source files and their companion json resource ones are
+stored under the usual src/test directories. That structure follows the
+scala package one from the scenario classes. The core framework expects
+such a directory structure for both the scala and resources (json data)
+files.
+
 ## How to stop
 
 Simply type CTRL+C on the window that started the environment
@@ -53,4 +77,3 @@
 ```
   $ docker-compose down
 ```
-
diff --git a/src/test/docker/etc/gerrit.config b/src/test/docker/etc/gerrit.config
index f7ee7ec..c110835 100644
--- a/src/test/docker/etc/gerrit.config
+++ b/src/test/docker/etc/gerrit.config
@@ -27,5 +27,3 @@
 	scheme = http
 	scheme = ssh
 	scheme = anon_http
-[plugin "plugin-manager"]
-	jenkinsUrl = https://archive-ci.gerritforge.com
diff --git a/src/test/docker/gerrit/Dockerfile b/src/test/docker/gerrit/Dockerfile
index fb4ab9c..0328827 100644
--- a/src/test/docker/gerrit/Dockerfile
+++ b/src/test/docker/gerrit/Dockerfile
@@ -1,18 +1,19 @@
-FROM gerritcodereview/gerrit:2.15.18
+FROM gerritcodereview/gerrit:2.16.19
 
-ENV GERRIT_BRANCH=stable-2.15
+ENV GERRIT_BRANCH=stable-2.16
 
-ENV GERRIT_CI_URL=https://archive-ci.gerritforge.com/job
+ENV GERRIT_CI_URL=https://gerrit-ci.gerritforge.com/job
 
 USER root
 
-RUN yum install -y iputils-ping netcat postgresql curl lsof gettext moreutils net-tools netcat inetutils-ping
+RUN yum install -y iputils-ping netcat postgresql curl lsof gettext moreutils net-tools netcat inetutils-ping sudo
 
 USER gerrit
 
-ADD $GERRIT_CI_URL/plugin-javamelody-bazel-$GERRIT_BRANCH/lastSuccessfulBuild/artifact/bazel-bin/plugins/javamelody/javamelody.jar /var/gerrit/plugins/javamelody.jar
-ADD $GERRIT_CI_URL/plugin-javamelody-bazel-$GERRIT_BRANCH/lastSuccessfulBuild/artifact/bazel-bin/plugins/javamelody/javamelody-deps_deploy.jar /var/gerrit/lib/javamelody-deps_deploy.jar
-ADD $GERRIT_CI_URL/plugin-high-availability-bazel-$GERRIT_BRANCH/lastSuccessfulBuild/artifact/bazel-bin/plugins/high-availability/high-availability.jar /var/gerrit/plugins/high-availability.jar
+ADD --chown=gerrit:gerrit $GERRIT_CI_URL/plugin-javamelody-bazel-$GERRIT_BRANCH/lastSuccessfulBuild/artifact/bazel-bin/plugins/javamelody/javamelody.jar /var/gerrit/plugins/javamelody.jar
+ADD --chown=gerrit:gerrit $GERRIT_CI_URL/plugin-javamelody-bazel-$GERRIT_BRANCH/lastSuccessfulBuild/artifact/bazel-bin/plugins/javamelody/javamelody-deps_deploy.jar /var/gerrit/lib/javamelody-deps_deploy.jar
+ADD --chown=gerrit:gerrit $GERRIT_CI_URL/plugin-high-availability-bazel-$GERRIT_BRANCH/lastSuccessfulBuild/artifact/bazel-bin/plugins/high-availability/high-availability.jar /var/gerrit/plugins/high-availability.jar
+ADD --chown=gerrit:gerrit $GERRIT_CI_URL/plugin-delete-project-bazel-$GERRIT_BRANCH/lastSuccessfulBuild/artifact/bazel-bin/plugins/delete-project/delete-project.jar /var/gerrit/plugins/delete-project.jar
 
 USER root
 
diff --git a/src/test/docker/gerrit/start.sh b/src/test/docker/gerrit/start.sh
index f3d4560..7aa3550 100755
--- a/src/test/docker/gerrit/start.sh
+++ b/src/test/docker/gerrit/start.sh
@@ -25,4 +25,4 @@
 sudo -u gerrit touch /var/gerrit/logs/{gc_log,error_log,httpd_log,sshd_log,replication_log} && tail -f /var/gerrit/logs/* | grep --line-buffered -v 'HEAD /' &
 
 echo "Running Gerrit ..."
-sudo -u gerrit /etc/init.d/gerrit run
+sudo -u gerrit /var/gerrit/bin/gerrit.sh run
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/ConfigurationTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/ConfigurationTest.java
index 63ab408..2cf12fc 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/ConfigurationTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/ConfigurationTest.java
@@ -34,7 +34,9 @@
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.Http.RETRY_INTERVAL_KEY;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.Http.SOCKET_TIMEOUT_KEY;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.Http.USER_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Index.DEFAULT_SYNCHRONIZE_FORCED;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.Index.INDEX_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.Index.SYNCHRONIZE_FORCED_KEY;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.JGroups.CLUSTER_NAME_KEY;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.JGroups.DEFAULT_CLUSTER_NAME;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.JGroups.DEFAULT_SKIP_INTERFACE_LIST;
@@ -384,4 +386,13 @@
     globalPluginConfig.setInt(INDEX_SECTION, null, NUM_STRIPED_LOCKS, 100);
     assertThat(getConfiguration().index().numStripedLocks()).isEqualTo(100);
   }
+
+  @Test
+  public void testGetIndexSynchronizeForced() throws Exception {
+    assertThat(getConfiguration().index().synchronizeForced())
+        .isEqualTo(DEFAULT_SYNCHRONIZE_FORCED);
+
+    globalPluginConfig.setBoolean(INDEX_SECTION, null, SYNCHRONIZE_FORCED_KEY, false);
+    assertThat(getConfiguration().index().synchronizeForced()).isFalse();
+  }
 }
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 7299238..7233aac 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
@@ -64,9 +64,9 @@
   private final CountDownLatch expectedRequestLatch = new CountDownLatch(1);
 
   @Override
-  public void setUp() throws Exception {
+  public void setUpTestPlugin() throws Exception {
     givenThat(any(anyUrl()).willReturn(aResponse().withStatus(HttpStatus.SC_NO_CONTENT)));
-    super.setUp();
+    super.setUpTestPlugin();
   }
 
   @Test
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
index 9d08f15..22ef89e 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/cache/ProjectListIT.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/cache/ProjectListIT.java
@@ -49,9 +49,9 @@
   @Rule public WireMockRule wireMockRule = new WireMockRule(options().port(PORT));
 
   @Override
-  public void setUp() throws Exception {
+  public void setUpTestPlugin() throws Exception {
     givenThat(any(anyUrl()).willReturn(aResponse().withStatus(HttpStatus.SC_NO_CONTENT)));
-    super.setUp();
+    super.setUpTestPlugin();
   }
 
   @Test
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedAwareEventBrokerTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedAwareEventBrokerTest.java
index 02b4a47..ec5b1de 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedAwareEventBrokerTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedAwareEventBrokerTest.java
@@ -18,9 +18,11 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
 
-import com.google.gerrit.common.EventListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.EventListener;
+import com.google.gerrit.server.plugincontext.PluginContext.PluginMetrics;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -32,9 +34,11 @@
 
   @Before
   public void setUp() {
+    PluginMetrics mockMetrics = mock(PluginMetrics.class);
     listenerMock = mock(EventListener.class);
-    DynamicSet<EventListener> listeners = DynamicSet.emptySet();
-    listeners.add(listenerMock);
+    DynamicSet<EventListener> set = DynamicSet.emptySet();
+    set.add("high-availability", listenerMock);
+    PluginSetContext<EventListener> listeners = new PluginSetContext<>(set, mockMetrics);
     broker = new ForwardedAwareEventBroker(null, listeners, null, null, null, null);
   }
 
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 3426b05..44bb1ea 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
@@ -15,6 +15,7 @@
 package com.ericsson.gerrit.plugins.highavailability.forwarder;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.verify;
@@ -24,9 +25,7 @@
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.reviewdb.client.Account;
 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;
@@ -35,7 +34,6 @@
 @RunWith(MockitoJUnitRunner.class)
 public class ForwardedCacheEvictionHandlerTest {
 
-  @Rule public ExpectedException exception = ExpectedException.none();
   @Mock private DynamicMap<Cache<?, ?>> cacheMapMock;
   @Mock private Cache<?, ?> cacheMock;
   private ForwardedCacheEvictionHandler handler;
@@ -48,11 +46,11 @@
   @Test
   public void shouldThrowAnExceptionWhenCacheNotFound() throws Exception {
     CacheEntry entry = new CacheEntry("somePlugin", "unexistingCache", null);
-
-    exception.expect(CacheNotFoundException.class);
-    exception.expectMessage(
-        String.format("cache %s.%s not found", entry.getPluginName(), entry.getCacheName()));
-    handler.evict(entry);
+    String expectedMessage =
+        String.format("cache %s.%s not found", entry.getPluginName(), entry.getCacheName());
+    CacheNotFoundException thrown =
+        assertThrows(CacheNotFoundException.class, () -> handler.evict(entry));
+    assertThat(thrown).hasMessageThat().contains(expectedMessage);
   }
 
   @Test
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 ecb976e..a855c5e 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
@@ -15,18 +15,16 @@
 package com.ericsson.gerrit.plugins.highavailability.forwarder;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.fail;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.verify;
 
-import com.google.gerrit.common.EventDispatcher;
 import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.EventDispatcher;
 import com.google.gerrit.server.events.ProjectCreatedEvent;
 import com.google.gwtorm.server.OrmException;
 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;
@@ -35,7 +33,6 @@
 @RunWith(MockitoJUnitRunner.class)
 public class ForwardedEventHandlerTest {
 
-  @Rule public ExpectedException exception = ExpectedException.none();
   @Mock private EventDispatcher dispatcherMock;
   private ForwardedEventHandler handler;
 
@@ -85,12 +82,8 @@
         .postEvent(event);
 
     assertThat(Context.isForwardedEvent()).isFalse();
-    try {
-      handler.dispatch(event);
-      fail("should have throw an OrmException");
-    } catch (OrmException e) {
-      assertThat(e.getMessage()).isEqualTo("someMessage");
-    }
+    OrmException thrown = assertThrows(OrmException.class, () -> handler.dispatch(event));
+    assertThat(thrown).hasMessageThat().contains("someMessage");
     assertThat(Context.isForwardedEvent()).isFalse();
 
     verify(dispatcherMock).postEvent(event);
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexAccountHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexAccountHandlerTest.java
index c2d3659..b88e9c0 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexAccountHandlerTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexAccountHandlerTest.java
@@ -15,6 +15,7 @@
 package com.ericsson.gerrit.plugins.highavailability.forwarder;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.verify;
@@ -27,9 +28,7 @@
 import java.io.IOException;
 import java.util.Optional;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
@@ -38,7 +37,6 @@
 @RunWith(MockitoJUnitRunner.class)
 public class ForwardedIndexAccountHandlerTest {
 
-  @Rule public ExpectedException exception = ExpectedException.none();
   @Mock private AccountIndexer indexerMock;
   @Mock private Configuration configMock;
   @Mock private Configuration.Index indexMock;
@@ -61,9 +59,11 @@
 
   @Test
   public void deleteIsNotSupported() throws Exception {
-    exception.expect(UnsupportedOperationException.class);
-    exception.expectMessage("Delete from account index not supported");
-    handler.index(id, Operation.DELETE, Optional.empty());
+    UnsupportedOperationException thrown =
+        assertThrows(
+            UnsupportedOperationException.class,
+            () -> handler.index(id, Operation.DELETE, Optional.empty()));
+    assertThat(thrown).hasMessageThat().contains("Delete from account index not supported");
   }
 
   @Test
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandlerTest.java
index 8bfe47d..6bfd47d 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandlerTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandlerTest.java
@@ -15,6 +15,7 @@
 package com.ericsson.gerrit.plugins.highavailability.forwarder;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doAnswer;
@@ -28,20 +29,18 @@
 import com.ericsson.gerrit.plugins.highavailability.index.ChangeChecker;
 import com.ericsson.gerrit.plugins.highavailability.index.ChangeCheckerImpl;
 import com.ericsson.gerrit.plugins.highavailability.index.ChangeDb;
-import com.google.gerrit.common.TimeUtil;
 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.notedb.ChangeNotes;
 import com.google.gerrit.server.util.OneOffRequestContext;
+import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
 import java.util.Optional;
 import java.util.concurrent.ScheduledExecutorService;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
@@ -62,7 +61,6 @@
   private static final boolean CHANGE_UP_TO_DATE = true;
   private static final boolean CHANGE_OUTDATED = false;
 
-  @Rule public ExpectedException exception = ExpectedException.none();
   @Mock private ChangeIndexer indexerMock;
   @Mock private ChangeDb changeDbMock;
   @Mock private ReviewDb dbMock;
@@ -120,22 +118,22 @@
   public void changeToIndexDoesNotExist() throws Exception {
     setupChangeAccessRelatedMocks(CHANGE_DOES_NOT_EXIST, CHANGE_OUTDATED);
     handler.index(TEST_CHANGE_ID, Operation.INDEX, Optional.empty());
-    verify(indexerMock, times(1)).delete(id);
+    verify(indexerMock, times(0)).delete(id);
   }
 
   @Test
   public void schemaThrowsExceptionWhenLookingUpForChange() throws Exception {
     setupChangeAccessRelatedMocks(CHANGE_EXISTS, THROW_ORM_EXCEPTION, CHANGE_UP_TO_DATE);
-    exception.expect(OrmException.class);
-    handler.index(TEST_CHANGE_ID, Operation.INDEX, Optional.empty());
+    assertThrows(
+        OrmException.class, () -> handler.index(TEST_CHANGE_ID, Operation.INDEX, Optional.empty()));
   }
 
   @Test
   public void indexerThrowsIOExceptionTryingToIndexChange() throws Exception {
     setupChangeAccessRelatedMocks(
         CHANGE_EXISTS, DO_NOT_THROW_ORM_EXCEPTION, THROW_IO_EXCEPTION, CHANGE_UP_TO_DATE);
-    exception.expect(IOException.class);
-    handler.index(TEST_CHANGE_ID, Operation.INDEX, Optional.empty());
+    assertThrows(
+        IOException.class, () -> handler.index(TEST_CHANGE_ID, Operation.INDEX, Optional.empty()));
   }
 
   @Test
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexGroupHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexGroupHandlerTest.java
index ab55b73..845abb0 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexGroupHandlerTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexGroupHandlerTest.java
@@ -15,6 +15,7 @@
 package com.ericsson.gerrit.plugins.highavailability.forwarder;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.verify;
@@ -27,9 +28,7 @@
 import java.io.IOException;
 import java.util.Optional;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
@@ -38,7 +37,6 @@
 @RunWith(MockitoJUnitRunner.class)
 public class ForwardedIndexGroupHandlerTest {
 
-  @Rule public ExpectedException exception = ExpectedException.none();
   @Mock private GroupIndexer indexerMock;
   @Mock private Configuration configMock;
   @Mock private Configuration.Index indexMock;
@@ -61,9 +59,11 @@
 
   @Test
   public void deleteIsNotSupported() throws Exception {
-    exception.expect(UnsupportedOperationException.class);
-    exception.expectMessage("Delete from group index not supported");
-    handler.index(uuid, Operation.DELETE, Optional.empty());
+    UnsupportedOperationException thrown =
+        assertThrows(
+            UnsupportedOperationException.class,
+            () -> handler.index(uuid, Operation.DELETE, Optional.empty()));
+    assertThat(thrown).hasMessageThat().contains("Delete from group index not supported");
   }
 
   @Test
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexProjectHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexProjectHandlerTest.java
new file mode 100644
index 0000000..a0c6979
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexProjectHandlerTest.java
@@ -0,0 +1,111 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.ericsson.gerrit.plugins.highavailability.forwarder;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.ericsson.gerrit.plugins.highavailability.Configuration;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexingHandler.Operation;
+import com.google.gerrit.index.project.ProjectIndexer;
+import com.google.gerrit.reviewdb.client.Project;
+import java.io.IOException;
+import java.util.Optional;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ForwardedIndexProjectHandlerTest {
+
+  @Mock private ProjectIndexer indexerMock;
+  @Mock private Configuration configMock;
+  @Mock private Configuration.Index indexMock;
+  private ForwardedIndexProjectHandler handler;
+  private Project.NameKey nameKey;
+
+  @Before
+  public void setUp() {
+    when(configMock.index()).thenReturn(indexMock);
+    when(indexMock.numStripedLocks()).thenReturn(10);
+    handler = new ForwardedIndexProjectHandler(indexerMock, configMock);
+    nameKey = new Project.NameKey("project/name");
+  }
+
+  @Test
+  public void testSuccessfulIndexing() throws Exception {
+    handler.index(nameKey, Operation.INDEX, Optional.empty());
+    verify(indexerMock).index(nameKey);
+  }
+
+  @Test
+  public void deleteIsNotSupported() throws Exception {
+    UnsupportedOperationException thrown =
+        assertThrows(
+            UnsupportedOperationException.class,
+            () -> handler.index(nameKey, Operation.DELETE, Optional.empty()));
+    assertThat(thrown).hasMessageThat().contains("Delete from project index not supported");
+  }
+
+  @Test
+  public void shouldSetAndUnsetForwardedContext() throws Exception {
+    // this doAnswer is to allow to assert that context is set to forwarded
+    // while cache eviction is called.
+    doAnswer(
+            (Answer<Void>)
+                invocation -> {
+                  assertThat(Context.isForwardedEvent()).isTrue();
+                  return null;
+                })
+        .when(indexerMock)
+        .index(nameKey);
+
+    assertThat(Context.isForwardedEvent()).isFalse();
+    handler.index(nameKey, Operation.INDEX, Optional.empty());
+    assertThat(Context.isForwardedEvent()).isFalse();
+
+    verify(indexerMock).index(nameKey);
+  }
+
+  @Test
+  public void shouldSetAndUnsetForwardedContextEvenIfExceptionIsThrown() throws Exception {
+    doAnswer(
+            (Answer<Void>)
+                invocation -> {
+                  assertThat(Context.isForwardedEvent()).isTrue();
+                  throw new IOException("someMessage");
+                })
+        .when(indexerMock)
+        .index(nameKey);
+
+    assertThat(Context.isForwardedEvent()).isFalse();
+    try {
+      handler.index(nameKey, Operation.INDEX, Optional.empty());
+      fail("should have thrown an IOException");
+    } catch (IOException e) {
+      assertThat(e.getMessage()).isEqualTo("someMessage");
+    }
+    assertThat(Context.isForwardedEvent()).isFalse();
+
+    verify(indexerMock).index(nameKey);
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedProjectListUpdateHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedProjectListUpdateHandlerTest.java
index 3ee8581..f5521c5 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedProjectListUpdateHandlerTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedProjectListUpdateHandlerTest.java
@@ -15,16 +15,14 @@
 package com.ericsson.gerrit.plugins.highavailability.forwarder;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.fail;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 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;
@@ -36,7 +34,6 @@
   private static final String PROJECT_NAME = "someProject";
   private static final String SOME_MESSAGE = "someMessage";
   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;
 
@@ -109,12 +106,9 @@
         .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(SOME_MESSAGE);
-    }
+    RuntimeException thrown =
+        assertThrows(RuntimeException.class, () -> handler.update(PROJECT_NAME, false));
+    assertThat(thrown).hasMessageThat().contains(SOME_MESSAGE);
     assertThat(Context.isForwardedEvent()).isFalse();
 
     verify(projectCacheMock).onCreateProject(PROJECT_KEY);
@@ -132,12 +126,9 @@
         .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(SOME_MESSAGE);
-    }
+    RuntimeException thrown =
+        assertThrows(RuntimeException.class, () -> handler.update(PROJECT_NAME, true));
+    assertThat(thrown).hasMessageThat().contains(SOME_MESSAGE);
     assertThat(Context.isForwardedEvent()).isFalse();
 
     verify(projectCacheMock).remove(PROJECT_KEY);
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexProjectRestApiServletTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexProjectRestApiServletTest.java
new file mode 100644
index 0000000..83420b9
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexProjectRestApiServletTest.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.ericsson.gerrit.plugins.highavailability.forwarder.rest;
+
+import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
+import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED;
+import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexProjectHandler;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexingHandler.Operation;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.Project;
+import java.io.IOException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class IndexProjectRestApiServletTest {
+  private static final String IO_ERROR = "io-error";
+  private static final String PROJECT_NAME = "test/project";
+
+  @Mock private ForwardedIndexProjectHandler handlerMock;
+  @Mock private HttpServletRequest requestMock;
+  @Mock private HttpServletResponse responseMock;
+
+  private Project.NameKey nameKey;
+  private IndexProjectRestApiServlet servlet;
+
+  @Before
+  public void setUpMocks() {
+    servlet = new IndexProjectRestApiServlet(handlerMock);
+    nameKey = new Project.NameKey(PROJECT_NAME);
+    when(requestMock.getRequestURI())
+        .thenReturn("http://gerrit.com/index/project/" + Url.encode(nameKey.get()));
+  }
+
+  @Test
+  public void projectIsIndexed() throws Exception {
+    servlet.doPost(requestMock, responseMock);
+    verify(handlerMock, times(1)).index(eq(nameKey), eq(Operation.INDEX), any());
+    verify(responseMock).setStatus(SC_NO_CONTENT);
+  }
+
+  @Test
+  public void cannotDeleteProject() throws Exception {
+    servlet.doDelete(requestMock, responseMock);
+    verify(responseMock).sendError(SC_METHOD_NOT_ALLOWED, "cannot delete project from index");
+  }
+
+  @Test
+  public void indexerThrowsIOExceptionTryingToIndexProject() throws Exception {
+    doThrow(new IOException(IO_ERROR))
+        .when(handlerMock)
+        .index(eq(nameKey), eq(Operation.INDEX), any());
+    servlet.doPost(requestMock, responseMock);
+    verify(responseMock).sendError(SC_CONFLICT, IO_ERROR);
+  }
+
+  @Test
+  public void sendErrorThrowsIOException() throws Exception {
+    doThrow(new IOException(IO_ERROR))
+        .when(handlerMock)
+        .index(eq(nameKey), eq(Operation.INDEX), any());
+    doThrow(new IOException("someError")).when(responseMock).sendError(SC_CONFLICT, IO_ERROR);
+    servlet.doPost(requestMock, responseMock);
+    verify(responseMock).sendError(SC_CONFLICT, IO_ERROR);
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/AbstractIndexForwardingIT.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/AbstractIndexForwardingIT.java
index 2eee205..af4637e 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
@@ -51,10 +51,10 @@
   @Rule public WireMockRule wireMockRule = new WireMockRule(options().port(PORT));
 
   @Override
-  public void setUp() throws Exception {
+  public void setUpTestPlugin() throws Exception {
     givenThat(any(anyUrl()).willReturn(aResponse().withStatus(HttpStatus.SC_NO_CONTENT)));
     beforeAction();
-    super.setUp();
+    super.setUpTestPlugin();
   }
 
   @Test
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 d9e1b22..2d12ca8 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
@@ -18,11 +18,13 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
+import com.ericsson.gerrit.plugins.highavailability.Configuration;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.Context;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.Forwarder;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.IndexEvent;
@@ -34,8 +36,12 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.util.OneOffRequestContext;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import java.util.Optional;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.function.Consumer;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -58,6 +64,15 @@
   private Change.Id changeId;
   private Account.Id accountId;
   private AccountGroup.UUID accountGroupUUID;
+  @Mock private RequestContext mockCtx;
+
+  private CurrentRequestContext currCtx =
+      new CurrentRequestContext(null, null, null) {
+        @Override
+        public void onlyWithContext(Consumer<RequestContext> body) {
+          body.accept(mockCtx);
+        }
+      };
 
   @Before
   public void setUpMocks() throws Exception {
@@ -66,9 +81,18 @@
     accountGroupUUID = new AccountGroup.UUID(UUID);
     when(changeCheckerFactoryMock.create(any())).thenReturn(changeCheckerMock);
     when(changeCheckerMock.newIndexEvent()).thenReturn(Optional.of(new IndexEvent()));
+
+    setUpIndexEventHandler(currCtx);
+  }
+
+  public void setUpIndexEventHandler(CurrentRequestContext currCtx) throws Exception {
     indexEventHandler =
         new IndexEventHandler(
-            MoreExecutors.directExecutor(), PLUGIN_NAME, forwarder, changeCheckerFactoryMock);
+            MoreExecutors.directExecutor(),
+            PLUGIN_NAME,
+            forwarder,
+            changeCheckerFactoryMock,
+            currCtx);
   }
 
   @Test
@@ -78,6 +102,33 @@
   }
 
   @Test
+  public void shouldNotIndexInRemoteWhenContextIsMissing() throws Exception {
+    ThreadLocalRequestContext threadLocalCtxMock = mock(ThreadLocalRequestContext.class);
+    OneOffRequestContext oneOffCtxMock = mock(OneOffRequestContext.class);
+    Configuration cfgMock = mock(Configuration.class);
+    Configuration.Index cfgIndex = mock(Configuration.Index.class);
+    when(cfgMock.index()).thenReturn(cfgIndex);
+
+    setUpIndexEventHandler(new CurrentRequestContext(threadLocalCtxMock, cfgMock, oneOffCtxMock));
+    indexEventHandler.onChangeIndexed(PROJECT_NAME, changeId.get());
+    verify(forwarder, never()).indexChange(eq(PROJECT_NAME), eq(CHANGE_ID), any());
+  }
+
+  @Test
+  public void shouldReindexInRemoteWhenContextIsMissingButForcedIndexingEnabled() throws Exception {
+    ThreadLocalRequestContext threadLocalCtxMock = mock(ThreadLocalRequestContext.class);
+    OneOffRequestContext oneOffCtxMock = mock(OneOffRequestContext.class);
+    Configuration cfgMock = mock(Configuration.class);
+    Configuration.Index cfgIndex = mock(Configuration.Index.class);
+    when(cfgMock.index()).thenReturn(cfgIndex);
+    when(cfgIndex.synchronizeForced()).thenReturn(true);
+
+    setUpIndexEventHandler(new CurrentRequestContext(threadLocalCtxMock, cfgMock, oneOffCtxMock));
+    indexEventHandler.onChangeIndexed(PROJECT_NAME, changeId.get());
+    verify(forwarder).indexChange(eq(PROJECT_NAME), eq(CHANGE_ID), any());
+  }
+
+  @Test
   public void shouldIndexInRemoteOnAccountIndexedEvent() throws Exception {
     indexEventHandler.onAccountIndexed(accountId.get());
     verify(forwarder).indexAccount(eq(ACCOUNT_ID), any());
@@ -128,7 +179,7 @@
   public void duplicateChangeEventOfAQueuedEventShouldGetDiscarded() {
     ScheduledThreadPoolExecutor poolMock = mock(ScheduledThreadPoolExecutor.class);
     indexEventHandler =
-        new IndexEventHandler(poolMock, PLUGIN_NAME, forwarder, changeCheckerFactoryMock);
+        new IndexEventHandler(poolMock, PLUGIN_NAME, forwarder, changeCheckerFactoryMock, currCtx);
     indexEventHandler.onChangeIndexed(PROJECT_NAME, changeId.get());
     indexEventHandler.onChangeIndexed(PROJECT_NAME, changeId.get());
     verify(poolMock, times(1))
@@ -139,7 +190,7 @@
   public void duplicateAccountEventOfAQueuedEventShouldGetDiscarded() {
     ScheduledThreadPoolExecutor poolMock = mock(ScheduledThreadPoolExecutor.class);
     indexEventHandler =
-        new IndexEventHandler(poolMock, PLUGIN_NAME, forwarder, changeCheckerFactoryMock);
+        new IndexEventHandler(poolMock, PLUGIN_NAME, forwarder, changeCheckerFactoryMock, currCtx);
     indexEventHandler.onAccountIndexed(accountId.get());
     indexEventHandler.onAccountIndexed(accountId.get());
     verify(poolMock, times(1)).execute(indexEventHandler.new IndexAccountTask(ACCOUNT_ID));
@@ -149,7 +200,7 @@
   public void duplicateGroupEventOfAQueuedEventShouldGetDiscarded() {
     ScheduledThreadPoolExecutor poolMock = mock(ScheduledThreadPoolExecutor.class);
     indexEventHandler =
-        new IndexEventHandler(poolMock, PLUGIN_NAME, forwarder, changeCheckerFactoryMock);
+        new IndexEventHandler(poolMock, PLUGIN_NAME, forwarder, changeCheckerFactoryMock, currCtx);
     indexEventHandler.onGroupIndexed(accountGroupUUID.get());
     indexEventHandler.onGroupIndexed(accountGroupUUID.get());
     verify(poolMock, times(1)).execute(indexEventHandler.new IndexGroupTask(UUID));
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/ProjectIndexForwardingIT.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/ProjectIndexForwardingIT.java
new file mode 100644
index 0000000..951e4f6
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/ProjectIndexForwardingIT.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.ericsson.gerrit.plugins.highavailability.index;
+
+import com.google.gerrit.extensions.restapi.Url;
+
+public class ProjectIndexForwardingIT extends AbstractIndexForwardingIT {
+  private String someProjectName;
+
+  @Override
+  public void beforeAction() throws Exception {
+    someProjectName = gApi.projects().create("someProject").get().name;
+  }
+
+  @Override
+  public String getExpectedRequest() {
+    return "/plugins/high-availability/index/project/" + Url.encode(someProjectName);
+  }
+
+  @Override
+  public void doAction() throws Exception {
+    gApi.projects().name(someProjectName).index(false);
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/MyUrlProviderTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/MyUrlProviderTest.java
index f3e8b7d..c337947 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/MyUrlProviderTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/MyUrlProviderTest.java
@@ -15,6 +15,7 @@
 package com.ericsson.gerrit.plugins.highavailability.peers.jgroups;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.net.InetAddress.getLocalHost;
 import static org.mockito.Answers.RETURNS_DEEP_STUBS;
 import static org.mockito.Mockito.when;
@@ -23,9 +24,7 @@
 import com.google.inject.ProvisionException;
 import org.eclipse.jgit.lib.Config;
 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;
@@ -38,8 +37,6 @@
   private static final String HTTPS = "https://";
   private static final String LISTEN_URL = "listenUrl";
 
-  @Rule public ExpectedException exception = ExpectedException.none();
-
   @Mock(answer = RETURNS_DEEP_STUBS)
   private Configuration configurationMock;
 
@@ -70,33 +67,29 @@
 
   @Test
   public void testGetJGroupsMyUrlFromListenUrlWhenNoListenUrlSpecified() throws Exception {
-    exception.expect(ProvisionException.class);
-    exception.expectMessage("exactly 1 value configured; found 0");
-    getMyUrlProvider();
+    ProvisionException thrown = assertThrows(ProvisionException.class, () -> getMyUrlProvider());
+    assertThat(thrown).hasMessageThat().contains("exactly 1 value configured; found 0");
   }
 
   @Test
   public void testGetJGroupsMyUrlFromListenUrlWhenMultipleListenUrlsSpecified() throws Exception {
     gerritServerConfig.setStringList(HTTPD, null, LISTEN_URL, Lists.newArrayList("a", "b"));
-    exception.expect(ProvisionException.class);
-    exception.expectMessage("exactly 1 value configured; found 2");
-    getMyUrlProvider();
+    ProvisionException thrown = assertThrows(ProvisionException.class, () -> getMyUrlProvider());
+    assertThat(thrown).hasMessageThat().contains("exactly 1 value configured; found 2");
   }
 
   @Test
   public void testGetJGroupsMyUrlFromListenUrlWhenReverseProxyConfigured() throws Exception {
     gerritServerConfig.setString(HTTPD, null, LISTEN_URL, "proxy-https://foo");
-    exception.expect(ProvisionException.class);
-    exception.expectMessage("when configured as reverse-proxy");
-    getMyUrlProvider();
+    ProvisionException thrown = assertThrows(ProvisionException.class, () -> getMyUrlProvider());
+    assertThat(thrown).hasMessageThat().contains("when configured as reverse-proxy");
   }
 
   @Test
   public void testGetJGroupsMyUrlFromListenUrlWhenWildcardConfigured() throws Exception {
     gerritServerConfig.setString(HTTPD, null, LISTEN_URL, "https://*");
-    exception.expect(ProvisionException.class);
-    exception.expectMessage("when configured with wildcard");
-    getMyUrlProvider();
+    ProvisionException thrown = assertThrows(ProvisionException.class, () -> getMyUrlProvider());
+    assertThat(thrown).hasMessageThat().contains("when configured with wildcard");
   }
 
   @Test
diff --git a/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/CheckProjectsCacheFlushEntriesUsingHAGerrit1.json b/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/CheckProjectsCacheFlushEntriesUsingHAGerrit1.json
new file mode 100644
index 0000000..281515d
--- /dev/null
+++ b/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/CheckProjectsCacheFlushEntriesUsingHAGerrit1.json
@@ -0,0 +1,6 @@
+[
+  {
+    "url": "http://HOSTNAME:HTTP_PORT1/a/config/server/caches/projects",
+    "entries": "PROJECTS_ENTRIES"
+  }
+]
diff --git a/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/CloneUsingHAGerrit2.json b/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/CloneUsingHAGerrit2.json
new file mode 100644
index 0000000..3fd506d
--- /dev/null
+++ b/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/CloneUsingHAGerrit2.json
@@ -0,0 +1,6 @@
+[
+  {
+    "url": "http://HOSTNAME:HTTP_PORT2/_PROJECT",
+    "cmd": "clone"
+  }
+]
diff --git a/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/CreateChangeUsingHAGerrit1-body.json b/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/CreateChangeUsingHAGerrit1-body.json
new file mode 100644
index 0000000..23bf26c
--- /dev/null
+++ b/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/CreateChangeUsingHAGerrit1-body.json
@@ -0,0 +1,5 @@
+{
+  "project": "${project}",
+  "branch": "master",
+  "subject": "Change"
+}
diff --git a/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/CreateChangeUsingHAGerrit1.json b/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/CreateChangeUsingHAGerrit1.json
new file mode 100644
index 0000000..b535c1d
--- /dev/null
+++ b/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/CreateChangeUsingHAGerrit1.json
@@ -0,0 +1,6 @@
+[
+  {
+    "url": "http://HOSTNAME:HTTP_PORT1/a/changes/",
+    "project": "_PROJECT"
+  }
+]
diff --git a/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/CreateProjectUsingHAGerrit1-body.json b/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/CreateProjectUsingHAGerrit1-body.json
new file mode 100644
index 0000000..bcf4708
--- /dev/null
+++ b/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/CreateProjectUsingHAGerrit1-body.json
@@ -0,0 +1,3 @@
+{
+  "create_empty_commit": "true"
+}
diff --git a/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/CreateProjectUsingHAGerrit1.json b/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/CreateProjectUsingHAGerrit1.json
new file mode 100644
index 0000000..da3f028
--- /dev/null
+++ b/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/CreateProjectUsingHAGerrit1.json
@@ -0,0 +1,5 @@
+[
+  {
+    "url": "http://HOSTNAME:HTTP_PORT1/a/projects/PROJECT"
+  }
+]
diff --git a/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/CreateProjectUsingHAGerritTwice.json b/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/CreateProjectUsingHAGerritTwice.json
new file mode 100644
index 0000000..da1a058
--- /dev/null
+++ b/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/CreateProjectUsingHAGerritTwice.json
@@ -0,0 +1,4 @@
+[
+  {
+  }
+]
diff --git a/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/DeleteChangeUsingHAGerrit2.json b/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/DeleteChangeUsingHAGerrit2.json
new file mode 100644
index 0000000..8e0a304
--- /dev/null
+++ b/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/DeleteChangeUsingHAGerrit2.json
@@ -0,0 +1,6 @@
+[
+  {
+    "url": "http://HOSTNAME:HTTP_PORT2/a/changes/",
+    "number": "NUMBER"
+  }
+]
diff --git a/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/DeleteProjectUsingHAGerrit.json b/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/DeleteProjectUsingHAGerrit.json
new file mode 100644
index 0000000..be47699
--- /dev/null
+++ b/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/DeleteProjectUsingHAGerrit.json
@@ -0,0 +1,5 @@
+[
+  {
+    "url": "http://HOSTNAME/a/projects/PROJECT/delete-project~delete"
+  }
+]
diff --git a/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/FlushProjectsCacheUsingHAGerrit2.json b/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/FlushProjectsCacheUsingHAGerrit2.json
new file mode 100644
index 0000000..c938973
--- /dev/null
+++ b/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/FlushProjectsCacheUsingHAGerrit2.json
@@ -0,0 +1,5 @@
+[
+  {
+    "url": "http://HOSTNAME:HTTP_PORT2/a/config/server/caches/projects/flush"
+  }
+]
diff --git a/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/GetProjectsCacheEntries.json b/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/GetProjectsCacheEntries.json
new file mode 100644
index 0000000..f7450a4
--- /dev/null
+++ b/src/test/resources/com/ericsson/gerrit/plugins/highavailability/scenarios/GetProjectsCacheEntries.json
@@ -0,0 +1,5 @@
+[
+  {
+    "url": "http://HOSTNAME/a/config/server/caches/projects"
+  }
+]
diff --git a/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/CheckProjectsCacheFlushEntriesUsingHAGerrit1.scala b/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/CheckProjectsCacheFlushEntriesUsingHAGerrit1.scala
new file mode 100644
index 0000000..10374f8
--- /dev/null
+++ b/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/CheckProjectsCacheFlushEntriesUsingHAGerrit1.scala
@@ -0,0 +1,52 @@
+// Copyright (C) 2020 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.scenarios
+
+import com.google.gerrit.scenarios.CacheFlushSimulation
+import io.gatling.core.Predef.{atOnceUsers, _}
+import io.gatling.core.feeder.FeederBuilder
+import io.gatling.core.structure.ScenarioBuilder
+import io.gatling.http.Predef._
+
+class CheckProjectsCacheFlushEntriesUsingHAGerrit1 extends CacheFlushSimulation {
+  private val data: FeederBuilder = jsonFile(resource).convert(keys).queue
+
+  override def replaceOverride(in: String): String = {
+    replaceProperty("http_port1", 8081, in)
+  }
+
+  def this(producer: CacheFlushSimulation) {
+    this()
+    this.producer = Some(producer)
+  }
+
+  val test: ScenarioBuilder = scenario(unique)
+    .feed(data)
+    .exec(session => {
+      if (producer.nonEmpty) {
+        session.set(entriesKey, producer.get.expectedEntriesAfterFlush())
+      } else {
+        session
+      }
+    })
+    .exec(http(unique).get("${url}")
+      .check(regex("\"" + memKey + "\": (\\d+)")
+        .is(session => session(entriesKey).as[String])))
+
+  setUp(
+    test.inject(
+      atOnceUsers(1)
+    )).protocols(httpProtocol)
+}
diff --git a/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/CloneUsingHAGerrit2.scala b/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/CloneUsingHAGerrit2.scala
new file mode 100644
index 0000000..13a741a
--- /dev/null
+++ b/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/CloneUsingHAGerrit2.scala
@@ -0,0 +1,59 @@
+// Copyright (C) 2020 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.scenarios
+
+import com.google.gerrit.scenarios.GitSimulation
+import io.gatling.core.Predef.{atOnceUsers, _}
+import io.gatling.core.feeder.FeederBuilder
+import io.gatling.core.structure.ScenarioBuilder
+
+import scala.concurrent.duration._
+
+class CloneUsingHAGerrit2 extends GitSimulation {
+  private val data: FeederBuilder = jsonFile(resource).convert(keys).queue
+  private var default: String = name
+
+  def this(default: String) {
+    this()
+    this.default = default
+  }
+
+  override def replaceOverride(in: String): String = {
+    val next = replaceProperty("http_port2", 8082, in)
+    replaceKeyWith("_project", default, next)
+  }
+
+  val test: ScenarioBuilder = scenario(unique)
+    .feed(data)
+    .exec(gitRequest)
+
+  private val createProject = new CreateProjectUsingHAGerrit1(default)
+  private val deleteProject = new DeleteProjectUsingHAGerrit(default)
+
+  setUp(
+    createProject.test.inject(
+      nothingFor(stepWaitTime(createProject) seconds),
+      atOnceUsers(1)
+    ),
+    test.inject(
+      nothingFor(stepWaitTime(this) seconds),
+      atOnceUsers(1)
+    ),
+    deleteProject.test.inject(
+      nothingFor(stepWaitTime(deleteProject) seconds),
+      atOnceUsers(1)
+    ),
+  ).protocols(gitProtocol, httpProtocol)
+}
diff --git a/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/CreateChangeUsingHAGerrit1.scala b/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/CreateChangeUsingHAGerrit1.scala
new file mode 100644
index 0000000..f07aabd
--- /dev/null
+++ b/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/CreateChangeUsingHAGerrit1.scala
@@ -0,0 +1,68 @@
+// Copyright (C) 2020 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.scenarios
+
+import com.google.gerrit.scenarios.GerritSimulation
+import io.gatling.core.Predef.{atOnceUsers, _}
+import io.gatling.core.feeder.FeederBuilder
+import io.gatling.core.structure.ScenarioBuilder
+import io.gatling.http.Predef._
+
+import scala.concurrent.duration._
+
+class CreateChangeUsingHAGerrit1 extends GerritSimulation {
+  private val data: FeederBuilder = jsonFile(resource).convert(keys).queue
+  private val default: String = name
+  private val numberKey = "_number"
+
+  override def relativeRuntimeWeight = 10
+
+  override def replaceOverride(in: String): String = {
+    replaceProperty("http_port1", 8081, in)
+  }
+
+  private val test: ScenarioBuilder = scenario(unique)
+    .feed(data)
+    .exec(httpRequest
+      .body(ElFileBody(body)).asJson
+      .check(regex("\"" + numberKey + "\":(\\d+),").saveAs(numberKey)))
+    .exec(session => {
+      deleteChange.number = Some(session(numberKey).as[Int])
+      session
+    })
+
+  private val createProject = new CreateProjectUsingHAGerrit1(default)
+  private val deleteProject = new DeleteProjectUsingHAGerrit(default)
+  private val deleteChange = new DeleteChangeUsingHAGerrit2
+
+  setUp(
+    createProject.test.inject(
+      nothingFor(stepWaitTime(createProject) seconds),
+      atOnceUsers(1)
+    ),
+    test.inject(
+      nothingFor(stepWaitTime(this) seconds),
+      atOnceUsers(1)
+    ),
+    deleteChange.test.inject(
+      nothingFor(stepWaitTime(deleteChange) seconds),
+      atOnceUsers(1)
+    ),
+    deleteProject.test.inject(
+      nothingFor(stepWaitTime(deleteProject) seconds),
+      atOnceUsers(1)
+    ),
+  ).protocols(httpProtocol)
+}
diff --git a/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/CreateProjectUsingHAGerrit1.scala b/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/CreateProjectUsingHAGerrit1.scala
new file mode 100644
index 0000000..964aadb
--- /dev/null
+++ b/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/CreateProjectUsingHAGerrit1.scala
@@ -0,0 +1,43 @@
+// Copyright (C) 2020 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.scenarios
+
+import com.google.gerrit.scenarios.ProjectSimulation
+import io.gatling.core.Predef._
+import io.gatling.core.feeder.FeederBuilder
+import io.gatling.core.structure.ScenarioBuilder
+
+class CreateProjectUsingHAGerrit1 extends ProjectSimulation {
+  private val data: FeederBuilder = jsonFile(resource).convert(keys).queue
+
+  def this(default: String) {
+    this()
+    this.default = default
+  }
+
+  override def replaceOverride(in: String): String = {
+    val next = replaceProperty("http_port1", 8081, in)
+    super.replaceOverride(next)
+  }
+
+  val test: ScenarioBuilder = scenario(unique)
+    .feed(data)
+    .exec(httpRequest.body(RawFileBody(body)).asJson)
+
+  setUp(
+    test.inject(
+      atOnceUsers(1)
+    )).protocols(httpProtocol)
+}
diff --git a/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/CreateProjectUsingHAGerritTwice.scala b/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/CreateProjectUsingHAGerritTwice.scala
new file mode 100644
index 0000000..a4da93c
--- /dev/null
+++ b/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/CreateProjectUsingHAGerritTwice.scala
@@ -0,0 +1,53 @@
+// Copyright (C) 2020 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.scenarios
+
+import com.google.gerrit.scenarios.GitSimulation
+import io.gatling.core.Predef.{atOnceUsers, _}
+
+import scala.concurrent.duration._
+
+class CreateProjectUsingHAGerritTwice extends GitSimulation {
+  private val default: String = name
+
+  private val createProject = new CreateProjectUsingHAGerrit1(default)
+  private val deleteProject = new DeleteProjectUsingHAGerrit(default)
+  private val createItAgain = new CreateProjectUsingHAGerrit1(default)
+  private val verifyProject = new CloneUsingHAGerrit2(default)
+  private val deleteItAfter = new DeleteProjectUsingHAGerrit(default)
+
+  setUp(
+    createProject.test.inject(
+      nothingFor(stepWaitTime(createProject) seconds),
+      atOnceUsers(1)
+    ),
+    deleteProject.test.inject(
+      nothingFor(stepWaitTime(deleteProject) seconds),
+      atOnceUsers(1)
+    ),
+    createItAgain.test.inject(
+      nothingFor(stepWaitTime(createItAgain) seconds),
+      atOnceUsers(1)
+    ),
+    verifyProject.test.inject(
+      nothingFor(stepWaitTime(verifyProject) seconds),
+      atOnceUsers(1)
+    ),
+    deleteItAfter.test.inject(
+      nothingFor(stepWaitTime(deleteItAfter) seconds),
+      atOnceUsers(1)
+    ),
+  ).protocols(gitProtocol, httpProtocol)
+}
diff --git a/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/DeleteChangeUsingHAGerrit2.scala b/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/DeleteChangeUsingHAGerrit2.scala
new file mode 100644
index 0000000..5b0f461
--- /dev/null
+++ b/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/DeleteChangeUsingHAGerrit2.scala
@@ -0,0 +1,48 @@
+// Copyright (C) 2020 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.scenarios
+
+import com.google.gerrit.scenarios.GerritSimulation
+import io.gatling.core.Predef.{atOnceUsers, _}
+import io.gatling.core.feeder.FeederBuilder
+import io.gatling.core.structure.ScenarioBuilder
+import io.gatling.http.Predef.http
+
+class DeleteChangeUsingHAGerrit2 extends GerritSimulation {
+  private val data: FeederBuilder = jsonFile(resource).convert(keys).queue
+  var number: Option[Int] = None
+
+  override def relativeRuntimeWeight = 10
+
+  override def replaceOverride(in: String): String = {
+    replaceProperty("http_port2", 8082, in)
+  }
+
+  val test: ScenarioBuilder = scenario(unique)
+    .feed(data)
+    .exec(session => {
+      if (number.nonEmpty) {
+        session.set("number", number.get)
+      } else {
+        session
+      }
+    })
+    .exec(http(unique).delete("${url}${number}"))
+
+  setUp(
+    test.inject(
+      atOnceUsers(1)
+    )).protocols(httpProtocol)
+}
diff --git a/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/DeleteProjectUsingHAGerrit.scala b/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/DeleteProjectUsingHAGerrit.scala
new file mode 100644
index 0000000..428085d
--- /dev/null
+++ b/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/DeleteProjectUsingHAGerrit.scala
@@ -0,0 +1,38 @@
+// Copyright (C) 2020 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.scenarios
+
+import com.google.gerrit.scenarios.ProjectSimulation
+import io.gatling.core.Predef._
+import io.gatling.core.feeder.FeederBuilder
+import io.gatling.core.structure.ScenarioBuilder
+
+class DeleteProjectUsingHAGerrit extends ProjectSimulation {
+  private val data: FeederBuilder = jsonFile(resource).convert(keys).queue
+
+  def this(default: String) {
+    this()
+    this.default = default
+  }
+
+  val test: ScenarioBuilder = scenario(unique)
+    .feed(data)
+    .exec(httpRequest)
+
+  setUp(
+    test.inject(
+      atOnceUsers(1)
+    )).protocols(httpProtocol)
+}
diff --git a/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/FlushProjectsCacheUsingHAGerrit2.scala b/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/FlushProjectsCacheUsingHAGerrit2.scala
new file mode 100644
index 0000000..9c317d4
--- /dev/null
+++ b/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/FlushProjectsCacheUsingHAGerrit2.scala
@@ -0,0 +1,65 @@
+// Copyright (C) 2020 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.scenarios
+
+import com.google.gerrit.scenarios.CacheFlushSimulation
+import io.gatling.core.Predef._
+import io.gatling.core.feeder.FeederBuilder
+import io.gatling.core.structure.ScenarioBuilder
+
+import scala.concurrent.duration._
+
+class FlushProjectsCacheUsingHAGerrit2 extends CacheFlushSimulation {
+  private val data: FeederBuilder = jsonFile(resource).convert(keys).queue
+  private val default: String = name
+
+  override def relativeRuntimeWeight = 2
+
+  override def replaceOverride(in: String): String = {
+    replaceProperty("http_port2", 8082, in)
+  }
+
+  private val flushCache: ScenarioBuilder = scenario(unique)
+    .feed(data)
+    .exec(httpRequest)
+
+  private val createProject = new CreateProjectUsingHAGerrit1(default)
+  private val getCacheEntriesAfterProject = new GetProjectsCacheEntries(this)
+  private val checkCacheEntriesAfterFlush = new CheckProjectsCacheFlushEntriesUsingHAGerrit1(this)
+  private val deleteProject = new DeleteProjectUsingHAGerrit(default)
+
+  setUp(
+    createProject.test.inject(
+      nothingFor(stepWaitTime(createProject) seconds),
+      atOnceUsers(1)
+    ),
+    getCacheEntriesAfterProject.test.inject(
+      nothingFor(stepWaitTime(getCacheEntriesAfterProject) seconds),
+      atOnceUsers(1)
+    ),
+    flushCache.inject(
+      nothingFor(stepWaitTime(this) seconds),
+      atOnceUsers(1)
+    ),
+    checkCacheEntriesAfterFlush.test.inject(
+      nothingFor(stepWaitTime(checkCacheEntriesAfterFlush) seconds),
+      atOnceUsers(1)
+    ),
+    deleteProject.test.inject(
+      nothingFor(stepWaitTime(deleteProject) seconds),
+      atOnceUsers(1)
+    ),
+  ).protocols(httpProtocol)
+}
diff --git a/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/GetProjectsCacheEntries.scala b/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/GetProjectsCacheEntries.scala
new file mode 100644
index 0000000..9565365
--- /dev/null
+++ b/src/test/scala/com/ericsson/gerrit/plugins/highavailability/scenarios/GetProjectsCacheEntries.scala
@@ -0,0 +1,46 @@
+// Copyright (C) 2020 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.scenarios
+
+import com.google.gerrit.scenarios.CacheFlushSimulation
+import io.gatling.core.Predef._
+import io.gatling.core.feeder.FeederBuilder
+import io.gatling.core.structure.ScenarioBuilder
+import io.gatling.http.Predef.{http, _}
+
+class GetProjectsCacheEntries extends CacheFlushSimulation {
+  private val data: FeederBuilder = jsonFile(resource).convert(keys).queue
+
+  def this(consumer: CacheFlushSimulation) {
+    this()
+    this.consumer = Some(consumer)
+  }
+
+  val test: ScenarioBuilder = scenario(unique)
+    .feed(data)
+    .exec(http(unique).get("${url}")
+      .check(regex("\"" + memKey + "\": (\\d+)").saveAs(entriesKey)))
+    .exec(session => {
+      if (consumer.nonEmpty) {
+        consumer.get.entriesBeforeFlush(session(entriesKey).as[Int])
+      }
+      session
+    })
+
+  setUp(
+    test.inject(
+      atOnceUsers(1)
+    )).protocols(httpProtocol)
+}