Merge branch 'stable-2.13' into stable-2.14

* stable-2.13:
  Fix usage of DEFAULT_SKIP_INTERFACE_LIST

Change-Id: I0988ff97618178cc8299783decc44b929b6ded3c
diff --git a/.buckconfig b/.buckconfig
deleted file mode 100644
index 5c0ead0..0000000
--- a/.buckconfig
+++ /dev/null
@@ -1,16 +0,0 @@
-[alias]
-  high-availability = //:high-availability
-  plugin = //:high-availability
-  src = //:high-availability-sources
-
-[java]
-  jar_spool_mode = direct_to_jar
-  src_roots = java, resources
-
-[project]
-  ignore = .git, eclipse-out/
-  parallel_parsing = true
-
-[cache]
-  mode = dir
-  dir = buck-out/cache
diff --git a/.gitignore b/.gitignore
index c27e17f..912f8a6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,11 @@
-/.buckd/
-/.buckversion
 /.classpath
 /.project
 /.settings/
-/.watchmanconfig
-/buck-out/
-/bucklets
+/.primary_build_tool
+/bazel-bin
+/bazel-genfiles
+/bazel-out
+/bazel-reviewers
+/bazel-testlogs
+/bazel-high-availability
 /eclipse-out/
diff --git a/BUCK b/BUCK
deleted file mode 100644
index 065f6c5..0000000
--- a/BUCK
+++ /dev/null
@@ -1,94 +0,0 @@
-include_defs('//bucklets/gerrit_plugin.bucklet')
-include_defs('//bucklets/java_sources.bucklet')
-include_defs('//bucklets/maven_jar.bucklet')
-
-SOURCES = glob(['src/main/java/**/*.java'])
-RESOURCES = glob(['src/main/resources/**/*'])
-
-DEPS = [
-  ':jgroups',
-]
-
-TEST_DEPS = GERRIT_PLUGIN_API + GERRIT_TESTS + [
-  ':high-availability__plugin',
-  ':mockito',
-  ':wiremock',
-]
-
-gerrit_plugin(
-  name = 'high-availability',
-  srcs = SOURCES,
-  resources = RESOURCES,
-  manifest_entries = [
-    'Gerrit-PluginName: high-availability',
-    'Gerrit-ApiType: plugin',
-    'Gerrit-Module: com.ericsson.gerrit.plugins.highavailability.Module',
-    'Gerrit-HttpModule: com.ericsson.gerrit.plugins.highavailability.HttpModule',
-    'Gerrit-InitStep: com.ericsson.gerrit.plugins.highavailability.Setup',
-    'Implementation-Title: high-availability plugin',
-    'Implementation-URL: https://gerrit-review.googlesource.com/#/admin/projects/plugins/high-availability',
-    'Implementation-Vendor: Ericsson',
-  ],
-  provided_deps = GERRIT_TESTS,
-  deps = DEPS,
-)
-
-java_sources(
-  name = 'high-availability-sources',
-  srcs = SOURCES + RESOURCES,
-)
-
-java_library(
-  name = 'classpath',
-  deps = TEST_DEPS,
-)
-
-java_test(
-  name = 'high-availability_tests',
-  srcs = glob(['src/test/java/**/*.java']),
-  resources = glob(['src/test/resources/**/']),
-  labels = ['high-availability'],
-  deps = TEST_DEPS,
-)
-
-maven_jar(
-  name = 'wiremock',
-  id = 'com.github.tomakehurst:wiremock-standalone:2.5.1',
-  sha1 = '9cda1bf1674c8de3a1116bae4d7ce0046a857d30',
-  license = 'Apache2.0',
-  attach_source = False,
-)
-
-maven_jar(
-  name = 'jgroups',
-  id = 'org.jgroups:jgroups:3.6.5.Final',
-  sha1 = 'fe575fe2d473566ad3f4ace4702ff4bfcf2587a6',
-  license = 'Apache2.0',
-)
-
-maven_jar(
-  name = 'mockito',
-  id = 'org.mockito:mockito-core:2.7.21',
-  sha1 = '23e9f7bfb9717e849a05b84c29ee3ac723f1a653',
-  license = 'DO_NOT_DISTRIBUTE',
-  deps = [
-    ':byte-buddy',
-    ':objenesis',
-  ],
-)
-
-maven_jar(
-  name = 'byte-buddy',
-  id = 'net.bytebuddy:byte-buddy:1.6.11',
-  sha1 = '8a8f9409e27f1d62c909c7eef2aa7b3a580b4901',
-  license = 'DO_NOT_DISTRIBUTE',
-  attach_source = False,
-)
-
-maven_jar(
-  name = 'objenesis',
-  id = 'org.objenesis:objenesis:2.5',
-  sha1 = '612ecb799912ccf77cba9b3ed8c813da086076e9',
-  license = 'DO_NOT_DISTRIBUTE',
-  attach_source = False,
-)
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..4660487
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,45 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+load(
+    "//tools/bzl:plugin.bzl",
+    "gerrit_plugin",
+    "PLUGIN_DEPS",
+    "PLUGIN_TEST_DEPS",
+)
+
+gerrit_plugin(
+    name = "high-availability",
+    srcs = glob(["src/main/java/**/*.java"]),
+    manifest_entries = [
+        "Gerrit-PluginName: high-availability",
+        "Gerrit-Module: com.ericsson.gerrit.plugins.highavailability.Module",
+        "Gerrit-HttpModule: com.ericsson.gerrit.plugins.highavailability.HttpModule",
+        "Implementation-Title: high-availability plugin",
+        "Implementation-URL: https://gerrit-review.googlesource.com/#/admin/projects/plugins/high-availability",
+    ],
+    deps = ["@jgroups//jar"],
+    resources = glob(["src/main/resources/**/*"]),
+)
+
+junit_tests(
+    name = "high_availability_tests",
+    srcs = glob(["src/test/java/**/*.java"]),
+    resources = glob(["src/test/resources/**/*"]),
+    tags = [
+        "high-availability",
+        "local",
+    ],
+    deps = [
+        ":high-availability__plugin_test_deps",
+    ],
+)
+
+java_library(
+    name = "high-availability__plugin_test_deps",
+    testonly = 1,
+    visibility = ["//visibility:public"],
+    exports = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
+        ":high-availability__plugin",
+        "@mockito//jar",
+        "@wiremock//jar",
+    ],
+)
diff --git a/WORKSPACE b/WORKSPACE
new file mode 100644
index 0000000..6430d1d
--- /dev/null
+++ b/WORKSPACE
@@ -0,0 +1,30 @@
+workspace(name = "high_availability")
+
+load("//:bazlets.bzl", "load_bazlets")
+
+load_bazlets(
+    commit = "c183e91a343af59e7bb021c19fee62a1dc6ea6ce",
+    #local_path = "/home/ehugare/workspaces/bazlets",
+)
+
+#Snapshot Plugin API
+#load(
+#    "@com_googlesource_gerrit_bazlets//:gerrit_api_maven_local.bzl",
+#    "gerrit_api_maven_local",
+#)
+
+# Load snapshot Plugin API
+#gerrit_api_maven_local()
+
+# Release Plugin API
+load(
+    "@com_googlesource_gerrit_bazlets//:gerrit_api.bzl",
+    "gerrit_api",
+)
+
+# Load release Plugin API
+gerrit_api()
+
+load("//:external_plugin_deps.bzl", "external_plugin_deps")
+
+external_plugin_deps()
diff --git a/bazlets.bzl b/bazlets.bzl
new file mode 100644
index 0000000..e14e488
--- /dev/null
+++ b/bazlets.bzl
@@ -0,0 +1,17 @@
+NAME = "com_googlesource_gerrit_bazlets"
+
+def load_bazlets(
+    commit,
+    local_path = None
+  ):
+  if not local_path:
+      native.git_repository(
+          name = NAME,
+          remote = "https://gerrit.googlesource.com/bazlets",
+          commit = commit,
+      )
+  else:
+      native.local_repository(
+          name = NAME,
+          path = local_path,
+      )
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
new file mode 100644
index 0000000..9fb12ce
--- /dev/null
+++ b/external_plugin_deps.bzl
@@ -0,0 +1,36 @@
+load("//tools/bzl:maven_jar.bzl", "maven_jar")
+
+def external_plugin_deps():
+  maven_jar(
+    name = "wiremock",
+    artifact = "com.github.tomakehurst:wiremock-standalone:2.8.0",
+    sha1 = "b4d91aca283a86b447d3906deac6e1509c3a94c5",
+  )
+
+  maven_jar(
+    name = "mockito",
+    artifact = "org.mockito:mockito-core:2.9.0",
+    sha1 = "f28b9606eca8da77e10df30a7e301f589733143e",
+    deps = [
+      '@byte-buddy//jar',
+      '@objenesis//jar',
+    ],
+  )
+
+  maven_jar(
+    name = "byte-buddy",
+    artifact = "net.bytebuddy:byte-buddy:1.7.0",
+    sha1 = "48481d20ed4334ee0abfe8212ecb44e0233a97b5",
+  )
+
+  maven_jar(
+    name = "objenesis",
+    artifact = "org.objenesis:objenesis:2.6",
+    sha1 = "639033469776fd37c08358c6b92a4761feb2af4b",
+  )
+
+  maven_jar(
+    name = "jgroups",
+    artifact = "org.jgroups:jgroups:3.6.5.Final",
+    sha1 = "fe575fe2d473566ad3f4ace4702ff4bfcf2587a6",
+  )
diff --git a/lib/gerrit/BUCK b/lib/gerrit/BUCK
deleted file mode 100644
index b7a40b3..0000000
--- a/lib/gerrit/BUCK
+++ /dev/null
@@ -1,22 +0,0 @@
-include_defs('//bucklets/maven_jar.bucklet')
-
-VER = '2.13.8'
-REPO = MAVEN_CENTRAL
-
-maven_jar(
-  name = 'acceptance-framework',
-  id = 'com.google.gerrit:gerrit-acceptance-framework:' + VER,
-  sha1 = 'b35d038d0727889837f0b9710a8a0442471ba8b6',
-  license = 'Apache2.0',
-  attach_source = False,
-  repository = REPO,
-)
-
-maven_jar(
-  name = 'plugin-api',
-  id = 'com.google.gerrit:gerrit-plugin-api:' + VER,
-  sha1 = 'd8137cc9b0cb34429959374ca44d5d2bcf0eff4b',
-  license = 'Apache2.0',
-  attach_source = False,
-  repository = REPO,
-)
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 f409119..3e87d01 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.gerrit.common.Nullable;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.PluginConfigFactory;
@@ -29,6 +30,9 @@
 import com.google.inject.Singleton;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
 import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -48,6 +52,12 @@
   static final String JGROUPS_SUBSECTION = PeerInfoStrategy.JGROUPS.name().toLowerCase();
   static final String URL_KEY = "url";
   static final String STRATEGY_KEY = "strategy";
+  static final String MY_URL_KEY = "myUrl";
+
+  // jgroups section
+  static final String JGROUPS_SECTION = "jgroups";
+  static final String SKIP_INTERFACE_KEY = "skipInterface";
+  static final String CLUSTER_NAME_KEY = "clusterName";
 
   // http section
   static final String HTTP_SECTION = "http";
@@ -60,6 +70,7 @@
 
   // cache section
   static final String CACHE_SECTION = "cache";
+  static final String PATTERN_KEY = "pattern";
 
   // event section
   static final String EVENT_SECTION = "event";
@@ -77,10 +88,6 @@
   static final String WEBSESSION_SECTION = "websession";
   static final String CLEANUP_INTERVAL_KEY = "cleanupInterval";
 
-  // jgroups section used if peerInfo.strategy == jgroups
-  static final String SKIP_INTERFACE_KEY = "skipInterface";
-  static final String CLUSTER_NAME_KEY = "clusterName";
-
   static final int DEFAULT_TIMEOUT_MS = 5000;
   static final int DEFAULT_MAX_TRIES = 5;
   static final int DEFAULT_RETRY_INTERVAL = 1000;
@@ -95,6 +102,7 @@
 
   private final Main main;
   private final PeerInfo peerInfo;
+  private final JGroups jgroups;
   private final Http http;
   private final Cache cache;
   private final Event event;
