Persist caches keys index at configurable pace
Introduce 'cache.persistIndexEvery' parameter (by default '15m') that
specifies how often indexes should be persisted to a disk.
Notes:
* in order to avoid race condition between evict and persist operations
the latter is performed after the former is finished
* considering the above the smallest duration between persist operations
is 30s amd smaller values of 'persistIndexEvery' will be rounded up to
30s
* higher values are rounded down (if needed) to the closest multiple of
30s
* persist operation is additionally called on Gerrit exit - regardless
of the configured duration but it will be skipped on a particular
index level if there is one already running (as a result of schedule)
* persist operation is scheduled in a dedicated thread
'ChronicleMap-Index-0'
Example when 'cache.persistIndexEvery = 2m' it means that persist
operation will be performed after every 4th eviction is finished.
And additionally on Gerrit exit provided that there is no one in
progress.
Bug: Issue 15121
Change-Id: Ib6ea9c0e2819bf6d93ab4654db9e8a2abc8b5b20
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/AutoAdjustCaches.java b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/AutoAdjustCaches.java
index 6b2b546..e7e1415 100644
--- a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/AutoAdjustCaches.java
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/AutoAdjustCaches.java
@@ -212,7 +212,8 @@
newMaxEntries,
averageKeySize,
averageValueSize,
- currentChronicleMapConfig.getMaxBloatFactor());
+ currentChronicleMapConfig.getMaxBloatFactor(),
+ currentChronicleMapConfig.getPersistIndexEvery());
}
private long newMaxEntries(ChronicleMapCacheImpl<Object, Object> currentCache) {
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/CacheKeysIndex.java b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/CacheKeysIndex.java
index 9e56321..c9aff80 100644
--- a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/CacheKeysIndex.java
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/CacheKeysIndex.java
@@ -41,6 +41,7 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
class CacheKeysIndex<T> {
@@ -178,6 +179,7 @@
private final String name;
private final File indexFile;
private final File tempIndexFile;
+ private final AtomicBoolean persistInProgress;
CacheKeysIndex(MetricMaker metricMaker, String name, File indexFile, boolean cacheFileExists) {
this.keys = Collections.synchronizedSet(new LinkedHashSet<>());
@@ -185,6 +187,7 @@
this.name = name;
this.indexFile = indexFile;
this.tempIndexFile = tempIndexFile(indexFile);
+ this.persistInProgress = new AtomicBoolean(false);
restore(cacheFileExists);
}
@@ -251,6 +254,14 @@
}
void persist() {
+ if (!persistInProgress.compareAndSet(false, true)) {
+ logger.atWarning().log(
+ "Persist cache keys index %s to %s file is already in progress. This persist request was"
+ + " skipped.",
+ name, indexFile);
+ return;
+ }
+
logger.atInfo().log("Persisting cache keys index %s to %s file", name, indexFile);
try (Timer0.Context timer = metrics.persistLatency.start()) {
ArrayList<TimedKey> toPersist;
@@ -277,6 +288,8 @@
logger.atSevere().withCause(e).log("Persisting cache keys index %s failed", name);
metrics.persistFailures.increment();
}
+ } finally {
+ persistInProgress.set(false);
}
}
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheConfig.java b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheConfig.java
index 2b81078..3c7f380 100644
--- a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheConfig.java
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheConfig.java
@@ -13,9 +13,11 @@
// limitations under the License.
package com.googlesource.gerrit.modules.cache.chroniclemap;
+import static com.googlesource.gerrit.modules.cache.chroniclemap.ChronicleMapCacheFactory.PRUNE_DELAY;
import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.common.collect.ImmutableMap;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
@@ -29,6 +31,8 @@
import org.eclipse.jgit.lib.Config;
public class ChronicleMapCacheConfig {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
private final File cacheFile;
private final boolean cacheFileExists;
private final File indexFile;
@@ -40,6 +44,8 @@
private final int maxBloatFactor;
private final int percentageFreeSpaceEvictionThreshold;
private final String configKey;
+ private final Duration persistIndexEvery;
+ private final long persistIndexEveryNthPrune;
public interface Factory {
ChronicleMapCacheConfig create(
@@ -56,7 +62,8 @@
@Assisted("maxEntries") long maxEntries,
@Assisted("avgKeySize") long avgKeySize,
@Assisted("avgValueSize") long avgValueSize,
- @Assisted("maxBloatFactor") int maxBloatFactor);
+ @Assisted("maxBloatFactor") int maxBloatFactor,
+ @Assisted("PersistIndexEvery") Duration persistIndexEver);
}
@AssistedInject
@@ -76,7 +83,14 @@
cfg.getLong("cache", configKey, "maxEntries", Defaults.maxEntriesFor(configKey)),
cfg.getLong("cache", configKey, "avgKeySize", Defaults.averageKeySizeFor(configKey)),
cfg.getLong("cache", configKey, "avgValueSize", Defaults.avgValueSizeFor(configKey)),
- cfg.getInt("cache", configKey, "maxBloatFactor", Defaults.maxBloatFactorFor(configKey)));
+ cfg.getInt("cache", configKey, "maxBloatFactor", Defaults.maxBloatFactorFor(configKey)),
+ Duration.ofSeconds(
+ cfg.getTimeUnit(
+ "cache",
+ null,
+ "persistIndexEvery",
+ Defaults.persistIndexEvery().toSeconds(),
+ SECONDS)));
}
@AssistedInject
@@ -89,7 +103,8 @@
@Assisted("maxEntries") long maxEntries,
@Assisted("avgKeySize") long avgKeySize,
@Assisted("avgValueSize") long avgValueSize,
- @Assisted("maxBloatFactor") int maxBloatFactor) {
+ @Assisted("maxBloatFactor") int maxBloatFactor,
+ @Assisted("PersistIndexEvery") Duration persistIndexEvery) {
this.cacheFile = cacheFile;
this.cacheFileExists = cacheFile.exists();
this.indexFile = resolveIndexFile(cacheFile);
@@ -120,6 +135,23 @@
configKey,
"percentageFreeSpaceEvictionThreshold",
Defaults.percentageFreeSpaceEvictionThreshold());
+
+ long persistIndexEverySeconds = persistIndexEvery.getSeconds();
+ if (persistIndexEverySeconds < PRUNE_DELAY) {
+ logger.atWarning().log(
+ "Configured 'persistIndexEvery' duration [%ds] is lower than minimal threshold [%ds]."
+ + " Minimal threshold will be used.",
+ persistIndexEverySeconds, PRUNE_DELAY);
+ persistIndexEverySeconds = PRUNE_DELAY;
+ } else if (persistIndexEverySeconds % PRUNE_DELAY != 0L) {
+ logger.atWarning().log(
+ "Configured 'persistIndexEvery' duration [%ds] will be rounded down to a multiple of"
+ + " [%ds]. [%ds] is the minimal 'persistIndexEvery' resolution.",
+ persistIndexEverySeconds, PRUNE_DELAY, PRUNE_DELAY);
+ persistIndexEverySeconds = (persistIndexEverySeconds / PRUNE_DELAY) * PRUNE_DELAY;
+ }
+ this.persistIndexEvery = Duration.ofSeconds(persistIndexEverySeconds);
+ this.persistIndexEveryNthPrune = persistIndexEverySeconds / PRUNE_DELAY;
}
public int getPercentageFreeSpaceEvictionThreshold() {
@@ -134,6 +166,14 @@
return refreshAfterWrite;
}
+ public Duration getPersistIndexEvery() {
+ return persistIndexEvery;
+ }
+
+ public long getPersistIndexEveryNthPrune() {
+ return persistIndexEveryNthPrune;
+ }
+
public long getMaxEntries() {
return maxEntries;
}
@@ -188,6 +228,8 @@
public static final int DEFAULT_PERCENTAGE_FREE_SPACE_EVICTION_THRESHOLD = 90;
+ public static final Duration DEFAULT_PERSIST_INDEX_EVERY = Duration.ofMinutes(15);
+
private static final ImmutableMap<String, DefaultConfig> defaultMap =
new ImmutableMap.Builder<String, DefaultConfig>()
.put("web_sessions", DefaultConfig.create(45, 221, 1000, 1))
@@ -232,5 +274,9 @@
public static int percentageFreeSpaceEvictionThreshold() {
return DEFAULT_PERCENTAGE_FREE_SPACE_EVICTION_THRESHOLD;
}
+
+ public static Duration persistIndexEvery() {
+ return DEFAULT_PERSIST_INDEX_EVERY;
+ }
}
}
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheFactory.java b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheFactory.java
index 1161699..e4474b0 100644
--- a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheFactory.java
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheFactory.java
@@ -46,6 +46,8 @@
@Singleton
class ChronicleMapCacheFactory extends PersistentCacheBaseFactory implements LifecycleListener {
+ static final long PRUNE_DELAY = 30;
+
private final ChronicleMapCacheConfig.Factory configFactory;
private final MetricMaker metricMaker;
private final DynamicMap<Cache<?, ?>> cacheMap;
@@ -53,6 +55,7 @@
private final ScheduledExecutorService cleanup;
private final LoggingContextAwareExecutorService storePersistenceExecutor;
+ private final LoggingContextAwareExecutorService indexPersistenceExecutor;
@Inject
ChronicleMapCacheFactory(
@@ -79,6 +82,10 @@
new LoggingContextAwareExecutorService(
Executors.newFixedThreadPool(
1, new ThreadFactoryBuilder().setNameFormat("ChronicleMap-Store-%d").build()));
+ this.indexPersistenceExecutor =
+ new LoggingContextAwareExecutorService(
+ Executors.newFixedThreadPool(
+ 1, new ThreadFactoryBuilder().setNameFormat("ChronicleMap-Index-%d").build()));
}
@Override
@@ -121,7 +128,8 @@
config,
metricMaker,
memLoader,
- new InMemoryCacheLoadingFromStoreImpl<>(mem, false));
+ new InMemoryCacheLoadingFromStoreImpl<>(mem, false),
+ indexPersistenceExecutor);
} catch (IOException e) {
throw new UncheckedIOException(e);
@@ -173,7 +181,8 @@
config,
metricMaker,
memLoader,
- new InMemoryCacheLoadingFromStoreImpl<>(mem, true));
+ new InMemoryCacheLoadingFromStoreImpl<>(mem, true),
+ indexPersistenceExecutor);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
@@ -198,13 +207,19 @@
@Override
public void start() {
for (ChronicleMapCacheImpl<?, ?> cache : caches) {
- cleanup.scheduleWithFixedDelay(cache::prune, 30, 30, TimeUnit.SECONDS);
+ cleanup.scheduleWithFixedDelay(cache::prune, PRUNE_DELAY, PRUNE_DELAY, TimeUnit.SECONDS);
}
}
@Override
public void stop() {
cleanup.shutdownNow();
+ for (ChronicleMapCacheImpl<?, ?> cache : caches) {
+ cache.close();
+ }
+ caches.clear();
+ storePersistenceExecutor.shutdown();
+ indexPersistenceExecutor.shutdown();
}
public static File fileName(Path cacheDir, String name, Integer version) {
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheImpl.java b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheImpl.java
index 8f33271..b9d5c29 100644
--- a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheImpl.java
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheImpl.java
@@ -28,6 +28,7 @@
import java.time.Instant;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
import java.util.concurrent.atomic.LongAdder;
import net.openhft.chronicle.map.ChronicleMap;
import net.openhft.chronicle.map.ChronicleMapBuilder;
@@ -48,6 +49,8 @@
private final PersistentCacheDef<K, V> cacheDefinition;
private final ChronicleMapCacheLoader<K, V> memLoader;
private final InMemoryCache<K, V> mem;
+ private final Executor indexPersistenceExecutor;
+ private long pruneCount;
ChronicleMapCacheImpl(PersistentCacheDef<K, V> def, ChronicleMapCacheConfig config)
throws IOException {
@@ -63,6 +66,7 @@
new ChronicleMapCacheLoader<>(
MoreExecutors.directExecutor(), store, config.getExpireAfterWrite());
this.mem = memLoader.asInMemoryCacheBypass();
+ this.indexPersistenceExecutor = MoreExecutors.directExecutor();
}
ChronicleMapCacheImpl(
@@ -70,7 +74,8 @@
ChronicleMapCacheConfig config,
MetricMaker metricMaker,
ChronicleMapCacheLoader<K, V> memLoader,
- InMemoryCache<K, V> mem) {
+ InMemoryCache<K, V> mem,
+ Executor indexPersistenceExecutor) {
this.cacheDefinition = def;
this.config = config;
@@ -80,6 +85,7 @@
this.memLoader = memLoader;
this.mem = mem;
this.store = memLoader.getStore();
+ this.indexPersistenceExecutor = indexPersistenceExecutor;
}
@SuppressWarnings({"unchecked", "cast", "rawtypes"})
@@ -121,7 +127,7 @@
logger.atInfo().log(
"Initialized '%s'|version: %s|avgKeySize: %s bytes|avgValueSize:"
+ " %s bytes|entries: %s|maxBloatFactor: %s|remainingAutoResizes:"
- + " %s|percentageFreeSpace: %s",
+ + " %s|percentageFreeSpace: %s|persistIndexEvery: %s",
def.name(),
def.version(),
mapBuilder.constantlySizedKeys() ? "CONSTANT" : config.getAverageKeySize(),
@@ -129,7 +135,8 @@
config.getMaxEntries(),
config.getMaxBloatFactor(),
store.remainingAutoResizes(),
- store.percentageFreeSpace());
+ store.percentageFreeSpace(),
+ config.getPersistIndexEvery());
return new ChronicleMapStore<>(store, config, metricMaker);
}
@@ -274,7 +281,9 @@
evictColdEntries();
}
- keysIndex.persist();
+ if (++pruneCount % config.getPersistIndexEveryNthPrune() == 0L) {
+ indexPersistenceExecutor.execute(keysIndex::persist);
+ }
}
private boolean needsRefresh(long created) {
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/H2MigrationServlet.java b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/H2MigrationServlet.java
index f96786d..c3959e9 100644
--- a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/H2MigrationServlet.java
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/H2MigrationServlet.java
@@ -14,6 +14,7 @@
package com.googlesource.gerrit.modules.cache.chroniclemap;
+import static com.googlesource.gerrit.modules.cache.chroniclemap.ChronicleMapCacheConfig.Defaults.persistIndexEvery;
import static com.googlesource.gerrit.modules.cache.chroniclemap.H2CacheCommand.H2_SUFFIX;
import static com.googlesource.gerrit.modules.cache.chroniclemap.H2CacheCommand.getStats;
import static com.googlesource.gerrit.modules.cache.chroniclemap.H2CacheCommand.jdbcUrl;
@@ -295,7 +296,8 @@
stats.size() * sizeMultiplier,
stats.avgKeySize(),
stats.avgValueSize(),
- maxBloatFactor);
+ maxBloatFactor,
+ persistIndexEvery());
}
private void doMigrate(
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 0cacd22..f554b6b 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -90,6 +90,26 @@
Default: *90*
+* `cache.persistIndexEvery`
+: Duration (in seconds if not specified) between caches keys index persist
+operations.
+
+Note that in order to avoid race condition between evict and persist operations
+the latter is performed after the former is finished. Therefore the lowest
+persist resolution is 30s. Smaller values will be rounded up to 30s whereas
+higher values will be rounded down (if needed) to the closest multiple of 30s.
+From practical point of view it means that persist operation will be performed
+after every n-th evict operation is finished.
+For instance if `cache.persistIndexEvery = 2m` then persist will be called
+after every 4th eviction is finished.
+
+Notes:
+* regardless of `cache.persistIndexEvery` persist will be called on
+ Gerrit termination unless there is one already in progress.
+* persist operation is scheduled in a dedicated thread ('ChronicleMap-Index-0')
+
+Default: *15m*
+
### Defaults
Unless overridden by configuration, sensible default values are be provided for
diff --git a/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheConfigTest.java b/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheConfigTest.java
index 61cacca..7f705f3 100644
--- a/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheConfigTest.java
+++ b/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/ChronicleMapCacheConfigTest.java
@@ -19,6 +19,8 @@
import static com.googlesource.gerrit.modules.cache.chroniclemap.ChronicleMapCacheConfig.Defaults.DEFAULT_MAX_BLOAT_FACTOR;
import static com.googlesource.gerrit.modules.cache.chroniclemap.ChronicleMapCacheConfig.Defaults.DEFAULT_MAX_ENTRIES;
import static com.googlesource.gerrit.modules.cache.chroniclemap.ChronicleMapCacheConfig.Defaults.DEFAULT_PERCENTAGE_FREE_SPACE_EVICTION_THRESHOLD;
+import static com.googlesource.gerrit.modules.cache.chroniclemap.ChronicleMapCacheConfig.Defaults.DEFAULT_PERSIST_INDEX_EVERY;
+import static com.googlesource.gerrit.modules.cache.chroniclemap.ChronicleMapCacheFactory.PRUNE_DELAY;
import com.google.gerrit.server.config.SitePaths;
import java.io.File;
@@ -188,6 +190,32 @@
.isEqualTo(DEFAULT_PERCENTAGE_FREE_SPACE_EVICTION_THRESHOLD);
}
+ @Test
+ public void shouldProvideDefaultIndexPersistEveryValuesWhenNotConfigured() {
+ ChronicleMapCacheConfig configUnderTest = configUnderTest(gerritConfig);
+ assertThat(configUnderTest.getPersistIndexEvery()).isEqualTo(DEFAULT_PERSIST_INDEX_EVERY);
+ assertThat(configUnderTest.getPersistIndexEveryNthPrune()).isEqualTo(30L);
+ }
+
+ @Test
+ public void shouldPersistIndexEveryBePruneDelayWhenPersistIndexEveryIsLowerThanPruneDelay() {
+ gerritConfig.setString(
+ "cache", null, "persistIndexEvery", String.format("%ds", PRUNE_DELAY - 1L));
+ ChronicleMapCacheConfig configUnderTest = configUnderTest(gerritConfig);
+ assertThat(configUnderTest.getPersistIndexEvery()).isEqualTo(Duration.ofSeconds(PRUNE_DELAY));
+ assertThat(configUnderTest.getPersistIndexEveryNthPrune()).isEqualTo(1L);
+ }
+
+ @Test
+ public void shouldPersistIndexEveryBeRoundedDownToAMultiplyOfPruneDelay() {
+ gerritConfig.setString(
+ "cache", null, "persistIndexEvery", String.format("%ds", 2L * PRUNE_DELAY + 1L));
+ ChronicleMapCacheConfig configUnderTest = configUnderTest(gerritConfig);
+ assertThat(configUnderTest.getPersistIndexEvery())
+ .isEqualTo(Duration.ofSeconds(2L * PRUNE_DELAY));
+ assertThat(configUnderTest.getPersistIndexEveryNthPrune()).isEqualTo(2L);
+ }
+
private ChronicleMapCacheConfig configUnderTest(StoredConfig gerritConfig) {
File cacheFile =
ChronicleMapCacheFactory.fileName(