Merge changes from topic "expire-after-access"

* changes:
  Use expireAfterAccess with a larger cache size for ExternalIdCache
  Use CacheModule for the external ID cache again
  Support expiring from in-memory caches after last access
  Convert cache expireAfterWrite to nullable Duration
This commit is contained in:
Dave Borowitz
2018-06-07 12:27:35 +00:00
committed by Gerrit Code Review
19 changed files with 149 additions and 71 deletions

View File

@@ -753,6 +753,7 @@ Default is 1024 for most caches, except:
* `"diff"`: default is `10m` (10 MiB of memory)
* `"diff_intraline"`: default is `10m` (10 MiB of memory)
* `"diff_summary"`: default is `10m` (10 MiB of memory)
* `"external_ids_map"`: default is `2` and should not be changed
* `"groups"`: default is unlimited
* `"groups_byname"`: default is unlimited
* `"groups_byuuid"`: default is unlimited
@@ -762,6 +763,16 @@ Default is 1024 for most caches, except:
If set to 0 the cache is disabled. Entries are removed immediately
after being stored by the cache. This is primarily useful for testing.
[[cache.name.expireFromMemoryAfterAccess]]cache.<name>.expireFromMemoryAfterAccess::
+
Time after last access to automatically expire entries from an in-memory
cache. If 0 or not specified, entries are never expired in this manner.
Values may use unit suffixes as in link:#cache.name.maxAge[maxAge].
+
This option only applies to in-memory caches; persistent cache values are
not expired in this manner, and are only pruned via
link:#cache.name.diskLimit[diskLimit].
[[cache.name.diskLimit]]cache.<name>.diskLimit::
+
Total size in bytes of the keys and values stored on disk. Caches that
@@ -844,6 +855,16 @@ Ideally, disk limit of this cache is large enough to cover all changes.
This should significantly speed up change reindexing, especially
full offline reindexing.
cache `"external_ids_map"`::
+
A singleton cache whose sole entry is a map of the parsed representation
of link:config-accounts.html#external-ids[all current external IDs]. The
cache may temporarily contain 2 entries, but the second one is promptly
expired.
+
It is not recommended to change the attributes of this cache away from
the defaults.
cache `"git_tags"`::
+
If branch or reference level READ access controls are used, this

View File

@@ -17,6 +17,7 @@ package com.google.gerrit.common;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.annotations.VisibleForTesting;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.function.LongSupplier;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.storage.file.FileBasedConfig;
@@ -36,6 +37,10 @@ public class TimeUtil {
return currentMillisSupplier.getAsLong();
}
public static Instant now() {
return Instant.ofEpochMilli(nowMs());
}
public static Timestamp nowTs() {
return new Timestamp(nowMs());
}

View File

@@ -44,10 +44,10 @@ import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
import java.io.IOException;
import java.time.Duration;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
@@ -119,7 +119,7 @@ public class GitOverHttpServlet extends GitServlet {
protected void configure() {
cache(ID_CACHE, AdvertisedObjectsCacheKey.class, new TypeLiteral<Set<ObjectId>>() {})
.maximumWeight(4096)
.expireAfterWrite(10, TimeUnit.MINUTES);
.expireAfterWrite(Duration.ofMinutes(10));
}
});
}

View File

