Allow to specify number of entries in auto-adjust-caches

When running the auto-adjust-caches command, either via SSH or via HTTP,
it is not possible to specify the maximum number of entries for the
resulting cache.

This means, that the maximum number of entries allowed in the cache
cannot be accommodated to make room for caches that are increasing in
size.

Introduce the `max-entries` parameter to tell the auto-adjust-caches
the maximum number of entries desired in the resulting auto-tuned cache.

When the parameter is not specified, auto-adjust-caches takes care of
increasing the number of `maxEntries`, in case the current utilization
goes over 50% threshold.

Bug: Issue 15412
Change-Id: I0ff43060a8fd2e22c3a60d11dcec1f875ba45e6e
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 a5849a8..948aaee 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
@@ -30,6 +30,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ConcurrentMap;
 import java.util.stream.Collectors;
@@ -44,12 +45,16 @@
   protected static final String CONFIG_HEADER = "__CONFIG__";
   protected static final String TUNED_INFIX = "_tuned_";
 
+  protected static final Integer MAX_ENTRIES_MULTIPLIER = 2;
+  protected static final Integer PERCENTAGE_SIZE_INCREASE_THRESHOLD = 50;
+
   private final DynamicMap<Cache<?, ?>> cacheMap;
   private final ChronicleMapCacheConfig.Factory configFactory;
   private final Path cacheDir;
   private final AdministerCachePermission adminCachePermission;
 
   private boolean dryRun;
+  private Optional<Long> optionalMaxEntries = Optional.empty();
   private Set<String> cacheNames = new HashSet<>();
 
   @Inject
@@ -73,6 +78,14 @@
     this.dryRun = dryRun;
   }
 
+  public Optional<Long> getOptionalMaxEntries() {
+    return optionalMaxEntries;
+  }
+
+  public void setOptionalMaxEntries(Optional<Long> maxEntries) {
+    this.optionalMaxEntries = maxEntries;
+  }
+
   public void addCacheNames(List<String> cacheNames) {
     this.cacheNames.addAll(cacheNames);
   }
@@ -106,21 +119,24 @@
         long averageValueSize = avgSizes.getValue();
 
         ChronicleMapCacheConfig currCacheConfig = currCache.getConfig();
+        long newMaxEntries = newMaxEntries(currCache);
 
         if (currCacheConfig.getAverageKeySize() == averageKeySize
-            && currCacheConfig.getAverageValueSize() == averageValueSize) {
+            && currCacheConfig.getAverageValueSize() == averageValueSize
+            && currCacheConfig.getMaxEntries() == newMaxEntries) {
           continue;
         }
 
         ChronicleMapCacheConfig newChronicleMapCacheConfig =
-            makeChronicleMapConfig(currCache.getConfig(), averageKeySize, averageValueSize);
+            makeChronicleMapConfig(
+                currCache.getConfig(), newMaxEntries, averageKeySize, averageValueSize);
 
         updateOutputConfig(
             outputChronicleMapConfig,
             cacheName,
             averageKeySize,
             averageValueSize,
-            currCache.getConfig().getMaxEntries(),
+            newMaxEntries,
             currCache.getConfig().getMaxBloatFactor());
 
         if (!dryRun) {
@@ -184,6 +200,7 @@
 
   private ChronicleMapCacheConfig makeChronicleMapConfig(
       ChronicleMapCacheConfig currentChronicleMapConfig,
+      long newMaxEntries,
       long averageKeySize,
       long averageValueSize) {
 
@@ -192,12 +209,30 @@
         resolveNewFile(currentChronicleMapConfig.getPersistedFile().getName()),
         currentChronicleMapConfig.getExpireAfterWrite(),
         currentChronicleMapConfig.getRefreshAfterWrite(),
-        currentChronicleMapConfig.getMaxEntries(),
+        newMaxEntries,
         averageKeySize,
         averageValueSize,
         currentChronicleMapConfig.getMaxBloatFactor());
   }
 
