Merge stable-2.13 into stable-2.14

* stable-2.13:
  Fix race condition when removing changeId/accountId locks

Change-Id: I1263f313a5e93c6ea962c1470a3f20e1babae6d8
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..339d9b0
--- /dev/null
+++ b/WORKSPACE
@@ -0,0 +1,30 @@
+workspace(name = "high_availability")
+
+load("//:bazlets.bzl", "load_bazlets")
+
+load_bazlets(
+    commit = "b7514d03a7798905ff1513295b46620e57b8f386",
+    #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 7abad3d..8cb57f3 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,7 @@
   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";
 
   // http section
   static final String HTTP_SECTION = "http";
@@ -60,6 +65,7 @@
 
   // cache section
   static final String CACHE_SECTION = "cache";
+  static final String PATTERN_KEY = "pattern";
 
   // event section
   static final String EVENT_SECTION = "event";
@@ -193,6 +199,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 +242,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() {
@@ -245,6 +254,7 @@
   public static class PeerInfoJGroups {
     private final ImmutableList<String> skipInterface;
     private final String clusterName;
+    private final String myUrl;
 
     private PeerInfoJGroups(Config cfg) {
       String[] skip = cfg.getStringList(PEER_INFO_SECTION, JGROUPS_SUBSECTION, SKIP_INTERFACE_KEY);
@@ -252,6 +262,7 @@
       clusterName =
           getString(
               cfg, PEER_INFO_SECTION, JGROUPS_SUBSECTION, CLUSTER_NAME_KEY, DEFAULT_CLUSTER_NAME);
+      myUrl = trimTrailingSlash(cfg.getString(PEER_INFO_SECTION, JGROUPS_SUBSECTION, MY_URL_KEY));
     }
 
     public ImmutableList<String> skipInterface() {
@@ -261,6 +272,10 @@
     public String clusterName() {
       return clusterName;
     }
+
+    public String myUrl() {
+      return myUrl;
+    }
   }
 
   public static class Http {
@@ -320,15 +335,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/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..c38e849 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,10 +123,12 @@
     }
 
     boolean execute() {
+      log.debug(name);
       for (; ; ) {
         try {
           execCnt++;
           tryOnce();
+          log.debug("{} OK", name);
           return true;
         } catch (ForwardingException e) {
           if (!e.isRecoverable()) {
@@ -128,7 +140,7 @@
             return false;
           }
 
-          logRetry(e);
+          log.debug("Retrying to {} caused by '{}'", name, e);
           try {
             Thread.sleep(cfg.http().retryInterval());
           } catch (InterruptedException ie) {
@@ -156,11 +168,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..04c67d4 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,6 +26,7 @@
 import java.util.Collections;
 import java.util.Enumeration;
 import java.util.List;
+import java.util.Optional;
 
 @Singleton
 public class InetAddressFinder {
@@ -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 bd96784..39ba578 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;
@@ -49,25 +45,20 @@
     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 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();
+      Configuration pluginConfiguration, InetAddressFinder finder, MyUrlProvider myUrlProvider) {
     this.jgroupsConfig = pluginConfiguration.peerInfoJGroups();
     this.finder = finder;
+    this.myUrl = myUrlProvider.get();
   }
 
   @Override
@@ -98,7 +89,7 @@
       if (peerAddress != null && !view.getMembers().contains(peerAddress)) {
         log.info("viewAccepted(): removed peerInfo");
         peerAddress = null;
-        peerInfo = Optional.absent();
+        peerInfo = Optional.empty();
       }
     }
     if (view.size() > 1) {
@@ -142,7 +133,7 @@
   public void stop() {
     log.info("closing jgroups channel {}", 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..1c7caaf 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -22,6 +22,7 @@
 [peerInfo]
 :  strategy = jgroups
 [peerInfo "jgroups"]
+:  myUrl = local_instance_url
 :  cluster = foo
 :  skipInterface = lo*
 :  skipInterface = eth2
@@ -66,6 +67,14 @@
     Defaults to the list of: `lo*`, `utun*`, `awdl*` which are known to be
     inappropriate for JGroups communication.
 
+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`.
+
 NOTE: To work properly in certain environments, JGroups needs the System property
 `java.net.preferIPv4Stack` to be set to `true`.
 See (http://jgroups.org/tutorial/index.html#_trouble_shooting).
@@ -107,6 +116,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..b1d9fcc 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/ConfigurationTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/ConfigurationTest.java
@@ -33,7 +33,9 @@
 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 +49,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 +67,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 +78,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 +89,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 +107,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);
   }
 
@@ -166,6 +182,121 @@
   }
 
   @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
   public void testGetUser() throws Exception {
     initializeConfiguration();
     assertThat(configuration.http().user()).isEqualTo(EMPTY);
@@ -384,4 +515,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/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 .)