Allow to configure maxBloatFactor

Allow maxBloatFactor to be configured by reading
cache.<name>.maxBloatFactor configuration from gerrit.config

Tuning this value allow to specify how much the actual size of the cache
can differ from the configured number of entries.

Bug: Issue 13416
Change-Id: Ib9a95ec2ef3945a5e87a250b777ab41f97a18cb1
diff --git a/config.md b/config.md
index 5bbea2b..c74c012 100644
--- a/config.md
+++ b/config.md
@@ -44,4 +44,20 @@
 
 [Official docs](
 https://www.javadoc.io/doc/net.openhft/chronicle-map/3.8.0/net/openhft/chronicle/map/ChronicleMapBuilder.html#entries-long-
+)
+
+```cache.<name>.maxBloatFactor```
+: the maximum number of times this cache is allowed to grow in size beyond the
+configured target number of entries.
+
+Chronicle Map will allocate memory until the actual number of entries inserted
+divided by the number configured through ChronicleMapBuilder.entries() is not
+higher than the configured `maxBloatFactor`.
+
+Chronicle Map works progressively slower when the actual size grows far beyond
+the configured size, so the maximum possible maxBloatFactor() is artificially
+limited to 1000. Default: *1*
+
+[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
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 cb345aa..782c216 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
@@ -39,12 +39,15 @@
   private final long averageValueSize;
   private final Duration expireAfterWrite;
   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,
@@ -84,6 +87,9 @@
                 "refreshAfterWrite",
                 toSeconds(refreshAfterWrite),
                 SECONDS));
+
+    this.maxBloatFactor =
+        cfg.getInt("cache", configKey, "maxBloatFactor", DEFAULT_MAX_BLOAT_FACTOR);
   }
 
   public Duration getExpireAfterWrite() {
@@ -114,6 +120,10 @@
     return diskLimit;
   }
 
+  public int getMaxBloatFactor() {
+    return maxBloatFactor;
+  }
+
   private static Path getCacheDir(SitePaths site, String name) {
     if (name == null) {
       return null;
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 c90381d..25d66e4 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
@@ -74,6 +74,8 @@
     // avgValueSize)
     mapBuilder.entries(config.getMaxEntries());
 
+    mapBuilder.maxBloatFactor(config.getMaxBloatFactor());
+
     if (config.getPersistedFile() == null || config.getDiskLimit() < 0) {
       store = mapBuilder.create();
     } else {
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 5f003d7..5eaeabb 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
@@ -16,6 +16,7 @@
 import static com.google.common.truth.Truth.assertThat;
 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 com.google.gerrit.server.config.SitePaths;
@@ -131,6 +132,21 @@
   }
 
   @Test
+  public void shouldProvideMaxDefaultBloatFactorWhenNotConfigured() {
+    assertThat(configUnderTest(gerritConfig).getMaxBloatFactor())
+        .isEqualTo(DEFAULT_MAX_BLOAT_FACTOR);
+  }
+
+  @Test
+  public void shouldProvideMaxBloatFactorWhenConfigured() throws Exception {
+    int bloatFactor = 3;
+    gerritConfig.setInt("cache", cacheKey, "maxBloatFactor", bloatFactor);
+    gerritConfig.save();
+
+    assertThat(configUnderTest(gerritConfig).getMaxBloatFactor()).isEqualTo(bloatFactor);
+  }
+
+  @Test
   public void shouldProvideExpireAfterWriteWhenMaxAgeIsConfgured() throws Exception {
     String maxAge = "3 minutes";
     gerritConfig.setString("cache", cacheKey, "maxAge", maxAge);