Merge branch 'evict-cache-plugin'

* evict-cache-plugin:
  Replace EasyMock with Mockito
  Compile against 2.13.3
  Change docs links to actual file extension (.md)
  Return "Bad Request" status code on failure to parse request entity
  Stop retrying if thread is interrupted
  Print proper name in show-queue for cache eviction
  Initial version of evict-cache plugin

evict-cache-plugin branch was created by fetching stable-2.13
(9691f0e417cdd49f8e66e76176be05b29076a4ee) branch of repository
https://gerrit.googlesource.com/plugins/evict-cache and pushing it to
this repository.

This merge adds the third of the 4 plugins that will be merged in this
repository. websession-flatfile will be done in follow up commit.

The reason for choosing stable-2.13 instead of master is that the only
commits done in master that are not in stable-2.13 were to adapt to
Gerrit master branch. Since all the evict-cache features/bug fixes are
in stable-2.13, use this branch so high-availability will support Gerrit
2.13.

Change-Id: Icf5a140dd0e30d00f0f6c52cdda5185fe068413e
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 30d035c..13f202d 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java
@@ -43,6 +43,7 @@
   private final int retryInterval;
   private final int indexThreadPoolSize;
   private final int eventThreadPoolSize;
+  private final int cacheThreadPoolSize;
 
   @Inject
   Configuration(PluginConfigFactory config,
@@ -59,6 +60,8 @@
         getInt(cfg, "indexThreadPoolSize", DEFAULT_THREAD_POOL_SIZE);
     eventThreadPoolSize =
         getInt(cfg, "eventThreadPoolSize", DEFAULT_THREAD_POOL_SIZE);
+    cacheThreadPoolSize =
+        getInt(cfg, "cacheThreadPoolSize", DEFAULT_THREAD_POOL_SIZE);
   }
 
   private int getInt(PluginConfig cfg, String name, int defaultValue) {
@@ -106,4 +109,8 @@
   public int getEventThreadPoolSize() {
     return eventThreadPoolSize;
   }
+
+  public int getCacheThreadPoolSize() {
+    return cacheThreadPoolSize;
+  }
 }
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 6eb961c..8481974 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Module.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Module.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.inject.Scopes;
 
+import com.ericsson.gerrit.plugins.highavailability.cache.CacheModule;
 import com.ericsson.gerrit.plugins.highavailability.event.EventModule;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.RestForwarderModule;
 import com.ericsson.gerrit.plugins.highavailability.index.IndexModule;
@@ -29,5 +30,6 @@
     install(new RestForwarderModule());
     install(new EventModule());
     install(new IndexModule());
