Merge branch 'stable-2.14'

* stable-2.14:
  CacheEvictionIT: Re-enable and adapt to use @GlobalPluginConfig
  Migrate plugin config to own configuration file
  Disallow DELETE when index does not support delete
  Factor common code out of Index*RestApiServlet classes
  Forward group indexing event
  Index{Account,Change}RestApiServlet: Use UTF_8 constant
  Revert "Ignore flaky CacheEvictionIT"
  Ignore flaky CacheEvictionIT
  IndexAccountRestApiServlet: Remove unnecessary semicolon

Change-Id: If56525eae31484d17cf59b2eb0ff324b7e8524be
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 cd56982..680a8ce 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java
@@ -21,11 +21,11 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.inject.Inject;
 import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
+import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -33,62 +33,88 @@
 public class Configuration {
   private static final Logger log = LoggerFactory.getLogger(Configuration.class);
 
+  // main section
+  static final String MAIN_SECTION = "main";
   static final String SHARED_DIRECTORY_KEY = "sharedDirectory";
+
+  // peerInfo section
+  static final String PEER_INFO_SECTION = "peerInfo";
   static final String URL_KEY = "url";
+
+  // http section
+  static final String HTTP_SECTION = "http";
   static final String USER_KEY = "user";
   static final String PASSWORD_KEY = "password";
   static final String CONNECTION_TIMEOUT_KEY = "connectionTimeout";
   static final String SOCKET_TIMEOUT_KEY = "socketTimeout";
   static final String MAX_TRIES_KEY = "maxTries";
   static final String RETRY_INTERVAL_KEY = "retryInterval";
-  static final String INDEX_THREAD_POOL_SIZE_KEY = "indexThreadPoolSize";
-  static final String CACHE_THREAD_POOL_SIZE_KEY = "cacheThreadPoolSize";
+
+  // cache section
+  static final String CACHE_SECTION = "cache";
+
+  // index section
+  static final String INDEX_SECTION = "index";
+
+  // common parameters to cache and index section
+  static final String THREAD_POOL_SIZE_KEY = "threadPoolSize";
+
+  // websession section
+  static final String WEBSESSION_SECTION = "websession";
   static final String CLEANUP_INTERVAL_KEY = "cleanupInterval";
 
   static final int DEFAULT_TIMEOUT_MS = 5000;
   static final int DEFAULT_MAX_TRIES = 5;
   static final int DEFAULT_RETRY_INTERVAL = 1000;
   static final int DEFAULT_THREAD_POOL_SIZE = 1;
+  static final String DEFAULT_CLEANUP_INTERVAL = "24 hours";
   static final long DEFAULT_CLEANUP_INTERVAL_MS = HOURS.toMillis(24);
 
-  private final String url;
-  private final String user;
-  private final String password;
-  private final int connectionTimeout;
-  private final int socketTimeout;
-  private final int maxTries;
-  private final int retryInterval;
-  private final int indexThreadPoolSize;
-  private final int cacheThreadPoolSize;
-  private final String sharedDirectory;
-  private final long cleanupInterval;
+  private final Main main;
+  private final PeerInfo peerInfo;
+  private final Http http;
+  private final Cache cache;
+  private final Index index;
+  private final Websession websession;
 
   @Inject
-  Configuration(PluginConfigFactory config, @PluginName String pluginName) {
-    PluginConfig cfg = config.getFromGerritConfig(pluginName, true);
-    url = Strings.nullToEmpty(cfg.getString(URL_KEY));
-    user = Strings.nullToEmpty(cfg.getString(USER_KEY));
-    password = Strings.nullToEmpty(cfg.getString(PASSWORD_KEY));
-    connectionTimeout = getInt(cfg, CONNECTION_TIMEOUT_KEY, DEFAULT_TIMEOUT_MS);
-    socketTimeout = getInt(cfg, SOCKET_TIMEOUT_KEY, DEFAULT_TIMEOUT_MS);
-    maxTries = getInt(cfg, MAX_TRIES_KEY, DEFAULT_MAX_TRIES);
-    retryInterval = getInt(cfg, RETRY_INTERVAL_KEY, DEFAULT_RETRY_INTERVAL);
-    indexThreadPoolSize = getInt(cfg, INDEX_THREAD_POOL_SIZE_KEY, DEFAULT_THREAD_POOL_SIZE);
-    cacheThreadPoolSize = getInt(cfg, CACHE_THREAD_POOL_SIZE_KEY, DEFAULT_THREAD_POOL_SIZE);
-    sharedDirectory = Strings.emptyToNull(cfg.getString(SHARED_DIRECTORY_KEY));
-    if (sharedDirectory == null) {
-      throw new ProvisionException(SHARED_DIRECTORY_KEY + " must be configured");
-    }
-    cleanupInterval =
-        ConfigUtil.getTimeUnit(
-            Strings.nullToEmpty(cfg.getString(CLEANUP_INTERVAL_KEY)),
-            DEFAULT_CLEANUP_INTERVAL_MS,
-            MILLISECONDS);
+  Configuration(PluginConfigFactory pluginConfigFactory, @PluginName String pluginName) {
+    Config cfg = pluginConfigFactory.getGlobalPluginConfig(pluginName);
+    main = new Main(cfg);
+    peerInfo = new PeerInfo(cfg);
+    http = new Http(cfg);
+    cache = new Cache(cfg);
+    index = new Index(cfg);
+    websession = new Websession(cfg);
   }
 
-  private int getInt(PluginConfig cfg, String name, int defaultValue) {
+  public Main main() {
+    return main;
+  }
+
+  public PeerInfo peerInfo() {
+    return peerInfo;
+  }
+
+  public Http http() {
+    return http;
+  }
+
+  public Cache cache() {
+    return cache;
+  }
+
+  public Index index() {
+    return index;
+  }
+
+  public Websession websession() {
+    return websession;
+  }
+
+  private static int getInt(Config cfg, String section, String name, int defaultValue) {
     try {
-      return cfg.getInt(name, defaultValue);
+      return cfg.getInt(section, name, defaultValue);
     } catch (IllegalArgumentException e) {
       log.error(String.format("invalid value for %s; using default value %d", name, defaultValue));
       log.debug("Failed retrieve integer value: " + e.getMessage(), e);
@@ -96,47 +122,116 @@
     }
   }
 
-  public int getConnectionTimeout() {
-    return connectionTimeout;
+  public static class Main {
+    private final String sharedDirectory;
+
+    private Main(Config cfg) {
+      sharedDirectory =
+          Strings.emptyToNull(cfg.getString(MAIN_SECTION, null, SHARED_DIRECTORY_KEY));
+      if (sharedDirectory == null) {
+        throw new ProvisionException(SHARED_DIRECTORY_KEY + " must be configured");
+      }
+    }
+
+    public String sharedDirectory() {
+      return sharedDirectory;
+    }
   }
 
-  public int getMaxTries() {
-    return maxTries;
+  public static class PeerInfo {
+    private final String url;
+
+    private PeerInfo(Config cfg) {
+      url =
+          CharMatcher.is('/')
+              .trimTrailingFrom(
+                  Strings.nullToEmpty(cfg.getString(PEER_INFO_SECTION, null, URL_KEY)));
+    }
+
+    public String url() {
+      return url;
+    }
   }
 
-  public int getRetryInterval() {
-    return retryInterval;
+  public static class Http {
+    private final String user;
+    private final String password;
+    private final int connectionTimeout;
+    private final int socketTimeout;
+    private final int maxTries;
+    private final int retryInterval;
+
+    private Http(Config cfg) {
+      user = Strings.nullToEmpty(cfg.getString(HTTP_SECTION, null, USER_KEY));
+      password = Strings.nullToEmpty(cfg.getString(HTTP_SECTION, null, PASSWORD_KEY));
+      connectionTimeout = getInt(cfg, HTTP_SECTION, CONNECTION_TIMEOUT_KEY, DEFAULT_TIMEOUT_MS);
+      socketTimeout = getInt(cfg, HTTP_SECTION, SOCKET_TIMEOUT_KEY, DEFAULT_TIMEOUT_MS);
+      maxTries = getInt(cfg, HTTP_SECTION, MAX_TRIES_KEY, DEFAULT_MAX_TRIES);
+      retryInterval = getInt(cfg, HTTP_SECTION, RETRY_INTERVAL_KEY, DEFAULT_RETRY_INTERVAL);
+    }
+
+    public String user() {
+      return user;
+    }
+
+    public String password() {
+      return password;
+    }
+
+    public int connectionTimeout() {
+      return connectionTimeout;
+    }
+
+    public int socketTimeout() {
+      return socketTimeout;
+    }
+
+    public int maxTries() {
+      return maxTries;
+    }
+
+    public int retryInterval() {
+      return retryInterval;
+    }
   }
 
-  public int getSocketTimeout() {
-    return socketTimeout;
+  public static class Cache {
+    private final int threadPoolSize;
+
+    private Cache(Config cfg) {
+      threadPoolSize = getInt(cfg, CACHE_SECTION, THREAD_POOL_SIZE_KEY, DEFAULT_THREAD_POOL_SIZE);
+    }
+
+    public int threadPoolSize() {
+      return threadPoolSize;
+    }
   }
 
-  public String getUrl() {
-    return CharMatcher.is('/').trimTrailingFrom(url);
+  public static class Index {
+    private final int threadPoolSize;
+
+    private Index(Config cfg) {
+      threadPoolSize = getInt(cfg, INDEX_SECTION, THREAD_POOL_SIZE_KEY, DEFAULT_THREAD_POOL_SIZE);
+    }
+
+    public int threadPoolSize() {
+      return threadPoolSize;
+    }
   }
 
-  public String getUser() {
-    return user;
-  }
+  public static class Websession {
+    private final long cleanupInterval;
 
-  public String getPassword() {
-    return password;
-  }
+    private Websession(Config cfg) {
+      this.cleanupInterval =
+          ConfigUtil.getTimeUnit(
+              Strings.nullToEmpty(cfg.getString(WEBSESSION_SECTION, null, CLEANUP_INTERVAL_KEY)),
+              DEFAULT_CLEANUP_INTERVAL_MS,
+              MILLISECONDS);
+    }
 
-  public int getIndexThreadPoolSize() {
-    return indexThreadPoolSize;
-  }
-
-  public int getCacheThreadPoolSize() {
-    return cacheThreadPoolSize;
-  }
-
-  public String getSharedDirectory() {
-    return sharedDirectory;
-  }
-
-  public Long getCleanupInterval() {
-    return cleanupInterval;
+    public long cleanupInterval() {
+      return cleanupInterval;
+    }
   }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Module.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Module.java
index b1276ba..ecef7cd 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Module.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Module.java
@@ -44,7 +44,7 @@
   @Singleton
   @SharedDirectory
   Path getSharedDirectory(Configuration cfg) throws IOException {
-    Path sharedDirectoryPath = Paths.get(cfg.getSharedDirectory());
+    Path sharedDirectoryPath = Paths.get(cfg.main().sharedDirectory());
     Files.createDirectories(sharedDirectoryPath);
     return sharedDirectoryPath;
   }
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 82f52d7..ab9d0a4 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Setup.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Setup.java
@@ -16,27 +16,27 @@
 
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.*;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.common.FileUtil;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitStep;
-import com.google.gerrit.pgm.init.api.Section;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
 import java.nio.file.Path;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
 
 public class Setup implements InitStep {
 
   private final ConsoleUI ui;
-  private final Section mySection;
   private final String pluginName;
   private final SitePaths site;
+  private FileBasedConfig config;
 
   @Inject
-  public Setup(
-      ConsoleUI ui, Section.Factory sections, @PluginName String pluginName, SitePaths site) {
+  public Setup(ConsoleUI ui, @PluginName String pluginName, SitePaths site) {
     this.ui = ui;
-    this.mySection = sections.get("plugin", pluginName);
     this.pluginName = pluginName;
     this.site = site;
   }
@@ -48,44 +48,91 @@
 
     if (ui.yesno(true, "Configure %s", pluginName)) {
       ui.header("Configuring %s", pluginName);
-      configureSharedDir();
-      configurePeer();
-      configureTimeouts();
-      configureRetry();
-      configureThreadPools();
+      Path pluginConfigFile = site.etc_dir.resolve(pluginName + ".config");
+      config = new FileBasedConfig(pluginConfigFile.toFile(), FS.DETECTED);
+      config.load();
+      configureMainSection();
+      configurePeerInfoSection();
+      configureHttp();
+      configureCacheSection();
+      configureIndexSection();
+      configureWebsessiosSection();
+      config.save();
     }
   }
 
-  private void configureSharedDir() {
-    String sharedDir = mySection.string("Shared directory", SHARED_DIRECTORY_KEY, null);
+  private void configureMainSection() {
+    ui.header("Main section");
+    String sharedDir =
+        promptAndSetString("Shared directory", MAIN_SECTION, SHARED_DIRECTORY_KEY, null);
     if (sharedDir != null) {
       Path shared = site.site_path.resolve(sharedDir);
       FileUtil.mkdirsOrDie(shared, "cannot create " + shared);
     }
   }
 
-  private void configurePeer() {
-    mySection.string("Peer URL", URL_KEY, null);
-    mySection.string("User", USER_KEY, null);
-    mySection.string("Password", PASSWORD_KEY, null);
+  private void configurePeerInfoSection() {
+    ui.header("PeerInfo section");
+    promptAndSetString("Peer URL", PEER_INFO_SECTION, URL_KEY, null);
   }
 
-  private void configureTimeouts() {
-    mySection.string("Connection timeout [ms]", CONNECTION_TIMEOUT_KEY, str(DEFAULT_TIMEOUT_MS));
-    mySection.string("Socket timeout [ms]", SOCKET_TIMEOUT_KEY, str(DEFAULT_TIMEOUT_MS));
+  private void configureHttp() {
+    ui.header("Http section");
+    promptAndSetString("User", HTTP_SECTION, USER_KEY, null);
+    promptAndSetString("Password", HTTP_SECTION, PASSWORD_KEY, null);
+    promptAndSetString(
+        "Max number of tries to forward to remote peer",
+        HTTP_SECTION,
+        MAX_TRIES_KEY,
+        str(DEFAULT_MAX_TRIES));
+    promptAndSetString(
+        "Retry interval [ms]", HTTP_SECTION, RETRY_INTERVAL_KEY, str(DEFAULT_RETRY_INTERVAL));
+    promptAndSetString(
+        "Connection timeout [ms]", HTTP_SECTION, CONNECTION_TIMEOUT_KEY, str(DEFAULT_TIMEOUT_MS));
+    promptAndSetString(
+        "Socket timeout [ms]", HTTP_SECTION, SOCKET_TIMEOUT_KEY, str(DEFAULT_TIMEOUT_MS));
   }
 
-  private void configureRetry() {
-    mySection.string(
-        "Max number of tries to forward to remote peer", MAX_TRIES_KEY, str(DEFAULT_MAX_TRIES));
-    mySection.string("Retry interval [ms]", RETRY_INTERVAL_KEY, str(DEFAULT_RETRY_INTERVAL));
+  private void configureCacheSection() {
+    ui.header("Cache section");
+    promptAndSetString(
+        "Cache thread pool size",
+        CACHE_SECTION,
+        THREAD_POOL_SIZE_KEY,
+        str(DEFAULT_THREAD_POOL_SIZE));
   }
 
-  private void configureThreadPools() {
-    mySection.string(
-        "Index thread pool size", INDEX_THREAD_POOL_SIZE_KEY, str(DEFAULT_THREAD_POOL_SIZE));
-    mySection.string(
-        "Cache thread pool size", CACHE_THREAD_POOL_SIZE_KEY, str(DEFAULT_THREAD_POOL_SIZE));
+  private void configureIndexSection() {
+    ui.header("Index section");
+    promptAndSetString(
+        "Index thread pool size",
+        INDEX_SECTION,
+        THREAD_POOL_SIZE_KEY,
+        str(DEFAULT_THREAD_POOL_SIZE));
+  }
+
+  private void configureWebsessiosSection() {
+    ui.header("Websession section");
+    promptAndSetString(
+        "Cleanup interval", WEBSESSION_SECTION, CLEANUP_INTERVAL_KEY, DEFAULT_CLEANUP_INTERVAL);
+  }
+
+  private String promptAndSetString(
+      String title, String section, String name, String defaultValue) {
+    String oldValue = Strings.emptyToNull(config.getString(section, null, name));
+    String newValue = ui.readString(oldValue != null ? oldValue : defaultValue, title);
+    if (!eq(oldValue, newValue)) {
+      if (newValue != null) {
+        config.setString(section, null, name, newValue);
+      } else {
+        config.unset(section, name, name);
+      }
+    }
+    return newValue;
+  }
+
+  private static boolean eq(String a, String b) {
+    return (a == null && b == null) || (a != null && a.equals(b));
   }
 
   private static String str(int n) {
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheExecutorProvider.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheExecutorProvider.java
index dd40155..aed590d 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheExecutorProvider.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheExecutorProvider.java
@@ -25,6 +25,6 @@
 
   @Inject
   CacheExecutorProvider(WorkQueue workQueue, Configuration config) {
-    super(workQueue, config.getCacheThreadPoolSize(), "Forward-cache-eviction-event");
+    super(workQueue, config.cache().threadPoolSize(), "Forward-cache-eviction-event");
   }
 }
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..fca807f
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/AbstractIndexRestApiServlet.java
@@ -0,0 +1,130 @@
+// 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));
+    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) {
+      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/HttpClientProvider.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpClientProvider.java
index e68875f..bd3077f 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpClientProvider.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpClientProvider.java
@@ -70,9 +70,9 @@
 
   private RequestConfig customRequestConfig() {
     return RequestConfig.custom()
-        .setConnectTimeout(cfg.getConnectionTimeout())
-        .setSocketTimeout(cfg.getSocketTimeout())
-        .setConnectionRequestTimeout(cfg.getConnectionTimeout())
+        .setConnectTimeout(cfg.http().connectionTimeout())
+        .setSocketTimeout(cfg.http().socketTimeout())
+        .setConnectionRequestTimeout(cfg.http().connectionTimeout())
         .build();
   }
 
@@ -109,7 +109,7 @@
   private BasicCredentialsProvider buildCredentials() {
     BasicCredentialsProvider creds = new BasicCredentialsProvider();
     creds.setCredentials(
-        AuthScope.ANY, new UsernamePasswordCredentials(cfg.getUser(), cfg.getPassword()));
+        AuthScope.ANY, new UsernamePasswordCredentials(cfg.http().user(), cfg.http().password()));
     return creds;
   }
 
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 b743db9..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,93 +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) {
-      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 75ca59b..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,33 +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) {
-      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 49326cc..18c8645 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);
   }
@@ -123,14 +133,14 @@
             log.error("Failed to {}", name, e);
             return false;
           }
-          if (execCnt >= cfg.getMaxTries()) {
-            log.error("Failed to {}, after {} tries", name, cfg.getMaxTries());
+          if (execCnt >= cfg.http().maxTries()) {
+            log.error("Failed to {}, after {} tries", name, cfg.http().maxTries());
             return false;
           }
 
           logRetry(e);
           try {
-            Thread.sleep(cfg.getRetryInterval());
+            Thread.sleep(cfg.http().retryInterval());
           } catch (InterruptedException ie) {
             log.error("{} was interrupted, giving up", name, ie);
             Thread.currentThread().interrupt();
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/index/IndexExecutorProvider.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/IndexExecutorProvider.java
index efe323e..7efcc3f 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/IndexExecutorProvider.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/IndexExecutorProvider.java
@@ -25,6 +25,6 @@
 
   @Inject
   IndexExecutorProvider(WorkQueue workQueue, Configuration config) {
-    super(workQueue, config.getIndexThreadPoolSize(), "Forward-index-event");
+    super(workQueue, config.index().threadPoolSize(), "Forward-index-event");
   }
 }
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 74e148b..a78b397 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
@@ -25,7 +25,7 @@
 
   @Inject
   PluginConfigPeerInfoProvider(Configuration cfg) {
-    peerInfo = Optional.of(new PeerInfo(cfg.getUrl()));
+    peerInfo = Optional.of(new PeerInfo(cfg.peerInfo().url()));
   }
 
   @Override
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/websession/file/FileBasedWebSessionCacheCleaner.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/websession/file/FileBasedWebSessionCacheCleaner.java
index f6b5542..68ffd17 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/websession/file/FileBasedWebSessionCacheCleaner.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/websession/file/FileBasedWebSessionCacheCleaner.java
@@ -41,7 +41,7 @@
       WorkQueue queue, Provider<CleanupTask> cleanupTaskProvider, Configuration config) {
     this.queue = queue;
     this.cleanupTaskProvider = cleanupTaskProvider;
-    this.cleanupIntervalMillis = config.getCleanupInterval();
+    this.cleanupIntervalMillis = config.websession().cleanupInterval();
   }
 
   @Override
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index f894822..3290f1b 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -2,57 +2,71 @@
 =========================
 
 The @PLUGIN@ plugin must be installed in both instances and the following fields
-should be specified in the corresponding Gerrit configuration file:
+should be specified in `$site_path/etc/@PLUGIN@.config` file:
 
-File 'gerrit.config'
+File '@PLUGIN@.config'
 --------------------
 
-[plugin "@PLUGIN@"]
+[main]
+:  sharedDirectory = /directory/accessible/from/both/instances
+[peerInfo]
 :  url = target_instance_url
+[http]
 :  user = username
 :  password = password
-:  sharedDirectory = /directory/accessible/from/both/instances
 
-plugin.@PLUGIN@.url
+main.sharedDirectory
+:   Path to a directory accessible from both master instances.
+
+peerInfo.url
 :   Specify the URL for the secondary (target) instance.
 
-plugin.@PLUGIN@.user
+http.user
 :   Username to connect to the secondary (target) instance.
 
-plugin.@PLUGIN@.password
-:   Password to connect to the secondary (target) instance. This value can
-     also be defined in secure.config.
-
-plugin.@PLUGIN@.sharedDirectory
-:   Path to a directory accessible from both master instances.
+http.password
+:   Password to connect to the secondary (target) instance.
 
 @PLUGIN@ plugin uses REST API calls to keep the target instance in-sync. It
 is possible to customize the parameters of the underlying http client doing these
 calls by specifying the following fields:
 
-@PLUGIN@.connectionTimeout
+http.connectionTimeout
 :   Maximum interval of time in milliseconds the plugin waits for a connection
     to the target instance. When not specified, the default value is set to 5000ms.
 
-@PLUGIN@.socketTimeout
+http.socketTimeout
 :   Maximum interval of time in milliseconds the plugin waits for a response from the
     target instance once the connection has been established. When not specified,
     the default value is set to 5000ms.
 
-@PLUGIN@.maxTries
+http.maxTries
 :   Maximum number of times the plugin should attempt when calling a REST API in
     the target instance. Setting this value to 0 will disable retries. When not
     specified, the default value is 5. After this number of failed tries, an
     error is logged.
 
-@PLUGIN@.retryInterval
+http.retryInterval
 :   The interval of time in milliseconds between the subsequent auto-retries.
     When not specified, the default value is set to 1000ms.
 
-@PLUGIN@.indexThreadPoolSize
+cache.threadPoolSize
+:   Maximum number of threads used to send cache evictions to the target instance.
+    Defaults to 1.
+
+index.threadPoolSize
 :   Maximum number of threads used to send index events to the target instance.
     Defaults to 1.
 
-@PLUGIN@.cacheThreadPoolSize
-:   Maximum number of threads used to send cache evictions to the target instance.
-    Defaults to 1.
\ No newline at end of file
+websession.cleanupInterval
+:   Frequency for deleting expired web sessions. Values should use common time
+    unit suffixes to express their setting:
+* s, sec, second, seconds
+* m, min, minute, minutes
+* h, hr, hour, hours
+* d, day, days
+* w, week, weeks (`1 week` is treated as `7 days`)
+* mon, month, months (`1 month` is treated as `30 days`)
+* y, year, years (`1 year` is treated as `365 days`)
+If a time unit suffix is not specified, `hours` is assumed.
+Defaults to 24 hours.
\ No newline at end of file
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 a828fb2..31449e3 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/ConfigurationTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/ConfigurationTest.java
@@ -14,7 +14,7 @@
 
 package com.ericsson.gerrit.plugins.highavailability;
 
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.CACHE_THREAD_POOL_SIZE_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.CACHE_SECTION;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.CLEANUP_INTERVAL_KEY;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.CONNECTION_TIMEOUT_KEY;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_CLEANUP_INTERVAL_MS;
@@ -22,21 +22,26 @@
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_RETRY_INTERVAL;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_THREAD_POOL_SIZE;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_TIMEOUT_MS;
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.INDEX_THREAD_POOL_SIZE_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.HTTP_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.INDEX_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.MAIN_SECTION;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.MAX_TRIES_KEY;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.PASSWORD_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;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.SOCKET_TIMEOUT_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.THREAD_POOL_SIZE_KEY;
 import static com.ericsson.gerrit.plugins.highavailability.Configuration.URL_KEY;
 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.util.concurrent.TimeUnit.SECONDS;
 import static org.mockito.Mockito.when;
 
-import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.inject.ProvisionException;
+import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -57,14 +62,15 @@
   private static final String ERROR_MESSAGE = "some error message";
 
   @Mock private PluginConfigFactory cfgFactoryMock;
-  @Mock private PluginConfig configMock;
+  @Mock private Config configMock;
   private Configuration configuration;
   private String pluginName = "high-availability";
 
   @Before
   public void setUp() {
-    when(cfgFactoryMock.getFromGerritConfig(pluginName, true)).thenReturn(configMock);
-    when(configMock.getString(SHARED_DIRECTORY_KEY)).thenReturn(SHARED_DIRECTORY);
+    when(cfgFactoryMock.getGlobalPluginConfig(pluginName)).thenReturn(configMock);
+    when(configMock.getString(MAIN_SECTION, null, SHARED_DIRECTORY_KEY))
+        .thenReturn(SHARED_DIRECTORY);
   }
 
   private void initializeConfiguration() {
@@ -74,152 +80,156 @@
   @Test
   public void testGetUrl() throws Exception {
     initializeConfiguration();
-    assertThat(configuration.getUrl()).isEqualTo(EMPTY);
+    assertThat(configuration.peerInfo().url()).isEqualTo(EMPTY);
 
-    when(configMock.getString(URL_KEY)).thenReturn(URL);
+    when(configMock.getString(PEER_INFO_SECTION, null, URL_KEY)).thenReturn(URL);
     initializeConfiguration();
-    assertThat(configuration.getUrl()).isEqualTo(URL);
+    assertThat(configuration.peerInfo().url()).isEqualTo(URL);
   }
 
   @Test
   public void testGetUrlIsDroppingTrailingSlash() throws Exception {
-    when(configMock.getString("url")).thenReturn(URL + "/");
+    when(configMock.getString(PEER_INFO_SECTION, null, URL_KEY)).thenReturn(URL + "/");
     initializeConfiguration();
     assertThat(configuration).isNotNull();
-    assertThat(configuration.getUrl()).isEqualTo(URL);
+    assertThat(configuration.peerInfo().url()).isEqualTo(URL);
   }
 
   @Test
   public void testGetUser() throws Exception {
     initializeConfiguration();
-    assertThat(configuration.getUser()).isEqualTo(EMPTY);
+    assertThat(configuration.http().user()).isEqualTo(EMPTY);
 
-    when(configMock.getString(USER_KEY)).thenReturn(USER);
+    when(configMock.getString(HTTP_SECTION, null, USER_KEY)).thenReturn(USER);
     initializeConfiguration();
-    assertThat(configuration.getUser()).isEqualTo(USER);
+    assertThat(configuration.http().user()).isEqualTo(USER);
   }
 
   @Test
   public void testGetPassword() throws Exception {
     initializeConfiguration();
-    assertThat(configuration.getPassword()).isEqualTo(EMPTY);
+    assertThat(configuration.http().password()).isEqualTo(EMPTY);
 
-    when(configMock.getString(PASSWORD_KEY)).thenReturn(PASS);
+    when(configMock.getString(HTTP_SECTION, null, PASSWORD_KEY)).thenReturn(PASS);
     initializeConfiguration();
-    assertThat(configuration.getPassword()).isEqualTo(PASS);
+    assertThat(configuration.http().password()).isEqualTo(PASS);
   }
 
   @Test
   public void testGetConnectionTimeout() throws Exception {
     initializeConfiguration();
-    assertThat(configuration.getConnectionTimeout()).isEqualTo(0);
+    assertThat(configuration.http().connectionTimeout()).isEqualTo(0);
 
-    when(configMock.getInt(CONNECTION_TIMEOUT_KEY, DEFAULT_TIMEOUT_MS)).thenReturn(TIMEOUT);
+    when(configMock.getInt(HTTP_SECTION, CONNECTION_TIMEOUT_KEY, DEFAULT_TIMEOUT_MS))
+        .thenReturn(TIMEOUT);
     initializeConfiguration();
-    assertThat(configuration.getConnectionTimeout()).isEqualTo(TIMEOUT);
+    assertThat(configuration.http().connectionTimeout()).isEqualTo(TIMEOUT);
 
-    when(configMock.getInt(CONNECTION_TIMEOUT_KEY, DEFAULT_TIMEOUT_MS))
+    when(configMock.getInt(HTTP_SECTION, CONNECTION_TIMEOUT_KEY, DEFAULT_TIMEOUT_MS))
         .thenThrow(new IllegalArgumentException(ERROR_MESSAGE));
     initializeConfiguration();
-    assertThat(configuration.getConnectionTimeout()).isEqualTo(DEFAULT_TIMEOUT_MS);
+    assertThat(configuration.http().connectionTimeout()).isEqualTo(DEFAULT_TIMEOUT_MS);
   }
 
   @Test
   public void testGetSocketTimeout() throws Exception {
     initializeConfiguration();
-    assertThat(configuration.getSocketTimeout()).isEqualTo(0);
+    assertThat(configuration.http().socketTimeout()).isEqualTo(0);
 
-    when(configMock.getInt(SOCKET_TIMEOUT_KEY, DEFAULT_TIMEOUT_MS)).thenReturn(TIMEOUT);
+    when(configMock.getInt(HTTP_SECTION, SOCKET_TIMEOUT_KEY, DEFAULT_TIMEOUT_MS))
+        .thenReturn(TIMEOUT);
     initializeConfiguration();
-    assertThat(configuration.getSocketTimeout()).isEqualTo(TIMEOUT);
+    assertThat(configuration.http().socketTimeout()).isEqualTo(TIMEOUT);
 
-    when(configMock.getInt(SOCKET_TIMEOUT_KEY, DEFAULT_TIMEOUT_MS))
+    when(configMock.getInt(HTTP_SECTION, SOCKET_TIMEOUT_KEY, DEFAULT_TIMEOUT_MS))
         .thenThrow(new IllegalArgumentException(ERROR_MESSAGE));
     initializeConfiguration();
-    assertThat(configuration.getSocketTimeout()).isEqualTo(DEFAULT_TIMEOUT_MS);
+    assertThat(configuration.http().socketTimeout()).isEqualTo(DEFAULT_TIMEOUT_MS);
   }
 
   @Test
   public void testGetMaxTries() throws Exception {
     initializeConfiguration();
-    assertThat(configuration.getMaxTries()).isEqualTo(0);
+    assertThat(configuration.http().maxTries()).isEqualTo(0);
 
-    when(configMock.getInt(MAX_TRIES_KEY, DEFAULT_MAX_TRIES)).thenReturn(MAX_TRIES);
+    when(configMock.getInt(HTTP_SECTION, MAX_TRIES_KEY, DEFAULT_MAX_TRIES)).thenReturn(MAX_TRIES);
     initializeConfiguration();
-    assertThat(configuration.getMaxTries()).isEqualTo(MAX_TRIES);
+    assertThat(configuration.http().maxTries()).isEqualTo(MAX_TRIES);
 
-    when(configMock.getInt(MAX_TRIES_KEY, DEFAULT_MAX_TRIES))
+    when(configMock.getInt(HTTP_SECTION, MAX_TRIES_KEY, DEFAULT_MAX_TRIES))
         .thenThrow(new IllegalArgumentException(ERROR_MESSAGE));
     initializeConfiguration();
-    assertThat(configuration.getMaxTries()).isEqualTo(DEFAULT_MAX_TRIES);
+    assertThat(configuration.http().maxTries()).isEqualTo(DEFAULT_MAX_TRIES);
   }
 
   @Test
   public void testGetRetryInterval() throws Exception {
     initializeConfiguration();
-    assertThat(configuration.getRetryInterval()).isEqualTo(0);
+    assertThat(configuration.http().retryInterval()).isEqualTo(0);
 
-    when(configMock.getInt(RETRY_INTERVAL_KEY, DEFAULT_RETRY_INTERVAL)).thenReturn(RETRY_INTERVAL);
+    when(configMock.getInt(HTTP_SECTION, RETRY_INTERVAL_KEY, DEFAULT_RETRY_INTERVAL))
+        .thenReturn(RETRY_INTERVAL);
     initializeConfiguration();
-    assertThat(configuration.getRetryInterval()).isEqualTo(RETRY_INTERVAL);
+    assertThat(configuration.http().retryInterval()).isEqualTo(RETRY_INTERVAL);
 
-    when(configMock.getInt(RETRY_INTERVAL_KEY, DEFAULT_RETRY_INTERVAL))
+    when(configMock.getInt(HTTP_SECTION, RETRY_INTERVAL_KEY, DEFAULT_RETRY_INTERVAL))
         .thenThrow(new IllegalArgumentException(ERROR_MESSAGE));
     initializeConfiguration();
-    assertThat(configuration.getRetryInterval()).isEqualTo(DEFAULT_RETRY_INTERVAL);
+    assertThat(configuration.http().retryInterval()).isEqualTo(DEFAULT_RETRY_INTERVAL);
   }
 
   @Test
   public void testGetIndexThreadPoolSize() throws Exception {
     initializeConfiguration();
-    assertThat(configuration.getIndexThreadPoolSize()).isEqualTo(0);
+    assertThat(configuration.index().threadPoolSize()).isEqualTo(0);
 
-    when(configMock.getInt(INDEX_THREAD_POOL_SIZE_KEY, DEFAULT_THREAD_POOL_SIZE))
+    when(configMock.getInt(INDEX_SECTION, THREAD_POOL_SIZE_KEY, DEFAULT_THREAD_POOL_SIZE))
         .thenReturn(THREAD_POOL_SIZE);
     initializeConfiguration();
-    assertThat(configuration.getIndexThreadPoolSize()).isEqualTo(THREAD_POOL_SIZE);
+    assertThat(configuration.index().threadPoolSize()).isEqualTo(THREAD_POOL_SIZE);
 
-    when(configMock.getInt(INDEX_THREAD_POOL_SIZE_KEY, DEFAULT_THREAD_POOL_SIZE))
+    when(configMock.getInt(INDEX_SECTION, THREAD_POOL_SIZE_KEY, DEFAULT_THREAD_POOL_SIZE))
         .thenThrow(new IllegalArgumentException(ERROR_MESSAGE));
     initializeConfiguration();
-    assertThat(configuration.getIndexThreadPoolSize()).isEqualTo(DEFAULT_THREAD_POOL_SIZE);
+    assertThat(configuration.index().threadPoolSize()).isEqualTo(DEFAULT_THREAD_POOL_SIZE);
   }
 
   @Test
   public void testGetCacheThreadPoolSize() throws Exception {
     initializeConfiguration();
-    assertThat(configuration.getCacheThreadPoolSize()).isEqualTo(0);
+    assertThat(configuration.cache().threadPoolSize()).isEqualTo(0);
 
-    when(configMock.getInt(CACHE_THREAD_POOL_SIZE_KEY, DEFAULT_THREAD_POOL_SIZE))
+    when(configMock.getInt(CACHE_SECTION, THREAD_POOL_SIZE_KEY, DEFAULT_THREAD_POOL_SIZE))
         .thenReturn(THREAD_POOL_SIZE);
     initializeConfiguration();
-    assertThat(configuration.getCacheThreadPoolSize()).isEqualTo(THREAD_POOL_SIZE);
+    assertThat(configuration.cache().threadPoolSize()).isEqualTo(THREAD_POOL_SIZE);
 
-    when(configMock.getInt(CACHE_THREAD_POOL_SIZE_KEY, DEFAULT_THREAD_POOL_SIZE))
+    when(configMock.getInt(CACHE_SECTION, THREAD_POOL_SIZE_KEY, DEFAULT_THREAD_POOL_SIZE))
         .thenThrow(new IllegalArgumentException(ERROR_MESSAGE));
     initializeConfiguration();
-    assertThat(configuration.getCacheThreadPoolSize()).isEqualTo(DEFAULT_THREAD_POOL_SIZE);
+    assertThat(configuration.cache().threadPoolSize()).isEqualTo(DEFAULT_THREAD_POOL_SIZE);
   }
 
   @Test
   public void testGetSharedDirectory() throws Exception {
     initializeConfiguration();
-    assertThat(configuration.getSharedDirectory()).isEqualTo(SHARED_DIRECTORY);
+    assertThat(configuration.main().sharedDirectory()).isEqualTo(SHARED_DIRECTORY);
   }
 
   @Test(expected = ProvisionException.class)
   public void shouldThrowExceptionIfSharedDirectoryNotConfigured() throws Exception {
-    when(configMock.getString(SHARED_DIRECTORY_KEY)).thenReturn(null);
+    when(configMock.getString(MAIN_SECTION, null, SHARED_DIRECTORY_KEY)).thenReturn(null);
     initializeConfiguration();
   }
 
   @Test
   public void testGetCleanupInterval() throws Exception {
     initializeConfiguration();
-    assertThat(configuration.getCleanupInterval()).isEqualTo(DEFAULT_CLEANUP_INTERVAL_MS);
+    assertThat(configuration.websession().cleanupInterval()).isEqualTo(DEFAULT_CLEANUP_INTERVAL_MS);
 
-    when(configMock.getString(CLEANUP_INTERVAL_KEY)).thenReturn("30 seconds");
+    when(configMock.getString(WEBSESSION_SECTION, null, CLEANUP_INTERVAL_KEY))
+        .thenReturn("30 seconds");
     initializeConfiguration();
-    assertThat(configuration.getCleanupInterval()).isEqualTo(SECONDS.toMillis(30));
+    assertThat(configuration.websession().cleanupInterval()).isEqualTo(SECONDS.toMillis(30));
   }
 }
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/ModuleTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/ModuleTest.java
index 8af8c51..c577a01 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/ModuleTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/ModuleTest.java
@@ -25,13 +25,15 @@
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
+import org.mockito.Answers;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 
 @RunWith(MockitoJUnitRunner.class)
 public class ModuleTest {
 
-  @Mock private Configuration config;
+  @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+  private Configuration configMock;
 
   @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
 
@@ -46,17 +48,17 @@
   public void shouldCreateSharedDirectoryIfItDoesNotExist() throws Exception {
     File configuredDirectory = tempFolder.newFolder();
     assertThat(configuredDirectory.delete()).isTrue();
-    when(config.getSharedDirectory()).thenReturn(configuredDirectory.getAbsolutePath());
+    when(configMock.main().sharedDirectory()).thenReturn(configuredDirectory.getAbsolutePath());
 
-    Path sharedDirectory = module.getSharedDirectory(config);
+    Path sharedDirectory = module.getSharedDirectory(configMock);
     assertThat(sharedDirectory.toFile().exists()).isTrue();
   }
 
   @Test(expected = IOException.class)
   public void shouldThrowAnExceptionIfAnErrorOccurCreatingSharedDirectory() throws Exception {
     File configuredDirectory = tempFolder.newFile();
-    when(config.getSharedDirectory()).thenReturn(configuredDirectory.getAbsolutePath());
+    when(configMock.main().sharedDirectory()).thenReturn(configuredDirectory.getAbsolutePath());
 
-    module.getSharedDirectory(config);
+    module.getSharedDirectory(configMock);
   }
 }
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 3ed3367..ac71b7b 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
@@ -27,10 +27,11 @@
 import com.github.tomakehurst.wiremock.http.RequestListener;
 import com.github.tomakehurst.wiremock.http.Response;
 import com.github.tomakehurst.wiremock.junit.WireMockRule;
-import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.GlobalPluginConfig;
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.acceptance.UseSsh;
 import java.util.concurrent.BrokenBarrierException;
 import java.util.concurrent.CyclicBarrier;
@@ -53,10 +54,19 @@
   @Rule public WireMockRule wireMockRule = new WireMockRule(options().port(PORT), false);
 
   @Test
-  @GerritConfig(name = "plugin.high-availability.url", value = URL)
-  @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.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);
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheExecutorProviderTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheExecutorProviderTest.java
index 08a1b19..fbc11e0 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheExecutorProviderTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheExecutorProviderTest.java
@@ -24,6 +24,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Answers;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 
@@ -31,14 +32,15 @@
 public class CacheExecutorProviderTest {
 
   @Mock private WorkQueue.Executor executorMock;
+
   private CacheExecutorProvider cacheExecutorProvider;
 
   @Before
   public void setUp() throws Exception {
     WorkQueue workQueueMock = mock(WorkQueue.class);
     when(workQueueMock.createQueue(4, "Forward-cache-eviction-event")).thenReturn(executorMock);
-    Configuration configMock = mock(Configuration.class);
-    when(configMock.getCacheThreadPoolSize()).thenReturn(4);
+    Configuration configMock = mock(Configuration.class, Answers.RETURNS_DEEP_STUBS);
+    when(configMock.cache().threadPoolSize()).thenReturn(4);
 
     cacheExecutorProvider = new CacheExecutorProvider(workQueueMock, configMock);
   }
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpClientProviderTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpClientProviderTest.java
index ac14ce4..a2aec9d 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpClientProviderTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/HttpClientProviderTest.java
@@ -26,6 +26,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Answers;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 
@@ -34,14 +35,15 @@
   private static final int TIME_INTERVAL = 1000;
   private static final String EMPTY = "";
 
-  @Mock private Configuration config;
+  @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+  private Configuration configMock;
 
   @Before
   public void setUp() throws Exception {
-    when(config.getUser()).thenReturn(EMPTY);
-    when(config.getPassword()).thenReturn(EMPTY);
-    when(config.getConnectionTimeout()).thenReturn(TIME_INTERVAL);
-    when(config.getSocketTimeout()).thenReturn(TIME_INTERVAL);
+    when(configMock.http().user()).thenReturn(EMPTY);
+    when(configMock.http().password()).thenReturn(EMPTY);
+    when(configMock.http().connectionTimeout()).thenReturn(TIME_INTERVAL);
+    when(configMock.http().socketTimeout()).thenReturn(TIME_INTERVAL);
   }
 
   @Test
@@ -58,7 +60,7 @@
   class TestModule extends AbstractModule {
     @Override
     protected void configure() {
-      bind(Configuration.class).toInstance(config);
+      bind(Configuration.class).toInstance(configMock);
       bind(CloseableHttpClient.class).toProvider(HttpClientProvider.class).in(Scopes.SINGLETON);
     }
   }
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 19a50df..d5e27b1 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
@@ -41,6 +41,7 @@
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.mockito.Answers;
 
 public class HttpSessionTest {
   private static final int MAX_TRIES = 3;
@@ -63,23 +64,24 @@
 
   @Rule public WireMockRule wireMockRule = new WireMockRule(0);
 
-  private Configuration cfg;
+  private Configuration configMock;
 
   @Before
   public void setUp() throws Exception {
     String url = "http://localhost:" + wireMockRule.port();
-    cfg = mock(Configuration.class);
-    when(cfg.getUser()).thenReturn("user");
-    when(cfg.getPassword()).thenReturn("pass");
-    when(cfg.getMaxTries()).thenReturn(MAX_TRIES);
-    when(cfg.getConnectionTimeout()).thenReturn(TIMEOUT);
-    when(cfg.getSocketTimeout()).thenReturn(TIMEOUT);
-    when(cfg.getRetryInterval()).thenReturn(RETRY_INTERVAL);
+    configMock = mock(Configuration.class, Answers.RETURNS_DEEP_STUBS);
+    when(configMock.http().user()).thenReturn("user");
+    when(configMock.http().password()).thenReturn("pass");
+    when(configMock.http().maxTries()).thenReturn(MAX_TRIES);
+    when(configMock.http().connectionTimeout()).thenReturn(TIMEOUT);
+    when(configMock.http().socketTimeout()).thenReturn(TIMEOUT);
+    when(configMock.http().retryInterval()).thenReturn(RETRY_INTERVAL);
 
     PeerInfo peerInfo = mock(PeerInfo.class);
     when(peerInfo.getDirectUrl()).thenReturn(url);
     httpSession =
-        new HttpSession(new HttpClientProvider(cfg).get(), Providers.of(Optional.of(peerInfo)));
+        new HttpSession(
+            new HttpClientProvider(configMock).get(), Providers.of(Optional.of(peerInfo)));
   }
 
   @Test
@@ -184,7 +186,7 @@
   public void testNoRequestWhenPeerInfoUnknown() throws IOException {
     httpSession =
         new HttpSession(
-            new HttpClientProvider(cfg).get(), Providers.of(Optional.<PeerInfo>absent()));
+            new HttpClientProvider(configMock).get(), Providers.of(Optional.<PeerInfo>absent()));
     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 ce4d56b..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
@@ -35,6 +35,7 @@
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
+import org.mockito.Answers;
 
 public class RestForwarderTest {
   private static final String PLUGIN_NAME = "high-availability";
@@ -49,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 =
@@ -58,7 +62,6 @@
 
   private RestForwarder forwarder;
   private HttpSession httpSessionMock;
-  private Configuration configurationMock;
 
   @BeforeClass
   public static void setup() {
@@ -68,10 +71,10 @@
   @Before
   public void setUp() {
     httpSessionMock = mock(HttpSession.class);
-    configurationMock = mock(Configuration.class);
-    when(configurationMock.getMaxTries()).thenReturn(3);
-    when(configurationMock.getRetryInterval()).thenReturn(10);
-    forwarder = new RestForwarder(httpSessionMock, PLUGIN_NAME, configurationMock);
+    Configuration configMock = mock(Configuration.class, Answers.RETURNS_DEEP_STUBS);
+    when(configMock.http().maxTries()).thenReturn(3);
+    when(configMock.http().retryInterval()).thenReturn(10);
+    forwarder = new RestForwarder(httpSessionMock, PLUGIN_NAME, configMock);
   }
 
   @Test
@@ -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 75cb675..e34b902 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,13 @@
   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 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 +61,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 +85,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 +109,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 +136,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,6 +161,13 @@
   }
 
   @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);
 
@@ -176,4 +212,25 @@
     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("test")).isFalse();
+    assertThat(task.hashCode()).isNotEqualTo("test".hashCode());
+
+    IndexGroupTask differentGroupIdTask = indexEventHandler.new IndexGroupTask("123");
+    assertThat(task.equals(differentGroupIdTask)).isFalse();
+    assertThat(task.hashCode()).isNotEqualTo(differentGroupIdTask.hashCode());
+  }
 }
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/IndexExecutorProviderTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/IndexExecutorProviderTest.java
index 66b84f1..cc4ba99 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/IndexExecutorProviderTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/IndexExecutorProviderTest.java
@@ -24,6 +24,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Answers;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 
@@ -37,8 +38,8 @@
     executorMock = mock(WorkQueue.Executor.class);
     WorkQueue workQueueMock = mock(WorkQueue.class);
     when(workQueueMock.createQueue(4, "Forward-index-event")).thenReturn(executorMock);
-    Configuration configMock = mock(Configuration.class);
-    when(configMock.getIndexThreadPoolSize()).thenReturn(4);
+    Configuration configMock = mock(Configuration.class, Answers.RETURNS_DEEP_STUBS);
+    when(configMock.index().threadPoolSize()).thenReturn(4);
     indexExecutorProvider = new IndexExecutorProvider(workQueueMock, configMock);
   }
 
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/websession/file/FileBasedWebSessionCacheCleanerTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/websession/file/FileBasedWebSessionCacheCleanerTest.java
index 61f6aa2..04c73d4 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/websession/file/FileBasedWebSessionCacheCleanerTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/websession/file/FileBasedWebSessionCacheCleanerTest.java
@@ -33,6 +33,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Answers;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 
@@ -46,7 +47,9 @@
   @Mock private ScheduledFuture<?> scheduledFutureMock;
   @Mock private WorkQueue workQueueMock;
   @Mock private Provider<CleanupTask> cleanupTaskProviderMock;
-  @Mock private Configuration configMock;
+
+  @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+  private Configuration configMock;
 
   private FileBasedWebSessionCacheCleaner cleaner;
 
@@ -57,7 +60,7 @@
     doReturn(scheduledFutureMock)
         .when(executorMock)
         .scheduleAtFixedRate(isA(CleanupTask.class), anyLong(), anyLong(), isA(TimeUnit.class));
-    when(configMock.getCleanupInterval()).thenReturn(CLEANUP_INTERVAL);
+    when(configMock.websession().cleanupInterval()).thenReturn(CLEANUP_INTERVAL);
     cleaner =
         new FileBasedWebSessionCacheCleaner(workQueueMock, cleanupTaskProviderMock, configMock);
   }