+  private long newMaxEntries(ChronicleMapCacheImpl<Object, Object> currentCache) {
+    return getOptionalMaxEntries()
+        .orElseGet(
+            () -> {
+              double percentageUsedAutoResizes = currentCache.percentageUsedAutoResizes();
+              long currMaxEntries = currentCache.getConfig().getMaxEntries();
+
+              long newMaxEntries = currMaxEntries;
+              if (percentageUsedAutoResizes > PERCENTAGE_SIZE_INCREASE_THRESHOLD) {
+                newMaxEntries = currMaxEntries * MAX_ENTRIES_MULTIPLIER;
+              }
+              logger.atInfo().log(
+                  "Cache '%s' (maxEntries: %s) used %s%% of available space. new maxEntries will be: %s",
+                  currentCache.name(), currMaxEntries, percentageUsedAutoResizes, newMaxEntries);
+              return newMaxEntries;
+            });
+  }
+
   private File resolveNewFile(String currentFileName) {
     String newFileName =
         String.format(
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/AutoAdjustCachesCommand.java b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/AutoAdjustCachesCommand.java
index e57e130..de41471 100644
--- a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/AutoAdjustCachesCommand.java
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/AutoAdjustCachesCommand.java
@@ -20,6 +20,7 @@
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.Optional;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.TextProgressMonitor;
 import org.kohsuke.args4j.Argument;
@@ -40,6 +41,14 @@
     autoAdjustCachesEngine.setDryRun(dryRun);
   }
 
+  @Option(
+      name = "--max-entries",
+      aliases = {"-m"},
+      usage = "The number of entries that the new tuned cache is going to hold.")
+  public void setMaxEntries(long maxEntries) {
+    autoAdjustCachesEngine.setOptionalMaxEntries(Optional.of(maxEntries));
+  }
+
   @Argument(
       index = 0,
       required = false,
diff --git a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/AutoAdjustCachesServlet.java b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/AutoAdjustCachesServlet.java
index 59d29ae..49f7a97 100644
--- a/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/AutoAdjustCachesServlet.java
+++ b/src/main/java/com/googlesource/gerrit/modules/cache/chroniclemap/AutoAdjustCachesServlet.java
@@ -57,6 +57,11 @@
             .or(() -> Optional.ofNullable(req.getParameter("d")))
             .isPresent());
 
+    autoAdjustCachesEngine.setOptionalMaxEntries(
+        Optional.ofNullable(req.getParameter("max-entries"))
+            .or(() -> Optional.ofNullable(req.getParameter("m")))
+            .map(Long::parseLong));
+
     String[] cacheNames = req.getParameterValues("CACHE_NAME");
     if (cacheNames != null) {
       autoAdjustCachesEngine.addCacheNames(Arrays.asList(cacheNames));
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 af35c88..04ef4d7 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
@@ -32,6 +32,7 @@
 import java.util.concurrent.atomic.LongAdder;
 import net.openhft.chronicle.map.ChronicleMap;
 import net.openhft.chronicle.map.ChronicleMapBuilder;
+import net.openhft.chronicle.map.VanillaChronicleMap;
 
 public class ChronicleMapCacheImpl<K, V> extends AbstractLoadingCache<K, V>
     implements PersistentCache {
@@ -392,4 +393,34 @@
   public void close() {
     store.close();
   }
+
+  @SuppressWarnings("rawtypes")
+  public double percentageUsedAutoResizes() {
+    /*
+     * Chronicle-map already exposes the number of _remaining_ auto-resizes, but
+     * this is an absolute value, and it is not enough to understand the
+     * percentage of auto-resizes that have been utilized.
+     *
+     * For that, we fist need to understand the _maximum_ number of possible
+     * resizes (inclusive of the resizes allowed by the max-bloat factor).
+     * This information is exposed at low level, by the VanillaChronicleMap,
+     * which has access to the number of allocated segments.
+     *
+     * So we proceed as follows:
+     *
+     * Calculate the maximum number of segments by multiplying the allocated
+     * segments (`actualSegments`) by the configured max-bloat-factor.
+     *
+     * The ratio between this value and the _current_ segment utilization
+     * (`getExtraTiersInUse`) shows the overall percentage.
+     */
+    VanillaChronicleMap vanillaStore = (VanillaChronicleMap) store;
+    double maxResizes = config.getMaxBloatFactor() * vanillaStore.actualSegments;
+    long usedResizes = vanillaStore.globalMutableState().getExtraTiersInUse();
+    return usedResizes * 100 / maxResizes;
+  }
+
+  public String name() {
+    return store.name();
+  }
 }
diff --git a/src/main/resources/Documentation/tuning.md b/src/main/resources/Documentation/tuning.md
index 95bea8a..77078a4 100644
--- a/src/main/resources/Documentation/tuning.md
+++ b/src/main/resources/Documentation/tuning.md
@@ -159,13 +159,31 @@
 
 * `--dry-run` or `-d` (SSH), `?dry-run` or `?d` (REST-API) optional parameter
 
+Calculate the average key and value size, but do not migrate current cache
+data into new files
+
+* `--max-entries` or `-m` (SSH), `?max-entries` or `?m` (REST-API) optional parameter
+
+The number of entries the tuned cache file is going to hold. This is typically
+useful when the auto-tuning is executed with the intent to increase the number
+of entries that the current cache can hold. When not specified, the
+auto-adjust-cache command checks the percentage utilization of the current
+cache.
+
+If the current utilization of the cache is higher than 50%, then `maxEntries`
+for the tuned cache will be increased by a factor of *2*.
+
+To _decrease_ the number of max entries during auto-tuning, the `max-entries`
+value should be passed _explicitly_.
+
+Note that this parameter will be used globally across all caches, so if you want
+to increase the size of a particular cache only you should be using this
+together with the `cache-name` parameter.
+
 * `cache-name` (SSH), `?CACHE_NAME=cache-name` (REST-API) optional restriction of the caches
   to analyze and auto-tune. The parameter can be repeated multiple times for analyzing
   multiple caches. By default, analyze and adjust all persistent caches.
 
-Calculate the average key and value size, but do not migrate current cache
-data into new files
-
 For each chronicle-map cache that needs tuning (i.e. `foo_1.dat` file) in
 the `cache` directory, a new one will be created (i.e. `foo_1_tuned_<timestamp>.dat`).
 The new cache will have these characteristics:
diff --git a/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/AutoAdjustCachesIT.java b/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/AutoAdjustCachesIT.java
index 24b60ef..cafaca2 100644
--- a/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/AutoAdjustCachesIT.java
+++ b/src/test/java/com/googlesource/gerrit/modules/cache/chroniclemap/AutoAdjustCachesIT.java
@@ -15,6 +15,8 @@
 package com.googlesource.gerrit.modules.cache.chroniclemap;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.googlesource.gerrit.modules.cache.chroniclemap.AutoAdjustCaches.MAX_ENTRIES_MULTIPLIER;
+import static com.googlesource.gerrit.modules.cache.chroniclemap.AutoAdjustCaches.PERCENTAGE_SIZE_INCREASE_THRESHOLD;
 import static com.googlesource.gerrit.modules.cache.chroniclemap.AutoAdjustCachesCommand.CONFIG_HEADER;
 import static com.googlesource.gerrit.modules.cache.chroniclemap.AutoAdjustCachesCommand.TUNED_INFIX;
 import static com.googlesource.gerrit.modules.cache.chroniclemap.ChronicleMapCacheConfig.Defaults.maxBloatFactorFor;
@@ -41,6 +43,7 @@
 import java.nio.file.Path;
 import java.util.Objects;
 import java.util.Set;
+import java.util.UUID;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -122,6 +125,43 @@
   }
 
   @Test
+  @GerritConfig(name = "cache.test_cache.maxEntries", value = "10")
+  @GerritConfig(name = "cache.test_cache.maxBloatFactor", value = "1")
+  public void shouldIncreaseCacheSizeWhenIsGettingFull() throws Exception {
+    ChronicleMapCacheImpl<String, String> chronicleMapCache =
+        (ChronicleMapCacheImpl<String, String>) testCache;
+
+    while (chronicleMapCache.percentageUsedAutoResizes() < PERCENTAGE_SIZE_INCREASE_THRESHOLD) {
+      String aString = UUID.randomUUID().toString();
+      testCache.put(aString, aString);
+    }
+
+    String tuneResult = adminSshSession.exec(SSH_CMD + " " + TEST_CACHE_NAME);
+    adminSshSession.assertSuccess();
+
+    Config tunedConfig = configResult(tuneResult, CONFIG_HEADER);
+    assertThat(tunedConfig.getSubsections("cache")).contains(TEST_CACHE_NAME);
+    assertThat(tunedConfig.getLong("cache", TEST_CACHE_NAME, "maxEntries", 0))
+        .isEqualTo(chronicleMapCache.getConfig().getMaxEntries() * MAX_ENTRIES_MULTIPLIER);
+  }
+
+  @Test
+  public void shouldHonourMaxEntriesParameter() throws Exception {
+    createChange();
+    Long wantedMaxEntries = 100L;
+
+    String result =
+        adminSshSession.exec(String.format("%s --max-entries %s", SSH_CMD, wantedMaxEntries));
+
+    adminSshSession.assertSuccess();
+    Config configResult = configResult(result, CONFIG_HEADER);
+
+    for (String cache : EXPECTED_CACHES) {
+      assertThat(configResult.getLong("cache", cache, "maxEntries", 0)).isEqualTo(wantedMaxEntries);
+    }
+  }
+
+  @Test
   public void shouldCreateNewCacheFiles() throws Exception {
     createChange();
 
@@ -153,7 +193,11 @@
   public void shouldNotRecreateTestCacheFileWhenAlreadyTuned() throws Exception {
     testCache.get(TEST_CACHE_KEY_100_CHARS);
 
-    String tuneResult = adminSshSession.exec(SSH_CMD);
+    String tuneResult =
+        adminSshSession.exec(
+            String.format(
+                "%s --max-entries %s",
+                SSH_CMD, ChronicleMapCacheConfig.Defaults.maxEntriesFor(TEST_CACHE_KEY_100_CHARS)));
     adminSshSession.assertSuccess();
 
     assertThat(configResult(tuneResult, CONFIG_HEADER).getSubsections("cache"))
@@ -197,6 +241,21 @@
   }
 
   @Test
+  public void shouldHonourMaxEntriesOverRestForAdmin() throws Exception {
+    Long wantedMaxEntries = 100L;
+
+    RestResponse resp =
+        adminRestSession.put(String.format("%s?max-entries=%s", REST_CMD, wantedMaxEntries));
+
+    resp.assertCreated();
+
+    assertThat(
+            configResult(resp.getEntityContent(), null)
+                .getLong("cache", ACCOUNTS, "maxEntries", 0L))
+        .isEqualTo(wantedMaxEntries);
+  }
+
+  @Test
   public void shouldAllowTuningOfSingleDiffCacheOverRestForAdmin() throws Exception {
     createChange();