Skip unnecessary auto-adjustments of caches

Auto-adjusting the caches is an expensive operation
that could also occupy a considerable amount of extra
disk-space.

Re-building a cache that is already tuned would waste
precious resources and would not communicate effectively
whether a config change is needed or not.

Show only the config changes needed and rebuild only
the caches that need adjusting, leaving everything else
unchanged.

Bug: Issue 14876
Change-Id: Ic4d703d04410924c855ad2b96f8f635994d968a8
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 a9f8bb7..c85dc43 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
@@ -38,7 +38,7 @@
 public class AutoAdjustCaches extends SshCommand {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
   protected static final String CONFIG_HEADER = "__CONFIG__";
-  protected static final String TUNED_INFIX = "tuned";
+  protected static final String TUNED_INFIX = "_tuned_";
 
   private final DynamicMap<Cache<?, ?>> cacheMap;
   private final ChronicleMapCacheConfig.Factory configFactory;
@@ -84,6 +84,14 @@
 
           long averageKeySize = avgSizes.getKey();
           long averageValueSize = avgSizes.getValue();
+
+          ChronicleMapCacheConfig currCacheConfig = currCache.getConfig();
+
+          if (currCacheConfig.getAverageKeySize() == averageKeySize
+              && currCacheConfig.getAverageValueSize() == averageValueSize) {
+            return;
+          }
+
           ChronicleMapCacheConfig newChronicleMapCacheConfig =
               makeChronicleMapConfig(currCache.getConfig(), averageKeySize, averageValueSize);
 
@@ -129,9 +137,15 @@
         });
 
     stdout.println();
-    stdout.println("****************************");
-    stdout.println("** Chronicle-map template **");
-    stdout.println("****************************");
+    stdout.println("**********************************");
+
+    if (outputChronicleMapConfig.getSections().isEmpty()) {
+      stdout.println("All exsting caches are already tuned: no changes needed.");
+      return;
+    }
+
+    stdout.println("** Chronicle-map config changes **");
+    stdout.println("**********************************");
     stdout.println();
     stdout.println(CONFIG_HEADER);
     stdout.println(outputChronicleMapConfig.toText());
