Introduce per-cache default values
Number of entries, average key and value size, max bloat factor might
differ greatly from cache to cache, so having one single default is not
enough to allow a smooth out of the box execution of chronicle-map
cache.
Introduce per-cache defaults, so that default configurations are
tailored for each specific cache.
Bug: Issue 13414
Change-Id: I0fbd5ffcc35976438fe9aa2d10aec5d55abf3141
diff --git a/config.md b/config.md
index c74c012..bffc481 100644
--- a/config.md
+++ b/config.md
@@ -40,7 +40,8 @@
)
```cache.<name>.entries```
-: The number of entries that this cache is going to hold, _at most_
+: The number of entries that this cache is going to hold, _at most_.
+The actual number of entries needs to be less or equal to this value.
[Official docs](
https://www.javadoc.io/doc/net.openhft/chronicle-map/3.8.0/net/openhft/chronicle/map/ChronicleMapBuilder.html#entries-long-
@@ -50,8 +51,11 @@
: the maximum number of times this cache is allowed to grow in size beyond the
configured target number of entries.
+Set this value to the theoretical maximum of stored entries, divided by the
+configured entries.
+
Chronicle Map will allocate memory until the actual number of entries inserted
-divided by the number configured through ChronicleMapBuilder.entries() is not
+divided by the number configured through `entries` is not
higher than the configured `maxBloatFactor`.
Chronicle Map works progressively slower when the actual size grows far beyond
@@ -60,4 +64,166 @@
[Official docs](
https://www.javadoc.io/doc/net.openhft/chronicle-map/3.8.0/net/openhft/chronicle/hash/ChronicleHashBuilder.html#maxBloatFactor-double-
-)
\ No newline at end of file
+)
+
+### Defaults
+
+Unless overridden by configuration, sensible default values are be provided for
+[standard caches](https://gerrit-review.googlesource.com/Documentation/config-gerrit.html#cache_names).
+
+Please note that even though defaults allow an out-of-the-box usage of the
+chronicle-map cache, they are not necessarily suitable for a production
+environment.
+
+A deep understanding of your Gerrit data is crucial to tune each cache
+accordingly. Please refer to the [configuration](#configuration-parameters) to
+understand how to choose sensible values.
+
+These defaults have been retrieved by observing _actual_ key and value sizes as
+explained in the
+[official documentation](https://github.com/OpenHFT/Chronicle-Map/blob/master/docs/CM_Tutorial_Behaviour.adoc#example---monitor-chronicle-map-statistics)
+
+They are based on the assumption that your Gerrit instance will not have more
+than 1000 accounts overall, logged-in at the same time and no more than 1000
+medium sized changes.
+
+Information on which values each cache is actually initialized with, can be found
+in the `error_log` at startup, in particular two values are also logged to help
+gather important statistics about the current file cache status:
+
+* `remainingAutoResizes`
+: the number of times in the future the cache can automatically expand its capacity.
+The limit to the number of times the map can expand is set via the `maxBloatFactor`.
+if `remainingAutoResizes` drops to zero,this cache is no longer able to expand
+and it will not be able to take more entries, failing with a `IllegalStateException`
+
+* `percentageFreeSpace`
+: the amount of free space in the cache as a percentage. When the free space gets
+ low ( around 5% ) the cache will automatically expand (see `remainingAutoResizes`).
+ If the cache expands you will see an increase in the available free space.
+
+These are the provided default values:
+
+* `web_sessions`:
+ * `avgKeySize`: 45 bytes
+ * `avgValueSize`: 221 bytes
+ * `entries`: 1000
+ * `maxBloatFactor`: 1
+
+Allows up to 1000 users to be logged in.
+
+* `change_notes`:
+ * `avgKeySize`: 36 bytes
+ * `avgValueSize`: 10240 bytes
+ * `entries`: 1000
+ * `maxBloatFactor`: 2
+
+Allow for a dozen review activities (votes, comments of medium length) to up to
+1000 operations. maxBloatFactor allows to go twice over this threshold.
+
+* `accounts`:
+ * `avgKeySize`: 30 bytes
+ * `avgValueSize`: 256 bytes
+ * `entries`: 1000
+ * `maxBloatFactor`: 1
+
+Allows to cache up to 1000 details of active users, including their display name,
+preferences, mail, etc.
+
+* `diff`:
+ * `avgKeySize`: 98 bytes
+ * `avgValueSize`: 10240 bytes
+ * `entries`: 1000
+ * `maxBloatFactor`: 3
+
+Allow for up to 1000 medium sized diffs between two commits to be cached.
+maxBloatFactor allows to go three times over this threshold.
+
+* `diff_intraline`:
+ * `avgKeySize`: 512 bytes
+ * `avgValueSize`: 2048 bytes
+ * `entries`: 1000
+ * `maxBloatFactor`: 2
+
+Allow for up to 1000 medium sized diffs between two files to be cached.
+maxBloatFactor allows to go twice over this threshold.
+
+* `external_ids_map`:
+ * `avgKeySize`: 24 bytes
+ * `avgValueSize`: 204800 bytes
+ * `entries`: 2
+ * `maxBloatFactor`: 1
+
+This cache holds a map of the parsed representation of all current external IDs.
+It may temporarily contain 2 entries, but the second one is promptly expired.
+This defaults allow to contain up to 1000 entries per map, roughly.
+
+* `oauth_tokens`:
+ * `avgKeySize`: 8 bytes
+ * `avgValueSize`: 2048 bytes
+ * `entries`: 1000
+ * `maxBloatFactor`: 1
+
+caches information about the operation performed by a change relative to its
+parent. Allow to cache up to 1000 entries.
+
+* `mergeability`:
+ * `avgKeySize`: 79 bytes
+ * `avgValueSize`: 16 bytes
+ * `entries`: 65000
+ * `maxBloatFactor`: 2
+
+Caches information about the mergeability status of up to 1000 open changes.
+
+* `pure_revert`:
+ * `avgKeySize`: 55 bytes
+ * `avgValueSize`: 16 bytes
+ * `entries`: 1000
+ * `maxBloatFactor`: 1
+
+Caches the result of checking if one change or commit is a pure/clean revert of
+another, for up to 1000 entries.
+
+* `persisted_projects`:
+ * `avgKeySize`: 128 bytes
+ * `avgValueSize`: 1024 bytes
+ * `entries`: 250
+ * `maxBloatFactor`: 2
+
+Caches the project description records from the refs/meta/config branch of each
+project. Allow up to 250 projects to be cached.
+maxBloatFactor allows to go twice over this threshold.
+
+* `conflicts`:
+ * `avgKeySize`: 70 bytes
+ * `avgValueSize`: 16 bytes
+ * `entries`: 1000
+ * `maxBloatFactor`: 1
+
+Caches whether two commits are in conflict with each other.
+Allows to hold up to 1000 conflicting changes.
+
+* `GENERAL DEFAULTS`:
+
+Caches that do not provide specific configuration in the `[cache "<name>"]`
+stanza and are not listed above, will fallback to use generic defaults:
+
+* `avgKeySize`: 128 bytes
+* `avgValueSize`: 2048 bytes
+* `entries`: 1000
+* `maxBloatFactor`: 1
+
+### Gotchas
+
+#### Configuration changes and persisted cache file
+
+When reading the persisted cache file, chronicle-map assumes the configuration
+that was used to create the file for the first time.
+This means, that changing the configuration is not going to update entries,
+average key size and average value size as your needs grow.
+
+If you need more entries, or different key values, you'll need to generate a
+brand new persistent cache (i.e. delete the old one).
+
+More information on recovery can be found in the
+[Official documentation](https://github.com/OpenHFT/Chronicle-Map/blob/master/docs/CM_Tutorial.adoc#recovery)
\ No newline at end of file
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 7c593f2..f3f59cf 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
@@ -15,6 +15,7 @@
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;
@@ -27,6 +28,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
+import java.util.Optional;
import org.eclipse.jgit.lib.Config;
public class ChronicleMapCacheConfig {
@@ -41,13 +43,6 @@
private final Duration refreshAfterWrite;
private final int maxBloatFactor;
- public static final long DEFAULT_MAX_ENTRIES = 1000;
-
- public static final long DEFAULT_AVG_KEY_SIZE = 128;
- public static final long DEFAULT_AVG_VALUE_SIZE = 2048;
-
- public static final int DEFAULT_MAX_BLOAT_FACTOR = 1;
-
public interface Factory {
ChronicleMapCacheConfig create(
@Assisted("Name") String name,
@@ -72,9 +67,12 @@
cacheDir != null ? cacheDir.resolve(String.format("%s.dat", name)).toFile() : null;
this.diskLimit = cfg.getLong("cache", configKey, "diskLimit", diskLimit);
- this.maxEntries = cfg.getLong("cache", configKey, "maxEntries", DEFAULT_MAX_ENTRIES);
- this.averageKeySize = cfg.getLong("cache", configKey, "avgKeySize", DEFAULT_AVG_KEY_SIZE);
- this.averageValueSize = cfg.getLong("cache", configKey, "avgValueSize", DEFAULT_AVG_VALUE_SIZE);
+ this.maxEntries =
+ cfg.getLong("cache", configKey, "maxEntries", Defaults.maxEntriesFor(configKey));
+ this.averageKeySize =
+ cfg.getLong("cache", configKey, "avgKeySize", Defaults.averageKeySizeFor(configKey));
+ this.averageValueSize =
+ cfg.getLong("cache", configKey, "avgValueSize", Defaults.avgValueSizeFor(configKey));
this.expireAfterWrite =
Duration.ofSeconds(
ConfigUtil.getTimeUnit(
@@ -90,7 +88,7 @@
SECONDS));
this.maxBloatFactor =
- cfg.getInt("cache", configKey, "maxBloatFactor", DEFAULT_MAX_BLOAT_FACTOR);
+ cfg.getInt("cache", configKey, "maxBloatFactor", Defaults.maxBloatFactorFor(configKey));
}
public Duration getExpireAfterWrite() {
@@ -143,4 +141,55 @@
private static long toSeconds(@Nullable Duration duration) {
return duration != null ? duration.getSeconds() : 0;
}
+
+ protected static class Defaults {
+
+ public static final long DEFAULT_MAX_ENTRIES = 1000;
+
+ public static final long DEFAULT_AVG_KEY_SIZE = 128;
+ public static final long DEFAULT_AVG_VALUE_SIZE = 2048;
+
+ public static final int DEFAULT_MAX_BLOAT_FACTOR = 1;
+
+ private static final ImmutableMap<String, DefaultConfig> defaultMap =
+ new ImmutableMap.Builder<String, DefaultConfig>()
+ .put("web_sessions", DefaultConfig.create(45, 221, 1000, 1))
+ .put("change_notes", DefaultConfig.create(36, 10240, 1000, 3))
+ .put("accounts", DefaultConfig.create(30, 256, 1000, 1))
+ .put("diff", DefaultConfig.create(98, 10240, 1000, 2))
+ .put("diff_intraline", DefaultConfig.create(512, 2048, 1000, 2))
+ .put("diff_summary", DefaultConfig.create(128, 2048, 1000, 1))
+ .put("external_ids_map", DefaultConfig.create(128, 204800, 2, 1))
+ .put("oauth_tokens", DefaultConfig.create(8, 2048, 1000, 1))
+ .put("change_kind", DefaultConfig.create(59, 26, 1000, 1))
+ .put("mergeability", DefaultConfig.create(79, 16, 65000, 2))
+ .put("pure_revert", DefaultConfig.create(55, 16, 1000, 1))
+ .put("persisted_projects", DefaultConfig.create(128, 1024, 250, 2))
+ .put("conflicts", DefaultConfig.create(70, 16, 1000, 1))
+ .build();
+
+ public static long averageKeySizeFor(String configKey) {
+ return Optional.ofNullable(defaultMap.get(configKey))
+ .map(DefaultConfig::averageKey)
+ .orElse(DEFAULT_AVG_KEY_SIZE);
+ }
+
+ public static long avgValueSizeFor(String configKey) {
+ return Optional.ofNullable(defaultMap.get(configKey))
+ .map(DefaultConfig::averageValue)
+ .orElse(DEFAULT_AVG_VALUE_SIZE);
+ }
+
+ public static long maxEntriesFor(String configKey) {
+ return Optional.ofNullable(defaultMap.get(configKey))
+ .map(DefaultConfig::entries)
+ .orElse(DEFAULT_MAX_ENTRIES);
+ }
+
+ public static int maxBloatFactorFor(String configKey) {
+ return Optional.ofNullable(defaultMap.get(configKey))
+ .map(DefaultConfig::maxBloatFactor)
+ .orElse(DEFAULT_MAX_BLOAT_FACTOR);
+ }
+ }
}
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 25d66e4..59f9f5a 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
@@ -16,6 +16,7 @@
import com.google.common.cache.AbstractLoadingCache;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.CacheStats;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.server.cache.PersistentCache;
import com.google.gerrit.server.cache.PersistentCacheDef;
import com.google.gerrit.server.util.time.TimeUtil;
@@ -31,6 +32,8 @@
public class ChronicleMapCacheImpl<K, V> extends AbstractLoadingCache<K, V>
implements PersistentCache {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
private final ChronicleMapCacheConfig config;
private final CacheLoader<K, V> loader;
private final ChronicleMap<K, TimedValue<V>> store;
@@ -58,7 +61,6 @@
// such as Boolean, Integer, for which size is statically determined.
// This means that even though a custom serializer was provided for a primitive
// it cannot be used.
- // TODO: Should we log.warn when this is the case?
if (!mapBuilder.constantlySizedKeys()) {
mapBuilder.averageKeySize(config.getAverageKeySize());
mapBuilder.keyMarshaller(new ChronicleMapMarshallerAdapter<>(def.keySerializer()));
@@ -81,6 +83,18 @@
} else {
store = mapBuilder.createOrRecoverPersistedTo(config.getPersistedFile());
}
+
+ logger.atInfo().log(
+ "Initialized '%s'|avgKeySize: %s bytes|avgValueSize: %s bytes|"
+ + "entries: %s|maxBloatFactor: %s|remainingAutoResizes: %s|"
+ + "percentageFreeSpace: %s",
+ def.name(),
+ mapBuilder.constantlySizedKeys() ? "CONSTANT" : config.getAverageKeySize(),
+ config.getAverageValueSize(),
+ config.getMaxEntries(),
+ config.getMaxBloatFactor(),
+ store.remainingAutoResizes(),
+ store.percentageFreeSpace());
}
public ChronicleMapCacheConfig getConfig() {
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/DefaultConfig.java b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/DefaultConfig.java
new file mode 100644
index 0000000..0551ea9
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/DefaultConfig.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2020 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.chroniclemap;
+
+import com.google.auto.value.AutoValue;
+
+@AutoValue
+abstract class DefaultConfig {
+ public static DefaultConfig create(
+ long averageKey, long averageValue, long entries, int maxBloatFactor) {
+ return new AutoValue_DefaultConfig(averageKey, averageValue, entries, maxBloatFactor);
+ }
+
+ abstract long averageKey();
+
+ abstract long averageValue();
+
+ abstract long entries();
+
+ abstract int maxBloatFactor();
+}
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 ee4d7c1..395b25c 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
@@ -15,10 +15,10 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
-import static com.googlesource.gerrit.modules.cache.chroniclemap.ChronicleMapCacheConfig.DEFAULT_AVG_KEY_SIZE;
-import static com.googlesource.gerrit.modules.cache.chroniclemap.ChronicleMapCacheConfig.DEFAULT_AVG_VALUE_SIZE;
-import static com.googlesource.gerrit.modules.cache.chroniclemap.ChronicleMapCacheConfig.DEFAULT_MAX_BLOAT_FACTOR;
-import static com.googlesource.gerrit.modules.cache.chroniclemap.ChronicleMapCacheConfig.DEFAULT_MAX_ENTRIES;
+import static com.googlesource.gerrit.modules.cache.chroniclemap.ChronicleMapCacheConfig.Defaults.DEFAULT_AVG_KEY_SIZE;
+import static com.googlesource.gerrit.modules.cache.chroniclemap.ChronicleMapCacheConfig.Defaults.DEFAULT_AVG_VALUE_SIZE;
+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 com.google.gerrit.server.config.SitePaths;
import java.io.IOException;