Introduce lib that contains Postgres based persistent cache impl
The following change introduces cache-postgres.jar library that contains
Postgres based persistence implementation for Gerrit caches.
It is heavily based on existing Gerrit's H2CacheImpl and could/should
be further optimized in the follow up changes.
One builds it by calling:
bazel build cache-postgres
Installation:
1. create Postgres DB e.g. gerrit_caches
2. copy 'bazel-genfiles/cache-postgres.jar' to GERRIT_SITE/lib dir
3. modify GERRIT_SITE/etc/gerrit.config with the following changes
a) inform Gerrit that main library's module should be loaded
gerrit.installModule
"com.googlesource.gerrit.modules.cache.pg.CacheModule"
b) specify connection to Postgres DB
cache.url
"jdbc:postgresql://localhost:5432/gerrit_caches?user=gerrit&password=gerrit"
4. restart Gerrit
5. remaining configuration parameters are mentioned in README.md
NOTES:
1. current implementation doesn't contain migration step and
existing data will be wiped out.
2. it uses INSERT INTO ... ON CONFLICT DO UPDATE ... upsert syntax [1]
therefore it is Postgres 9.5+ compatible.
[1] https://stackoverflow.com/questions/17267417/how-to-upsert-merge-insert-on-duplicate-update-in-postgresql
Depends-On: I7562b210fad4c5f6dc67887f627cf76815a378cb
Change-Id: I33fa1cd3dba1c1b246aecaa05a052f7348ddd64a
Signed-off-by: Jacek Centkowski <jcentkowski@collab.net>
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b4fb4c2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.classpath
+.project
+.settings
+/eclipse-out
+/bazel-*
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..6f34b87
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,36 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+load(
+ "//tools/bzl:plugin.bzl",
+ "gerrit_plugin",
+ "PLUGIN_DEPS",
+ "PLUGIN_TEST_DEPS",
+)
+
+gerrit_plugin(
+ name = "cache-postgres",
+ srcs = glob(["src/main/java/**/*.java"]),
+ manifest_entries = [
+ "Gerrit-PluginName: cache-postgres",
+ ],
+ resources = glob(["src/main/resources/**/*"]),
+)
+
+junit_tests(
+ name = "cache_postgres_tests",
+ testonly = 1,
+ srcs = glob(["src/test/java/**/*.java"]),
+ tags = ["cache-postgres"],
+ deps = [
+ ":cache-postgres__plugin_test_deps",
+ ],
+)
+
+java_library(
+ name = "cache-postgres__plugin_test_deps",
+ testonly = 1,
+ visibility = ["//visibility:public"],
+ exports = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
+ ":cache-postgres__plugin",
+ "@mockito//jar",
+ ],
+)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0a3788b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,65 @@
+# Gerrit Postgres based persistent cache
+
+Gerrit lib module to swap existing persistent cache implementation
+(that is H2 based) with implementation that stores data in Postgres.
+Note that is uses [INSERT INTO ... ON CONFLICT DO UPDATE ...](https://stackoverflow.com/questions/17267417/how-to-upsert-merge-insert-on-duplicate-update-in-postgresql)
+upsert syntax therefore it is Postgres *9.5+* compatible.
+
+## How to build
+
+Build this module similarly to standalone build for any other,
+bazel based Gerrit plugin:
+
+- Clone the cache-postgres source tree
+- Run ```bazel build cache-postgres```
+- The ```cache-postgres.jar``` module is generated under ```/bazel-genfiles/cache-postgres.jar```
+
+## How install
+
+Copy ```cache-postgres.jar``` library to Gerrit ```/lib``` and add the following
+two extra settings to ```gerrit.config```:
+
+```
+[gerrit]
+ installModule = com.googlesource.gerrit.modules.cache.pg.CacheModule
+
+[cache]
+ url = jdbc:postgresql://localhost:5432/gerrit_caches?user=gerrit&password=gerrit
+```
+
+## Core Gerrit settings: section `cache`
+
+cache.url
+: URI that specifies connection to existing DB (including both
+username and passwword).
+
+cache.poolLimit
+: Maximum number of open database connections. If the server needs
+more than this number, request processing threads will wait up
+to `cache.poolMaxWait` seconds for a connection to be released before
+they abort with an exception.
+Default value is taken from `database.poolLimit`.
+
+cache.poolMinIdle
+: Minimum number of connections to keep idle in the pool.
+Default is `4`.
+
+cache.poolMaxIdle
+: Maximum number of connections to keep idle in the pool. If there
+are more idle connections, connections will be closed instead of
+being returned back to the pool.
+Default is min(`cache.poolLimit`, `16`).
+
+cache.poolMaxWait
+Maximum amount of time a request processing thread will wait to
+acquire a database connection from the pool. If no connection is
+released within this time period, the processing thread will abort
+its current operations and return an error to the client.
+Values should use common unit suffixes to express their setting:
+* ms, milliseconds
+* s, sec, second, seconds
+* m, min, minute, minutes
+* h, hr, hour, hours
+
+If a unit suffix is not specified, `milliseconds` is assumed.
+Default is `30 seconds`.
diff --git a/WORKSPACE b/WORKSPACE
new file mode 100644
index 0000000..527eb8b
--- /dev/null
+++ b/WORKSPACE
@@ -0,0 +1,30 @@
+workspace(name = "cache_postgres")
+
+load("//:bazlets.bzl", "load_bazlets")
+
+load_bazlets(
+ commit = "2c89b78092943e040377db8aad4568f426ac785a",
+ # local_path = "/home/<user>/projects/bazlets",
+)
+
+#Snapshot Plugin API
+load(
+ "@com_googlesource_gerrit_bazlets//:gerrit_api_maven_local.bzl",
+ "gerrit_api_maven_local",
+)
+
+# Load snapshot Plugin API
+gerrit_api_maven_local()
+
+# Release Plugin API
+#load(
+# "@com_googlesource_gerrit_bazlets//:gerrit_api.bzl",
+# "gerrit_api",
+#)
+
+# Load release Plugin API
+#gerrit_api()
+
+load("//:external_plugin_deps.bzl", "external_plugin_deps")
+
+external_plugin_deps()
diff --git a/bazlets.bzl b/bazlets.bzl
new file mode 100644
index 0000000..e14e488
--- /dev/null
+++ b/bazlets.bzl
@@ -0,0 +1,17 @@
+NAME = "com_googlesource_gerrit_bazlets"
+
+def load_bazlets(
+ commit,
+ local_path = None
+ ):
+ if not local_path:
+ native.git_repository(
+ name = NAME,
+ remote = "https://gerrit.googlesource.com/bazlets",
+ commit = commit,
+ )
+ else:
+ native.local_repository(
+ name = NAME,
+ path = local_path,
+ )
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
new file mode 100644
index 0000000..135e5f6
--- /dev/null
+++ b/external_plugin_deps.bzl
@@ -0,0 +1,24 @@
+load("//tools/bzl:maven_jar.bzl", "maven_jar")
+
+def external_plugin_deps():
+ maven_jar(
+ name = "mockito",
+ artifact = "org.mockito:mockito-core:2.5.0",
+ sha1 = "be28d46a52c7f2563580adeca350145e9ce916f8",
+ deps = [
+ "@byte_buddy//jar",
+ "@objenesis//jar",
+ ],
+ )
+
+ maven_jar(
+ name = "byte_buddy",
+ artifact = "net.bytebuddy:byte-buddy:1.5.12",
+ sha1 = "b1ba1d15f102b36ed43b826488114678d6d413da",
+ )
+
+ maven_jar(
+ name = "objenesis",
+ artifact = "org.objenesis:objenesis:2.4",
+ sha1 = "2916b6c96b50c5b3ec4452ed99401db745aabb27",
+ )
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/pg/CacheModule.java b/src/main/java/com/googlesource/gerrit/modules/cache/pg/CacheModule.java
new file mode 100644
index 0000000..9263e9c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/pg/CacheModule.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2018 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.googlesource.gerrit.modules.cache.pg;
+
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.ModuleImpl;
+import com.google.gerrit.server.cache.PersistentCacheFactory;
+
+@ModuleImpl(name = com.google.gerrit.server.cache.CacheModule.PERSISTENT_MODULE)
+public class CacheModule extends LifecycleModule {
+ @Override
+ protected void configure() {
+ bind(PersistentCacheFactory.class).to(PgCacheFactory.class);
+ listener().to(PgCacheFactory.class);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/pg/KeyType.java b/src/main/java/com/googlesource/gerrit/modules/cache/pg/KeyType.java
new file mode 100644
index 0000000..d624f5b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/pg/KeyType.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2018 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.googlesource.gerrit.modules.cache.pg;
+
+import com.google.common.hash.Funnel;
+import com.google.common.hash.Funnels;
+import com.google.common.hash.PrimitiveSink;
+import com.google.inject.TypeLiteral;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Types;
+
+class KeyType<K> {
+ String columnType() {
+ return "BYTEA"; // H2 uses OTHER
+ }
+
+ @SuppressWarnings("unchecked")
+ K get(ResultSet rs, int col) throws SQLException {
+ return (K) PgSqlHandle.deserialize(rs.getBytes(col)); // H2 doesn't require deserialization
+ }
+
+ void set(PreparedStatement ps, int col, K value) throws SQLException {
+ ps.setObject(
+ col,
+ PgSqlHandle.serialize(value),
+ Types.BINARY); // H2 uses JAVA_OBJECT and doesn't require serialization
+ }
+
+ Funnel<K> funnel() {
+ return new Funnel<K>() {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void funnel(K from, PrimitiveSink into) {
+ try (ObjectOutputStream ser = new ObjectOutputStream(new SinkOutputStream(into))) {
+ ser.writeObject(from);
+ ser.flush();
+ } catch (IOException err) {
+ throw new RuntimeException("Cannot hash as Serializable", err);
+ }
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ static <K> KeyType<K> create(TypeLiteral<K> type) {
+ if (type.getRawType() == String.class) {
+ return (KeyType<K>) STRING;
+ }
+ return (KeyType<K>) OTHER;
+ }
+
+ static final KeyType<?> OTHER = new KeyType<>();
+ static final KeyType<String> STRING =
+ new KeyType<String>() {
+ @Override
+ String columnType() {
+ return "text";
+ }
+
+ @Override
+ String get(ResultSet rs, int col) throws SQLException {
+ return rs.getString(col);
+ }
+
+ @Override
+ void set(PreparedStatement ps, int col, String value) throws SQLException {
+ ps.setString(col, value);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ Funnel<String> funnel() {
+ Funnel<?> s = Funnels.unencodedCharsFunnel();
+ return (Funnel<String>) s;
+ }
+ };
+}
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/pg/PgCacheBindingProxy.java b/src/main/java/com/googlesource/gerrit/modules/cache/pg/PgCacheBindingProxy.java
new file mode 100644
index 0000000..e648fbc
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/pg/PgCacheBindingProxy.java
@@ -0,0 +1,111 @@
+// Copyright (C) 2018 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.googlesource.gerrit.modules.cache.pg;
+
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.Weigher;
+import com.google.gerrit.server.cache.CacheBinding;
+import com.google.inject.TypeLiteral;
+import java.util.concurrent.TimeUnit;
+
+class PgCacheBindingProxy<K, V> implements CacheBinding<K, V> {
+ private static final String MSG_NOT_SUPPORTED =
+ "This is a read-only wrapper. Modifications are not supported";
+
+ private final CacheBinding<K, V> source;
+
+ PgCacheBindingProxy(CacheBinding<K, V> source) {
+ this.source = source;
+ }
+
+ @Override
+ public Long expireAfterWrite(TimeUnit unit) {
+ return source.expireAfterWrite(unit);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Weigher<K, V> weigher() {
+ Weigher<K, V> weigher = source.weigher();
+ if (weigher == null) {
+ return null;
+ }
+
+ // introduce weigher that performs calculations
+ // on value that is being stored not on ValueHolder
+ return (Weigher<K, V>)
+ new Weigher<K, ValueHolder<V>>() {
+ @Override
+ public int weigh(K key, ValueHolder<V> value) {
+ return weigher.weigh(key, value.value);
+ }
+ };
+ }
+
+ @Override
+ public String name() {
+ return source.name();
+ }
+
+ @Override
+ public TypeLiteral<K> keyType() {
+ return source.keyType();
+ }
+
+ @Override
+ public TypeLiteral<V> valueType() {
+ return source.valueType();
+ }
+
+ @Override
+ public long maximumWeight() {
+ return source.maximumWeight();
+ }
+
+ @Override
+ public long diskLimit() {
+ return source.diskLimit();
+ }
+
+ @Override
+ public CacheLoader<K, V> loader() {
+ return source.loader();
+ }
+
+ @Override
+ public CacheBinding<K, V> maximumWeight(long weight) {
+ throw new RuntimeException(MSG_NOT_SUPPORTED);
+ }
+
+ @Override
+ public CacheBinding<K, V> diskLimit(long limit) {
+ throw new RuntimeException(MSG_NOT_SUPPORTED);
+ }
+
+ @Override
+ public CacheBinding<K, V> expireAfterWrite(long duration, TimeUnit durationUnits) {
+ throw new RuntimeException(MSG_NOT_SUPPORTED);
+ }
+
+ @Override
+ public CacheBinding<K, V> loader(Class<? extends CacheLoader<K, V>> clazz) {
+ throw new RuntimeException(MSG_NOT_SUPPORTED);
+ }
+
+ @Override
+ public CacheBinding<K, V> weigher(Class<? extends Weigher<K, V>> clazz) {
+ throw new RuntimeException(MSG_NOT_SUPPORTED);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/pg/PgCacheFactory.java b/src/main/java/com/googlesource/gerrit/modules/cache/pg/PgCacheFactory.java
new file mode 100644
index 0000000..01a6785
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/pg/PgCacheFactory.java
@@ -0,0 +1,178 @@
+// Copyright (C) 2018 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.googlesource.gerrit.modules.cache.pg;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.server.cache.CacheBinding;
+import com.google.gerrit.server.cache.MemoryCacheFactory;
+import com.google.gerrit.server.cache.PersistentCacheFactory;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class PgCacheFactory implements PersistentCacheFactory, LifecycleListener {
+ private static final Logger log = LoggerFactory.getLogger(PgCacheFactory.class);
+
+ private final PgSqlSource ds;
+ private final Config cfg;
+ private final MemoryCacheFactory memFactory;
+ private final List<PgCacheImpl<?, ?>> caches;
+ private final DynamicMap<Cache<?, ?>> cacheMap;
+ private final ExecutorService executor;
+ private final ScheduledExecutorService cleanup;
+
+ @Inject
+ PgCacheFactory(
+ PgSqlSource ds,
+ @GerritServerConfig Config cfg,
+ MemoryCacheFactory memFactory,
+ DynamicMap<Cache<?, ?>> cacheMap) {
+ this.ds = ds;
+ this.cfg = cfg;
+ this.memFactory = memFactory;
+ this.caches = new LinkedList<>();
+ this.cacheMap = cacheMap;
+
+ executor =
+ Executors.newFixedThreadPool(
+ 1, new ThreadFactoryBuilder().setNameFormat("PgCache-Store-%d").build());
+ cleanup =
+ Executors.newScheduledThreadPool(
+ 1,
+ new ThreadFactoryBuilder().setNameFormat("PgCache-Prune-%d").setDaemon(true).build());
+ }
+
+ @Override
+ public void start() {
+ if (executor != null) {
+ for (PgCacheImpl<?, ?> cache : caches) {
+ executor.execute(cache::start);
+ @SuppressWarnings("unused")
+ Future<?> possiblyIgnoredError =
+ cleanup.schedule(() -> cache.prune(cleanup), 30, TimeUnit.SECONDS);
+ }
+ }
+ }
+
+ @Override
+ public void stop() {
+ if (executor != null) {
+ try {
+ cleanup.shutdownNow();
+
+ List<Runnable> pending = executor.shutdownNow();
+ if (executor.awaitTermination(15, TimeUnit.MINUTES)) {
+ if (pending != null && !pending.isEmpty()) {
+ log.info("Finishing {} disk cache updates", pending.size());
+ for (Runnable update : pending) {
+ update.run();
+ }
+ }
+ } else {
+ log.info("Timeout waiting for disk cache to close");
+ }
+ } catch (InterruptedException e) {
+ log.warn("Interrupted waiting for disk cache to shutdown");
+ }
+ }
+ synchronized (caches) {
+ for (PgCacheImpl<?, ?> cache : caches) {
+ cache.stop();
+ }
+ }
+ }
+
+ @SuppressWarnings({"unchecked"})
+ @Override
+ public <K, V> Cache<K, V> build(CacheBinding<K, V> in) {
+ long limit = cfg.getLong("cache", in.name(), "diskLimit", in.diskLimit());
+
+ if (limit <= 0) {
+ return memFactory.build(in);
+ }
+
+ PgCacheBindingProxy<K, V> def = new PgCacheBindingProxy<>(in);
+ PgSqlStore<K, V> store =
+ newSqlStore(def.name(), def.keyType(), limit, def.expireAfterWrite(TimeUnit.SECONDS));
+ PgCacheImpl<K, V> cache =
+ new PgCacheImpl<>(
+ def.name(),
+ executor,
+ store,
+ def.keyType(),
+ (Cache<K, ValueHolder<V>>) memFactory.build(def));
+ synchronized (caches) {
+ caches.add(cache);
+ }
+ return cache;
+ }
+
+ @SuppressWarnings({"unchecked"})
+ @Override
+ public <K, V> LoadingCache<K, V> build(CacheBinding<K, V> in, CacheLoader<K, V> loader) {
+ long limit = cfg.getLong("cache", in.name(), "diskLimit", in.diskLimit());
+
+ if (limit <= 0) {
+ return memFactory.build(in, loader);
+ }
+
+ PgCacheBindingProxy<K, V> def = new PgCacheBindingProxy<>(in);
+ PgSqlStore<K, V> store =
+ newSqlStore(def.name(), def.keyType(), limit, def.expireAfterWrite(TimeUnit.SECONDS));
+ Cache<K, ValueHolder<V>> mem =
+ (Cache<K, ValueHolder<V>>)
+ memFactory.build(def, (CacheLoader<K, V>) new PgCacheLoader<>(executor, store, loader));
+ PgCacheImpl<K, V> cache = new PgCacheImpl<>(def.name(), executor, store, def.keyType(), mem);
+ caches.add(cache);
+ return cache;
+ }
+
+ @Override
+ public void onStop(String plugin) {
+ synchronized (caches) {
+ for (Map.Entry<String, Provider<Cache<?, ?>>> entry : cacheMap.byPlugin(plugin).entrySet()) {
+ Cache<?, ?> cache = entry.getValue().get();
+ if (caches.remove(cache)) {
+ ((PgCacheImpl<?, ?>) cache).stop();
+ }
+ }
+ }
+ }
+
+ private <V, K> PgSqlStore<K, V> newSqlStore(
+ String name, TypeLiteral<K> keyType, long maxSize, Long expireAfterWrite) {
+ return new PgSqlStore<>(
+ ds, name, keyType, maxSize, expireAfterWrite == null ? 0 : expireAfterWrite.longValue());
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/pg/PgCacheImpl.java b/src/main/java/com/googlesource/gerrit/modules/cache/pg/PgCacheImpl.java
new file mode 100644
index 0000000..08aa480
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/pg/PgCacheImpl.java
@@ -0,0 +1,182 @@
+// Copyright (C) 2018 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.googlesource.gerrit.modules.cache.pg;
+
+import com.google.common.cache.AbstractLoadingCache;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheStats;
+import com.google.common.cache.LoadingCache;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.server.cache.PersistentCache;
+import com.google.inject.TypeLiteral;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalAdjusters;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class PgCacheImpl<K, V> extends AbstractLoadingCache<K, V> implements PersistentCache {
+ private static final Logger log = LoggerFactory.getLogger(PgCacheImpl.class);
+
+ private final String name;
+ private final Executor executor;
+ private final PgSqlStore<K, V> store;
+ private final TypeLiteral<K> keyType;
+ private final Cache<K, ValueHolder<V>> mem;
+
+ public PgCacheImpl(
+ String name,
+ Executor executor,
+ PgSqlStore<K, V> store,
+ TypeLiteral<K> keyType,
+ Cache<K, ValueHolder<V>> mem) {
+ this.name = name;
+ this.executor = executor;
+ this.store = store;
+ this.keyType = keyType;
+ this.mem = mem;
+ }
+
+ @Override
+ public V get(K key) throws ExecutionException {
+ if (mem instanceof LoadingCache) {
+ return ((LoadingCache<K, ValueHolder<V>>) mem).get(key).value;
+ }
+
+ log.error("Memory cache for persistent backend {} should be of LoadingCache type", name);
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public V getIfPresent(Object objKey) {
+ if (!keyType.getRawType().isInstance(objKey)) {
+ log.warn("Invalid object key type [{}] was provided for cache {}", objKey, name);
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ K key = (K) objKey;
+
+ ValueHolder<V> h = mem.getIfPresent(key);
+ if (h != null) {
+ return h.value;
+ }
+
+ if (store.mightContain(key)) {
+ h = store.getIfPresent(key);
+ if (h != null) {
+ mem.put(key, h);
+ return h.value;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public V get(K key, Callable<? extends V> valueLoader) throws ExecutionException {
+ return mem.get(
+ key,
+ () -> {
+ if (store.mightContain(key)) {
+ ValueHolder<V> h = store.getIfPresent(key);
+ if (h != null) {
+ return h;
+ }
+ }
+
+ ValueHolder<V> h = new ValueHolder<>(valueLoader.call());
+ h.created = TimeUtil.nowMs();
+ executor.execute(() -> store.put(key, h));
+ return h;
+ })
+ .value;
+ }
+
+ @Override
+ public void put(K key, V val) {
+ final ValueHolder<V> h = new ValueHolder<>(val);
+ h.created = TimeUtil.nowMs();
+ mem.put(key, h);
+ executor.execute(() -> store.put(key, h));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void invalidate(Object key) {
+ if (keyType.getRawType().isInstance(key) && store.mightContain((K) key)) {
+ executor.execute(() -> store.invalidate((K) key));
+ }
+ mem.invalidate(key);
+ }
+
+ @Override
+ public void invalidateAll() {
+ store.invalidateAll();
+ mem.invalidateAll();
+ }
+
+ @Override
+ public long size() {
+ return mem.size();
+ }
+
+ @Override
+ public CacheStats stats() {
+ return mem.stats();
+ }
+
+ @Override
+ public DiskStats diskStats() {
+ return store.diskStats();
+ }
+
+ void start() {
+ store.open();
+ }
+
+ void stop() {
+ for (Map.Entry<K, ValueHolder<V>> e : mem.asMap().entrySet()) {
+ ValueHolder<V> h = e.getValue();
+ if (!h.clean) {
+ store.put(e.getKey(), h);
+ }
+ }
+ store.close();
+ }
+
+ void prune(ScheduledExecutorService service) {
+ store.prune(mem);
+
+ long delay =
+ LocalDateTime.now()
+ .with(TemporalAdjusters.firstDayOfMonth())
+ .withHour(01)
+ .truncatedTo(ChronoUnit.HOURS)
+ .toInstant(OffsetDateTime.now().getOffset())
+ .minusMillis(TimeUtil.nowMs())
+ .toEpochMilli();
+ @SuppressWarnings("unused")
+ Future<?> possiblyIgnoredError =
+ service.schedule(() -> prune(service), delay, TimeUnit.MILLISECONDS);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/pg/PgCacheLoader.java b/src/main/java/com/googlesource/gerrit/modules/cache/pg/PgCacheLoader.java
new file mode 100644
index 0000000..7e47d6a
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/pg/PgCacheLoader.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2018 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.googlesource.gerrit.modules.cache.pg;
+
+import com.google.common.cache.CacheLoader;
+import com.google.gerrit.common.TimeUtil;
+import java.util.concurrent.Executor;
+
+public class PgCacheLoader<K, V> extends CacheLoader<K, ValueHolder<V>> {
+ private final Executor executor;
+ private final PgSqlStore<K, V> store;
+ private final CacheLoader<K, V> loader;
+
+ PgCacheLoader(Executor executor, PgSqlStore<K, V> store, CacheLoader<K, V> loader) {
+ this.executor = executor;
+ this.store = store;
+ this.loader = loader;
+ }
+
+ @Override
+ public ValueHolder<V> load(K key) throws Exception {
+ if (store.mightContain(key)) {
+ ValueHolder<V> h = store.getIfPresent(key);
+ if (h != null) {
+ return h;
+ }
+ }
+
+ final ValueHolder<V> h = new ValueHolder<>(loader.load(key));
+ h.created = TimeUtil.nowMs();
+ executor.execute(() -> store.put(key, h));
+ return h;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/pg/PgSqlHandle.java b/src/main/java/com/googlesource/gerrit/modules/cache/pg/PgSqlHandle.java
new file mode 100644
index 0000000..fa78287
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/pg/PgSqlHandle.java
@@ -0,0 +1,143 @@
+// Copyright (C) 2018 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.googlesource.gerrit.modules.cache.pg;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.ObjectStreamClass;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Statement;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class PgSqlHandle {
+ private static final Logger log = LoggerFactory.getLogger(PgSqlHandle.class);
+
+ Connection conn;
+ PreparedStatement get;
+ PreparedStatement put;
+ PreparedStatement touch;
+ PreparedStatement invalidate;
+
+ private final String name;
+
+ PgSqlHandle(PgSqlSource store, String name, KeyType<?> type) throws SQLException {
+ this.name = name;
+ this.conn = store.getConnection();
+ try (Statement stmt = conn.createStatement()) {
+ stmt.addBatch(
+ "CREATE TABLE IF NOT EXISTS \"data_"
+ + this.name
+ + "\" (k "
+ + type.columnType()
+ + " NOT NULL PRIMARY KEY" // H2 uses HASH however it is discouraged in Postgres doc
+ + ",v BYTEA NOT NULL"
+ + ",created TIMESTAMP NOT NULL"
+ + ",accessed TIMESTAMP NOT NULL"
+ + ",space BIGINT"
+ + ")");
+ // Postgres has no 'computed columns' concept, however one can achieve that
+ // with trigger
+ stmt.addBatch(
+ "CREATE OR REPLACE FUNCTION compute_space()\n"
+ + "RETURNS trigger\n"
+ + "LANGUAGE plpgsql\n"
+ + "SECURITY DEFINER\n"
+ + "AS $BODY$\n"
+ + "BEGIN\n"
+ + " NEW.space = OCTET_LENGTH(NEW.k) + OCTET_LENGTH(NEW.v);\n"
+ + " RETURN NEW;\n"
+ + "END\n"
+ + "$BODY$;");
+ stmt.addBatch("DROP TRIGGER IF EXISTS computed_space ON \"data_" + this.name + "\"");
+ stmt.addBatch(
+ "CREATE TRIGGER computed_space "
+ + "BEFORE INSERT OR UPDATE "
+ + "ON \"data_"
+ + this.name
+ + "\" "
+ + "FOR EACH ROW "
+ + "EXECUTE PROCEDURE compute_space()");
+ stmt.executeBatch();
+ }
+ }
+
+ void close() {
+ get = closeStatement(get);
+ put = closeStatement(put);
+ touch = closeStatement(touch);
+ invalidate = closeStatement(invalidate);
+
+ if (conn != null) {
+ try {
+ conn.close();
+ } catch (SQLException e) {
+ log.warn("Cannot close connection to " + name, e);
+ } finally {
+ conn = null;
+ }
+ }
+ }
+
+ private PreparedStatement closeStatement(PreparedStatement ps) {
+ if (ps != null) {
+ try {
+ ps.close();
+ } catch (SQLException e) {
+ log.warn("Cannot close statement for " + name, e);
+ }
+ }
+ return null;
+ }
+
+ static Object deserialize(byte[] d) {
+ try {
+ ByteArrayInputStream in = new ByteArrayInputStream(d);
+ ClassLoader loader = Thread.currentThread().getContextClassLoader();
+ ObjectInputStream is =
+ new ObjectInputStream(in) {
+ @Override
+ protected Class<?> resolveClass(ObjectStreamClass desc)
+ throws IOException, ClassNotFoundException {
+ try {
+ return Class.forName(desc.getName(), true, loader);
+ } catch (ClassNotFoundException e) {
+ return super.resolveClass(desc);
+ }
+ }
+ };
+ return is.readObject();
+ } catch (Throwable e) {
+ log.error("Object deserialization failed", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ static byte[] serialize(Object o) {
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ObjectOutputStream os = new ObjectOutputStream(out)) {
+ os.writeObject(o);
+ return out.toByteArray();
+ } catch (Throwable e) {
+ log.error("Object serialization failed {}", o, e);
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/pg/PgSqlSource.java b/src/main/java/com/googlesource/gerrit/modules/cache/pg/PgSqlSource.java
new file mode 100644
index 0000000..fd635e8
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/pg/PgSqlSource.java
@@ -0,0 +1,63 @@
+// Copyright (C) 2018 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.googlesource.gerrit.modules.cache.pg;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.ThreadSettingsConfig;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.sql.Connection;
+import java.sql.SQLException;
+import javax.sql.DataSource;
+import org.apache.commons.dbcp.BasicDataSource;
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+public class PgSqlSource {
+ private static final String CACHE = "cache";
+
+ private final DataSource ds;
+
+ @Inject
+ PgSqlSource(@GerritServerConfig Config cfg, ThreadSettingsConfig threadSettingsConfig) {
+ this.ds = createDs(cfg, threadSettingsConfig);
+ }
+
+ Connection getConnection() throws SQLException {
+ return ds.getConnection();
+ }
+
+ private static final DataSource createDs(Config cfg, ThreadSettingsConfig threadSettingsConfig) {
+ BasicDataSource ds = new BasicDataSource();
+ ds.setUrl(cfg.getString(CACHE, null, "url"));
+ int poolLimit = threadSettingsConfig.getDatabasePoolLimit();
+ ds.setMaxActive(cfg.getInt(CACHE, "poolLimit", poolLimit));
+ ds.setDriverClassName("org.postgresql.Driver");
+ ds.setMinIdle(cfg.getInt(CACHE, "poolminidle", 4));
+ ds.setMaxIdle(cfg.getInt(CACHE, "poolmaxidle", Math.min(poolLimit, 16)));
+ ds.setInitialSize(ds.getMinIdle());
+ ds.setMaxWait(
+ ConfigUtil.getTimeUnit(
+ cfg, CACHE, null, "poolmaxwait", MILLISECONDS.convert(30, SECONDS), MILLISECONDS));
+ long evictIdleTimeMs = 1000L * 60;
+ ds.setMinEvictableIdleTimeMillis(evictIdleTimeMs);
+ ds.setTimeBetweenEvictionRunsMillis(evictIdleTimeMs / 2);
+ return ds;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/pg/PgSqlStore.java b/src/main/java/com/googlesource/gerrit/modules/cache/pg/PgSqlStore.java
new file mode 100644
index 0000000..31ae338
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/pg/PgSqlStore.java
@@ -0,0 +1,375 @@
+// Copyright (C) 2018 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.googlesource.gerrit.modules.cache.pg;
+
+import com.google.common.cache.Cache;
+import com.google.common.hash.BloomFilter;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.server.cache.PersistentCache.DiskStats;
+import com.google.inject.TypeLiteral;
+import java.io.InvalidClassException;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Timestamp;
+import java.sql.Types;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.atomic.AtomicLong;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class PgSqlStore<K, V> {
+ static final Logger log = LoggerFactory.getLogger(PgSqlStore.class);
+
+ private final PgSqlSource source;
+ private final String name;
+ private final KeyType<K> keyType;
+ private final long maxSize;
+ private final long expireAfterWrite;
+ private final BlockingQueue<PgSqlHandle> handles;
+ private final AtomicLong hitCount = new AtomicLong();
+ private final AtomicLong missCount = new AtomicLong();
+ private volatile BloomFilter<K> bloomFilter;
+ private int estimatedSize;
+
+ private final String qCount;
+ private final String qKeys;
+ private final String qValue;
+ private final String qTouch;
+ private final String qPut;
+ private final String qInvalidateKey;
+ private final String qInvalidateAll;
+ private final String qSum;
+ private final String qOrderByAccessed;
+ private final String qStats;
+
+ PgSqlStore(
+ PgSqlSource source,
+ String name,
+ TypeLiteral<K> keyType,
+ long maxSize,
+ long expireAfterWrite) {
+ this.source = source;
+ this.name = name;
+ this.keyType = KeyType.create(keyType);
+ this.maxSize = maxSize;
+ this.expireAfterWrite = expireAfterWrite;
+
+ int cores = Runtime.getRuntime().availableProcessors();
+ int keep = Math.min(cores, 16);
+ this.handles = new ArrayBlockingQueue<>(keep);
+
+ // initiate all query strings
+ this.qCount = "SELECT COUNT(*) FROM \"data_" + this.name + "\"";
+ this.qKeys = "SELECT k FROM \"data_" + this.name + "\"";
+ this.qValue = "SELECT v, created FROM \"data_" + this.name + "\" WHERE k=?";
+ this.qTouch = "UPDATE \"data_" + this.name + "\" SET accessed=? WHERE k=?";
+ this.qPut =
+ "INSERT INTO \"data_"
+ + this.name
+ + "\" (k, v, created, accessed) VALUES(?,?,?,?) "
+ + "ON CONFLICT (k) DO UPDATE "
+ + "SET v = EXCLUDED.v, created = EXCLUDED.created, accessed = EXCLUDED.accessed";
+ this.qInvalidateKey = "DELETE FROM \"data_" + this.name + "\" WHERE k=?";
+ this.qInvalidateAll = "DELETE FROM \"data_" + this.name + "\"";
+ this.qSum = "SELECT SUM(space) FROM \"data_" + this.name + "\"";
+ this.qOrderByAccessed =
+ "SELECT k,space,created FROM \"data_" + this.name + "\" ORDER BY accessed";
+ this.qStats = "SELECT COUNT(*),SUM(space) FROM \"data_" + this.name + "\"";
+ }
+
+ synchronized void open() {
+ if (bloomFilter == null) {
+ bloomFilter = buildBloomFilter();
+ }
+ }
+
+ void close() {
+ PgSqlHandle h;
+ while ((h = handles.poll()) != null) {
+ h.close();
+ }
+ }
+
+ boolean mightContain(K key) {
+ BloomFilter<K> b = bloomFilter;
+ if (b == null) {
+ synchronized (this) {
+ b = bloomFilter;
+ if (b == null) {
+ b = buildBloomFilter();
+ bloomFilter = b;
+ }
+ }
+ }
+ return b == null || b.mightContain(key);
+ }
+
+ private BloomFilter<K> buildBloomFilter() {
+ PgSqlHandle c = null;
+ try {
+ c = acquire();
+ try (Statement s = c.conn.createStatement()) {
+ if (estimatedSize <= 0) {
+ try (ResultSet r = s.executeQuery(qCount)) {
+ estimatedSize = r.next() ? r.getInt(1) : 0;
+ }
+ }
+
+ BloomFilter<K> b = newBloomFilter();
+ try (ResultSet r = s.executeQuery(qKeys)) {
+ while (r.next()) {
+ b.put(keyType.get(r, 1));
+ }
+ } catch (SQLException e) {
+ if (e.getCause() instanceof InvalidClassException) {
+ log.warn(
+ "Entries cached for {} "
+ + "have an incompatible class and can't be deserialized. "
+ + "Cache is flushed.",
+ name);
+ invalidateAll();
+ } else {
+ throw e;
+ }
+ }
+ return b;
+ }
+ } catch (SQLException e) {
+ log.warn("Cannot build BloomFilter for {}: {}", name, e.getMessage());
+ c = close(c);
+ return null;
+ } finally {
+ release(c);
+ }
+ }
+
+ ValueHolder<V> getIfPresent(K key) {
+ PgSqlHandle c = null;
+ try {
+ c = acquire();
+ if (c.get == null) {
+ c.get = c.conn.prepareStatement(qValue);
+ }
+ keyType.set(c.get, 1, key);
+ try (ResultSet r = c.get.executeQuery()) {
+ if (!r.next()) {
+ missCount.incrementAndGet();
+ return null;
+ }
+
+ Timestamp created = r.getTimestamp(2);
+ if (expired(created)) {
+ invalidate(key);
+ missCount.incrementAndGet();
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ V val = (V) PgSqlHandle.deserialize(r.getBytes(1));
+ ValueHolder<V> h = new ValueHolder<>(val);
+ h.clean = true;
+ hitCount.incrementAndGet();
+ touch(c, key);
+ return h;
+ } finally {
+ c.get.clearParameters();
+ }
+ } catch (SQLException e) {
+ log.warn("Cannot read cache {} for {}", name, key, e);
+ c = close(c);
+ return null;
+ } finally {
+ release(c);
+ }
+ }
+
+ private boolean expired(Timestamp created) {
+ if (expireAfterWrite == 0) {
+ return false;
+ }
+ long age = TimeUtil.nowMs() - created.getTime();
+ return 1000 * expireAfterWrite < age;
+ }
+
+ private void touch(PgSqlHandle c, K key) throws SQLException {
+ if (c.touch == null) {
+ c.touch = c.conn.prepareStatement(qTouch);
+ }
+ try {
+ c.touch.setTimestamp(1, TimeUtil.nowTs());
+ keyType.set(c.touch, 2, key);
+ c.touch.executeUpdate();
+ } finally {
+ c.touch.clearParameters();
+ }
+ }
+
+ void put(K key, ValueHolder<V> holder) {
+ if (holder.clean) {
+ return;
+ }
+
+ BloomFilter<K> b = bloomFilter;
+ if (b != null) {
+ b.put(key);
+ bloomFilter = b;
+ }
+
+ PgSqlHandle c = null;
+ try {
+ c = acquire();
+ if (c.put == null) {
+ c.put = c.conn.prepareStatement(qPut);
+ }
+ try {
+ keyType.set(c.put, 1, key);
+ c.put.setObject(2, PgSqlHandle.serialize(holder.value), Types.BINARY);
+ c.put.setTimestamp(3, new Timestamp(holder.created));
+ c.put.setTimestamp(4, TimeUtil.nowTs());
+ c.put.executeUpdate();
+ holder.clean = true;
+ } finally {
+ c.put.clearParameters();
+ }
+ } catch (SQLException e) {
+ log.warn("Cannot put into cache {}", name, e);
+ c = close(c);
+ } finally {
+ release(c);
+ }
+ }
+
+ void invalidate(K key) {
+ PgSqlHandle c = null;
+ try {
+ c = acquire();
+ invalidate(c, key);
+ } catch (SQLException e) {
+ log.warn("Cannot invalidate cache {}", name, e);
+ c = close(c);
+ } finally {
+ release(c);
+ }
+ }
+
+ private void invalidate(PgSqlHandle c, K key) throws SQLException {
+ if (c.invalidate == null) {
+ c.invalidate = c.conn.prepareStatement(qInvalidateKey);
+ }
+ try {
+ keyType.set(c.invalidate, 1, key);
+ c.invalidate.executeUpdate();
+ } finally {
+ c.invalidate.clearParameters();
+ }
+ }
+
+ void invalidateAll() {
+ PgSqlHandle c = null;
+ try {
+ c = acquire();
+ try (Statement s = c.conn.createStatement()) {
+ s.executeUpdate(qInvalidateAll);
+ }
+ bloomFilter = newBloomFilter();
+ } catch (SQLException e) {
+ log.warn("Cannot invalidate cache {}", name, e);
+ c = close(c);
+ } finally {
+ release(c);
+ }
+ }
+
+ void prune(Cache<K, ?> mem) {
+ PgSqlHandle c = null;
+ try {
+ c = acquire();
+ try (Statement s = c.conn.createStatement()) {
+ long used = 0;
+ try (ResultSet r = s.executeQuery(qSum)) {
+ used = r.next() ? r.getLong(1) : 0;
+ }
+ if (used <= maxSize) {
+ return;
+ }
+
+ try (ResultSet r = s.executeQuery(qOrderByAccessed)) {
+ while (maxSize < used && r.next()) {
+ K key = keyType.get(r, 1);
+ Timestamp created = r.getTimestamp(3);
+ if (mem.getIfPresent(key) != null && !expired(created)) {
+ touch(c, key);
+ } else {
+ invalidate(c, key);
+ used -= r.getLong(2);
+ }
+ }
+ }
+ }
+ } catch (SQLException e) {
+ log.warn("Cannot prune cache {}", name, e);
+ c = close(c);
+ } finally {
+ release(c);
+ }
+ }
+
+ DiskStats diskStats() {
+ long size = 0;
+ long space = 0;
+ PgSqlHandle c = null;
+ try {
+ c = acquire();
+ try (Statement s = c.conn.createStatement();
+ ResultSet r = s.executeQuery(qStats)) {
+ if (r.next()) {
+ size = r.getLong(1);
+ space = r.getLong(2);
+ }
+ }
+ } catch (SQLException e) {
+ log.warn("Cannot get DiskStats for {}", name, e);
+ c = close(c);
+ } finally {
+ release(c);
+ }
+ return new DiskStats(size, space, hitCount.get(), missCount.get());
+ }
+
+ private PgSqlHandle acquire() throws SQLException {
+ PgSqlHandle h = handles.poll();
+ return h != null ? h : new PgSqlHandle(source, name, keyType);
+ }
+
+ private void release(PgSqlHandle h) {
+ if (h != null && !handles.offer(h)) {
+ h.close();
+ }
+ }
+
+ private PgSqlHandle close(PgSqlHandle h) {
+ if (h != null) {
+ h.close();
+ }
+ return null;
+ }
+
+ private BloomFilter<K> newBloomFilter() {
+ int cnt = Math.max(64 * 1024, 2 * estimatedSize);
+ return BloomFilter.create(keyType.funnel(), cnt);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/pg/SinkOutputStream.java b/src/main/java/com/googlesource/gerrit/modules/cache/pg/SinkOutputStream.java
new file mode 100644
index 0000000..6e1d021
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/pg/SinkOutputStream.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2018 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.googlesource.gerrit.modules.cache.pg;
+
+import com.google.common.hash.PrimitiveSink;
+import java.io.OutputStream;
+
+class SinkOutputStream extends OutputStream {
+ private final PrimitiveSink sink;
+
+ SinkOutputStream(PrimitiveSink sink) {
+ this.sink = sink;
+ }
+
+ @Override
+ public void write(int b) {
+ sink.putByte((byte) b);
+ }
+
+ @Override
+ public void write(byte[] b, int p, int n) {
+ sink.putBytes(b, p, n);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/pg/ValueHolder.java b/src/main/java/com/googlesource/gerrit/modules/cache/pg/ValueHolder.java
new file mode 100644
index 0000000..bf0c71e
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/pg/ValueHolder.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2018 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.googlesource.gerrit.modules.cache.pg;
+
+class ValueHolder<V> {
+ final V value;
+ long created;
+ volatile boolean clean;
+
+ ValueHolder(V value) {
+ this.value = value;
+ }
+}
diff --git a/tools/bazel.rc b/tools/bazel.rc
new file mode 100644
index 0000000..4ed16cf
--- /dev/null
+++ b/tools/bazel.rc
@@ -0,0 +1,2 @@
+build --workspace_status_command=./tools/workspace-status.sh
+test --build_tests_only
diff --git a/tools/bzl/BUILD b/tools/bzl/BUILD
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tools/bzl/BUILD
diff --git a/tools/bzl/classpath.bzl b/tools/bzl/classpath.bzl
new file mode 100644
index 0000000..d5764f7
--- /dev/null
+++ b/tools/bzl/classpath.bzl
@@ -0,0 +1,4 @@
+load(
+ "@com_googlesource_gerrit_bazlets//tools:classpath.bzl",
+ "classpath_collector",
+)
diff --git a/tools/bzl/junit.bzl b/tools/bzl/junit.bzl
new file mode 100644
index 0000000..3af7e58
--- /dev/null
+++ b/tools/bzl/junit.bzl
@@ -0,0 +1,4 @@
+load(
+ "@com_googlesource_gerrit_bazlets//tools:junit.bzl",
+ "junit_tests",
+)
diff --git a/tools/bzl/maven_jar.bzl b/tools/bzl/maven_jar.bzl
new file mode 100644
index 0000000..2eabedb
--- /dev/null
+++ b/tools/bzl/maven_jar.bzl
@@ -0,0 +1 @@
+load("@com_googlesource_gerrit_bazlets//tools:maven_jar.bzl", "maven_jar")
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
new file mode 100644
index 0000000..a2e438f
--- /dev/null
+++ b/tools/bzl/plugin.bzl
@@ -0,0 +1,6 @@
+load(
+ "@com_googlesource_gerrit_bazlets//:gerrit_plugin.bzl",
+ "gerrit_plugin",
+ "PLUGIN_DEPS",
+ "PLUGIN_TEST_DEPS",
+)
diff --git a/tools/eclipse/BUILD b/tools/eclipse/BUILD
new file mode 100644
index 0000000..2d7b28a
--- /dev/null
+++ b/tools/eclipse/BUILD
@@ -0,0 +1,9 @@
+load("//tools/bzl:classpath.bzl", "classpath_collector")
+
+classpath_collector(
+ name = "main_classpath_collect",
+ testonly = 1,
+ deps = [
+ "//:cache-postgres__plugin_test_deps",
+ ],
+)
diff --git a/tools/eclipse/project.sh b/tools/eclipse/project.sh
new file mode 100755
index 0000000..c87efea
--- /dev/null
+++ b/tools/eclipse/project.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+#Copyright (C) 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+`bazel query @com_googlesource_gerrit_bazlets//tools/eclipse:project --output location | sed s/BUILD:.*//`project.py -n external-cache -r .
+
+
diff --git a/tools/workspace-status.sh b/tools/workspace-status.sh
new file mode 100755
index 0000000..1d4e3dd
--- /dev/null
+++ b/tools/workspace-status.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+#This script will be run by bazel when the build process starts to
+# generate key-value information that represents the status of the
+# workspace. The output should be like
+#
+# KEY1 VALUE1
+# KEY2 VALUE2
+#
+# If the script exits with non-zero code, it's considered as a failure
+# and the output will be discarded.
+
+function rev() {
+ cd $1; git describe --always --match "v[0-9].*" --dirty
+}
+
+echo STABLE_BUILD_CACHE-POSTGRES_LABEL $(rev .)