@@ -186,7 +200,7 @@
   private File resolveNewFile(String currentFileName) {
     String newFileName =
         String.format(
-            "%s_%s_%s.%s",
+            "%s%s%s.%s",
             FilenameUtils.getBaseName(currentFileName),
             TUNED_INFIX,
             System.currentTimeMillis(),
diff --git a/src/main/resources/Documentation/tuning.md b/src/main/resources/Documentation/tuning.md
index a27cb7c..18f7eca 100644
--- a/src/main/resources/Documentation/tuning.md
+++ b/src/main/resources/Documentation/tuning.md
@@ -156,15 +156,15 @@
 Calculate the average key and value size, but do not migrate current cache
 data into new files
 
-For each chronicle-map cache (i.e. `foo_1.dat` file) in the `cache` directory, a
-new one will be created (i.e. `foo_1_tuned_<timestamp>.dat`).
+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:
 - Will have the same entries as the original cache.
 - Will be configured with the *actual* average key size and values calculated by
   looking at the content of the original cache.
 
-An output will also be generated with the new configuration that should be put
-into `gerrit.config`, should you decide to use the new caches.
+An output will also be generated with the configuration changes that should
+be included into `gerrit.config`, should you decide to use the new caches.
 
 An example of the output is the following:
 
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 bc6a592..7d63239 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
@@ -20,15 +20,24 @@
 import static com.googlesource.gerrit.modules.cache.chroniclemap.ChronicleMapCacheConfig.Defaults.maxBloatFactorFor;
 import static com.googlesource.gerrit.modules.cache.chroniclemap.ChronicleMapCacheConfig.Defaults.maxEntriesFor;
 
+import com.google.common.base.Joiner;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestPlugin;
 import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.server.ModuleImpl;
+import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
+import com.google.inject.name.Named;
 import java.io.File;
+import java.nio.file.Path;
+import java.util.List;
 import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -50,15 +59,44 @@
   private static final String DIFF_SUMMARY = "diff_summary";
   private static final String ACCOUNTS = "accounts";
   private static final String PERSISTED_PROJECTS = "persisted_projects";
+  private static final String TEST_CACHE_NAME = "test_cache";
+  private static final int TEST_CACHE_VERSION = 1;
+  private static final String TEST_CACHE_FILENAME_TUNED =
+      TEST_CACHE_NAME + "_" + TEST_CACHE_VERSION + AutoAdjustCaches.TUNED_INFIX;
+  private static final String TEST_CACHE_KEY_100_CHARS = new String(new char[100]);
 
   private static final ImmutableList<String> EXPECTED_CACHES =
       ImmutableList.of(MERGEABILITY, DIFF, DIFF_SUMMARY, ACCOUNTS, PERSISTED_PROJECTS);
 
   @Inject private SitePaths sitePaths;
 
+  @Inject
+  @Named(TEST_CACHE_NAME)
+  LoadingCache<String, String> testCache;
+
+  @ModuleImpl(name = CacheModule.PERSISTENT_MODULE)
+  public static class TestPersistentCacheModule extends CacheModule {
+
+    @Override
+    protected void configure() {
+      persist(TEST_CACHE_NAME, String.class, String.class)
+          .loader(TestCacheLoader.class)
+          .version(TEST_CACHE_VERSION);
+      install(new ChronicleMapCacheModule());
+    }
+  }
+
+  public static class TestCacheLoader extends CacheLoader<String, String> {
+
+    @Override
+    public String load(String key) throws Exception {
+      return key;
+    }
+  }
+
   @Override
   public com.google.inject.Module createModule() {
-    return new ChronicleMapCacheModule();
+    return new TestPersistentCacheModule();
   }
 
   @Test
@@ -100,6 +138,31 @@
   }
 
   @Test
+  @GerritConfig(name = "cache.test_cache.avgKeySize", value = "207")
+  @GerritConfig(name = "cache.test_cache.avgValueSize", value = "207")
+  public void shouldNotRecreateTestCacheFileWhenAlreadyTuned() throws Exception {
+    testCache.get(TEST_CACHE_KEY_100_CHARS);
+
+    String tuneResult = adminSshSession.exec(CMD);
+    adminSshSession.assertSuccess();
+
+    assertThat(configResult(tuneResult).getSubsections("cache")).doesNotContain(TEST_CACHE_NAME);
+    assertThat(Joiner.on('\n').join(listTunedFileNames()))
+        .doesNotContain(TEST_CACHE_FILENAME_TUNED);
+  }
+
+  @Test
+  public void shouldCreateTestCacheTuned() throws Exception {
+    testCache.get(TEST_CACHE_KEY_100_CHARS);
+
+    String tuneResult = adminSshSession.exec(CMD);
+    adminSshSession.assertSuccess();
+
+    assertThat(configResult(tuneResult).getSubsections("cache")).contains(TEST_CACHE_NAME);
+    assertThat(Joiner.on('\n').join(listTunedFileNames())).contains(TEST_CACHE_FILENAME_TUNED);
+  }
+
+  @Test
   public void shouldDenyAccessToCreateNewCacheFiles() throws Exception {
     userSshSession.exec(CMD);
     userSshSession.assertFailure("not permitted");
@@ -110,4 +173,13 @@
     configResult.fromText((result.split(CONFIG_HEADER))[1]);
     return configResult;
   }
+
+  private List<String> listTunedFileNames() {
+    Path cachePath = sitePaths.resolve(cfg.getString("cache", null, "directory"));
+    return Stream.of(Objects.requireNonNull(cachePath.toFile().listFiles()))
+        .filter(file -> !file.isDirectory())
+        .map(File::getName)
+        .filter(n -> n.contains(TUNED_INFIX))
+        .collect(Collectors.toList());
+  }
 }