@@ -14,8 +14,6 @@
package com.google.gerrit.httpd;
import static java.util.concurrent.TimeUnit.MINUTES;
import com.google.common.cache.Cache;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.registration.DynamicItem;
@@ -30,6 +28,7 @@ import com.google.inject.Provider;
import com.google.inject.assistedinject.FactoryModuleBuilder;
import com.google.inject.name.Named;
import com.google.inject.servlet.RequestScoped;
import java.time.Duration;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -41,10 +40,8 @@ public class H2CacheBasedWebSession extends CacheBasedWebSession {
protected void configure() {
persist(WebSessionManager.CACHE_NAME, String.class, Val.class)
.maximumWeight(1024) // reasonable default for many sites
.expireAfterWrite(
CacheBasedWebSession.MAX_AGE_MINUTES,
MINUTES) // expire sessions if they are inactive
;
// expire sessions if they are inactive
.expireAfterWrite(Duration.ofMinutes(CacheBasedWebSession.MAX_AGE_MINUTES));
install(new FactoryModuleBuilder().build(WebSessionManagerFactory.class));
DynamicItem.itemOf(binder(), WebSession.class);
DynamicItem.bind(binder(), WebSession.class)

View File

@@ -16,7 +16,6 @@ package com.google.gerrit.server.account.externalids;
import com.google.auto.value.AutoValue;
import com.google.common.base.Strings;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableSetMultimap;
@@ -29,6 +28,7 @@ import com.google.common.flogger.FluentLogger;
import com.google.gerrit.reviewdb.client.Account;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import java.io.IOException;
import java.util.Collection;
import java.util.Set;
@@ -43,31 +43,17 @@ import org.eclipse.jgit.lib.ObjectId;
class ExternalIdCacheImpl implements ExternalIdCache {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
public static final String CACHE_NAME = "external_ids_map";
private final LoadingCache<ObjectId, AllExternalIds> extIdsByAccount;
private final ExternalIdReader externalIdReader;
private final Lock lock;
@Inject
ExternalIdCacheImpl(ExternalIdReader externalIdReader) {
this.extIdsByAccount =
CacheBuilder.newBuilder()
// The cached data is potentially pretty large and we are always only interested
// in the latest value, hence the maximum cache size is set to 1.
// This can lead to extra cache loads in case of the following race:
// 1. thread 1 reads the notes ref at revision A
// 2. thread 2 updates the notes ref to revision B and stores the derived value
// for B in the cache
// 3. thread 1 attempts to read the data for revision A from the cache, and misses
// 4. later threads attempt to read at B
// In this race unneeded reloads are done in step 3 (reload from revision A) and
// step 4 (reload from revision B, because the value for revision B was lost when the
// reload from revision A was done, since the cache can hold only one entry).
// These reloads could be avoided by increasing the cache size to 2. However the race
// window between reading the ref and looking it up in the cache is small so that
// it's rare that this race happens. Therefore it's not worth to double the memory
// usage of this cache, just to avoid this.
.maximumSize(1)
.build(new Loader(externalIdReader));
ExternalIdCacheImpl(
@Named(CACHE_NAME) LoadingCache<ObjectId, AllExternalIds> extIdsByAccount,
ExternalIdReader externalIdReader) {
this.extIdsByAccount = extIdsByAccount;
this.externalIdReader = externalIdReader;
this.lock = new ReentrantLock(true /* fair */);
}
@@ -159,9 +145,10 @@ class ExternalIdCacheImpl implements ExternalIdCache {
}
}
private static class Loader extends CacheLoader<ObjectId, AllExternalIds> {
static class Loader extends CacheLoader<ObjectId, AllExternalIds> {
private final ExternalIdReader externalIdReader;
@Inject
Loader(ExternalIdReader externalIdReader) {
this.externalIdReader = externalIdReader;
}

View File

@@ -14,11 +14,27 @@
package com.google.gerrit.server.account.externalids;
import com.google.inject.AbstractModule;
import com.google.gerrit.server.account.externalids.ExternalIdCacheImpl.AllExternalIds;
import com.google.gerrit.server.account.externalids.ExternalIdCacheImpl.Loader;
import com.google.gerrit.server.cache.CacheModule;
import com.google.inject.TypeLiteral;
import java.time.Duration;
import org.eclipse.jgit.lib.ObjectId;
public class ExternalIdModule extends AbstractModule {
public class ExternalIdModule extends CacheModule {
@Override
protected void configure() {
cache(ExternalIdCacheImpl.CACHE_NAME, ObjectId.class, new TypeLiteral<AllExternalIds>() {})
// The cached data is potentially pretty large and we are always only interested
// in the latest value. However, due to a race condition, it is possible for different
// threads to observe different values of the meta ref, and hence request different keys
// from the cache. Extend the cache size by 1 to cover this case, but expire the extra
// object after a short period of time, since it may be a potentially large amount of
// memory.
.maximumWeight(2)
.expireFromMemoryAfterAccess(Duration.ofMinutes(1))
.loader(Loader.class);
bind(ExternalIdCacheImpl.class);
bind(ExternalIdCache.class).to(ExternalIdCacheImpl.class);
}

View File

@@ -14,8 +14,6 @@
package com.google.gerrit.server.auth.ldap;
import static java.util.concurrent.TimeUnit.HOURS;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.reviewdb.client.Account;
@@ -25,6 +23,7 @@ import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.cache.CacheModule;
import com.google.inject.Scopes;
import com.google.inject.TypeLiteral;
import java.time.Duration;
import java.util.Optional;
import java.util.Set;
@@ -37,18 +36,18 @@ public class LdapModule extends CacheModule {
@Override
protected void configure() {
cache(GROUP_CACHE, String.class, new TypeLiteral<Set<AccountGroup.UUID>>() {})
.expireAfterWrite(1, HOURS)
.expireAfterWrite(Duration.ofHours(1))
.loader(LdapRealm.MemberLoader.class);
cache(USERNAME_CACHE, String.class, new TypeLiteral<Optional<Account.Id>>() {})
.loader(LdapRealm.UserLoader.class);
cache(GROUP_EXIST_CACHE, String.class, new TypeLiteral<Boolean>() {})
.expireAfterWrite(1, HOURS)
.expireAfterWrite(Duration.ofHours(1))
.loader(LdapRealm.ExistenceLoader.class);
cache(PARENT_GROUPS_CACHE, String.class, new TypeLiteral<ImmutableSet<String>>() {})
.expireAfterWrite(1, HOURS);
.expireAfterWrite(Duration.ofHours(1));
bind(Helper.class);
bind(Realm.class).to(LdapRealm.class).in(Scopes.SINGLETON);

View File

@@ -16,15 +16,18 @@ package com.google.gerrit.server.cache;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.Weigher;
import java.util.concurrent.TimeUnit;
import java.time.Duration;
/** Configure a cache declared within a {@link CacheModule} instance. */
public interface CacheBinding<K, V> {
/** Set the total size of the cache. */
CacheBinding<K, V> maximumWeight(long weight);
/** Set the time an element lives before being expired. */
CacheBinding<K, V> expireAfterWrite(long duration, TimeUnit durationUnits);
/** Set the time an element lives after last write before being expired. */
CacheBinding<K, V> expireAfterWrite(Duration duration);
/** Set the time an element lives after last access before being expired. */
CacheBinding<K, V> expireFromMemoryAfterAccess(Duration duration);
/** Populate the cache with items from the CacheLoader. */
CacheBinding<K, V> loader(Class<? extends CacheLoader<K, V>> clazz);

View File

@@ -18,7 +18,7 @@ import com.google.common.cache.CacheLoader;
import com.google.common.cache.Weigher;
import com.google.gerrit.common.Nullable;
import com.google.inject.TypeLiteral;
import java.util.concurrent.TimeUnit;
import java.time.Duration;
public interface CacheDef<K, V> {
/**
@@ -45,7 +45,10 @@ public interface CacheDef<K, V> {
long maximumWeight();
@Nullable
Long expireAfterWrite(TimeUnit unit);
Duration expireAfterWrite();
@Nullable
Duration expireFromMemoryAfterAccess();
@Nullable
Weigher<K, V> weigher();

View File

@@ -16,7 +16,6 @@ package com.google.gerrit.server.cache;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.common.base.Strings;
import com.google.common.cache.Cache;
@@ -27,7 +26,7 @@ import com.google.gerrit.extensions.annotations.PluginName;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.TypeLiteral;
import java.util.concurrent.TimeUnit;
import java.time.Duration;
class CacheProvider<K, V> implements Provider<Cache<K, V>>, CacheBinding<K, V>, CacheDef<K, V> {
private final CacheModule module;
@@ -36,7 +35,8 @@ class CacheProvider<K, V> implements Provider<Cache<K, V>>, CacheBinding<K, V>,
private final TypeLiteral<V> valType;
private String configKey;
private long maximumWeight;
private Long expireAfterWrite;
private Duration expireAfterWrite;
private Duration expireFromMemoryAfterAccess;
private Provider<CacheLoader<K, V>> loader;
private Provider<Weigher<K, V>> weigher;
@@ -69,9 +69,16 @@ class CacheProvider<K, V> implements Provider<Cache<K, V>>, CacheBinding<K, V>,
}
@Override
public CacheBinding<K, V> expireAfterWrite(long duration, TimeUnit unit) {
public CacheBinding<K, V> expireAfterWrite(Duration duration) {
checkNotFrozen();
expireAfterWrite = SECONDS.convert(duration, unit);
expireAfterWrite = duration;
return this;
}
@Override
public CacheBinding<K, V> expireFromMemoryAfterAccess(Duration duration) {
checkNotFrozen();
expireFromMemoryAfterAccess = duration;
return this;
}
@@ -126,8 +133,14 @@ class CacheProvider<K, V> implements Provider<Cache<K, V>>, CacheBinding<K, V>,
@Override
@Nullable
public Long expireAfterWrite(TimeUnit unit) {
return expireAfterWrite != null ? unit.convert(expireAfterWrite, SECONDS) : null;
public Duration expireAfterWrite() {
return expireAfterWrite;
}
@Override
@Nullable
public Duration expireFromMemoryAfterAccess() {
return expireFromMemoryAfterAccess;
}
@Override

View File

@@ -16,7 +16,7 @@ package com.google.gerrit.server.cache;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.Weigher;
import java.util.concurrent.TimeUnit;
import java.time.Duration;
/** Configure a persistent cache declared within a {@link CacheModule} instance. */
public interface PersistentCacheBinding<K, V> extends CacheBinding<K, V> {
@@ -24,7 +24,7 @@ public interface PersistentCacheBinding<K, V> extends CacheBinding<K, V> {
PersistentCacheBinding<K, V> maximumWeight(long weight);
@Override
PersistentCacheBinding<K, V> expireAfterWrite(long duration, TimeUnit durationUnits);
PersistentCacheBinding<K, V> expireAfterWrite(Duration duration);
@Override
PersistentCacheBinding<K, V> loader(Class<? extends CacheLoader<K, V>> clazz);

View File

@@ -24,7 +24,7 @@ import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.TypeLiteral;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
import java.time.Duration;
class PersistentCacheProvider<K, V> extends CacheProvider<K, V>
implements Provider<Cache<K, V>>, PersistentCacheBinding<K, V>, PersistentCacheDef<K, V> {
@@ -53,8 +53,8 @@ class PersistentCacheProvider<K, V> extends CacheProvider<K, V>
}
@Override
public PersistentCacheBinding<K, V> expireAfterWrite(long duration, TimeUnit durationUnits) {
return (PersistentCacheBinding<K, V>) super.expireAfterWrite(duration, durationUnits);
public PersistentCacheBinding<K, V> expireAfterWrite(Duration duration) {
return (PersistentCacheBinding<K, V>) super.expireAfterWrite(duration);
}
@Override

View File

@@ -3,6 +3,7 @@ java_library(
srcs = glob(["**/*.java"]),
visibility = ["//visibility:public"],
deps = [
"//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/common:server",
"//java/com/google/gerrit/extensions:api",
"//java/com/google/gerrit/lifecycle",

View File

@@ -16,11 +16,12 @@ package com.google.gerrit.server.cache.h2;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.Weigher;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.server.cache.CacheSerializer;
import com.google.gerrit.server.cache.PersistentCacheDef;
import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
import com.google.inject.TypeLiteral;
import java.util.concurrent.TimeUnit;
import java.time.Duration;
class H2CacheDefProxy<K, V> implements PersistentCacheDef<K, V> {
private final PersistentCacheDef<K, V> source;
@@ -30,8 +31,15 @@ class H2CacheDefProxy<K, V> implements PersistentCacheDef<K, V> {
}
@Override
public Long expireAfterWrite(TimeUnit unit) {
return source.expireAfterWrite(unit);
@Nullable
public Duration expireAfterWrite() {
return source.expireAfterWrite();
}
@Override
@Nullable
public Duration expireFromMemoryAfterAccess() {
return source.expireFromMemoryAfterAccess();
}
@SuppressWarnings("unchecked")

View File

@@ -215,7 +215,6 @@ class H2CacheFactory implements PersistentCacheFactory, LifecycleListener {
if (h2AutoServer) {
url.append(";AUTO_SERVER=TRUE");
}
Long expireAfterWrite = def.expireAfterWrite(TimeUnit.SECONDS);
return new SqlStore<>(
url.toString(),
def.keyType(),
@@ -223,6 +222,6 @@ class H2CacheFactory implements PersistentCacheFactory, LifecycleListener {
def.valueSerializer(),
def.version(),
maxSize,
expireAfterWrite == null ? 0 : expireAfterWrite.longValue());
def.expireAfterWrite());
}
}

View File

@@ -23,6 +23,7 @@ import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.common.hash.BloomFilter;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.server.cache.CacheSerializer;
import com.google.gerrit.server.cache.PersistentCache;
@@ -35,6 +36,7 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.time.Duration;
import java.util.Calendar;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
@@ -254,7 +256,7 @@ public class H2CacheImpl<K, V> extends AbstractLoadingCache<K, V> implements Per
private final CacheSerializer<V> valueSerializer;
private final int version;
private final long maxSize;
private final long expireAfterWrite;
@Nullable private final Duration expireAfterWrite;
private final BlockingQueue<SqlHandle> handles;
private final AtomicLong hitCount = new AtomicLong();
private final AtomicLong missCount = new AtomicLong();
@@ -268,7 +270,7 @@ public class H2CacheImpl<K, V> extends AbstractLoadingCache<K, V> implements Per
CacheSerializer<V> valueSerializer,
int version,
long maxSize,
long expireAfterWrite) {
@Nullable Duration expireAfterWrite) {
this.url = jdbcUrl;
this.keyType = createKeyType(keyType, keySerializer);
this.valueSerializer = valueSerializer;
@@ -421,11 +423,11 @@ public class H2CacheImpl<K, V> extends AbstractLoadingCache<K, V> implements Per
}
private boolean expired(Timestamp created) {
if (expireAfterWrite == 0) {
if (expireAfterWrite == null) {
return false;
}
long age = TimeUtil.nowMs() - created.getTime();
return 1000 * expireAfterWrite < age;
Duration age = Duration.between(created.toInstant(), TimeUtil.now());
return age.compareTo(expireAfterWrite) > 0;
}
private void touch(SqlHandle c, K key) throws IOException, SQLException {

View File

@@ -3,6 +3,7 @@ java_library(
srcs = glob(["*.java"]),
visibility = ["//visibility:public"],
deps = [
"//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/extensions:api",
"//java/com/google/gerrit/server",
"//lib:guava",

View File

@@ -14,19 +14,23 @@
package com.google.gerrit.server.cache.mem;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.common.base.Strings;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.Weigher;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.server.cache.CacheDef;
import com.google.gerrit.server.cache.ForwardingRemovalListener;
import com.google.gerrit.server.cache.MemoryCacheFactory;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.inject.Inject;
import java.util.concurrent.TimeUnit;
import java.time.Duration;
import org.eclipse.jgit.lib.Config;
class DefaultMemoryCacheFactory implements MemoryCacheFactory {
@@ -66,19 +70,38 @@ class DefaultMemoryCacheFactory implements MemoryCacheFactory {
}
builder.weigher(weigher);
Long age = def.expireAfterWrite(TimeUnit.SECONDS);
Duration expireAfterWrite = def.expireAfterWrite();
if (has(def.configKey(), "maxAge")) {
builder.expireAfterWrite(
ConfigUtil.getTimeUnit(
cfg, "cache", def.configKey(), "maxAge", age != null ? age : 0, TimeUnit.SECONDS),
TimeUnit.SECONDS);
} else if (age != null) {
builder.expireAfterWrite(age, TimeUnit.SECONDS);
cfg, "cache", def.configKey(), "maxAge", toSeconds(expireAfterWrite), SECONDS),
SECONDS);
} else if (expireAfterWrite != null) {
builder.expireAfterWrite(expireAfterWrite.toNanos(), NANOSECONDS);
}
Duration expireAfterAccess = def.expireFromMemoryAfterAccess();
if (has(def.configKey(), "expireFromMemoryAfterAccess")) {
builder.expireAfterAccess(
ConfigUtil.getTimeUnit(
cfg,
"cache",
def.configKey(),
"expireFromMemoryAfterAccess",
toSeconds(expireAfterAccess),
SECONDS),
SECONDS);
} else if (expireAfterAccess != null) {
builder.expireAfterAccess(expireAfterAccess.toNanos(), NANOSECONDS);
}
return builder;
}
private static long toSeconds(@Nullable Duration duration) {
return duration != null ? duration.getSeconds() : 0;
}
private boolean has(String name, String var) {
return !Strings.isNullOrEmpty(cfg.getString("cache", name, var));
}

View File

@@ -47,7 +47,7 @@ public class H2CacheTest {
StringSerializer.INSTANCE,
version,
1 << 20,
0);
null);
return new H2CacheImpl<>(MoreExecutors.directExecutor(), store, KEY_TYPE, mem);
}