Make cache pruning schedule configurable

Pruning H2 caches negatively affect performance. It will currently
executed on start up and at 01:00h every day. If cache pruning happens
during times of high load, this can significantly affect the user
experience.

To avoid this, this change adds an option to disable cache pruning on
start up and allows to configure the schedule at which the cache
pruning tasks are run.

Release-Notes: Make cache pruning schedule configurable
Change-Id: Id42eb86268ce4b011cc2b61286aeee43d24457d8
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 9ce2eee..b276cdf 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1352,6 +1352,33 @@
 
 Default is 00:00 if the project_list cache warmer is enabled.
 
+[[cachePruning]]
+=== Section cachePruning
+
+[[cachePruning.pruneOnStartup]]cachePruning.pruneOnStartup::
++
+Whether to asynchronously prune all cache when starting Gerrit.
++
+Defaults to `true`.
+
+[[cachePruning.startTime]]cachePruning.startTime::
++
+The link:#schedule-configuration-startTime[start time] for running
+cache pruning.
++
+Defaults to `01:00`.
+
+[[cachePruning.interval]]cachePruning.interval::
++
+The link:#schedule-configuration-interval[interval] for running
+cache pruning.
++
+Defaults to `1d`.
+
+link:#schedule-configuration-examples[Schedule examples] can be found
+in the link:#schedule-configuration[Schedule Configuration] section.
+
+
 [[capability]]
 === Section capability
 
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
index fdd55ac..4ccbab5 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -32,11 +32,13 @@
 import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.ScheduleConfig;
+import com.google.gerrit.server.config.ScheduleConfig.Schedule;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.index.options.BuildBloomFilter;
 import com.google.gerrit.server.index.options.IsFirstInsertForEntry;
 import com.google.gerrit.server.logging.LoggingContextAwareExecutorService;
-import com.google.gerrit.server.logging.LoggingContextAwareScheduledExecutorService;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -67,6 +69,8 @@
   private final boolean h2AutoServer;
   private final boolean isOfflineReindex;
   private final boolean buildBloomFilter;
+  private final boolean pruneOnStartup;
+  private final Schedule schedule;
 
   @Inject
   H2CacheFactory(
@@ -74,12 +78,18 @@
       @GerritServerConfig Config cfg,
       SitePaths site,
       DynamicMap<Cache<?, ?>> cacheMap,
+      WorkQueue queue,
       @Nullable IsFirstInsertForEntry isFirstInsertForEntry,
       @Nullable BuildBloomFilter buildBloomFilter) {
     super(memCacheFactory, cfg, site);
     h2CacheSize = cfg.getLong("cache", null, "h2CacheSize", -1);
     h2AutoServer = cfg.getBoolean("cache", null, "h2AutoServer", false);
+    pruneOnStartup = cfg.getBoolean("cachePruning", null, "pruneOnStartup", true);
     caches = new ArrayList<>();
+    schedule =
+        ScheduleConfig.createSchedule(cfg, "cachePruning")
+            .orElse(Schedule.createOrFail(Duration.ofDays(1).toMillis(), "01:00"));
+    logger.atInfo().log("Scheduling cache pruning with schedule %s", schedule);
     this.cacheMap = cacheMap;
     this.isOfflineReindex =
         isFirstInsertForEntry != null && isFirstInsertForEntry.equals(IsFirstInsertForEntry.YES);
@@ -92,16 +102,7 @@
               Executors.newFixedThreadPool(
                   1, new ThreadFactoryBuilder().setNameFormat("DiskCache-Store-%d").build()));
 
-      cleanup =
-          isOfflineReindex
-              ? null
-              : new LoggingContextAwareScheduledExecutorService(
-                  Executors.newScheduledThreadPool(
-                      1,
-                      new ThreadFactoryBuilder()
-                          .setNameFormat("DiskCache-Prune-%d")
-                          .setDaemon(true)
-                          .build()));
+      cleanup = isOfflineReindex ? null : queue.createQueue(1, "DiskCache-Prune", true);
     } else {
       executor = null;
       cleanup = null;
@@ -114,9 +115,19 @@
       for (H2CacheImpl<?, ?> cache : caches) {
         executor.execute(cache::start);
         if (cleanup != null) {
+          if (pruneOnStartup) {
+            @SuppressWarnings("unused")
+            Future<?> possiblyIgnoredError =
+                cleanup.schedule(() -> cache.prune(), 30, TimeUnit.SECONDS);
+          }
+
           @SuppressWarnings("unused")
           Future<?> possiblyIgnoredError =
-              cleanup.schedule(() -> cache.prune(cleanup), 30, TimeUnit.SECONDS);
+              cleanup.scheduleAtFixedRate(
+                  () -> cache.prune(),
+                  schedule.initialDelay(),
+                  schedule.interval(),
+                  TimeUnit.MILLISECONDS);
         }
       }
     }
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index 27a09ed..ad46483 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -47,7 +47,6 @@
 import java.time.Duration;
 import java.time.Instant;
 import java.util.ArrayList;
-import java.util.Calendar;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -56,9 +55,6 @@
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
-import java.util.concurrent.Future;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
 
 /**
@@ -92,6 +88,7 @@
   private final SqlStore<K, V> store;
   private final TypeLiteral<K> keyType;
   private final Cache<K, ValueHolder<V>> mem;
+  private final String cacheName;
 
   H2CacheImpl(
       Executor executor,
@@ -102,6 +99,7 @@
     this.store = store;
     this.keyType = keyType;
     this.mem = mem;
+    this.cacheName = store.url.substring(store.url.lastIndexOf('/') + 1);
   }
 
   @Nullable
@@ -230,20 +228,10 @@
     store.close();
   }
 
-  void prune(ScheduledExecutorService service) {
+  void prune() {
+    logger.atInfo().log("Pruning cache %s...", cacheName);
     store.prune(mem);
-
-    Calendar cal = Calendar.getInstance();
-    cal.set(Calendar.HOUR_OF_DAY, 01);
-    cal.set(Calendar.MINUTE, 0);
-    cal.set(Calendar.SECOND, 0);
-    cal.set(Calendar.MILLISECOND, 0);
-    cal.add(Calendar.DAY_OF_MONTH, 1);
-
-    long delay = cal.getTimeInMillis() - TimeUtil.nowMs();
-    @SuppressWarnings("unused")
-    Future<?> possiblyIgnoredError =
-        service.schedule(() -> prune(service), delay, TimeUnit.MILLISECONDS);
+    logger.atInfo().log("Finished pruning cache %s...", cacheName);
   }
 
   static class ValueHolder<V> {