+    install(new CacheModule());
   }
 }
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
new file mode 100644
index 0000000..89229fc
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheEvictionHandler.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2015 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.cache;
+
+import com.google.common.cache.RemovalNotification;
+import com.google.gerrit.server.cache.CacheRemovalListener;
+import com.google.inject.Inject;
+
+import com.ericsson.gerrit.plugins.highavailability.forwarder.Context;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.Forwarder;
+
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.regex.Pattern;
+
+class CacheEvictionHandler<K, V> implements CacheRemovalListener<K, V> {
+  private final ScheduledThreadPoolExecutor executor;
+  private final Forwarder forwarder;
+  private final Pattern pattern;
+
+  @Inject
+  CacheEvictionHandler(Forwarder forwarder,
+      @CacheExecutor ScheduledThreadPoolExecutor executor) {
+    this.forwarder = forwarder;
+    this.executor = executor;
+    pattern = Pattern.compile(
+        "^accounts.*|^groups.*|ldap_groups|ldap_usernames|^project.*|sshkeys|web_sessions");
+  }
+
+  @Override
+  public void onRemoval(String pluginName, String cacheName,
+      RemovalNotification<K, V> notification) {
+    if (!Context.isForwardedEvent() && !notification.wasEvicted()
+        && isSynchronized(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;
+
+    CacheEvictionTask(String cacheName, Object key) {
+      this.cacheName = cacheName;
+      this.key = key;
+    }
+
+    @Override
+    public void run() {
+      forwarder.evict(cacheName, key);
+    }
+
+    @Override
+    public String toString() {
+      return String.format("Evict key '%s' from cache '%s' in target instance",
+          key, cacheName);
+    }
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheExecutor.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheExecutor.java
new file mode 100644
index 0000000..bd3dd40
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheExecutor.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2015 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.cache;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+@interface CacheExecutor {
+}
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
new file mode 100644
index 0000000..c427d17
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheExecutorProvider.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2015 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.cache;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import com.ericsson.gerrit.plugins.highavailability.Configuration;
+
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+
+@Singleton
+class CacheExecutorProvider
+    implements Provider<ScheduledThreadPoolExecutor>, LifecycleListener {
+  private WorkQueue.Executor executor;
+
+  @Inject
+  CacheExecutorProvider(WorkQueue workQueue,
+      Configuration config) {
+    executor = workQueue.createQueue(config.getCacheThreadPoolSize(),
+        "Forward-cache-eviction-event");
+  }
+
+  @Override
+  public void start() {
+    // do nothing
+  }
+
+  @Override
+  public void stop() {
+    executor.shutdown();
+    executor.unregisterWorkQueue();
+    executor = null;
+  }
+
+  @Override
+  public ScheduledThreadPoolExecutor get() {
+    return executor;
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheModule.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheModule.java
new file mode 100644
index 0000000..671264b
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheModule.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2015 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.cache;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.cache.CacheRemovalListener;
+
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+
+public class CacheModule extends LifecycleModule {
+
+  @Override
+  protected void configure() {
+    bind(ScheduledThreadPoolExecutor.class)
+        .annotatedWith(CacheExecutor.class)
+        .toProvider(CacheExecutorProvider.class);
+    listener().to(CacheExecutorProvider.class);
+    DynamicSet.bind(binder(), CacheRemovalListener.class).to(
+        CacheEvictionHandler.class);
+  }
+}
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
new file mode 100644
index 0000000..12f92bd
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/Constants.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2015 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.cache;
+
+public final class Constants {
+
+  public static final String PROJECT_LIST = "project_list";
+  public static final String ACCOUNTS = "accounts";
+  public static final String GROUPS = "groups";
+  public static final String GROUPS_BYINCLUDE = "groups_byinclude";
+  public static final String GROUPS_MEMBERS = "groups_members";
+  public static final String PROJECTS = "projects";
+
+  private Constants() {
+  }
+}
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 c75d439..0d1a0bb 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
@@ -17,7 +17,7 @@
 import com.google.gerrit.server.events.Event;
 
 /**
- * Forward indexing and stream events to the other master
+ * Forward indexing, stream events and cache evictions to the other master
  */
 public interface Forwarder {
 
@@ -44,4 +44,13 @@
    * @return true if successful, otherwise false.
    */
   boolean send(Event event);
+
+  /**
+   * Forward a cache eviction event to the other master.
+   *
+   * @param cacheName the name of the cache to evict an entry from.
+   * @param key the key identifying the entry to evict from the cache.
+   * @return true if successful, otherwise false.
+   */
+  boolean evict(String cacheName, Object key);
 }
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
new file mode 100644
index 0000000..ed5e593
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/CacheRestApiServlet.java
@@ -0,0 +1,95 @@
+// Copyright (C) 2015 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_BAD_REQUEST;
+import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
+
+import com.google.common.base.Splitter;
+import com.google.common.cache.Cache;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import com.ericsson.gerrit.plugins.highavailability.cache.Constants;
+import com.ericsson.gerrit.plugins.highavailability.forwarder.Context;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.List;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+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;
+
+  @Inject
+  CacheRestApiServlet(DynamicMap<Cache<?, ?>> cacheMap) {
+    this.cacheMap = cacheMap;
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest req, HttpServletResponse rsp)
+      throws IOException, ServletException {
+    rsp.setContentType("text/plain");
+    rsp.setCharacterEncoding("UTF-8");
+    try {
+      List<String> params = Splitter.on('/').splitToList(req.getPathInfo());
+      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);
+    } catch (IOException e) {
+      logger.error("Failed to process eviction request: " + e.getMessage(), e);
+      sendError(rsp, SC_BAD_REQUEST, e.getMessage());
+    } 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 evictCache(Cache<?, ?> cache, String cacheName, Object key) {
+    if (Constants.PROJECT_LIST.equals(cacheName)) {
+      // One key is holding the list of projects
+      cache.invalidateAll();
+    } else {
+      cache.invalidate(key);
+    }
+    logger.debug("Invalidated " + cacheName);
+  }
+}
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
new file mode 100644
index 0000000..650bc28
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/GsonParser.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2015 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.common.base.Strings;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import com.ericsson.gerrit.plugins.highavailability.cache.Constants;
+
+final class GsonParser {
+
+  private GsonParser() {
+  }
+
+  static Object fromJson(String cacheName, String json) {
+    Gson gson = new GsonBuilder().create();
+    Object key;
+    // Need to add a case for 'adv_bases'
+    switch (cacheName) {
+      case Constants.ACCOUNTS:
+        key = gson.fromJson(Strings.nullToEmpty(json).trim(), Account.Id.class);
+        break;
+      case Constants.GROUPS:
+        key = gson.fromJson(Strings.nullToEmpty(json).trim(),
+            AccountGroup.Id.class);
+        break;
+      case Constants.GROUPS_BYINCLUDE:
+      case Constants.GROUPS_MEMBERS:
+        key = gson.fromJson(Strings.nullToEmpty(json).trim(),
+            AccountGroup.UUID.class);
+        break;
+      case Constants.PROJECT_LIST:
+        key = gson.fromJson(Strings.nullToEmpty(json), Object.class);
+        break;
+      default:
+        key = gson.fromJson(Strings.nullToEmpty(json).trim(), String.class);
+    }
+    return key;
+  }
+
+  static String toJson(String cacheName, Object key) {
+    Gson gson = new GsonBuilder().create();
+    String json;
+    // Need to add a case for 'adv_bases'
+    switch (cacheName) {
+      case Constants.ACCOUNTS:
+        json = gson.toJson(key, Account.Id.class);
+        break;
+      case Constants.GROUPS:
+        json = gson.toJson(key, AccountGroup.Id.class);
+        break;
+      case Constants.GROUPS_BYINCLUDE:
+      case Constants.GROUPS_MEMBERS:
+        json = gson.toJson(key, AccountGroup.UUID.class);
+        break;
+      case Constants.PROJECT_LIST:
+      default:
+        json = gson.toJson(key);
+    }
+    return json;
+  }
+}
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 8a81eb8..75481b3 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
@@ -97,4 +97,18 @@
     }
     return false;
   }
+
+  @Override
+  public boolean evict(String cacheName, Object key) {
+    try {
+      String json = GsonParser.toJson(cacheName, key);
+      return httpSession
+          .post(Joiner.on("/").join("/plugins", pluginName, "cache", cacheName),
+              json)
+          .isSuccessful();
+    } catch (IOException e) {
+      log.error("Error trying to evict for cache " + cacheName, e);
+      return false;
+    }
+  }
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderServletModule.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderServletModule.java
index f9832e5..71b9bf0 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,5 +21,6 @@
   protected void configureServlets() {
     serveRegex("/index/\\d+$").with(IndexRestApiServlet.class);
     serve("/event").with(EventRestApiServlet.class);
+    serve("/cache/*").with(CacheRestApiServlet.class);
   }
 }
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index 02db878..f0f371b 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -1,12 +1,17 @@
-The @PLUGIN@ plugin allows to synchronize secondary indexes and stream events
-between two Gerrit instances sharing the same git repositories and database.
-The plugin needs to be installed in both instances.
+The @PLUGIN@ plugin allows to synchronize eviction of caches, secondary indexes
+and stream events between two Gerrit instances sharing the same git repositories
+and database. The plugin needs to be installed in both instances.
+
+Every time a cache eviction occurs in one of the instances, the other instance's
+cache is updated.
+
+This way, both caches are kept synchronized.
 
 Every time the secondary index is modified in one of the instances, i.e., a
 change is added, updated or removed from the index, the other instance index is
 updated accordingly. This way, both indexes are kept synchronized.
 
-Eevery time a stream event occurs in one of the instances (see [more events info]
+Every time a stream event occurs in one of the instances (see [more events info]
 (https://gerrit-review.googlesource.com/Documentation/cmd-stream-events.html#events)),
 the event is forwarded to the other instance which re-plays it. This way, the
 output of the stream-events command is the same, no matter what instance a
@@ -14,4 +19,4 @@
 
 For this to work, http must be enabled in both instances and the plugin
 must be configured with valid credentials. For further information, refer to
-[config](config.md) documentation.
\ No newline at end of file
+[config](config.md) documentation.
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index f6c1ccc..e3865fd 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -52,3 +52,7 @@
 @PLUGIN@.eventThreadPoolSize
 :   Maximum number of threads used to send stream 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
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 9902cc4..f9225c3 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/ConfigurationTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/ConfigurationTest.java
@@ -68,6 +68,8 @@
         .isEqualTo(THREAD_POOL_SIZE);
     assertThat(configuration.getEventThreadPoolSize())
         .isEqualTo(THREAD_POOL_SIZE);
+    assertThat(configuration.getCacheThreadPoolSize())
+        .isEqualTo(THREAD_POOL_SIZE);
   }
 
   @Test
@@ -82,6 +84,7 @@
     assertThat(configuration.getRetryInterval()).isEqualTo(0);
     assertThat(configuration.getIndexThreadPoolSize()).isEqualTo(0);
     assertThat(configuration.getEventThreadPoolSize()).isEqualTo(0);
+    assertThat(configuration.getCacheThreadPoolSize()).isEqualTo(0);
   }
 
   @Test
@@ -105,6 +108,7 @@
     assertThat(configuration.getRetryInterval()).isEqualTo(1000);
     assertThat(configuration.getIndexThreadPoolSize()).isEqualTo(1);
     assertThat(configuration.getEventThreadPoolSize()).isEqualTo(1);
+    assertThat(configuration.getCacheThreadPoolSize()).isEqualTo(1);
   }
 
   private void buildMocks(boolean values) {
@@ -123,6 +127,8 @@
         .thenReturn(values ? THREAD_POOL_SIZE : 0);
     when(configMock.getInt("eventThreadPoolSize", THREAD_POOL_SIZE))
         .thenReturn(values ? THREAD_POOL_SIZE : 0);
+    when(configMock.getInt("cacheThreadPoolSize", THREAD_POOL_SIZE))
+        .thenReturn(values ? THREAD_POOL_SIZE : 0);
 
     configuration = new Configuration(cfgFactoryMock, pluginName);
   }
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
new file mode 100644
index 0000000..0c8ba85
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheEvictionIT.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2015 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.cache;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.givenThat;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.verify;
+
+import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.GerritConfigs;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PluginDaemonTest;
+
+import com.ericsson.gerrit.plugins.highavailability.cache.Constants;
+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 org.apache.http.HttpStatus;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.concurrent.TimeUnit;
+
+@NoHttpd
+public class CacheEvictionIT extends PluginDaemonTest {
+
+  @Rule
+  public WireMockRule wireMockRule = new WireMockRule(18888);
+
+  @Test
+  @GerritConfigs({
+      @GerritConfig(name = "plugin.high-availability.url", value = "http://localhost:18888"),
+      @GerritConfig(name = "plugin.high-availability.user", value = "admin")})
+  public void flushAndSendPost() throws Exception {
+    final String flushRequest =
+        "/plugins/high-availability/cache/" + Constants.PROJECT_LIST;
+    wireMockRule.addMockServiceRequestListener(new RequestListener() {
+      @Override
+      public void requestReceived(Request request, Response response) {
+        if (request.getAbsoluteUrl().contains(flushRequest)) {
+          synchronized (flushRequest) {
+            flushRequest.notify();
+          }
+        }
+      }
+    });
+    givenThat(post(urlEqualTo(flushRequest))
+        .willReturn(aResponse().withStatus(HttpStatus.SC_OK)));
+
+    adminSshSession
+        .exec("gerrit flush-caches --cache " + Constants.PROJECT_LIST);
+    synchronized (flushRequest) {
+      flushRequest.wait(TimeUnit.SECONDS.toMillis(5));
+    }
+    verify(postRequestedFor(urlEqualTo(flushRequest)));
+  }
+}
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
new file mode 100644
index 0000000..21e6cab
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/cache/CacheExecutorProviderTest.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2015 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.cache;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.gerrit.server.git.WorkQueue;
+
+import com.ericsson.gerrit.plugins.highavailability.Configuration;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class 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);
+
+    cacheExecutorProvider =
+        new CacheExecutorProvider(workQueueMock, configMock);
+  }
+
+  @Test
+  public void shouldReturnExecutor() throws Exception {
+    assertThat(cacheExecutorProvider.get()).isEqualTo(executorMock);
+  }
+
+  @Test
+  public void testStop() throws Exception {
+    cacheExecutorProvider.start();
+    assertThat(cacheExecutorProvider.get()).isEqualTo(executorMock);
+    cacheExecutorProvider.stop();
+    verify(executorMock).shutdown();
+    verify(executorMock).unregisterWorkQueue();
+    assertThat(cacheExecutorProvider.get()).isNull();
+  }
+}
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
new file mode 100644
index 0000000..4813a7d
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/CacheRestApiServletTest.java
@@ -0,0 +1,133 @@
+// Copyright (C) 2015 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_BAD_REQUEST;
+import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.common.cache.Cache;
+import com.google.gerrit.extensions.registration.DynamicMap;
+
+import com.ericsson.gerrit.plugins.highavailability.cache.Constants;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@RunWith(MockitoJUnitRunner.class)
+public class CacheRestApiServletTest {
+  @Mock
+  private HttpServletRequest request;
+  @Mock
+  private HttpServletResponse response;
+  @Mock
+  private BufferedReader reader;
+  @Mock
+  private DynamicMap<Cache<?, ?>> cacheMap;
+  private CacheRestApiServlet servlet;
+
+  @Before
+  public void setUp() {
+    servlet = new CacheRestApiServlet(cacheMap);
+  }
+
+  @Test
+  public void evictAccounts() throws Exception {
+    configureMocksFor(Constants.ACCOUNTS);
+    verifyResponseIsOK();
+  }
+
+  @Test
+  public void evictProjectList() throws Exception {
+    configureMocksFor(Constants.PROJECT_LIST);
+    verifyResponseIsOK();
+  }
+
+  @Test
+  public void evictGroups() throws Exception {
+    configureMocksFor(Constants.GROUPS);
+    verifyResponseIsOK();
+  }
+
+  @Test
+  public void evictGroupsByInclude() throws Exception {
+    configureMocksFor(Constants.GROUPS_BYINCLUDE);
+    verifyResponseIsOK();
+  }
+
+  @Test
+  public void evictGroupsMembers() throws Exception {
+    configureMocksFor(Constants.GROUPS_MEMBERS);
+    servlet.doPost(request, response);
+
+  }
+
+  @Test
+  public void evictDefault() throws Exception {
+    configureMocksFor(Constants.PROJECTS);
+    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");
+    String errorMessage = "someError";
+    doThrow(new IOException(errorMessage)).when(request).getReader();
+    servlet.doPost(request, response);
+    verify(response).sendError(SC_BAD_REQUEST, errorMessage);
+  }
+
+  @Test
+  public void errorWhileSendingErrorMessage() throws Exception {
+    when(request.getPathInfo()).thenReturn("/someCache");
+    String errorMessage = "someError";
+    doThrow(new IOException(errorMessage)).when(request).getReader();
+    servlet.doPost(request, response);
+    verify(response).sendError(SC_BAD_REQUEST, errorMessage);
+  }
+
+  @SuppressWarnings("unchecked")
+  private void configureMocksFor(String cacheName) throws IOException {
+    when(cacheMap.get("gerrit", cacheName)).thenReturn(mock(Cache.class));
+    when(request.getPathInfo()).thenReturn("/" + cacheName);
+    when(request.getReader()).thenReturn(reader);
+
+    if (Constants.PROJECTS.equals(cacheName)) {
+      when(reader.readLine()).thenReturn("abc");
+    } else if (Constants.GROUPS_BYINCLUDE.equals(cacheName)
+        || Constants.GROUPS_MEMBERS.equals(cacheName)) {
+      when(reader.readLine()).thenReturn("{\"uuid\":\"abcd1234\"}");
+    } else {
+      when(reader.readLine()).thenReturn("{}");
+    }
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/GsonParserTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/GsonParserTest.java
new file mode 100644
index 0000000..b72c21a
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/GsonParserTest.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2015 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 com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+import com.ericsson.gerrit.plugins.highavailability.cache.Constants;
+
+import org.junit.Test;
+
+public class GsonParserTest {
+  private static final Object EMPTY_JSON = "{}";
+
+  @Test
+  public void AccountIDParse() {
+    Account.Id accountId = new Account.Id(1);
+    String json = GsonParser.toJson(Constants.ACCOUNTS, accountId);
+    assertThat(accountId)
+        .isEqualTo(GsonParser.fromJson(Constants.ACCOUNTS, json));
+  }
+
+  @Test
+  public void AccountGroupIDParse() {
+    AccountGroup.Id accountGroupId = new AccountGroup.Id(1);
+    String json = GsonParser.toJson(Constants.GROUPS, accountGroupId);
+    assertThat(accountGroupId)
+        .isEqualTo(GsonParser.fromJson(Constants.GROUPS, json));
+  }
+
+  @Test
+  public void AccountGroupUUIDParse() {
+    AccountGroup.UUID accountGroupUuid = new AccountGroup.UUID("abc123");
+    String json =
+        GsonParser.toJson(Constants.GROUPS_BYINCLUDE, accountGroupUuid);
+    assertThat(accountGroupUuid)
+        .isEqualTo(GsonParser.fromJson(Constants.GROUPS_BYINCLUDE, json));
+  }
+
+  @Test
+  public void StringParse() {
+    String key = "key";
+    String json = GsonParser.toJson(Constants.PROJECTS, key);
+    assertThat(key)
+        .isEqualTo(GsonParser.fromJson(Constants.PROJECTS, json));
+  }
+
+  @Test
+  public void NoKeyParse() {
+    Object object = new Object();
+    String json = GsonParser.toJson(Constants.PROJECT_LIST, object);
+    assertThat(json).isEqualTo(EMPTY_JSON);
+  }
+}
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 c4968cd..72e2462 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
@@ -20,9 +20,12 @@
 import static org.mockito.Mockito.when;
 
 import com.google.common.base.Joiner;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.events.Event;
 import com.google.gson.GsonBuilder;
 
+import com.ericsson.gerrit.plugins.highavailability.cache.Constants;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.HttpResponseHandler.HttpResult;
 
 import org.junit.Test;
@@ -30,9 +33,6 @@
 import java.io.IOException;
 
 public class RestForwarderTest {
-  private static final int CHANGE_NUMBER = 1;
-  private static final String DELETE_OP = "delete";
-  private static final String INDEX_OP = "index";
   private static final String PLUGIN_NAME = "high-availability";
   private static final String EMPTY_MSG = "";
   private static final String ERROR_MSG = "Error";
@@ -42,6 +42,16 @@
   private static final boolean DO_NOT_THROW_EXCEPTION = false;
   private static final boolean THROW_EXCEPTION = true;
 
+  //Index
+  private static final int CHANGE_NUMBER = 1;
+  private static final String DELETE_OP = "delete";
+  private static final String INDEX_OP = "index";
+
+  //Evict cache
+  private static final String EMPTY_JSON = "{}";
+  private static final String EMPTY_JSON2 = "\"{}\"";
+  private static final String ID_JSON = "{\"id\":0}";
+
   private RestForwarder restForwarder;
 
   @Test
@@ -106,19 +116,19 @@
 
   @Test
   public void testEventSentOK() throws Exception {
-    Event event = setUpMocksForEvent(true, "", false);
+    Event event = setUpMocksForEvent(SUCCESSFUL, EMPTY_MSG, DO_NOT_THROW_EXCEPTION);
     assertThat(restForwarder.send(event)).isTrue();
   }
 
   @Test
   public void testEventSentFailed() throws Exception {
-    Event event = setUpMocksForEvent(false, "Error", false);
+    Event event = setUpMocksForEvent(FAILED, ERROR_MSG, DO_NOT_THROW_EXCEPTION);
     assertThat(restForwarder.send(event)).isFalse();
   }
 
   @Test
   public void testEventSentThrowsException() throws Exception {
-    Event event = setUpMocksForEvent(false, "Exception", true);
+    Event event = setUpMocksForEvent(FAILED, EXCEPTION_MSG, THROW_EXCEPTION);
     assertThat(restForwarder.send(event)).isFalse();
   }
 
@@ -143,4 +153,80 @@
       super("test-event");
     }
   }
+
+  @Test
+  public void testEvictCacheOK() throws Exception {
+    setupMocksForCache(Constants.PROJECTS, EMPTY_JSON2, SUCCESSFUL,
+        DO_NOT_THROW_EXCEPTION);
+    assertThat(restForwarder.evict(Constants.PROJECTS, EMPTY_JSON)).isTrue();
+  }
+
+  @Test
+  public void testEvictAccountsOK() throws Exception {
+    setupMocksForCache(Constants.ACCOUNTS, ID_JSON, SUCCESSFUL,
+        DO_NOT_THROW_EXCEPTION);
+    assertThat(restForwarder.evict(Constants.ACCOUNTS, mock(Account.Id.class)))
+        .isTrue();
+  }
+
+  @Test
+  public void testEvictGroupsOK() throws Exception {
+    setupMocksForCache(Constants.GROUPS, ID_JSON, SUCCESSFUL,
+        DO_NOT_THROW_EXCEPTION);
+    assertThat(
+        restForwarder.evict(Constants.GROUPS, mock(AccountGroup.Id.class)))
+            .isTrue();
+  }
+
+  @Test
+  public void testEvictGroupsByIncludeOK() throws Exception {
+    setupMocksForCache(Constants.GROUPS_BYINCLUDE, EMPTY_JSON, SUCCESSFUL,
+        DO_NOT_THROW_EXCEPTION);
+    assertThat(restForwarder.evict(Constants.GROUPS_BYINCLUDE,
+        mock(AccountGroup.UUID.class))).isTrue();
+  }
+
+  @Test
+  public void testEvictGroupsMembersOK() throws Exception {
+    setupMocksForCache(Constants.GROUPS_MEMBERS, EMPTY_JSON, SUCCESSFUL,
+        DO_NOT_THROW_EXCEPTION);
+    assertThat(restForwarder.evict(Constants.GROUPS_MEMBERS,
+        mock(AccountGroup.UUID.class))).isTrue();
+  }
+
+  @Test
+  public void testEvictProjectListOK() throws Exception {
+    setupMocksForCache(Constants.PROJECT_LIST, EMPTY_JSON, SUCCESSFUL,
+        DO_NOT_THROW_EXCEPTION);
+    assertThat(restForwarder.evict(Constants.PROJECT_LIST, new Object()))
+        .isTrue();
+  }
+
+  @Test
+  public void testEvictCacheFailed() throws Exception {
+    setupMocksForCache(Constants.PROJECTS, EMPTY_JSON2, FAILED,
+        DO_NOT_THROW_EXCEPTION);
+    assertThat(restForwarder.evict(Constants.PROJECTS, EMPTY_JSON)).isFalse();
+  }
+
+  @Test
+  public void testEvictCacheThrowsException() throws Exception {
+    setupMocksForCache(Constants.PROJECTS, EMPTY_JSON2, FAILED,
+        THROW_EXCEPTION);
+    assertThat(restForwarder.evict(Constants.PROJECTS, EMPTY_JSON)).isFalse();
+  }
+
+  private void setupMocksForCache(String cacheName, String json,
+      boolean isOperationSuccessful, boolean exception) throws IOException {
+    String request =
+        Joiner.on("/").join("/plugins", PLUGIN_NAME, "cache", cacheName);
+    HttpSession httpSession = mock(HttpSession.class);
+    if (exception) {
+      doThrow(new IOException()).when(httpSession).post(request, json);
+    } else {
+      HttpResult result = new HttpResult(isOperationSuccessful, "Error");
+      when(httpSession.post(request, json)).thenReturn(result);
+    }
+    restForwarder = new RestForwarder(httpSession, PLUGIN_NAME);
+  }
 }