@@ -124,6 +132,7 @@
       default:
         throw new IllegalArgumentException("Not supported strategy: " + peerInfo.strategy);
     }
+    jgroups = new JGroups(cfg);
     http = new Http(cfg);
     cache = new Cache(cfg);
     event = new Event(cfg);
@@ -147,6 +156,10 @@
     return peerInfoJGroups;
   }
 
+  public JGroups jgroups() {
+    return jgroups;
+  }
+
   public Http http() {
     return http;
   }
@@ -193,6 +206,11 @@
     return ((value == null) ? defaultValue : value);
   }
 
+  @Nullable
+  private static String trimTrailingSlash(@Nullable String in) {
+    return in == null ? null : CharMatcher.is('/').trimTrailingFrom(in);
+  }
+
   public static class Main {
     private final Path sharedDirectory;
 
@@ -231,10 +249,8 @@
 
     private PeerInfoStatic(Config cfg) {
       url =
-          CharMatcher.is('/')
-              .trimTrailingFrom(
-                  Strings.nullToEmpty(
-                      cfg.getString(PEER_INFO_SECTION, STATIC_SUBSECTION, URL_KEY)));
+          trimTrailingSlash(
+              Strings.nullToEmpty(cfg.getString(PEER_INFO_SECTION, STATIC_SUBSECTION, URL_KEY)));
     }
 
     public String url() {
@@ -243,15 +259,25 @@
   }
 
   public static class PeerInfoJGroups {
+    private final String myUrl;
+
+    private PeerInfoJGroups(Config cfg) {
+      myUrl = trimTrailingSlash(cfg.getString(PEER_INFO_SECTION, JGROUPS_SUBSECTION, MY_URL_KEY));
+    }
+
+    public String myUrl() {
+      return myUrl;
+    }
+  }
+
+  public static class JGroups {
     private final ImmutableList<String> skipInterface;
     private final String clusterName;
 
-    private PeerInfoJGroups(Config cfg) {
-      String[] skip = cfg.getStringList(PEER_INFO_SECTION, JGROUPS_SUBSECTION, SKIP_INTERFACE_KEY);
+    private JGroups(Config cfg) {
+      String[] skip = cfg.getStringList(JGROUPS_SECTION, null, SKIP_INTERFACE_KEY);
       skipInterface = skip.length == 0 ? DEFAULT_SKIP_INTERFACE_LIST : ImmutableList.copyOf(skip);
-      clusterName =
-          getString(
-              cfg, PEER_INFO_SECTION, JGROUPS_SUBSECTION, CLUSTER_NAME_KEY, DEFAULT_CLUSTER_NAME);
+      clusterName = getString(cfg, JGROUPS_SECTION, null, CLUSTER_NAME_KEY, DEFAULT_CLUSTER_NAME);
     }
 
     public ImmutableList<String> skipInterface() {
@@ -320,15 +346,21 @@
 
   public static class Cache extends Forwarding {
     private final int threadPoolSize;
+    private final List<String> patterns;
 
     private Cache(Config cfg) {
       super(cfg, CACHE_SECTION);
       threadPoolSize = getInt(cfg, CACHE_SECTION, THREAD_POOL_SIZE_KEY, DEFAULT_THREAD_POOL_SIZE);
+      patterns = Arrays.asList(cfg.getStringList(CACHE_SECTION, null, PATTERN_KEY));
     }
 
     public int threadPoolSize() {
       return threadPoolSize;
     }
+
+    public List<String> patterns() {
+      return Collections.unmodifiableList(patterns);
+    }
   }
 
   public static class Event extends Forwarding {
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/ExecutorProvider.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/ExecutorProvider.java
index 9a91fcf..666375f 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/ExecutorProvider.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/ExecutorProvider.java
@@ -17,10 +17,9 @@
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.inject.Provider;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.Executor;
 
-public abstract class ExecutorProvider
-    implements Provider<ScheduledThreadPoolExecutor>, LifecycleListener {
+public abstract class ExecutorProvider implements Provider<Executor>, LifecycleListener {
   private WorkQueue.Executor executor;
 
   protected ExecutorProvider(WorkQueue workQueue, int threadPoolSize, String threadNamePrefix) {
@@ -40,7 +39,7 @@
   }
 
   @Override
-  public ScheduledThreadPoolExecutor get() {
+  public Executor get() {
     return executor;
   }
 }
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 a8a9a5e..5534927 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Setup.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Setup.java
@@ -54,6 +54,7 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.EnumSet;
 import java.util.Objects;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.util.FS;
@@ -119,7 +120,9 @@
 
   private void configurePeerInfoSection() {
     ui.header("PeerInfo section");
-    PeerInfoStrategy strategy = ui.readEnum(PeerInfoStrategy.JGROUPS, "Peer info strategy");
+    PeerInfoStrategy strategy =
+        ui.readEnum(
+            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);
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheEvictionHandler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheEvictionHandler.java
index 7e32689..94894be 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheEvictionHandler.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheEvictionHandler.java
@@ -21,37 +21,33 @@
 import com.google.gerrit.server.cache.CacheRemovalListener;
 import com.google.inject.Inject;
 import java.util.concurrent.Executor;
-import java.util.regex.Pattern;
 
 class CacheEvictionHandler<K, V> implements CacheRemovalListener<K, V> {
   private final Executor executor;
   private final Forwarder forwarder;
   private final String pluginName;
-  private final Pattern pattern;
+  private final CachePatternMatcher matcher;
 
   @Inject
   CacheEvictionHandler(
-      Forwarder forwarder, @CacheExecutor Executor executor, @PluginName String pluginName) {
+      Forwarder forwarder,
+      @CacheExecutor Executor executor,
+      @PluginName String pluginName,
+      CachePatternMatcher matcher) {
     this.forwarder = forwarder;
     this.executor = executor;
     this.pluginName = pluginName;
-    pattern =
-        Pattern.compile(
-            "^accounts.*|^groups.*|ldap_groups|ldap_usernames|^project.*|sshkeys|web_sessions");
+    this.matcher = matcher;
   }
 
   @Override
   public void onRemoval(
       String pluginName, String cacheName, RemovalNotification<K, V> notification) {
-    if (!Context.isForwardedEvent() && !notification.wasEvicted() && isSynchronized(cacheName)) {
+    if (!Context.isForwardedEvent() && !notification.wasEvicted() && matcher.matches(cacheName)) {
       executor.execute(new CacheEvictionTask(cacheName, notification.getKey()));
     }
   }
 
-  private boolean isSynchronized(String cacheName) {
-    return pattern.matcher(cacheName).matches();
-  }
-
   class CacheEvictionTask implements Runnable {
     private String cacheName;
     private Object key;
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CachePatternMatcher.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CachePatternMatcher.java
new file mode 100644
index 0000000..f8d71f3
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CachePatternMatcher.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.ericsson.gerrit.plugins.highavailability.cache;
+
+import com.ericsson.gerrit.plugins.highavailability.Configuration;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+@Singleton
+public class CachePatternMatcher {
+  private static final List<String> DEFAULT_PATTERNS =
+      ImmutableList.of(
+          "^accounts.*",
+          "^groups.*",
+          "ldap_groups",
+          "ldap_usernames",
+          "^project.*",
+          "sshkeys",
+          "web_sessions");
+
+  private final Pattern pattern;
+
+  @Inject
+  public CachePatternMatcher(Configuration cfg) {
+    List<String> patterns = new ArrayList<>(DEFAULT_PATTERNS);
+    patterns.addAll(cfg.cache().patterns());
+    this.pattern = Pattern.compile(Joiner.on("|").join(patterns));
+  }
+
+  public boolean matches(String cacheName) {
+    return pattern.matcher(cacheName).matches();
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/Constants.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/Constants.java
index 6b047d3..94c3d30 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/Constants.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/Constants.java
@@ -15,7 +15,7 @@
 package com.ericsson.gerrit.plugins.highavailability.cache;
 
 public final class Constants {
-
+  public static final String GERRIT = "gerrit";
   public static final String PROJECT_LIST = "project_list";
   public static final String ACCOUNTS = "accounts";
   public static final String GROUPS = "groups";
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 99dff1d..651f609 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
@@ -44,6 +44,14 @@
   boolean deleteChangeFromIndex(int changeId);
 
   /**
+   * Forward a group indexing event to the other master.
+   *
+   * @param uuid the group to index.
+   * @return true if successful, otherwise false.
+   */
+  boolean indexGroup(String uuid);
+
+  /**
    * 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/rest/AbstractIndexRestApiServlet.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/AbstractIndexRestApiServlet.java
new file mode 100644
index 0000000..5b7cc60
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/AbstractIndexRestApiServlet.java
@@ -0,0 +1,133 @@
+// Copyright (C) 2017 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 java.nio.charset.StandardCharsets.UTF_8;
+import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
+import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
+import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
+
+import com.ericsson.gerrit.plugins.highavailability.forwarder.Context;
+import com.google.gwtorm.server.OrmException;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class AbstractIndexRestApiServlet<T> extends HttpServlet {
+  private static final long serialVersionUID = -1L;
+  private static final Logger logger = LoggerFactory.getLogger(AbstractIndexRestApiServlet.class);
+  private final Map<T, AtomicInteger> idLocks = new HashMap<>();
+  private final String type;
+  private final boolean allowDelete;
+
+  enum Operation {
+    INDEX,
+    DELETE
+  }
+
+  abstract T parse(String id);
+
+  abstract void index(T id, Operation operation) throws IOException, OrmException;
+
+  AbstractIndexRestApiServlet(String type, boolean allowDelete) {
+    this.type = type;
+    this.allowDelete = allowDelete;
+  }
+
+  AbstractIndexRestApiServlet(String type) {
+    this(type, false);
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest req, HttpServletResponse rsp)
+      throws IOException, ServletException {
+    process(req, rsp, Operation.INDEX);
+  }
+
+  @Override
+  protected void doDelete(HttpServletRequest req, HttpServletResponse rsp)
+      throws IOException, ServletException {
+    if (!allowDelete) {
+      sendError(rsp, SC_METHOD_NOT_ALLOWED, String.format("cannot delete %s from index", type));
+    } else {
+      process(req, rsp, Operation.DELETE);
+    }
+  }
+
+  private void process(HttpServletRequest req, HttpServletResponse rsp, Operation operation) {
+    rsp.setContentType("text/plain");
+    rsp.setCharacterEncoding(UTF_8.name());
+    String path = req.getPathInfo();
+    T id = parse(path.substring(path.lastIndexOf('/') + 1));
+    logger.debug("{} {} {}", operation.name(), type, id);
+    try {
+      Context.setForwardedEvent(true);
+      AtomicInteger idLock = getAndIncrementIdLock(id);
+      synchronized (idLock) {
+        index(id, operation);
+      }
+      if (idLock.decrementAndGet() == 0) {
+        removeIdLock(id);
+      }
+      rsp.setStatus(SC_NO_CONTENT);
+    } catch (IOException e) {
+      sendError(rsp, SC_CONFLICT, e.getMessage());
+      logger.error(String.format("Unable to update %s index", type), e);
+    } catch (OrmException e) {
+      String msg = String.format("Error trying to find %s \n", type);
+      sendError(rsp, SC_NOT_FOUND, msg);
+      logger.debug(msg, e);
+    } finally {
+      Context.unsetForwardedEvent();
+    }
+  }
+
+  private AtomicInteger getAndIncrementIdLock(T id) {
+    synchronized (idLocks) {
+      AtomicInteger lock = idLocks.get(id);
+      if (lock == null) {
+        lock = new AtomicInteger(1);
+        idLocks.put(id, lock);
+      } else {
+        lock.incrementAndGet();
+      }
+      return lock;
+    }
+  }
+
+  private void removeIdLock(T id) {
+    synchronized (idLocks) {
+      if (idLocks.get(id).get() == 0) {
+        idLocks.remove(id);
+      }
+    }
+  }
+
+  private void sendError(HttpServletResponse rsp, int statusCode, String message) {
+    try {
+      rsp.sendError(statusCode, message);
+    } catch (IOException e) {
+      logger.error("Failed to send error messsage: " + e.getMessage(), e);
+    }
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/CacheRestApiServlet.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/CacheRestApiServlet.java
index b388b0b..253ff4a 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
@@ -19,6 +19,7 @@
 
 import com.ericsson.gerrit.plugins.highavailability.cache.Constants;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.Context;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Splitter;
 import com.google.common.cache.Cache;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -37,7 +38,6 @@
 class CacheRestApiServlet extends HttpServlet {
   private static final int CACHENAME_INDEX = 1;
   private static final long serialVersionUID = -1L;
-  private static final String GERRIT = "gerrit";
   private static final Logger logger = LoggerFactory.getLogger(CacheRestApiServlet.class);
 
   private final DynamicMap<Cache<?, ?>> cacheMap;
@@ -57,10 +57,17 @@
       String cacheName = params.get(CACHENAME_INDEX);
       String json = req.getReader().readLine();
       Object key = GsonParser.fromJson(cacheName, json);
-      Cache<?, ?> cache = cacheMap.get(GERRIT, cacheName);
-      Context.setForwardedEvent(true);
-      evictCache(cache, cacheName, key);
-      rsp.setStatus(SC_NO_CONTENT);
+      CacheParameters cacheKey = getCacheParameters(cacheName);
+      Cache<?, ?> cache = cacheMap.get(cacheKey.pluginName, cacheKey.cacheName);
+      if (cache == null) {
+        String msg = String.format("cache %s not found", cacheName);
+        logger.error("Failed to process eviction request: " + msg);
+        sendError(rsp, SC_BAD_REQUEST, msg);
+      } else {
+        Context.setForwardedEvent(true);
+        evictCache(cache, cacheKey.cacheName, key);
+        rsp.setStatus(SC_NO_CONTENT);
+      }
     } catch (IOException e) {
       logger.error("Failed to process eviction request: " + e.getMessage(), e);
       sendError(rsp, SC_BAD_REQUEST, e.getMessage());
@@ -69,6 +76,26 @@
     }
   }
 
+  @VisibleForTesting
+  public static class CacheParameters {
+    public final String pluginName;
+    public final String cacheName;
+
+    public CacheParameters(String pluginName, String cacheName) {
+      this.pluginName = pluginName;
+      this.cacheName = cacheName;
+    }
+  }
+
+  @VisibleForTesting
+  public static CacheParameters getCacheParameters(String cache) {
+    int dot = cache.indexOf(".");
+    if (dot > 0) {
+      return new CacheParameters(cache.substring(0, dot), cache.substring(dot + 1));
+    }
+    return new CacheParameters(Constants.GERRIT, cache);
+  }
+
   private static void sendError(HttpServletResponse rsp, int statusCode, String message) {
     try {
       rsp.sendError(statusCode, message);
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 677d2eb..da08a83 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
@@ -66,6 +66,7 @@
         return;
       }
       Event event = getEventFromRequest(req);
+      logger.debug("event {}", event.getType());
       dispatcher.postEvent(event);
       rsp.setStatus(SC_NO_CONTENT);
     } catch (OrmException e) {
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/GsonParser.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/GsonParser.java
index 23f42ab..8a177de 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/GsonParser.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/GsonParser.java
@@ -44,7 +44,11 @@
         key = gson.fromJson(Strings.nullToEmpty(json), Object.class);
         break;
       default:
-        key = gson.fromJson(Strings.nullToEmpty(json).trim(), String.class);
+        try {
+          key = gson.fromJson(Strings.nullToEmpty(json).trim(), String.class);
+        } catch (Exception e) {
+          key = gson.fromJson(Strings.nullToEmpty(json), Object.class);
+        }
     }
     return key;
   }
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 0a0e898..136e7f3 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
@@ -16,13 +16,13 @@
 
 import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.HttpResponseHandler.HttpResult;
 import com.ericsson.gerrit.plugins.highavailability.peers.PeerInfo;
-import com.google.common.base.Optional;
 import com.google.common.base.Strings;
 import com.google.common.net.MediaType;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.util.Optional;
 import org.apache.http.client.methods.HttpDelete;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.entity.StringEntity;
@@ -57,7 +57,7 @@
   }
 
   private PeerInfo getPeerInfo() throws PeerInfoNotAvailableException {
-    PeerInfo info = peerInfo.get().orNull();
+    PeerInfo info = peerInfo.get().orElse(null);
     if (info == null) {
       throw new PeerInfoNotAvailableException();
     }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexAccountRestApiServlet.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexAccountRestApiServlet.java
index 936e223..94c9a4f 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexAccountRestApiServlet.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexAccountRestApiServlet.java
@@ -14,95 +14,35 @@
 
 package com.ericsson.gerrit.plugins.highavailability.forwarder.rest;
 
-import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
-import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
-
-import com.ericsson.gerrit.plugins.highavailability.forwarder.Context;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicInteger;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 @Singleton
-class IndexAccountRestApiServlet extends HttpServlet {
+class IndexAccountRestApiServlet extends AbstractIndexRestApiServlet<Account.Id> {
   private static final long serialVersionUID = -1L;
   private static final Logger logger = LoggerFactory.getLogger(IndexAccountRestApiServlet.class);
-  private static final Map<Account.Id, AtomicInteger> accountIdLocks = new HashMap<>();
 
   private final AccountIndexer indexer;
 
   @Inject
   IndexAccountRestApiServlet(AccountIndexer indexer) {
+    super("account");
     this.indexer = indexer;
   }
 
   @Override
-  protected void doPost(HttpServletRequest req, HttpServletResponse rsp)
-      throws IOException, ServletException {
-    rsp.setContentType("text/plain");
-    rsp.setCharacterEncoding("UTF-8");
-    String path = req.getPathInfo();
-    String accountId = path.substring(path.lastIndexOf('/') + 1);
-    Account.Id id = Account.Id.parse(accountId);
-    try {
-      Context.setForwardedEvent(true);
-      index(id);
-      rsp.setStatus(SC_NO_CONTENT);
-    } catch (IOException e) {
-      sendError(rsp, SC_CONFLICT, e.getMessage());
-      logger.error("Unable to update account index", e);
-    } finally {
-      Context.unsetForwardedEvent();
-    }
+  Account.Id parse(String id) {
+    return Account.Id.parse(id);
   }
 
-  private static void sendError(HttpServletResponse rsp, int statusCode, String message) {
-    try {
-      rsp.sendError(statusCode, message);
-    } catch (IOException e) {
-      logger.error("Failed to send error messsage: " + e.getMessage(), e);
-    }
-  }
-
-  private void index(Account.Id id) throws IOException {
-    AtomicInteger accountIdLock = getAndIncrementAccountIdLock(id);
-    synchronized (accountIdLock) {
-      indexer.index(id);
-      logger.debug("Account {} successfully indexed", id);
-    }
-    if (accountIdLock.decrementAndGet() == 0) {
-      removeAccountIdLock(id);
-    }
-  }
-
-  private AtomicInteger getAndIncrementAccountIdLock(Account.Id id) {
-    synchronized (accountIdLocks) {
-      AtomicInteger accountIdLock = accountIdLocks.get(id);
-      if (accountIdLock == null) {
-        accountIdLock = new AtomicInteger(1);
-        accountIdLocks.put(id, accountIdLock);
-      } else {
-        accountIdLock.incrementAndGet();
-      }
-      return accountIdLock;
-    }
-  }
-
-  private void removeAccountIdLock(Account.Id id) {
-    synchronized (accountIdLocks) {
-      if (accountIdLocks.get(id).get() == 0) {
-        accountIdLocks.remove(id);
-      }
-    }
+  @Override
+  void index(Account.Id id, Operation operation) throws IOException {
+    indexer.index(id);
+    logger.debug("Account {} successfully indexed", id);
   }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexChangeRestApiServlet.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexChangeRestApiServlet.java
index d08f3fa..f8a3c42 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexChangeRestApiServlet.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexChangeRestApiServlet.java
@@ -14,11 +14,6 @@
 
 package com.ericsson.gerrit.plugins.highavailability.forwarder.rest;
 
-import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
-import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
-import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
-
-import com.ericsson.gerrit.plugins.highavailability.forwarder.Context;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.index.change.ChangeIndexer;
@@ -27,77 +22,33 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicInteger;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 @Singleton
-class IndexChangeRestApiServlet extends HttpServlet {
+class IndexChangeRestApiServlet extends AbstractIndexRestApiServlet<Change.Id> {
   private static final long serialVersionUID = -1L;
   private static final Logger logger = LoggerFactory.getLogger(IndexChangeRestApiServlet.class);
-  private static final Map<Change.Id, AtomicInteger> changeIdLocks = new HashMap<>();
 
   private final ChangeIndexer indexer;
   private final SchemaFactory<ReviewDb> schemaFactory;
 
   @Inject
   IndexChangeRestApiServlet(ChangeIndexer indexer, SchemaFactory<ReviewDb> schemaFactory) {
+    super("change", true);
     this.indexer = indexer;
     this.schemaFactory = schemaFactory;
   }
 
   @Override
-  protected void doPost(HttpServletRequest req, HttpServletResponse rsp)
-      throws IOException, ServletException {
-    process(req, rsp, "index");
+  Change.Id parse(String id) {
+    return Change.Id.parse(id);
   }
 
   @Override
-  protected void doDelete(HttpServletRequest req, HttpServletResponse rsp)
-      throws IOException, ServletException {
-    process(req, rsp, "delete");
-  }
-
-  private void process(HttpServletRequest req, HttpServletResponse rsp, String operation) {
-    rsp.setContentType("text/plain");
-    rsp.setCharacterEncoding("UTF-8");
-    String path = req.getPathInfo();
-    String changeId = path.substring(path.lastIndexOf('/') + 1);
-    Change.Id id = Change.Id.parse(changeId);
-    try {
-      Context.setForwardedEvent(true);
-      index(id, operation);
-      rsp.setStatus(SC_NO_CONTENT);
-    } catch (IOException e) {
-      sendError(rsp, SC_CONFLICT, e.getMessage());
-      logger.error("Unable to update change index", e);
-    } catch (OrmException e) {
-      String msg = "Error trying to find a change \n";
-      sendError(rsp, SC_NOT_FOUND, msg);
-      logger.debug(msg, e);
-    } finally {
-      Context.unsetForwardedEvent();
-    }
-  }
-
-  private static void sendError(HttpServletResponse rsp, int statusCode, String message) {
-    try {
-      rsp.sendError(statusCode, message);
-    } catch (IOException e) {
-      logger.error("Failed to send error messsage: " + e.getMessage(), e);
-    }
-  }
-
-  private void index(Change.Id id, String operation) throws IOException, OrmException {
-    AtomicInteger changeIdLock = getAndIncrementChangeIdLock(id);
-    synchronized (changeIdLock) {
-      if ("index".equals(operation)) {
+  void index(Change.Id id, Operation operation) throws IOException, OrmException {
+    switch (operation) {
+      case INDEX:
         try (ReviewDb db = schemaFactory.open()) {
           Change change = db.changes().get(id);
           if (change == null) {
@@ -107,35 +58,11 @@
           indexer.index(db, change);
         }
         logger.debug("Change {} successfully indexed", id);
-      }
-      if ("delete".equals(operation)) {
+        break;
+      case DELETE:
         indexer.delete(id);
         logger.debug("Change {} successfully deleted from index", id);
-      }
-    }
-    if (changeIdLock.decrementAndGet() == 0) {
-      removeChangeIdLock(id);
-    }
-  }
-
-  private AtomicInteger getAndIncrementChangeIdLock(Change.Id id) {
-    synchronized (changeIdLocks) {
-      AtomicInteger changeIdLock = changeIdLocks.get(id);
-      if (changeIdLock == null) {
-        changeIdLock = new AtomicInteger(1);
-        changeIdLocks.put(id, changeIdLock);
-      } else {
-        changeIdLock.incrementAndGet();
-      }
-      return changeIdLock;
-    }
-  }
-
-  private void removeChangeIdLock(Change.Id id) {
-    synchronized (changeIdLocks) {
-      if (changeIdLocks.get(id).get() == 0) {
-        changeIdLocks.remove(id);
-      }
+        break;
     }
   }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexGroupRestApiServlet.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexGroupRestApiServlet.java
new file mode 100644
index 0000000..0fcb0ca
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexGroupRestApiServlet.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2017 Ericsson
+//
+// 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.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.index.group.GroupIndexer;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+class IndexGroupRestApiServlet extends AbstractIndexRestApiServlet<AccountGroup.UUID> {
+  private static final long serialVersionUID = -1L;
+  private static final Logger logger = LoggerFactory.getLogger(IndexGroupRestApiServlet.class);
+
+  private final GroupIndexer indexer;
+
+  @Inject
+  IndexGroupRestApiServlet(GroupIndexer indexer) {
+    super("group");
+    this.indexer = indexer;
+  }
+
+  @Override
+  AccountGroup.UUID parse(String id) {
+    return AccountGroup.UUID.parse(id);
+  }
+
+  @Override
+  void index(AccountGroup.UUID uuid, Operation operation) throws IOException {
+    indexer.index(uuid);
+    logger.debug("Group {} successfully indexed", uuid);
+  }
+}
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 6d18be3..c1c46d9 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
@@ -74,6 +74,16 @@
     }.execute();
   }
 
+  @Override
+  public boolean indexGroup(final String uuid) {
+    return new Request("index group " + uuid) {
+      @Override
+      HttpResult send() throws IOException {
+        return httpSession.post(Joiner.on("/").join(pluginRelativePath, "index/group", uuid));
+      }
+    }.execute();
+  }
+
   private String buildIndexEndpoint(int changeId) {
     return Joiner.on("/").join(pluginRelativePath, "index/change", changeId);
   }
@@ -113,26 +123,30 @@
     }
 
     boolean execute() {
+      log.debug(name);
       for (; ; ) {
         try {
           execCnt++;
           tryOnce();
+          log.debug("{} OK", name);
           return true;
         } catch (ForwardingException e) {
+          int maxTries = cfg.http().maxTries();
+          log.debug("Failed to {} [{}/{}]", name, execCnt, maxTries, e);
           if (!e.isRecoverable()) {
-            log.error("Failed to {}", name, e);
+            log.error("{} failed with unrecoverable error; giving up", name);
             return false;
           }
-          if (execCnt >= cfg.http().maxTries()) {
-            log.error("Failed to {}, after {} tries", name, cfg.http().maxTries());
+          if (execCnt >= maxTries) {
+            log.error("Failed to {} after {} tries; giving up", name, maxTries);
             return false;
           }
 
-          logRetry(e);
+          log.debug("Retrying to {}", name);
           try {
             Thread.sleep(cfg.http().retryInterval());
           } catch (InterruptedException ie) {
-            log.error("{} was interrupted, giving up", name, ie);
+            log.error("{} was interrupted; giving up", name, ie);
             Thread.currentThread().interrupt();
             return false;
           }
@@ -156,11 +170,5 @@
     boolean isRecoverable(IOException e) {
       return !(e instanceof SSLException);
     }
-
-    void logRetry(Throwable cause) {
-      if (log.isDebugEnabled()) {
-        log.debug("Retrying to {} caused by '{}'", name, cause);
-      }
-    }
   }
 }
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 bd093ae..d5027d1 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
@@ -21,6 +21,7 @@
   protected void configureServlets() {
     serveRegex("/index/account/\\d+$").with(IndexAccountRestApiServlet.class);
     serveRegex("/index/change/\\d+$").with(IndexChangeRestApiServlet.class);
+    serveRegex("/index/group/\\w+$").with(IndexGroupRestApiServlet.class);
     serve("/event").with(EventRestApiServlet.class);
     serve("/cache/*").with(CacheRestApiServlet.class);
   }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/IndexEventHandler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/IndexEventHandler.java
index b08052a..525c7ed 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
@@ -20,13 +20,15 @@
 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.inject.Inject;
 import java.util.Collections;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executor;
 
-class IndexEventHandler implements ChangeIndexedListener, AccountIndexedListener {
+class IndexEventHandler
+    implements ChangeIndexedListener, AccountIndexedListener, GroupIndexedListener {
   private final Executor executor;
   private final Forwarder forwarder;
   private final String pluginName;
@@ -61,6 +63,16 @@
     executeIndexChangeTask(id, true);
   }
 
+  @Override
+  public void onGroupIndexed(String groupUUID) {
+    if (!Context.isForwardedEvent()) {
+      IndexGroupTask task = new IndexGroupTask(groupUUID);
+      if (queuedTasks.add(task)) {
+        executor.execute(task);
+      }
+    }
+  }
+
   private void executeIndexChangeTask(int id, boolean deleted) {
     if (!Context.isForwardedEvent()) {
       IndexChangeTask task = new IndexChangeTask(id, deleted);
@@ -71,12 +83,6 @@
   }
 
   abstract class IndexTask implements Runnable {
-    protected int id;
-
-    IndexTask(int id) {
-      this.id = id;
-    }
-
     @Override
     public void run() {
       queuedTasks.remove(this);
@@ -88,24 +94,25 @@
 
   class IndexChangeTask extends IndexTask {
     private boolean deleted;
+    private int changeId;
 
     IndexChangeTask(int changeId, boolean deleted) {
-      super(changeId);
+      this.changeId = changeId;
       this.deleted = deleted;
     }
 
     @Override
     public void execute() {
       if (deleted) {
-        forwarder.deleteChangeFromIndex(id);
+        forwarder.deleteChangeFromIndex(changeId);
       } else {
-        forwarder.indexChange(id);
+        forwarder.indexChange(changeId);
       }
     }
 
     @Override
     public int hashCode() {
-      return Objects.hashCode(IndexChangeTask.class, id, deleted);
+      return Objects.hashCode(IndexChangeTask.class, changeId, deleted);
     }
 
     @Override
@@ -114,29 +121,30 @@
         return false;
       }
       IndexChangeTask other = (IndexChangeTask) obj;
-      return id == other.id && deleted == other.deleted;
+      return changeId == other.changeId && deleted == other.deleted;
     }
 
     @Override
     public String toString() {
-      return String.format("[%s] Index change %s in target instance", pluginName, id);
+      return String.format("[%s] Index change %s in target instance", pluginName, changeId);
     }
   }
 
   class IndexAccountTask extends IndexTask {
+    private int accountId;
 
     IndexAccountTask(int accountId) {
-      super(accountId);
+      this.accountId = accountId;
     }
 
     @Override
     public void execute() {
-      forwarder.indexAccount(id);
+      forwarder.indexAccount(accountId);
     }
 
     @Override
     public int hashCode() {
-      return Objects.hashCode(IndexAccountTask.class, id);
+      return Objects.hashCode(accountId);
     }
 
     @Override
@@ -145,12 +153,44 @@
         return false;
       }
       IndexAccountTask other = (IndexAccountTask) obj;
-      return id == other.id;
+      return accountId == other.accountId;
     }
 
     @Override
     public String toString() {
-      return String.format("[%s] Index account %s in target instance", pluginName, id);
+      return String.format("[%s] Index account %s in target instance", pluginName, accountId);
+    }
+  }
+
+  class IndexGroupTask extends IndexTask {
+    private String groupUUID;
+
+    IndexGroupTask(String groupUUID) {
+      this.groupUUID = groupUUID;
+    }
+
+    @Override
+    public void execute() {
+      forwarder.indexGroup(groupUUID);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(IndexGroupTask.class, groupUUID);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (!(obj instanceof IndexGroupTask)) {
+        return false;
+      }
+      IndexGroupTask other = (IndexGroupTask) obj;
+      return groupUUID == other.groupUUID;
+    }
+
+    @Override
+    public String toString() {
+      return String.format("[%s] Index group %s in target instance", pluginName, groupUUID);
     }
   }
 }
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 ca3393f..25e0c41 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
@@ -16,9 +16,9 @@
 
 import com.ericsson.gerrit.plugins.highavailability.Configuration;
 import com.ericsson.gerrit.plugins.highavailability.peers.jgroups.JGroupsPeerInfoProvider;
-import com.google.common.base.Optional;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.inject.TypeLiteral;
+import java.util.Optional;
 
 public class PeerInfoModule extends LifecycleModule {
 
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/PeerInfoProvider.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/PeerInfoProvider.java
index 262e2af..2649f50 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/PeerInfoProvider.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/PeerInfoProvider.java
@@ -16,11 +16,11 @@
 import com.ericsson.gerrit.plugins.highavailability.Configuration;
 import com.ericsson.gerrit.plugins.highavailability.Configuration.PeerInfoStrategy;
 import com.ericsson.gerrit.plugins.highavailability.peers.jgroups.JGroupsPeerInfoProvider;
-import com.google.common.base.Optional;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import java.util.Optional;
 
 @Singleton
 public class PeerInfoProvider implements Provider<Optional<PeerInfo>> {
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 4177fc8..f9f182b 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
@@ -15,9 +15,9 @@
 package com.ericsson.gerrit.plugins.highavailability.peers;
 
 import com.ericsson.gerrit.plugins.highavailability.Configuration;
-import com.google.common.base.Optional;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import java.util.Optional;
 
 public class PluginConfigPeerInfoProvider implements Provider<Optional<PeerInfo>> {
 
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/InetAddressFinder.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/InetAddressFinder.java
index 848d411..c973b4a 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/InetAddressFinder.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/InetAddressFinder.java
@@ -16,7 +16,6 @@
 
 import com.ericsson.gerrit.plugins.highavailability.Configuration;
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Optional;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.net.Inet4Address;
@@ -27,17 +26,18 @@
 import java.util.Collections;
 import java.util.Enumeration;
 import java.util.List;
+import java.util.Optional;
 
 @Singleton
 public class InetAddressFinder {
 
   private final boolean preferIPv4;
-  private final Configuration.PeerInfoJGroups jgroupsConfig;
+  private final Configuration.JGroups jgroupsConfig;
 
   @Inject
   InetAddressFinder(Configuration pluginConfiguration) {
     preferIPv4 = Boolean.getBoolean("java.net.preferIPv4Stack");
-    jgroupsConfig = pluginConfiguration.peerInfoJGroups();
+    jgroupsConfig = pluginConfiguration.jgroups();
   }
 
   /**
@@ -73,7 +73,7 @@
         }
       }
     }
-    return Optional.absent();
+    return Optional.empty();
   }
 
   @VisibleForTesting
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 ec1f433..cc573fc 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
@@ -11,21 +11,17 @@
 // 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.peers.jgroups;
 
 import com.ericsson.gerrit.plugins.highavailability.Configuration;
 import com.ericsson.gerrit.plugins.highavailability.peers.PeerInfo;
-import com.google.common.base.Optional;
 import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.net.InetAddress;
-import java.net.URISyntaxException;
-import java.net.UnknownHostException;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.transport.URIish;
+import java.util.Optional;
 import org.jgroups.Address;
 import org.jgroups.JChannel;
 import org.jgroups.Message;
@@ -36,38 +32,33 @@
 
 /**
  * Provider which uses JGroups to find the peer gerrit instances. On startup every gerrit instance
- * joins a jgroups channel. Whenever the set of channel members changes each gerrit server publishes
- * its url to all channel members.
+ * creates its own channel and joins jgroup cluster. Whenever the set of cluster members changes
+ * each gerrit server publishes its url to all cluster members (publishes it to all channels).
  *
- * <p>This provider maintains a list of all members which joined the jgroups channel. This may be
+ * <p>This provider maintains a list of all members which joined the jgroups cluster. This may be
  * more than two. But will always pick the first node which sent its url as the peer to be returned
  * by {@link #get()}. It will continue to return that node until that node leaves the jgroups
- * channel.
+ * cluster.
  */
 @Singleton
 public class JGroupsPeerInfoProvider extends ReceiverAdapter
     implements Provider<Optional<PeerInfo>>, LifecycleListener {
   private static final Logger log = LoggerFactory.getLogger(JGroupsPeerInfoProvider.class);
 
-  private final String myUrl;
-  private final Configuration.PeerInfoJGroups jgroupsConfig;
+  private final Configuration.JGroups jgroupsConfig;
   private final InetAddressFinder finder;
+  private final String myUrl;
 
   private JChannel channel;
-  private Optional<PeerInfo> peerInfo = Optional.absent();
+  private Optional<PeerInfo> peerInfo = Optional.empty();
   private Address peerAddress;
 
   @Inject
   JGroupsPeerInfoProvider(
-      @GerritServerConfig Config srvConfig,
-      Configuration pluginConfiguration,
-      InetAddressFinder finder)
-      throws UnknownHostException, URISyntaxException {
-    String hostName = InetAddress.getLocalHost().getHostName();
-    URIish u = new URIish(srvConfig.getString("httpd", null, "listenUrl"));
-    this.myUrl = u.setHost(hostName).toString();
-    this.jgroupsConfig = pluginConfiguration.peerInfoJGroups();
+      Configuration pluginConfiguration, InetAddressFinder finder, MyUrlProvider myUrlProvider) {
+    this.jgroupsConfig = pluginConfiguration.jgroups();
     this.finder = finder;
+    this.myUrl = myUrlProvider.get();
   }
 
   @Override
@@ -90,15 +81,17 @@
     synchronized (this) {
       if (view.getMembers().size() > 2) {
         log.warn(
-            "{} members joined the jgroups channel {}. Only two members are supported. Members: {}",
+            "{} members joined the jgroups cluster {} ({}). "
+                + " Only two members are supported. Members: {}",
             view.getMembers().size(),
+            jgroupsConfig.clusterName(),
             channel.getName(),
             view.getMembers());
       }
       if (peerAddress != null && !view.getMembers().contains(peerAddress)) {
         log.info("viewAccepted(): removed peerInfo");
         peerAddress = null;
-        peerInfo = Optional.absent();
+        peerInfo = Optional.empty();
       }
     }
     if (view.size() > 1) {
@@ -107,7 +100,10 @@
       } catch (Exception e) {
         // channel communication caused an error. Can't do much about it.
         log.error(
-            "Sending a message over jgroups channel {} failed", jgroupsConfig.clusterName(), e);
+            "Sending a message over channel {} to cluster {} failed",
+            channel.getName(),
+            jgroupsConfig.clusterName(),
+            e);
       }
     }
   }
@@ -122,9 +118,16 @@
       channel.setReceiver(this);
       channel.setDiscardOwnMessages(true);
       channel.connect(jgroupsConfig.clusterName());
-      log.info("Successfully joined jgroups channel {}", channel.getName());
+      log.info(
+          "Channel {} successfully joined jgroups cluster {}",
+          channel.getName(),
+          jgroupsConfig.clusterName());
     } catch (Exception e) {
-      log.error("joining jgroups channel {} failed", channel.getName(), e);
+      log.error(
+          "joining cluster {} for channel {} failed",
+          jgroupsConfig.clusterName(),
+          channel.getName(),
+          e);
     }
   }
 
@@ -140,9 +143,10 @@
 
   @Override
   public void stop() {
-    log.info("closing jgroups channel {}", jgroupsConfig.clusterName());
+    log.info(
+        "closing jgroups channel {} (cluster {})", channel.getName(), jgroupsConfig.clusterName());
     channel.close();
-    peerInfo = Optional.absent();
+    peerInfo = Optional.empty();
     peerAddress = null;
   }
 }
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
new file mode 100644
index 0000000..dbb5d1d
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/MyUrlProvider.java
@@ -0,0 +1,101 @@
+// Copyright (C) 2017 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.peers.jgroups;
+
+import com.ericsson.gerrit.plugins.highavailability.Configuration;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.CharMatcher;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+import com.google.inject.Singleton;
+import java.net.InetAddress;
+import java.net.URISyntaxException;
+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
+public class MyUrlProvider implements Provider<String> {
+  private static final Logger log = LoggerFactory.getLogger(MyUrlProvider.class);
+
+  private static final String HTTPD_SECTION = "httpd";
+  private static final String LISTEN_URL_KEY = "listenUrl";
+  private static final String LISTEN_URL = HTTPD_SECTION + "." + LISTEN_URL_KEY;
+  private static final String PROXY_PREFIX = "proxy-";
+
+  private final String myUrl;
+
+  @Inject
+  @VisibleForTesting
+  public 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);
+      try {
+        url = CharMatcher.is('/').trimTrailingFrom(getMyUrlFromListenUrl(srvConfig));
+      } catch (MyUrlProviderException e) {
+        throw new ProvisionException(e.getMessage());
+      }
+    }
+    this.myUrl = url;
+  }
+
+  @Override
+  public String get() {
+    return myUrl;
+  }
+
+  private String getMyUrlFromListenUrl(Config srvConfig) throws MyUrlProviderException {
+    String[] listenUrls = srvConfig.getStringList(HTTPD_SECTION, null, LISTEN_URL_KEY);
+    if (listenUrls.length != 1) {
+      throw new MyUrlProviderException(
+          String.format(
+              "Can only determine myUrl from %s when there is exactly 1 value configured; found %d",
+              LISTEN_URL, listenUrls.length));
+    }
+    String url = listenUrls[0];
+    if (url.startsWith(PROXY_PREFIX)) {
+      throw new MyUrlProviderException(
+          String.format(
+              "Cannot determine myUrl from %s when configured as reverse-proxy: %s",
+              LISTEN_URL, url));
+    }
+    if (url.contains("*")) {
+      throw new MyUrlProviderException(
+          String.format(
+              "Cannot determine myUrl from %s when configured with wildcard: %s", LISTEN_URL, url));
+    }
+    try {
+      URIish u = new URIish(url);
+      return u.setHost(InetAddress.getLocalHost().getHostName()).toString();
+    } catch (URISyntaxException | UnknownHostException e) {
+      throw new MyUrlProviderException(
+          String.format(
+              "Unable to determine myUrl from %s value [%s]: %s", LISTEN_URL, url, e.getMessage()));
+    }
+  }
+
+  private static class MyUrlProviderException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    MyUrlProviderException(String message) {
+      super(message);
+    }
+  }
+}
diff --git a/src/main/resources/Documentation/build.md b/src/main/resources/Documentation/build.md
index 3babed2..f0f3237 100644
--- a/src/main/resources/Documentation/build.md
+++ b/src/main/resources/Documentation/build.md
@@ -1,106 +1,104 @@
-Build
-=====
+# Build
 
-This plugin is built with Buck.
+This plugin can be built with Bazel, and two build modes are supported:
 
-Two build modes are supported: Standalone and in Gerrit tree. Standalone
-build mode is recommended, as this mode doesn't require local Gerrit
-tree to exist.
+* Standalone
+* In Gerrit tree
 
-Build standalone
-----------------
+Standalone build mode is recommended, as this mode doesn't require local Gerrit
+tree to exist. Moreover, there are some limitations and additional manual steps
+required when building in Gerrit tree mode (see corresponding sections).
 
-Clone bucklets library:
-
-```
-  git clone https://gerrit.googlesource.com/bucklets
-
-```
-and link it to @PLUGIN@ directory:
-
-```
-  cd @PLUGIN@ && ln -s ../bucklets .
-```
-
-Add link to the .buckversion file:
-
-```
-  cd @PLUGIN@ && ln -s bucklets/buckversion .buckversion
-```
-
-Add link to the .watchmanconfig file:
-
-```
-  cd @PLUGIN@ && ln -s bucklets/watchmanconfig .watchmanconfig
-```
+## Build standalone
 
 To build the plugin, issue the following command:
 
 ```
-  buck build plugin
+  bazel build @PLUGIN@
+```
+
+The output is created in
+
+```
+  bazel-genfiles/@PLUGIN@.jar
+```
+
+To package the plugin sources run:
+
+```
+  bazel build lib@PLUGIN@__plugin-src.jar
 ```
 
 The output is created in:
 
 ```
-  buck-out/gen/@PLUGIN@.jar
-```
-
-This project can be imported into the Eclipse IDE:
-
-```
-  ./bucklets/tools/eclipse.py
+  bazel-bin/lib@PLUGIN@__plugin-src.jar
 ```
 
 To execute the tests run:
 
 ```
-  buck test
-```
-
-To build plugin sources run:
-
-```
-  buck build src
-```
-
-The output is created in:
-
-```
-  buck-out/gen/@PLUGIN@-sources.jar
-```
-
-Build in Gerrit tree
---------------------
-
-Clone or link this plugin to the plugins directory of Gerrit's source
-tree, and issue the command:
-
-```
-  buck build plugins/@PLUGIN@
-```
-
-The output is created in:
-
-```
-  buck-out/gen/plugins/@PLUGIN@/@PLUGIN@.jar
+  bazel test //...
 ```
 
 This project can be imported into the Eclipse IDE:
 
 ```
+  ./tools/eclipse/project.sh
+```
+
+## Build in Gerrit tree
+
+Clone or link this plugin to the plugins directory of Gerrit's
+source tree. Put the external dependency Bazel build file into
+the Gerrit /plugins directory, replacing the existing empty one.
+
+```
+  cd gerrit/plugins
+  rm external_plugin_deps.bzl
+  ln -s @PLUGIN@/external_plugin_deps.bzl .
+```
+
+From Gerrit source tree issue the command:
+
+```
+  bazel build plugins/@PLUGIN@
+```
+
+Note that due to a [known issue in Bazel][bazelissue], if the plugin
+has previously been built in standalone mode, it is necessary to clean
+the workspace before building in-tree:
+
+```
+  cd plugins/@PLUGIN@
+  bazel clean --expunge
+```
+
+The output is created in
+
+```
+  bazel-genfiles/plugins/@PLUGIN@/@PLUGIN@.jar
+```
+
+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
+`tools/bzl/plugins.bzl`, and execute:
+
+```
   ./tools/eclipse/project.py
 ```
 
 To execute the tests run:
 
 ```
-  buck test --include @PLUGIN@
+  bazel test --test_tag_filters=@PLUGIN@
 ```
 
 How to build the Gerrit Plugin API is described in the [Gerrit
-documentation](../../../Documentation/dev-buck.html#_extension_and_plugin_api_jar_files).
+documentation](../../../Documentation/dev-bazel.html#_extension_and_plugin_api_jar_files).
 
 [Back to @PLUGIN@ documentation index][index]
 
 [index]: index.html
+[bazelissue]: https://github.com/bazelbuild/bazel/issues/2797
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 95076f4..42ce673 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -22,7 +22,9 @@
 [peerInfo]
 :  strategy = jgroups
 [peerInfo "jgroups"]
-:  cluster = foo
+:  myUrl = local_instance_url
+[jgroups]
+:  clusterName = foo
 :  skipInterface = lo*
 :  skipInterface = eth2
 [http]
@@ -52,13 +54,21 @@
 peerInfo.static.url
 :   Specify the URL for the peer instance.
 
-peerInfo.jgroups.clusterName
+peerInfo.jgroups.myUrl
+:   The URL of this instance to be broadcast to other peers. If not specified, the
+    URL is determined from the `httpd.listenUrl` in the `gerrit.config`.
+    If `httpd.listenUrl` is configured with multiple values, is configured to work
+    with a reverse proxy (i.e. uses `proxy-http` or `proxy-https` scheme), or is
+    configured to listen on all local addresses (i.e. using hostname `*`), then
+    the URL must be explicitly specified with `myUrl`.
+
+jgroups.clusterName
 :   The name of the high-availability cluster. When peers discover themselves dynamically this
     name is used to determine which instances should work together.  Only those Gerrit
     interfaces which are configured for the same clusterName will communicate with each other.
     Defaults to "GerritHA".
 
-peerInfo.jgroups.skipInterface
+jgroups.skipInterface
 :   A name or a wildcard of network interface(s) which should be skipped
     for JGroups communication. Peer discovery may fail if the host has multiple
     network interfaces and an inappropriate interface is chosen by JGroups.
@@ -107,6 +117,13 @@
 :   Maximum number of threads used to send cache evictions to the target instance.
     Defaults to 1.
 
+cache.pattern
+:   Pattern to match names of custom caches for which evictions should be
+    forwarded (in addition to the core caches that are always forwarded). May be
+    specified more than once to add multiple patterns.
+    Defaults to an empty list, meaning only evictions of the core caches are
+    forwarded.
+
 event.synchronize
 :   Whether to synchronize stream events.
     Defaults to true.
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/ConfigurationTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/ConfigurationTest.java
index 488f12a..85dc137 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/ConfigurationTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/ConfigurationTest.java
@@ -30,10 +30,13 @@
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.EVENT_SECTION;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.HTTP_SECTION;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.INDEX_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.JGROUPS_SECTION;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.JGROUPS_SUBSECTION;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.MAIN_SECTION;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.MAX_TRIES_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.MY_URL_KEY;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.PASSWORD_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.PATTERN_KEY;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.PEER_INFO_SECTION;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.RETRY_INTERVAL_KEY;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.SHARED_DIRECTORY_KEY;
@@ -47,10 +50,16 @@
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.USER_KEY;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.WEBSESSION_SECTION;
 import static com.google.common.truth.Truth.assertThat;
+import static java.net.InetAddress.getLocalHost;
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import com.ericsson.gerrit.plugins.highavailability.cache.CachePatternMatcher;
+import com.ericsson.gerrit.plugins.highavailability.peers.jgroups.MyUrlProvider;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.ProvisionException;
@@ -59,7 +68,9 @@
 import java.nio.file.Paths;
 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;
@@ -68,7 +79,7 @@
 public class ConfigurationTest {
   private static final String PASS = "fakePass";
   private static final String USER = "fakeUser";
-  private static final String URL = "fakeUrl";
+  private static final String URL = "http://fakeUrl";
   private static final String EMPTY = "";
   private static final int TIMEOUT = 5000;
   private static final int MAX_TRIES = 5;
@@ -79,13 +90,17 @@
   private static final String RELATIVE_SHARED_DIRECTORY = "relative/dir";
   private static final Path SITE_PATH = Paths.get("/site_path");
   private static final String ERROR_MESSAGE = "some error message";
+  private static final String[] CUSTOM_CACHE_PATTERNS = {"^my_cache.*", "other"};
 
   @Mock private PluginConfigFactory cfgFactoryMock;
   @Mock private Config configMock;
+  @Mock private Config serverConfigMock;
   private SitePaths site;
   private Configuration configuration;
   private String pluginName = "high-availability";
 
+  @Rule public ExpectedException exception = ExpectedException.none();
+
   @Before
   public void setUp() throws IOException {
     when(cfgFactoryMock.getGlobalPluginConfig(pluginName)).thenReturn(configMock);
@@ -93,6 +108,8 @@
         .thenReturn(SHARED_DIRECTORY);
     when(configMock.getEnum(PEER_INFO_SECTION, null, STRATEGY_KEY, DEFAULT_PEER_INFO_STRATEGY))
         .thenReturn(DEFAULT_PEER_INFO_STRATEGY);
+    when(configMock.getStringList(CACHE_SECTION, null, PATTERN_KEY))
+        .thenReturn(CUSTOM_CACHE_PATTERNS);
     site = new SitePaths(SITE_PATH);
   }
 
@@ -137,16 +154,15 @@
   }
 
   @Test
-  public void testGetJGroupsChannel() throws Exception {
+  public void testGetJGroupsCluster() throws Exception {
     when(configMock.getEnum(PEER_INFO_SECTION, null, STRATEGY_KEY, DEFAULT_PEER_INFO_STRATEGY))
         .thenReturn(Configuration.PeerInfoStrategy.JGROUPS);
     initializeConfiguration();
-    assertThat(configuration.peerInfoJGroups().clusterName()).isEqualTo(DEFAULT_CLUSTER_NAME);
+    assertThat(configuration.jgroups().clusterName()).isEqualTo(DEFAULT_CLUSTER_NAME);
 
-    when(configMock.getString(PEER_INFO_SECTION, JGROUPS_SUBSECTION, CLUSTER_NAME_KEY))
-        .thenReturn("foo");
+    when(configMock.getString(JGROUPS_SECTION, null, CLUSTER_NAME_KEY)).thenReturn("foo");
     initializeConfiguration();
-    assertThat(configuration.peerInfoJGroups().clusterName()).isEqualTo("foo");
+    assertThat(configuration.jgroups().clusterName()).isEqualTo("foo");
   }
 
   @Test
@@ -154,15 +170,127 @@
     when(configMock.getEnum(PEER_INFO_SECTION, null, STRATEGY_KEY, DEFAULT_PEER_INFO_STRATEGY))
         .thenReturn(Configuration.PeerInfoStrategy.JGROUPS);
     initializeConfiguration();
-    assertThat(configuration.peerInfoJGroups().skipInterface())
-        .isEqualTo(DEFAULT_SKIP_INTERFACE_LIST);
+    assertThat(configuration.jgroups().skipInterface()).isEqualTo(DEFAULT_SKIP_INTERFACE_LIST);
 
-    when(configMock.getStringList(PEER_INFO_SECTION, JGROUPS_SUBSECTION, SKIP_INTERFACE_KEY))
+    when(configMock.getStringList(JGROUPS_SECTION, null, SKIP_INTERFACE_KEY))
         .thenReturn(new String[] {"lo*", "eth0"});
     initializeConfiguration();
-    assertThat(configuration.peerInfoJGroups().skipInterface())
-        .containsAllOf("lo*", "eth0")
-        .inOrder();
+    assertThat(configuration.jgroups().skipInterface()).containsAllOf("lo*", "eth0").inOrder();
+  }
+
+  @Test
+  public void testGetJGroupsMyUrl() throws Exception {
+    when(configMock.getEnum(PEER_INFO_SECTION, null, STRATEGY_KEY, DEFAULT_PEER_INFO_STRATEGY))
+        .thenReturn(Configuration.PeerInfoStrategy.JGROUPS);
+    initializeConfiguration();
+    assertThat(configuration.peerInfoJGroups().myUrl()).isNull();
+
+    when(configMock.getString(PEER_INFO_SECTION, JGROUPS_SUBSECTION, MY_URL_KEY)).thenReturn(URL);
+    initializeConfiguration();
+    assertThat(configuration.peerInfoJGroups().myUrl()).isEqualTo(URL);
+
+    when(configMock.getString(PEER_INFO_SECTION, JGROUPS_SUBSECTION, MY_URL_KEY))
+        .thenReturn(URL + "/");
+    initializeConfiguration();
+    assertThat(configuration.peerInfoJGroups().myUrl()).isEqualTo(URL);
+  }
+
+  @Test
+  public void testGetJGroupsMyUrlFromListenUrlWhenNoListenUrlSpecified() throws Exception {
+    when(configMock.getEnum(PEER_INFO_SECTION, null, STRATEGY_KEY, DEFAULT_PEER_INFO_STRATEGY))
+        .thenReturn(Configuration.PeerInfoStrategy.JGROUPS);
+    initializeConfiguration();
+
+    when(serverConfigMock.getStringList("httpd", null, "listenUrl")).thenReturn(new String[] {});
+
+    exception.expect(ProvisionException.class);
+    exception.expectMessage("exactly 1 value configured; found 0");
+    getMyUrlProvider();
+  }
+
+  @Test
+  public void testGetJGroupsMyUrlFromListenUrlWhenMultipleListenUrlsSpecified() throws Exception {
+    when(configMock.getEnum(PEER_INFO_SECTION, null, STRATEGY_KEY, DEFAULT_PEER_INFO_STRATEGY))
+        .thenReturn(Configuration.PeerInfoStrategy.JGROUPS);
+    initializeConfiguration();
+
+    when(serverConfigMock.getStringList("httpd", null, "listenUrl"))
+        .thenReturn(new String[] {"a", "b"});
+
+    exception.expect(ProvisionException.class);
+    exception.expectMessage("exactly 1 value configured; found 2");
+    getMyUrlProvider();
+  }
+
+  @Test
+  public void testGetJGroupsMyUrlFromListenUrlWhenReverseProxyConfigured() throws Exception {
+    when(configMock.getEnum(PEER_INFO_SECTION, null, STRATEGY_KEY, DEFAULT_PEER_INFO_STRATEGY))
+        .thenReturn(Configuration.PeerInfoStrategy.JGROUPS);
+    initializeConfiguration();
+
+    when(serverConfigMock.getStringList("httpd", null, "listenUrl"))
+        .thenReturn(new String[] {"proxy-https://foo"});
+
+    exception.expect(ProvisionException.class);
+    exception.expectMessage("when configured as reverse-proxy");
+    getMyUrlProvider();
+  }
+
+  @Test
+  public void testGetJGroupsMyUrlFromListenUrlWhenWildcardConfigured() throws Exception {
+    when(configMock.getEnum(PEER_INFO_SECTION, null, STRATEGY_KEY, DEFAULT_PEER_INFO_STRATEGY))
+        .thenReturn(Configuration.PeerInfoStrategy.JGROUPS);
+    initializeConfiguration();
+
+    when(serverConfigMock.getStringList("httpd", null, "listenUrl"))
+        .thenReturn(new String[] {"https://*"});
+
+    exception.expect(ProvisionException.class);
+    exception.expectMessage("when configured with wildcard");
+    getMyUrlProvider();
+  }
+
+  @Test
+  public void testGetJGroupsMyUrlOverridesListenUrl() throws Exception {
+    when(configMock.getEnum(PEER_INFO_SECTION, null, STRATEGY_KEY, DEFAULT_PEER_INFO_STRATEGY))
+        .thenReturn(Configuration.PeerInfoStrategy.JGROUPS);
+    when(configMock.getString(PEER_INFO_SECTION, JGROUPS_SUBSECTION, MY_URL_KEY)).thenReturn(URL);
+    initializeConfiguration();
+    assertThat(configuration.peerInfoJGroups().myUrl()).isEqualTo(URL);
+
+    verify(serverConfigMock, never()).getStringList("httpd", null, "listenUrl");
+    assertThat(getMyUrlProvider().get()).isEqualTo(URL);
+  }
+
+  @Test
+  public void testGetJGroupsMyUrlFromListenUrl() throws Exception {
+    when(configMock.getEnum(PEER_INFO_SECTION, null, STRATEGY_KEY, DEFAULT_PEER_INFO_STRATEGY))
+        .thenReturn(Configuration.PeerInfoStrategy.JGROUPS);
+    when(configMock.getString(PEER_INFO_SECTION, JGROUPS_SUBSECTION, MY_URL_KEY)).thenReturn(null);
+    initializeConfiguration();
+    assertThat(configuration.peerInfoJGroups().myUrl()).isNull();
+
+    when(serverConfigMock.getStringList("httpd", null, "listenUrl"))
+        .thenReturn(new String[] {"https://foo:8080"});
+
+    String hostName = getLocalHost().getHostName();
+    String expected = "https://" + hostName + ":8080";
+    assertThat(getMyUrlProvider().get()).isEqualTo(expected);
+
+    when(serverConfigMock.getStringList("httpd", null, "listenUrl"))
+        .thenReturn(new String[] {"https://foo"});
+
+    expected = "https://" + hostName;
+    assertThat(getMyUrlProvider().get()).isEqualTo(expected);
+
+    when(serverConfigMock.getStringList("httpd", null, "listenUrl"))
+        .thenReturn(new String[] {"https://foo/"});
+
+    assertThat(getMyUrlProvider().get()).isEqualTo(expected);
+  }
+
+  private MyUrlProvider getMyUrlProvider() {
+    return new MyUrlProvider(serverConfigMock, configuration);
   }
 
   @Test
@@ -384,4 +512,23 @@
     initializeConfiguration();
     assertThat(configuration.websession().synchronize()).isTrue();
   }
+
+  @Test
+  public void testGetCachePatterns() throws Exception {
+    initializeConfiguration();
+    CachePatternMatcher matcher = new CachePatternMatcher(configuration);
+    for (String cache :
+        ImmutableList.of(
+            "accounts_byemail",
+            "ldap_groups",
+            "project_list",
+            "my_cache_a",
+            "my_cache_b",
+            "other")) {
+      assertThat(matcher.matches(cache)).isTrue();
+    }
+    for (String cache : ImmutableList.of("ldap_groups_by_include", "foo")) {
+      assertThat(matcher.matches(cache)).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 11fc496..97c496a 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
@@ -21,37 +21,53 @@
 import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
 import static com.github.tomakehurst.wiremock.client.WireMock.verify;
 import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
+import static org.junit.Assert.fail;
 
 import com.github.tomakehurst.wiremock.http.Request;
 import com.github.tomakehurst.wiremock.http.RequestListener;
 import com.github.tomakehurst.wiremock.http.Response;
 import com.github.tomakehurst.wiremock.junit.WireMockRule;
-import com.google.common.base.Throwables;
-import com.google.gerrit.acceptance.GerritConfig;
-import com.google.gerrit.acceptance.GerritConfigs;
+import com.google.gerrit.acceptance.GlobalPluginConfig;
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
-import com.google.gerrit.acceptance.PluginDaemonTest;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.UseSsh;
 import java.util.concurrent.BrokenBarrierException;
 import java.util.concurrent.CyclicBarrier;
 import java.util.concurrent.TimeUnit;
 import org.apache.http.HttpStatus;
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 
 @NoHttpd
-@Ignore
-public class CacheEvictionIT extends PluginDaemonTest {
+@UseSsh
+@TestPlugin(
+  name = "high-availability",
+  sysModule = "com.ericsson.gerrit.plugins.highavailability.Module",
+  httpModule = "com.ericsson.gerrit.plugins.highavailability.HttpModule"
+)
+public class CacheEvictionIT extends LightweightPluginDaemonTest {
+  private static final int PORT = 18888;
+  private static final String URL = "http://localhost:" + PORT;
 
-  @Rule public WireMockRule wireMockRule = new WireMockRule(options().port(18888), false);
+  @Rule public WireMockRule wireMockRule = new WireMockRule(options().port(PORT), false);
 
   @Test
-  @GerritConfigs({
-    @GerritConfig(name = "plugin.high-availability.url", value = "http://localhost:18888"),
-    @GerritConfig(name = "plugin.high-availability.user", value = "admin"),
-    @GerritConfig(name = "plugin.high-availability.cacheThreadPoolSize", value = "10"),
-    @GerritConfig(name = "plugin.high-availability.sharedDirectory", value = "directory")
-  })
+  @UseLocalDisk
+  @GlobalPluginConfig(
+    pluginName = "high-availability",
+    name = "peerInfo.strategy",
+    value = "static"
+  )
+  @GlobalPluginConfig(pluginName = "high-availability", name = "peerInfo.static.url", value = URL)
+  @GlobalPluginConfig(pluginName = "high-availability", name = "http.user", value = "admin")
+  @GlobalPluginConfig(pluginName = "high-availability", name = "cache.threadPoolSize", value = "10")
+  @GlobalPluginConfig(
+    pluginName = "high-availability",
+    name = "main.sharedDirectory",
+    value = "directory"
+  )
   public void flushAndSendPost() throws Exception {
     final String flushRequest = "/plugins/high-availability/cache/" + Constants.PROJECT_LIST;
     final CyclicBarrier checkPoint = new CyclicBarrier(2);
@@ -63,7 +79,7 @@
               try {
                 checkPoint.await();
               } catch (InterruptedException | BrokenBarrierException e) {
-                Throwables.propagateIfPossible(e);
+                fail();
               }
             }
           }
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/CacheRestApiServletTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/CacheRestApiServletTest.java
index c0d13c8..84ac4db 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/CacheRestApiServletTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/CacheRestApiServletTest.java
@@ -14,6 +14,7 @@
 
 package com.ericsson.gerrit.plugins.highavailability.forwarder.rest;
 
+import static com.google.common.truth.Truth.assertThat;
 import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
 import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
 import static org.mockito.Mockito.doThrow;
@@ -22,6 +23,7 @@
 import static org.mockito.Mockito.when;
 
 import com.ericsson.gerrit.plugins.highavailability.cache.Constants;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.CacheRestApiServlet.CacheParameters;
 import com.google.common.cache.Cache;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import java.io.BufferedReader;
@@ -74,7 +76,13 @@
   @Test
   public void evictGroupsMembers() throws Exception {
     configureMocksFor(Constants.GROUPS_MEMBERS);
-    servlet.doPost(request, response);
+    verifyResponseIsOK();
+  }
+
+  @Test
+  public void evictPluginCache() throws Exception {
+    configureMocksFor("my-plugin", "my-cache");
+    verifyResponseIsOK();
   }
 
   @Test
@@ -83,11 +91,6 @@
     verifyResponseIsOK();
   }
 
-  private void verifyResponseIsOK() throws Exception {
-    servlet.doPost(request, response);
-    verify(response).setStatus(SC_NO_CONTENT);
-  }
-
   @Test
   public void badRequest() throws Exception {
     when(request.getPathInfo()).thenReturn("/someCache");
@@ -106,10 +109,34 @@
     verify(response).sendError(SC_BAD_REQUEST, errorMessage);
   }
 
+  @Test
+  public void cacheParameters() throws Exception {
+    CacheParameters key = CacheRestApiServlet.getCacheParameters("accounts_by_name");
+    assertThat(key.pluginName).isEqualTo(Constants.GERRIT);
+    assertThat(key.cacheName).isEqualTo("accounts_by_name");
+
+    key = CacheRestApiServlet.getCacheParameters("my_plugin.my_cache");
+    assertThat(key.pluginName).isEqualTo("my_plugin");
+    assertThat(key.cacheName).isEqualTo("my_cache");
+  }
+
+  private void verifyResponseIsOK() throws Exception {
+    servlet.doPost(request, response);
+    verify(response).setStatus(SC_NO_CONTENT);
+  }
+
+  private void configureMocksFor(String cacheName) throws Exception {
+    configureMocksFor(Constants.GERRIT, cacheName);
+  }
+
   @SuppressWarnings("unchecked")
-  private void configureMocksFor(String cacheName) throws IOException {
-    when(cacheMap.get("gerrit", cacheName)).thenReturn(mock(Cache.class));
-    when(request.getPathInfo()).thenReturn("/" + cacheName);
+  private void configureMocksFor(String pluginName, String cacheName) throws Exception {
+    when(cacheMap.get(pluginName, cacheName)).thenReturn(mock(Cache.class));
+    if (Constants.GERRIT.equals(pluginName)) {
+      when(request.getPathInfo()).thenReturn("/" + cacheName);
+    } else {
+      when(request.getPathInfo()).thenReturn("/" + pluginName + "." + cacheName);
+    }
     when(request.getReader()).thenReturn(reader);
 
     if (Constants.PROJECTS.equals(cacheName)) {
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 d5e27b1..2a4f9cc 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
@@ -34,10 +34,10 @@
 import com.github.tomakehurst.wiremock.http.Fault;
 import com.github.tomakehurst.wiremock.junit.WireMockRule;
 import com.github.tomakehurst.wiremock.stubbing.Scenario;
-import com.google.common.base.Optional;
 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;
@@ -186,7 +186,7 @@
   public void testNoRequestWhenPeerInfoUnknown() throws IOException {
     httpSession =
         new HttpSession(
-            new HttpClientProvider(configMock).get(), Providers.of(Optional.<PeerInfo>absent()));
+            new HttpClientProvider(configMock).get(), Providers.of(Optional.<PeerInfo>empty()));
     try {
       httpSession.post(ENDPOINT);
       fail("Expected PeerInfoNotAvailableException");
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexAccountRestApiServletTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexAccountRestApiServletTest.java
index cc80fbb..9893c0a 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexAccountRestApiServletTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexAccountRestApiServletTest.java
@@ -15,6 +15,7 @@
 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.Mockito.doThrow;
 import static org.mockito.Mockito.times;
@@ -66,6 +67,12 @@
   }
 
   @Test
+  public void cannotDeleteAccount() throws Exception {
+    servlet.doDelete(req, rsp);
+    verify(rsp).sendError(SC_METHOD_NOT_ALLOWED, "cannot delete account from index");
+  }
+
+  @Test
   public void indexerThrowsIOExceptionTryingToIndexAccount() throws Exception {
     doThrow(new IOException("io-error")).when(indexer).index(id);
     servlet.doPost(req, rsp);
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexChangeRestApiServletTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexChangeRestApiServletTest.java
index 0c791b8..af6c31a 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexChangeRestApiServletTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexChangeRestApiServletTest.java
@@ -95,7 +95,7 @@
   public void schemaThrowsExceptionWhenLookingUpForChange() throws Exception {
     setupPostMocks(CHANGE_EXISTS, THROW_ORM_EXCEPTION);
     indexRestApiServlet.doPost(req, rsp);
-    verify(rsp).sendError(SC_NOT_FOUND, "Error trying to find a change \n");
+    verify(rsp).sendError(SC_NOT_FOUND, "Error trying to find change \n");
   }
 
   @Test
@@ -123,10 +123,10 @@
   public void sendErrorThrowsIOException() throws Exception {
     doThrow(new IOException("someError"))
         .when(rsp)
-        .sendError(SC_NOT_FOUND, "Error trying to find a change \n");
+        .sendError(SC_NOT_FOUND, "Error trying to find change \n");
     setupPostMocks(CHANGE_EXISTS, THROW_ORM_EXCEPTION);
     indexRestApiServlet.doPost(req, rsp);
-    verify(rsp).sendError(SC_NOT_FOUND, "Error trying to find a change \n");
+    verify(rsp).sendError(SC_NOT_FOUND, "Error trying to find change \n");
     verifyZeroInteractions(indexer);
   }
 
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexGroupRestApiServletTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexGroupRestApiServletTest.java
new file mode 100644
index 0000000..0994b9b
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/IndexGroupRestApiServletTest.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2017 Ericsson
+//
+// 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.Mockito.doThrow;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.index.group.GroupIndexer;
+import com.google.gwtorm.client.KeyUtil;
+import com.google.gwtorm.server.StandardKeyEncoder;
+import java.io.IOException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class IndexGroupRestApiServletTest {
+  private static final String UUID = "we235jdf92nfj2351";
+
+  @Mock private GroupIndexer indexer;
+  @Mock private HttpServletRequest req;
+  @Mock private HttpServletResponse rsp;
+
+  private AccountGroup.UUID uuid;
+  private IndexGroupRestApiServlet servlet;
+
+  @BeforeClass
+  public static void setup() {
+    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
+  }
+
+  @Before
+  public void setUpMocks() {
+    servlet = new IndexGroupRestApiServlet(indexer);
+    uuid = AccountGroup.UUID.parse(UUID);
+    when(req.getPathInfo()).thenReturn("/index/group/" + UUID);
+  }
+
+  @Test
+  public void groupIsIndexed() throws Exception {
+    servlet.doPost(req, rsp);
+    verify(indexer, times(1)).index(uuid);
+    verify(rsp).setStatus(SC_NO_CONTENT);
+  }
+
+  @Test
+  public void cannotDeleteGroup() throws Exception {
+    servlet.doDelete(req, rsp);
+    verify(rsp).sendError(SC_METHOD_NOT_ALLOWED, "cannot delete group from index");
+  }
+
+  @Test
+  public void indexerThrowsIOExceptionTryingToIndexGroup() throws Exception {
+    doThrow(new IOException("io-error")).when(indexer).index(uuid);
+    servlet.doPost(req, rsp);
+    verify(rsp).sendError(SC_CONFLICT, "io-error");
+  }
+
+  @Test
+  public void sendErrorThrowsIOException() throws Exception {
+    doThrow(new IOException("io-error")).when(indexer).index(uuid);
+    doThrow(new IOException("someError")).when(rsp).sendError(SC_CONFLICT, "io-error");
+    servlet.doPost(req, rsp);
+    verify(rsp).sendError(SC_CONFLICT, "io-error");
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderTest.java
index 3a736cb..e19f6f2 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
@@ -50,6 +50,9 @@
   private static final int ACCOUNT_NUMBER = 2;
   private static final String INDEX_ACCOUNT_ENDPOINT =
       Joiner.on("/").join("/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);
 
   //Event
   private static final String EVENT_ENDPOINT =
@@ -95,6 +98,25 @@
   }
 
   @Test
+  public void testIndexGroupOK() throws Exception {
+    when(httpSessionMock.post(INDEX_GROUP_ENDPOINT))
+        .thenReturn(new HttpResult(SUCCESSFUL, EMPTY_MSG));
+    assertThat(forwarder.indexGroup(UUID)).isTrue();
+  }
+
+  @Test
+  public void testIndexGroupFailed() throws Exception {
+    when(httpSessionMock.post(INDEX_GROUP_ENDPOINT)).thenReturn(new HttpResult(FAILED, EMPTY_MSG));
+    assertThat(forwarder.indexGroup(UUID)).isFalse();
+  }
+
+  @Test
+  public void testIndexGroupThrowsException() throws Exception {
+    doThrow(new IOException()).when(httpSessionMock).post(INDEX_GROUP_ENDPOINT);
+    assertThat(forwarder.indexGroup(UUID)).isFalse();
+  }
+
+  @Test
   public void testIndexChangeOK() throws Exception {
     when(httpSessionMock.post(INDEX_CHANGE_ENDPOINT))
         .thenReturn(new HttpResult(SUCCESSFUL, EMPTY_MSG));
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 e50d713..a8a568e 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
@@ -24,8 +24,10 @@
 import com.ericsson.gerrit.plugins.highavailability.forwarder.Forwarder;
 import com.ericsson.gerrit.plugins.highavailability.index.IndexEventHandler.IndexAccountTask;
 import com.ericsson.gerrit.plugins.highavailability.index.IndexEventHandler.IndexChangeTask;
+import com.ericsson.gerrit.plugins.highavailability.index.IndexEventHandler.IndexGroupTask;
 import com.google.common.util.concurrent.MoreExecutors;
 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.git.WorkQueue.Executor;
 import com.google.gwtorm.client.KeyUtil;
@@ -42,11 +44,14 @@
   private static final String PLUGIN_NAME = "high-availability";
   private static final int CHANGE_ID = 1;
   private static final int ACCOUNT_ID = 2;
+  private static final String UUID = "3";
+  private static final String OTHER_UUID = "4";
 
   private IndexEventHandler indexEventHandler;
   @Mock private Forwarder forwarder;
   private Change.Id changeId;
   private Account.Id accountId;
+  private AccountGroup.UUID accountGroupUUID;
 
   @BeforeClass
   public static void setUp() {
@@ -57,6 +62,7 @@
   public void setUpMocks() {
     changeId = Change.Id.parse(Integer.toString(CHANGE_ID));
     accountId = Account.Id.parse(Integer.toString(ACCOUNT_ID));
+    accountGroupUUID = AccountGroup.UUID.parse(UUID);
     indexEventHandler =
         new IndexEventHandler(MoreExecutors.directExecutor(), PLUGIN_NAME, forwarder);
   }
@@ -80,6 +86,12 @@
   }
 
   @Test
+  public void shouldIndexInRemoteOnGroupIndexedEvent() throws Exception {
+    indexEventHandler.onGroupIndexed(accountGroupUUID.get());
+    verify(forwarder).indexGroup(UUID);
+  }
+
+  @Test
   public void shouldNotCallRemoteWhenChangeEventIsForwarded() throws Exception {
     Context.setForwardedEvent(true);
     indexEventHandler.onChangeIndexed(changeId.get());
@@ -98,6 +110,15 @@
   }
 
   @Test
+  public void shouldNotCallRemoteWhenGroupEventIsForwarded() throws Exception {
+    Context.setForwardedEvent(true);
+    indexEventHandler.onGroupIndexed(accountGroupUUID.get());
+    indexEventHandler.onGroupIndexed(accountGroupUUID.get());
+    Context.unsetForwardedEvent();
+    verifyZeroInteractions(forwarder);
+  }
+
+  @Test
   public void duplicateChangeEventOfAQueuedEventShouldGetDiscarded() {
     Executor poolMock = mock(Executor.class);
     indexEventHandler = new IndexEventHandler(poolMock, PLUGIN_NAME, forwarder);
@@ -116,6 +137,15 @@
   }
 
   @Test
+  public void duplicateGroupEventOfAQueuedEventShouldGetDiscarded() {
+    Executor poolMock = mock(Executor.class);
+    indexEventHandler = new IndexEventHandler(poolMock, PLUGIN_NAME, forwarder);
+    indexEventHandler.onGroupIndexed(accountGroupUUID.get());
+    indexEventHandler.onGroupIndexed(accountGroupUUID.get());
+    verify(poolMock, times(1)).execute(indexEventHandler.new IndexGroupTask(UUID));
+  }
+
+  @Test
   public void testIndexChangeTaskToString() throws Exception {
     IndexChangeTask task = indexEventHandler.new IndexChangeTask(CHANGE_ID, false);
     assertThat(task.toString())
@@ -132,18 +162,26 @@
   }
 
   @Test
+  public void testIndexGroupTaskToString() throws Exception {
+    IndexGroupTask task = indexEventHandler.new IndexGroupTask(UUID);
+    assertThat(task.toString())
+        .isEqualTo(String.format("[%s] Index group %s in target instance", PLUGIN_NAME, UUID));
+  }
+
+  @Test
   public void testIndexChangeTaskHashCodeAndEquals() {
     IndexChangeTask task = indexEventHandler.new IndexChangeTask(CHANGE_ID, false);
 
-    assertThat(task.equals(task)).isTrue();
-    assertThat(task.hashCode()).isEqualTo(task.hashCode());
+    IndexChangeTask sameTask = task;
+    assertThat(task.equals(sameTask)).isTrue();
+    assertThat(task.hashCode()).isEqualTo(sameTask.hashCode());
 
     IndexChangeTask identicalTask = indexEventHandler.new IndexChangeTask(CHANGE_ID, false);
     assertThat(task.equals(identicalTask)).isTrue();
     assertThat(task.hashCode()).isEqualTo(identicalTask.hashCode());
 
     assertThat(task.equals(null)).isFalse();
-    assertThat(task.equals("test")).isFalse();
+    assertThat(task.equals(indexEventHandler.new IndexChangeTask(CHANGE_ID + 1, false))).isFalse();
     assertThat(task.hashCode()).isNotEqualTo("test".hashCode());
 
     IndexChangeTask differentChangeIdTask = indexEventHandler.new IndexChangeTask(123, false);
@@ -159,19 +197,41 @@
   public void testIndexAccountTaskHashCodeAndEquals() {
     IndexAccountTask task = indexEventHandler.new IndexAccountTask(ACCOUNT_ID);
 
-    assertThat(task.equals(task)).isTrue();
-    assertThat(task.hashCode()).isEqualTo(task.hashCode());
+    IndexAccountTask sameTask = task;
+    assertThat(task.equals(sameTask)).isTrue();
+    assertThat(task.hashCode()).isEqualTo(sameTask.hashCode());
 
     IndexAccountTask identicalTask = indexEventHandler.new IndexAccountTask(ACCOUNT_ID);
     assertThat(task.equals(identicalTask)).isTrue();
     assertThat(task.hashCode()).isEqualTo(identicalTask.hashCode());
 
     assertThat(task.equals(null)).isFalse();
-    assertThat(task.equals("test")).isFalse();
+    assertThat(task.equals(indexEventHandler.new IndexAccountTask(ACCOUNT_ID + 1))).isFalse();
     assertThat(task.hashCode()).isNotEqualTo("test".hashCode());
 
     IndexAccountTask differentAccountIdTask = indexEventHandler.new IndexAccountTask(123);
     assertThat(task.equals(differentAccountIdTask)).isFalse();
     assertThat(task.hashCode()).isNotEqualTo(differentAccountIdTask.hashCode());
   }
+
+  @Test
+  public void testIndexGroupTaskHashCodeAndEquals() {
+    IndexGroupTask task = indexEventHandler.new IndexGroupTask(UUID);
+
+    IndexGroupTask sameTask = task;
+    assertThat(task.equals(sameTask)).isTrue();
+    assertThat(task.hashCode()).isEqualTo(sameTask.hashCode());
+
+    IndexGroupTask identicalTask = indexEventHandler.new IndexGroupTask(UUID);
+    assertThat(task.equals(identicalTask)).isTrue();
+    assertThat(task.hashCode()).isEqualTo(identicalTask.hashCode());
+
+    assertThat(task.equals(null)).isFalse();
+    assertThat(task.equals(indexEventHandler.new IndexGroupTask(OTHER_UUID))).isFalse();
+    assertThat(task.hashCode()).isNotEqualTo("test".hashCode());
+
+    IndexGroupTask differentGroupIdTask = indexEventHandler.new IndexGroupTask("123");
+    assertThat(task.equals(differentGroupIdTask)).isFalse();
+    assertThat(task.hashCode()).isNotEqualTo(differentGroupIdTask.hashCode());
+  }
 }
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/InetAddressFinderTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/InetAddressFinderTest.java
index 7b1dea4..3fb5a90 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/InetAddressFinderTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/InetAddressFinderTest.java
@@ -29,12 +29,12 @@
 public class InetAddressFinderTest {
 
   @Mock private Configuration configuration;
-  @Mock private Configuration.PeerInfoJGroups jgroupsConfig;
+  @Mock private Configuration.JGroups jgroupsConfig;
   private InetAddressFinder finder;
 
   @Before
   public void setUp() {
-    when(configuration.peerInfoJGroups()).thenReturn(jgroupsConfig);
+    when(configuration.jgroups()).thenReturn(jgroupsConfig);
     finder = new InetAddressFinder(configuration);
   }
 
diff --git a/tools/BUILD b/tools/BUILD
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tools/BUILD
diff --git a/tools/bazel.rc b/tools/bazel.rc
new file mode 100644
index 0000000..4ed16cf
--- /dev/null
+++ b/tools/bazel.rc
@@ -0,0 +1,2 @@
+build --workspace_status_command=./tools/workspace-status.sh
+test --build_tests_only
diff --git a/tools/bzl/BUILD b/tools/bzl/BUILD
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tools/bzl/BUILD
diff --git a/tools/bzl/classpath.bzl b/tools/bzl/classpath.bzl
new file mode 100644
index 0000000..dfcbe9c
--- /dev/null
+++ b/tools/bzl/classpath.bzl
@@ -0,0 +1,2 @@
+load("@com_googlesource_gerrit_bazlets//tools:classpath.bzl",
+     "classpath_collector")
diff --git a/tools/bzl/junit.bzl b/tools/bzl/junit.bzl
new file mode 100644
index 0000000..3af7e58
--- /dev/null
+++ b/tools/bzl/junit.bzl
@@ -0,0 +1,4 @@
+load(
+    "@com_googlesource_gerrit_bazlets//tools:junit.bzl",
+    "junit_tests",
+)
diff --git a/tools/bzl/maven_jar.bzl b/tools/bzl/maven_jar.bzl
new file mode 100644
index 0000000..2eabedb
--- /dev/null
+++ b/tools/bzl/maven_jar.bzl
@@ -0,0 +1 @@
+load("@com_googlesource_gerrit_bazlets//tools:maven_jar.bzl", "maven_jar")
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
new file mode 100644
index 0000000..a2e438f
--- /dev/null
+++ b/tools/bzl/plugin.bzl
@@ -0,0 +1,6 @@
+load(
+    "@com_googlesource_gerrit_bazlets//:gerrit_plugin.bzl",
+    "gerrit_plugin",
+    "PLUGIN_DEPS",
+    "PLUGIN_TEST_DEPS",
+)
diff --git a/tools/eclipse/BUILD b/tools/eclipse/BUILD
new file mode 100644
index 0000000..8a8bd40
--- /dev/null
+++ b/tools/eclipse/BUILD
@@ -0,0 +1,9 @@
+load("//tools/bzl:classpath.bzl", "classpath_collector")
+
+classpath_collector(
+    name = "main_classpath_collect",
+    testonly = 1,
+    deps = [
+        "//:high-availability__plugin_test_deps",
+    ],
+)
diff --git a/tools/eclipse/project.sh b/tools/eclipse/project.sh
new file mode 100755
index 0000000..66d75fb
--- /dev/null
+++ b/tools/eclipse/project.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+# Copyright (C) 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+`bazel query @com_googlesource_gerrit_bazlets//tools/eclipse:project --output location | sed s/BUILD:.*//`project.py -n high-availability -r .
diff --git a/tools/workspace-status.sh b/tools/workspace-status.sh
new file mode 100755
index 0000000..c83d416
--- /dev/null
+++ b/tools/workspace-status.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+# This script will be run by bazel when the build process starts to
+# generate key-value information that represents the status of the
+# workspace. The output should be like
+#
+# KEY1 VALUE1
+# KEY2 VALUE2
+#
+# If the script exits with non-zero code, it's considered as a failure
+# and the output will be discarded.
+
+function rev() {
+  cd $1; git describe --always --match "v[0-9].*" --dirty
+}
+
+echo STABLE_BUILD_HIGH-AVAILABILITY_LABEL $(rev .)