Merge branch 'stable-2.14' into stable-2.15

* stable-2.14:
  RestForwarder: Replace lambda with method reference
  Always use the stored timestamp when checking for updates
  Use always the last TS of the reindex across runs
  GroupReindexRunnable: Replace lambdas with method reference
  Support n nodes when using static strategy

Change-Id: Ia17b19a6fb881eb0fc863e0d77b3c4521a4fdea7
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 8f1963a..d4c1272 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java
@@ -20,6 +20,7 @@
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.server.config.ConfigUtil;
@@ -32,8 +33,11 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -153,11 +157,6 @@
     }
   }
 
-  @Nullable
-  private static String trimTrailingSlash(@Nullable String in) {
-    return in == null ? null : CharMatcher.is('/').trimTrailingFrom(in);
-  }
-
   public static class Main {
     static final String MAIN_SECTION = "main";
     static final String SHARED_DIRECTORY_KEY = "sharedDirectory";
@@ -247,17 +246,20 @@
     static final String STATIC_SUBSECTION = PeerInfoStrategy.STATIC.name().toLowerCase();
     static final String URL_KEY = "url";
 
-    private final String url;
+    private final Set<String> urls;
 
     private PeerInfoStatic(Config cfg) {
-      url =
-          trimTrailingSlash(
-              Strings.nullToEmpty(cfg.getString(PEER_INFO_SECTION, STATIC_SUBSECTION, URL_KEY)));
-      log.debug("Url: {}", url);
+      urls =
+          Arrays.stream(cfg.getStringList(PEER_INFO_SECTION, STATIC_SUBSECTION, URL_KEY))
+              .filter(Objects::nonNull)
+              .filter(s -> !s.isEmpty())
+              .map(s -> CharMatcher.is('/').trimTrailingFrom(s))
+              .collect(Collectors.toSet());
+      log.debug("Urls: {}", urls);
     }
 
-    public String url() {
-      return url;
+    public Set<String> urls() {
+      return ImmutableSet.copyOf(urls);
     }
   }
 
@@ -275,6 +277,11 @@
     public String myUrl() {
       return myUrl;
     }
+
+    @Nullable
+    private static String trimTrailingSlash(@Nullable String in) {
+      return in == null ? in : CharMatcher.is('/').trimTrailingFrom(in);
+    }
   }
 
   public static class JGroups {
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 e58f62f..86c757e 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Setup.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Setup.java
@@ -164,7 +164,8 @@
             PeerInfoStrategy.JGROUPS, EnumSet.allOf(PeerInfoStrategy.class), "Peer info strategy");
     config.setEnum(PEER_INFO_SECTION, null, STRATEGY_KEY, strategy);
     if (strategy == PeerInfoStrategy.STATIC) {
-      promptAndSetString("Peer URL", PEER_INFO_SECTION, STATIC_SUBSECTION, URL_KEY, null);
+      promptAndSetString(
+          titleWithNote("Peer URL", "urls"), PEER_INFO_SECTION, STATIC_SUBSECTION, URL_KEY, null);
     } else {
       promptAndSetString(
           "JGroups cluster name",
@@ -291,7 +292,11 @@
   }
 
   private static String titleForOptionalWithNote(String prefix, String suffix) {
-    return prefix + " (optional); manually repeat this line to configure more " + suffix;
+    return titleWithNote(prefix + " (optional)", suffix);
+  }
+
+  private static String titleWithNote(String prefix, String suffix) {
+    return prefix + "; manually repeat this line to configure more " + suffix;
   }
 
   private static String numberToString(int number) {
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 d14b5da..af99064 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
@@ -20,6 +20,7 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.util.OneOffRequestContext;
@@ -62,14 +63,17 @@
               .byGroup(g.getId())
               .toList()
               .stream()
-              .map(ga -> ga.getRemovedOn())
+              .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(ga -> ga.getRemovedOn()).filter(Objects::nonNull);
+          groupMembersAud
+              .stream()
+              .map(AccountGroupMemberAudit::getRemovedOn)
+              .filter(Objects::nonNull);
       Optional<Timestamp> groupLastTs =
           Streams.concat(groupIdAudTs, groupMemberAudAddedTs, groupMemberAudRemovedTs)
               .max(Comparator.naturalOrder());
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 944dcdb..b491468 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
@@ -34,8 +34,6 @@
 import java.nio.file.Path;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
-import java.util.HashMap;
-import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.ScheduledExecutorService;
 import org.slf4j.Logger;
@@ -58,7 +56,6 @@
   private volatile LocalDateTime groupTs;
 
   class FlusherRunner implements Runnable {
-    private Map<AbstractIndexRestApiServlet.IndexName, LocalDateTime> storedTs = new HashMap<>();
 
     @Override
     public void run() {
@@ -68,12 +65,11 @@
     }
 
     private void store(AbstractIndexRestApiServlet.IndexName index, LocalDateTime latestTs) {
-      LocalDateTime currTs = storedTs.get(index);
-      if (currTs == null || latestTs.isAfter(currTs)) {
+      Optional<LocalDateTime> currTs = getUpdateTs(index);
+      if (!currTs.isPresent() || latestTs.isAfter(currTs.get())) {
         Path indexTsFile = dataDir.resolve(index.name().toLowerCase());
         try {
           Files.write(indexTsFile, latestTs.format(formatter).getBytes(StandardCharsets.UTF_8));
-          storedTs.put(index, currTs);
         } catch (IOException e) {
           log.error("Unable to update last timestamp for index " + index, e);
         }
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 7891bbf..df3a0fd 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
@@ -33,6 +33,7 @@
   private final AbstractIndexRestApiServlet.IndexName itemName;
   private final OneOffRequestContext ctx;
   private final IndexTs indexTs;
+  private Timestamp newLastIndexTs;
 
   @Inject
   public ReindexRunnable(
@@ -47,9 +48,8 @@
     Optional<LocalDateTime> maybeIndexTs = indexTs.getUpdateTs(itemName);
     String itemNameString = itemName.name().toLowerCase();
     if (maybeIndexTs.isPresent()) {
-      Timestamp lastIndexTs = Timestamp.valueOf(maybeIndexTs.get());
-      Timestamp newLastIndexTs = lastIndexTs;
-      log.debug("Scanning for all the {}s after {}", itemNameString, lastIndexTs);
+      newLastIndexTs = maxTimestamp(newLastIndexTs, Timestamp.valueOf(maybeIndexTs.get()));
+      log.debug("Scanning for all the {}s after {}", itemNameString, newLastIndexTs);
       try (ManualRequestContext mctx = ctx.open();
           ReviewDb db = mctx.getReviewDbProvider().get()) {
         int count = 0;
@@ -57,12 +57,10 @@
         Stopwatch stopwatch = Stopwatch.createStarted();
         for (T c : fetchItems(db)) {
           try {
-            Optional<Timestamp> itemTs = indexIfNeeded(db, c, lastIndexTs);
+            Optional<Timestamp> itemTs = indexIfNeeded(db, c, newLastIndexTs);
             if (itemTs.isPresent()) {
               count++;
-              if (itemTs.get().after(newLastIndexTs)) {
-                newLastIndexTs = itemTs.get();
-              }
+              newLastIndexTs = maxTimestamp(newLastIndexTs, itemTs.get());
             }
           } catch (Exception e) {
             log.error("Unable to reindex {} {}", itemNameString, c, e);
@@ -90,6 +88,21 @@
     }
   }
 
+  private Timestamp maxTimestamp(Timestamp ts1, Timestamp ts2) {
+    if (ts1 == null) {
+      return ts2;
+    }
+
+    if (ts2 == null) {
+      return ts1;
+    }
+
+    if (ts1.after(ts2)) {
+      return ts1;
+    }
+    return ts2;
+  }
+
   protected abstract Iterable<T> fetchItems(ReviewDb db) throws Exception;
 
   protected abstract Optional<Timestamp> indexIfNeeded(ReviewDb db, T item, Timestamp sinceTs);
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpSession.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpSession.java
index 253dca7..f2ac080 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpSession.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpSession.java
@@ -15,18 +15,15 @@
 package com.ericsson.gerrit.plugins.highavailability.forwarder.rest;
 
 import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.HttpResponseHandler.HttpResult;
-import com.ericsson.gerrit.plugins.highavailability.peers.PeerInfo;
 import com.google.common.base.Supplier;
 import com.google.common.net.MediaType;
 import com.google.gerrit.server.events.SupplierSerializer;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import java.io.IOException;
 import java.net.URI;
 import java.nio.charset.StandardCharsets;
-import java.util.Optional;
 import org.apache.http.client.methods.HttpDelete;
 import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
 import org.apache.http.client.methods.HttpPost;
@@ -35,40 +32,30 @@
 
 class HttpSession {
   private final CloseableHttpClient httpClient;
-  private final Provider<Optional<PeerInfo>> peerInfo;
   private final Gson gson =
       new GsonBuilder().registerTypeAdapter(Supplier.class, new SupplierSerializer()).create();
 
   @Inject
-  HttpSession(CloseableHttpClient httpClient, Provider<Optional<PeerInfo>> peerInfo) {
+  HttpSession(CloseableHttpClient httpClient) {
     this.httpClient = httpClient;
-    this.peerInfo = peerInfo;
   }
 
-  HttpResult post(String endpoint) throws IOException {
-    return post(endpoint, null);
+  HttpResult post(String uri) throws IOException {
+    return post(uri, null);
   }
 
-  HttpResult post(String endpoint, Object content) throws IOException {
-    HttpPost post = new HttpPost(getPeerInfo().getDirectUrl() + endpoint);
+  HttpResult post(String uri, Object content) throws IOException {
+    HttpPost post = new HttpPost(uri);
     setContent(post, content);
     return httpClient.execute(post, new HttpResponseHandler());
   }
 
-  HttpResult delete(String endpoint) throws IOException {
-    return delete(endpoint, null);
+  HttpResult delete(String uri) throws IOException {
+    return delete(uri, null);
   }
 
-  private PeerInfo getPeerInfo() throws PeerInfoNotAvailableException {
-    PeerInfo info = peerInfo.get().orElse(null);
-    if (info == null) {
-      throw new PeerInfoNotAvailableException();
-    }
-    return info;
-  }
-
-  HttpResult delete(String endpoint, Object content) throws IOException {
-    HttpDeleteWithBody delete = new HttpDeleteWithBody(getPeerInfo().getDirectUrl() + endpoint);
+  HttpResult delete(String uri, Object content) throws IOException {
+    HttpDeleteWithBody delete = new HttpDeleteWithBody(uri);
     setContent(delete, content);
     return httpClient.execute(delete, new HttpResponseHandler());
   }
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 1e9e931..ab7b81a 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
@@ -19,12 +19,18 @@
 import com.ericsson.gerrit.plugins.highavailability.forwarder.Forwarder;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.IndexEvent;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.HttpResponseHandler.HttpResult;
+import com.ericsson.gerrit.plugins.highavailability.peers.PeerInfo;
 import com.google.common.base.Joiner;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.server.events.Event;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
 import javax.net.ssl.SSLException;
 import org.apache.http.HttpException;
 import org.apache.http.client.ClientProtocolException;
@@ -32,59 +38,54 @@
 import org.slf4j.LoggerFactory;
 
 class RestForwarder implements Forwarder {
+  enum RequestMethod {
+    POST,
+    DELETE
+  }
+
   private static final Logger log = LoggerFactory.getLogger(RestForwarder.class);
 
   private final HttpSession httpSession;
   private final String pluginRelativePath;
   private final Configuration cfg;
+  private final Provider<Set<PeerInfo>> peerInfoProvider;
 
   @Inject
-  RestForwarder(HttpSession httpClient, @PluginName String pluginName, Configuration cfg) {
+  RestForwarder(
+      HttpSession httpClient,
+      @PluginName String pluginName,
+      Configuration cfg,
+      Provider<Set<PeerInfo>> peerInfoProvider) {
     this.httpSession = httpClient;
-    this.pluginRelativePath = Joiner.on("/").join("/plugins", pluginName);
+    this.pluginRelativePath = Joiner.on("/").join("plugins", pluginName);
     this.cfg = cfg;
+    this.peerInfoProvider = peerInfoProvider;
   }
 
   @Override
   public boolean indexAccount(final int accountId, IndexEvent event) {
-    return new Request("index account", accountId) {
-      @Override
-      HttpResult send() throws IOException {
-        return httpSession.post(
-            Joiner.on("/").join(pluginRelativePath, "index/account", accountId), event);
-      }
-    }.execute();
+    return execute(RequestMethod.POST, "index account", "index/account", accountId, event);
   }
 
   @Override
   public boolean indexChange(String projectName, int changeId, IndexEvent event) {
-    return new Request("index change", changeId) {
-      @Override
-      HttpResult send() throws IOException {
-        return httpSession.post(buildIndexEndpoint(projectName, changeId), event);
-      }
-    }.execute();
+    return execute(
+        RequestMethod.POST,
+        "index change",
+        "index/change",
+        buildIndexEndpoint(projectName, changeId),
+        event);
   }
 
   @Override
   public boolean deleteChangeFromIndex(final int changeId, IndexEvent event) {
-    return new Request("delete change", changeId) {
-      @Override
-      HttpResult send() throws IOException {
-        return httpSession.delete(buildIndexEndpoint(changeId), event);
-      }
-    }.execute();
+    return execute(
+        RequestMethod.DELETE, "delete change", "index/change", buildIndexEndpoint(changeId), event);
   }
 
   @Override
   public boolean indexGroup(final String uuid, IndexEvent event) {
-    return new Request("index group", uuid) {
-      @Override
-      HttpResult send() throws IOException {
-        return httpSession.post(
-            Joiner.on("/").join(pluginRelativePath, "index/group", uuid), event);
-      }
-    }.execute();
+    return execute(RequestMethod.POST, "index group", "index/group", uuid, event);
   }
 
   private String buildIndexEndpoint(int changeId) {
@@ -93,91 +94,131 @@
 
   private String buildIndexEndpoint(String projectName, int changeId) {
     String escapedProjectName = Url.encode(projectName);
-    return Joiner.on("/")
-        .join(pluginRelativePath, "index/change", escapedProjectName + '~' + changeId);
+    return escapedProjectName + '~' + changeId;
   }
 
   @Override
   public boolean send(final Event event) {
-    return new Request("send event", event.type) {
-      @Override
-      HttpResult send() throws IOException {
-        return httpSession.post(Joiner.on("/").join(pluginRelativePath, "event"), event);
-      }
-    }.execute();
+    return execute(RequestMethod.POST, "send event", "event", event.type, event);
   }
 
   @Override
   public boolean evict(final String cacheName, final Object key) {
-    return new Request("invalidate cache " + cacheName, key) {
-      @Override
-      HttpResult send() throws IOException {
-        String json = GsonParser.toJson(cacheName, key);
-        return httpSession.post(Joiner.on("/").join(pluginRelativePath, "cache", cacheName), json);
-      }
-    }.execute();
+    String json = GsonParser.toJson(cacheName, key);
+    return execute(RequestMethod.POST, "invalidate cache " + cacheName, "cache", cacheName, json);
   }
 
   @Override
   public boolean addToProjectList(String projectName) {
-    return new Request("Update project_list, add ", projectName) {
-      @Override
-      HttpResult send() throws IOException {
-        return httpSession.post(buildProjectListEndpoint(projectName));
-      }
-    }.execute();
+    return execute(
+        RequestMethod.POST,
+        "Update project_list, add ",
+        buildProjectListEndpoint(),
+        Url.encode(projectName));
   }
 
   @Override
   public boolean removeFromProjectList(String projectName) {
-    return new Request("Update project_list, remove ", projectName) {
-      @Override
-      HttpResult send() throws IOException {
-        return httpSession.delete(buildProjectListEndpoint(projectName));
-      }
-    }.execute();
+    return execute(
+        RequestMethod.DELETE,
+        "Update project_list, remove ",
+        buildProjectListEndpoint(),
+        Url.encode(projectName));
   }
 
-  private String buildProjectListEndpoint(String projectName) {
-    return Joiner.on("/")
-        .join(pluginRelativePath, "cache", Constants.PROJECT_LIST, Url.encode(projectName));
+  private static String buildProjectListEndpoint() {
+    return Joiner.on("/").join("cache", Constants.PROJECT_LIST);
+  }
+
+  private boolean execute(RequestMethod method, String action, String endpoint, Object id) {
+    return execute(method, action, endpoint, id, null);
+  }
+
+  private boolean execute(
+      RequestMethod method, String action, String endpoint, Object id, Object payload) {
+    List<CompletableFuture<Boolean>> futures =
+        peerInfoProvider
+            .get()
+            .stream()
+            .map(peer -> createRequest(method, peer, action, endpoint, id, payload))
+            .map(request -> CompletableFuture.supplyAsync(request::execute))
+            .collect(Collectors.toList());
+    CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+    return futures.stream().allMatch(CompletableFuture::join);
+  }
+
+  private Request createRequest(
+      RequestMethod method,
+      PeerInfo peer,
+      String action,
+      String endpoint,
+      Object id,
+      Object payload) {
+    String destination = peer.getDirectUrl();
+    return new Request(action, id, destination) {
+      @Override
+      HttpResult send() throws IOException {
+        String request = Joiner.on("/").join(destination, pluginRelativePath, endpoint, id);
+        switch (method) {
+          case POST:
+            return httpSession.post(request, payload);
+          case DELETE:
+          default:
+            return httpSession.delete(request);
+        }
+      }
+    };
   }
 
   private abstract class Request {
     private final String action;
     private final Object key;
+    private final String destination;
+
     private int execCnt;
 
-    Request(String action, Object key) {
+    Request(String action, Object key, String destination) {
       this.action = action;
       this.key = key;
+      this.destination = destination;
     }
 
     boolean execute() {
-      log.debug("Executing {} {}", action, key);
+      log.debug("Executing {} {} towards {}", action, key, destination);
       for (; ; ) {
         try {
           execCnt++;
           tryOnce();
-          log.debug("{} {} OK", action, key);
+          log.debug("{} {} towards {} OK", action, key, destination);
           return true;
         } catch (ForwardingException e) {
           int maxTries = cfg.http().maxTries();
-          log.debug("Failed to {} {} [{}/{}]", action, key, execCnt, maxTries, e);
+          log.debug(
+              "Failed to {} {} on {} [{}/{}]", action, key, destination, execCnt, maxTries, e);
           if (!e.isRecoverable()) {
-            log.error("{} {} failed with unrecoverable error; giving up", action, key, e);
+            log.error(
+                "{} {} towards {} failed with unrecoverable error; giving up",
+                action,
+                key,
+                destination,
+                e);
             return false;
           }
           if (execCnt >= maxTries) {
-            log.error("Failed to {} {} after {} tries; giving up", action, key, maxTries);
+            log.error(
+                "Failed to {} {} on {} after {} tries; giving up",
+                action,
+                key,
+                destination,
+                maxTries);
             return false;
           }
 
-          log.debug("Retrying to {} {}", action, key);
+          log.debug("Retrying to {} {} on {}", action, key, destination);
           try {
             Thread.sleep(cfg.http().retryInterval());
           } catch (InterruptedException ie) {
-            log.error("{} {} was interrupted; giving up", action, key, ie);
+            log.error("{} {} towards {} was interrupted; giving up", action, key, destination, ie);
             Thread.currentThread().interrupt();
             return false;
           }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/PeerInfoModule.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/PeerInfoModule.java
index 577d66d..68707c2 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/PeerInfoModule.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/PeerInfoModule.java
@@ -18,7 +18,7 @@
 import com.ericsson.gerrit.plugins.highavailability.peers.jgroups.JGroupsPeerInfoProvider;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.inject.TypeLiteral;
-import java.util.Optional;
+import java.util.Set;
 
 public class PeerInfoModule extends LifecycleModule {
 
@@ -32,11 +32,10 @@
   protected void configure() {
     switch (strategy) {
       case STATIC:
-        bind(new TypeLiteral<Optional<PeerInfo>>() {})
-            .toProvider(PluginConfigPeerInfoProvider.class);
+        bind(new TypeLiteral<Set<PeerInfo>>() {}).toProvider(PluginConfigPeerInfoProvider.class);
         break;
       case JGROUPS:
-        bind(new TypeLiteral<Optional<PeerInfo>>() {}).toProvider(JGroupsPeerInfoProvider.class);
+        bind(new TypeLiteral<Set<PeerInfo>>() {}).toProvider(JGroupsPeerInfoProvider.class);
         listener().to(JGroupsPeerInfoProvider.class);
         break;
       default:
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/PluginConfigPeerInfoProvider.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/PluginConfigPeerInfoProvider.java
index f39eb6b..9233c79 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/PluginConfigPeerInfoProvider.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/PluginConfigPeerInfoProvider.java
@@ -18,20 +18,21 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 @Singleton
-public class PluginConfigPeerInfoProvider implements Provider<Optional<PeerInfo>> {
+public class PluginConfigPeerInfoProvider implements Provider<Set<PeerInfo>> {
 
-  private final Optional<PeerInfo> peerInfo;
+  private final Set<PeerInfo> peers;
 
   @Inject
   PluginConfigPeerInfoProvider(Configuration cfg) {
-    peerInfo = Optional.of(new PeerInfo(cfg.peerInfoStatic().url()));
+    peers = cfg.peerInfoStatic().urls().stream().map(PeerInfo::new).collect(Collectors.toSet());
   }
 
   @Override
-  public Optional<PeerInfo> get() {
-    return peerInfo;
+  public Set<PeerInfo> get() {
+    return peers;
   }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProvider.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProvider.java
index cf46ac3..596f86e 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProvider.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProvider.java
@@ -16,6 +16,7 @@
 
 import com.ericsson.gerrit.plugins.highavailability.Configuration;
 import com.ericsson.gerrit.plugins.highavailability.peers.PeerInfo;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -23,6 +24,7 @@
 import java.net.InetAddress;
 import java.nio.file.Path;
 import java.util.Optional;
+import java.util.Set;
 import org.jgroups.Address;
 import org.jgroups.JChannel;
 import org.jgroups.Message;
@@ -43,7 +45,7 @@
  */
 @Singleton
 public class JGroupsPeerInfoProvider extends ReceiverAdapter
-    implements Provider<Optional<PeerInfo>>, LifecycleListener {
+    implements Provider<Set<PeerInfo>>, LifecycleListener {
   private static final Logger log = LoggerFactory.getLogger(JGroupsPeerInfoProvider.class);
   private static final String JGROUPS_LOG_FACTORY_PROPERTY = "jgroups.logging.log_factory_class";
 
@@ -85,7 +87,6 @@
   @Override
   public void viewAccepted(View view) {
     log.info("viewAccepted(view: {}) called", view);
-
     synchronized (this) {
       if (view.getMembers().size() > 2) {
         log.warn(
@@ -164,8 +165,8 @@
   }
 
   @Override
-  public Optional<PeerInfo> get() {
-    return peerInfo;
+  public Set<PeerInfo> get() {
+    return peerInfo.isPresent() ? ImmutableSet.of(peerInfo.get()) : ImmutableSet.of();
   }
 
   @Override
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index 331dae5..b2040d5 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -1,3 +1,4 @@
+
 This plugin allows making Gerrit highly available by having redundant Gerrit
 masters.
 
@@ -7,10 +8,10 @@
 * sharing the git repositories using a shared file system (e.g. NFS)
 * behind a load balancer (e.g. HAProxy)
 
-Currently, the only mode supported is one primary (active) master and one backup
-(passive) master but eventually the plan is to support `n` active masters. In
+Currently, the mode supported is one primary (active) master and multiple backup
+(passive) masters but eventually the plan is to support `n` active masters. In
 the active/passive mode, the active master is handling all traffic while the
-passive is kept updated to be always ready to take over.
+passives are kept updated to be always ready to take over.
 
 Even if database and git repositories are shared by the masters, there are a few
 areas of concern in order to be able to switch traffic between masters in a
@@ -23,38 +24,38 @@
 * web sessions
 
 They need either to be shared or kept local to each master but synchronized.
-This plugin needs to be installed in both masters and will take care of sharing
+This plugin needs to be installed in all the masters and it will take care of sharing
 or synchronizing them.
 
 #### Caches
 Every time a cache eviction occurs in one of the masters, the eviction will be
-forwarded the other master so its caches do not contain stale entries.
+forwarded the other masters so their caches do not contain stale entries.
 
 #### Secondary indexes
 Every time the secondary index is modified in one of the masters, e.g., a change
-is added, updated or removed from the index, the other master's index is
+is added, updated or removed from the index, the others master's index are
 updated accordingly. This way, both indexes are kept synchronized.
 
 #### Stream events
 Every time a stream event occurs in one of the masters (see [more events info]
 (https://gerrit-review.googlesource.com/Documentation/cmd-stream-events.html#events)),
-the event is forwarded to the other master which re-plays it. This way, the
+the event is forwarded to the other masters which re-plays it. This way, the
 output of the stream-events command is the same, no matter which master a client
 is connected to.
 
 #### Web session
 The built-in Gerrit H2 based web session cache is replaced with a file based
-implementation that is shared amongst both masters.
+implementation that is shared amongst the masters.
 
 ## Setup
 
 Prerequisites:
 
-* Unique database server must be accessible from both masters
+* Unique database server must be accessible from all the masters
 * Git repositories must be located on a shared file system
 * A directory on a shared file system must be available for @PLUGIN@ to use
 
-For both masters:
+For the masters:
 
 * Configure database section in gerrit.config to use the shared database
 * Configure gerrit.basePath in gerrit.config to the shared repositories location
@@ -69,7 +70,7 @@
   sharedDirectory = /directory/accessible/from/both/masters
 
 [peerInfo "static"]
-  url = http://backupMasterHost:8081/
+  url = http://backupMasterHost1:8081/
 
 [http]
   user = username
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index b59b1fc..ca5a6e5 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -1,7 +1,8 @@
+
 @PLUGIN@ Configuration
 =========================
 
-The @PLUGIN@ plugin must be installed on both instances and the following fields
+The @PLUGIN@ plugin must be installed on all the instances and the following fields
 should be specified in `$site_path/etc/@PLUGIN@.config` file:
 
 File '@PLUGIN@.config'
@@ -17,7 +18,8 @@
 [peerInfo]
   strategy = static
 [peerInfo "static"]
-  url = target_instance_url
+  url = first_target_instance_url
+  url = second_target_instance_url
 [http]
   user = username
   password = password
@@ -93,7 +95,8 @@
 a member joins or leaves the cluster.
 
 ```peerInfo.static.url```
-:   Specify the URL for the peer instance.
+:   Specify the URL for the peer instance. If more than one peer instance is to be
+    configured, add as many url entries as necessary.
 
 ```peerInfo.jgroups.myUrl```
 :   The URL of this instance to be broadcast to other peers. If not specified, the
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 efb3b75..63ab408 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/ConfigurationTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/ConfigurationTest.java
@@ -69,6 +69,7 @@
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.List;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
 import org.junit.Test;
@@ -84,6 +85,7 @@
   private static final String PASS = "fakePass";
   private static final String USER = "fakeUser";
   private static final String URL = "http://fakeUrl";
+  private static final List<String> URLS = ImmutableList.of(URL, "http://anotherUrl/");
   private static final int TIMEOUT = 5000;
   private static final int MAX_TRIES = 5;
   private static final int RETRY_INTERVAL = 1000;
@@ -122,17 +124,12 @@
   }
 
   @Test
-  public void testGetUrl() throws Exception {
-    assertThat(getConfiguration().peerInfoStatic().url()).isEmpty();
+  public void testGetUrls() throws Exception {
+    assertThat(getConfiguration().peerInfoStatic().urls()).isEmpty();
 
-    globalPluginConfig.setString(PEER_INFO_SECTION, STATIC_SUBSECTION, URL_KEY, URL);
-    assertThat(getConfiguration().peerInfoStatic().url()).isEqualTo(URL);
-  }
-
-  @Test
-  public void testGetUrlIsDroppingTrailingSlash() throws Exception {
-    globalPluginConfig.setString(PEER_INFO_SECTION, STATIC_SUBSECTION, URL_KEY, URL + "/");
-    assertThat(getConfiguration().peerInfoStatic().url()).isEqualTo(URL);
+    globalPluginConfig.setStringList(PEER_INFO_SECTION, STATIC_SUBSECTION, URL_KEY, URLS);
+    assertThat(getConfiguration().peerInfoStatic().urls())
+        .containsAllIn(ImmutableList.of(URL, "http://anotherUrl"));
   }
 
   @Test
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpSessionTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpSessionTest.java
index b6eba3d..9b3c85c 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpSessionTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpSessionTest.java
@@ -15,35 +15,27 @@
 package com.ericsson.gerrit.plugins.highavailability.forwarder.rest;
 
 import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
-import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor;
-import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
 import static com.github.tomakehurst.wiremock.client.WireMock.delete;
 import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
-import static com.github.tomakehurst.wiremock.client.WireMock.exactly;
 import static com.github.tomakehurst.wiremock.client.WireMock.post;
 import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
-import static com.github.tomakehurst.wiremock.client.WireMock.verify;
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.fail;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import com.ericsson.gerrit.plugins.highavailability.Configuration;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.HttpResponseHandler.HttpResult;
-import com.ericsson.gerrit.plugins.highavailability.peers.PeerInfo;
 import com.github.tomakehurst.wiremock.http.Fault;
 import com.github.tomakehurst.wiremock.junit.WireMockRule;
 import com.github.tomakehurst.wiremock.stubbing.Scenario;
-import com.google.inject.util.Providers;
-import java.io.IOException;
 import java.net.SocketTimeoutException;
-import java.util.Optional;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.Answers;
 
 public class HttpSessionTest {
+
   private static final int MAX_TRIES = 3;
   private static final int RETRY_INTERVAL = 250;
   private static final int TIMEOUT = 500;
@@ -65,10 +57,12 @@
   @Rule public WireMockRule wireMockRule = new WireMockRule(0);
 
   private Configuration configMock;
+  private String uri;
 
   @Before
   public void setUp() throws Exception {
     String url = "http://localhost:" + wireMockRule.port();
+    uri = url + ENDPOINT;
     configMock = mock(Configuration.class, Answers.RETURNS_DEEP_STUBS);
     when(configMock.http().user()).thenReturn("user");
     when(configMock.http().password()).thenReturn("pass");
@@ -77,11 +71,7 @@
     when(configMock.http().socketTimeout()).thenReturn(TIMEOUT);
     when(configMock.http().retryInterval()).thenReturn(RETRY_INTERVAL);
 
-    PeerInfo peerInfo = mock(PeerInfo.class);
-    when(peerInfo.getDirectUrl()).thenReturn(url);
-    httpSession =
-        new HttpSession(
-            new HttpClientProvider(configMock).get(), Providers.of(Optional.of(peerInfo)));
+    httpSession = new HttpSession(new HttpClientProvider(configMock).get());
   }
 
   @Test
@@ -89,7 +79,7 @@
     wireMockRule.givenThat(
         post(urlEqualTo(ENDPOINT)).willReturn(aResponse().withStatus(NO_CONTENT)));
 
-    assertThat(httpSession.post(ENDPOINT).isSuccessful()).isTrue();
+    assertThat(httpSession.post(uri).isSuccessful()).isTrue();
   }
 
   @Test
@@ -98,7 +88,7 @@
         post(urlEqualTo(ENDPOINT))
             .withRequestBody(equalTo(BODY))
             .willReturn(aResponse().withStatus(NO_CONTENT)));
-    assertThat(httpSession.post(ENDPOINT, BODY).isSuccessful()).isTrue();
+    assertThat(httpSession.post(uri, BODY).isSuccessful()).isTrue();
   }
 
   @Test
@@ -106,7 +96,7 @@
     wireMockRule.givenThat(
         delete(urlEqualTo(ENDPOINT)).willReturn(aResponse().withStatus(NO_CONTENT)));
 
-    assertThat(httpSession.delete(ENDPOINT).isSuccessful()).isTrue();
+    assertThat(httpSession.delete(uri).isSuccessful()).isTrue();
   }
 
   @Test
@@ -116,7 +106,7 @@
         post(urlEqualTo(ENDPOINT))
             .willReturn(aResponse().withStatus(UNAUTHORIZED).withBody(expected)));
 
-    HttpResult result = httpSession.post(ENDPOINT);
+    HttpResult result = httpSession.post(uri);
     assertThat(result.isSuccessful()).isFalse();
     assertThat(result.getMessage()).isEqualTo(expected);
   }
@@ -128,7 +118,7 @@
         post(urlEqualTo(ENDPOINT))
             .willReturn(aResponse().withStatus(NOT_FOUND).withBody(expected)));
 
-    HttpResult result = httpSession.post(ENDPOINT);
+    HttpResult result = httpSession.post(uri);
     assertThat(result.isSuccessful()).isFalse();
     assertThat(result.getMessage()).isEqualTo(expected);
   }
@@ -139,7 +129,7 @@
         post(urlEqualTo(ENDPOINT))
             .willReturn(aResponse().withStatus(ERROR).withBody(ERROR_MESSAGE)));
 
-    HttpResult result = httpSession.post(ENDPOINT);
+    HttpResult result = httpSession.post(uri);
     assertThat(result.isSuccessful()).isFalse();
     assertThat(result.getMessage()).isEqualTo(ERROR_MESSAGE);
   }
@@ -170,7 +160,7 @@
             .whenScenarioStateIs(THIRD_TRY)
             .willReturn(aResponse().withFixedDelay(TIMEOUT)));
 
-    httpSession.post(ENDPOINT);
+    httpSession.post(uri);
   }
 
   @Test
@@ -179,19 +169,6 @@
         post(urlEqualTo(ENDPOINT))
             .willReturn(aResponse().withFault(Fault.MALFORMED_RESPONSE_CHUNK)));
 
-    assertThat(httpSession.post(ENDPOINT).isSuccessful()).isFalse();
-  }
-
-  @Test
-  public void testNoRequestWhenPeerInfoUnknown() throws IOException {
-    httpSession =
-        new HttpSession(new HttpClientProvider(configMock).get(), Providers.of(Optional.empty()));
-    try {
-      httpSession.post(ENDPOINT);
-      fail("Expected PeerInfoNotAvailableException");
-    } catch (PeerInfoNotAvailableException e) {
-      // good
-    }
-    verify(exactly(0), anyRequestedFor(anyUrl()));
+    assertThat(httpSession.post(uri).isSuccessful()).isFalse();
   }
 }
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderTest.java
index fd2aa52..c5b52a0 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderTest.java
@@ -26,22 +26,27 @@
 import com.ericsson.gerrit.plugins.highavailability.cache.Constants;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.IndexEvent;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.HttpResponseHandler.HttpResult;
+import com.ericsson.gerrit.plugins.highavailability.peers.PeerInfo;
 import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.events.Event;
 import com.google.gson.GsonBuilder;
+import com.google.inject.Provider;
 import java.io.IOException;
+import java.util.Set;
 import javax.net.ssl.SSLException;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Answers;
 
 public class RestForwarderTest {
+  private static final String URL = "http://fake.com";
   private static final String PLUGIN_NAME = "high-availability";
   private static final String EMPTY_MSG = "";
   private static final String ERROR = "Error";
-  private static final String PLUGINS = "/plugins";
+  private static final String PLUGINS = "plugins";
   private static final String PROJECT_TO_ADD = "projectToAdd";
   private static final String PROJECT_TO_DELETE = "projectToDelete";
   private static final String SUCCESS = "Success";
@@ -54,30 +59,41 @@
   private static final String PROJECT_NAME_URL_END = "test%2Fproject";
   private static final String INDEX_CHANGE_ENDPOINT =
       Joiner.on("/")
-          .join(PLUGINS, PLUGIN_NAME, "index/change", PROJECT_NAME_URL_END + "~" + CHANGE_NUMBER);
+          .join(
+              URL,
+              PLUGINS,
+              PLUGIN_NAME,
+              "index/change",
+              PROJECT_NAME_URL_END + "~" + CHANGE_NUMBER);
   private static final String DELETE_CHANGE_ENDPOINT =
-      Joiner.on("/").join("/plugins", PLUGIN_NAME, "index/change", "~" + CHANGE_NUMBER);
+      Joiner.on("/").join(URL, PLUGINS, PLUGIN_NAME, "index/change", "~" + CHANGE_NUMBER);
   private static final int ACCOUNT_NUMBER = 2;
   private static final String INDEX_ACCOUNT_ENDPOINT =
-      Joiner.on("/").join(PLUGINS, PLUGIN_NAME, "index/account", ACCOUNT_NUMBER);
+      Joiner.on("/").join(URL, PLUGINS, PLUGIN_NAME, "index/account", ACCOUNT_NUMBER);
   private static final String UUID = "we235jdf92nfj2351";
   private static final String INDEX_GROUP_ENDPOINT =
-      Joiner.on("/").join(PLUGINS, PLUGIN_NAME, "index/group", UUID);
+      Joiner.on("/").join(URL, PLUGINS, PLUGIN_NAME, "index/group", UUID);
 
   // Event
-  private static final String EVENT_ENDPOINT = Joiner.on("/").join(PLUGINS, PLUGIN_NAME, "event");
   private static Event event = new Event("test-event") {};
+  private static final String EVENT_ENDPOINT =
+      Joiner.on("/").join(URL, PLUGINS, PLUGIN_NAME, "event", event.type);
 
   private RestForwarder forwarder;
   private HttpSession httpSessionMock;
 
+  @SuppressWarnings("unchecked")
   @Before
   public void setUp() {
     httpSessionMock = mock(HttpSession.class);
     Configuration configMock = mock(Configuration.class, Answers.RETURNS_DEEP_STUBS);
     when(configMock.http().maxTries()).thenReturn(3);
     when(configMock.http().retryInterval()).thenReturn(10);
-    forwarder = new RestForwarder(httpSessionMock, PLUGIN_NAME, configMock);
+    Provider<Set<PeerInfo>> peersMock = mock(Provider.class);
+    when(peersMock.get()).thenReturn(ImmutableSet.of(new PeerInfo(URL)));
+    forwarder =
+        new RestForwarder(
+            httpSessionMock, PLUGIN_NAME, configMock, peersMock); // TODO: Create provider
   }
 
   @Test
@@ -142,21 +158,21 @@
 
   @Test
   public void testChangeDeletedFromIndexOK() throws Exception {
-    when(httpSessionMock.delete(eq(DELETE_CHANGE_ENDPOINT), any()))
+    when(httpSessionMock.delete(eq(DELETE_CHANGE_ENDPOINT)))
         .thenReturn(new HttpResult(SUCCESSFUL, EMPTY_MSG));
     assertThat(forwarder.deleteChangeFromIndex(CHANGE_NUMBER, new IndexEvent())).isTrue();
   }
 
   @Test
   public void testChangeDeletedFromIndexFailed() throws Exception {
-    when(httpSessionMock.delete(eq(DELETE_CHANGE_ENDPOINT), any()))
+    when(httpSessionMock.delete(eq(DELETE_CHANGE_ENDPOINT)))
         .thenReturn(new HttpResult(FAILED, EMPTY_MSG));
     assertThat(forwarder.deleteChangeFromIndex(CHANGE_NUMBER, new IndexEvent())).isFalse();
   }
 
   @Test
   public void testChangeDeletedFromThrowsException() throws Exception {
-    doThrow(new IOException()).when(httpSessionMock).delete(eq(DELETE_CHANGE_ENDPOINT), any());
+    doThrow(new IOException()).when(httpSessionMock).delete(eq(DELETE_CHANGE_ENDPOINT));
     assertThat(forwarder.deleteChangeFromIndex(CHANGE_NUMBER, new IndexEvent())).isFalse();
   }
 
@@ -201,8 +217,8 @@
   public void testEvictGroupsOK() throws Exception {
     AccountGroup.Id key = new AccountGroup.Id(123);
     String keyJson = new GsonBuilder().create().toJson(key);
-    when(httpSessionMock.post(buildCacheEndpoint(Constants.GROUPS), keyJson))
-        .thenReturn(new HttpResult(SUCCESSFUL, EMPTY_MSG));
+    String endpoint = buildCacheEndpoint(Constants.GROUPS);
+    when(httpSessionMock.post(endpoint, keyJson)).thenReturn(new HttpResult(SUCCESSFUL, EMPTY_MSG));
     assertThat(forwarder.evict(Constants.GROUPS, key)).isTrue();
   }
 
@@ -244,13 +260,13 @@
   }
 
   private static String buildCacheEndpoint(String name) {
-    return Joiner.on("/").join(PLUGINS, PLUGIN_NAME, "cache", name);
+    return Joiner.on("/").join(URL, PLUGINS, PLUGIN_NAME, "cache", name);
   }
 
   @Test
   public void testAddToProjectListOK() throws Exception {
     String projectName = PROJECT_TO_ADD;
-    when(httpSessionMock.post(buildProjectListCacheEndpoint(projectName)))
+    when(httpSessionMock.post(buildProjectListCacheEndpoint(projectName), null))
         .thenReturn(new HttpResult(SUCCESSFUL, EMPTY_MSG));
     assertThat(forwarder.addToProjectList(projectName)).isTrue();
   }
@@ -258,7 +274,7 @@
   @Test
   public void testAddToProjectListFailed() throws Exception {
     String projectName = PROJECT_TO_ADD;
-    when(httpSessionMock.post(buildProjectListCacheEndpoint(projectName)))
+    when(httpSessionMock.post(buildProjectListCacheEndpoint(projectName), null))
         .thenReturn(new HttpResult(FAILED, EMPTY_MSG));
     assertThat(forwarder.addToProjectList(projectName)).isFalse();
   }
@@ -268,7 +284,7 @@
     String projectName = PROJECT_TO_ADD;
     doThrow(new IOException())
         .when(httpSessionMock)
-        .post(buildProjectListCacheEndpoint(projectName));
+        .post(buildProjectListCacheEndpoint(projectName), null);
     assertThat(forwarder.addToProjectList(projectName)).isFalse();
   }