Add grace period to avoid collecting stats too often

Some stats might be too heavy to compute.
Added a grace period to avoid too frequent collection.

Bug: Issue 15971

Change-Id: I54adfe04e8aec8f7fd1b7d8bf12367422b5e04e1
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsCache.java b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsCache.java
index dfabe10..f1d46b2 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsCache.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsCache.java
@@ -14,42 +14,62 @@
 
 package com.googlesource.gerrit.plugins.gitrepometrics;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Supplier;
 import com.google.common.collect.Maps;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.inject.Inject;
-import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.gitrepometrics.collectors.GitRepoMetric;
 import com.googlesource.gerrit.plugins.gitrepometrics.collectors.GitStats;
+import java.time.Clock;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 
-@Singleton
 public class GitRepoMetricsCache {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
   private Map<String, Long> metrics;
   private final MetricMaker metricMaker;
   private final List<String> projects;
+  private Map<String, Long> collectedAt;
+  private final long gracePeriodMs;
+
+  private final Clock clock;
 
   public static List<GitRepoMetric> metricsNames = new ArrayList<>(GitStats.availableMetrics());
 
-  @Inject
-  GitRepoMetricsCache(MetricMaker metricMaker, GitRepoMetricsConfig config) {
+  @VisibleForTesting
+  GitRepoMetricsCache(MetricMaker metricMaker, GitRepoMetricsConfig config, Clock clock) {
     this.metricMaker = metricMaker;
     this.projects = config.getRepositoryNames();
     this.metrics = Maps.newHashMap();
+    this.collectedAt = Maps.newHashMap();
+    this.clock = clock;
+    this.gracePeriodMs = config.getGracePeriodMs();
+  }
+
+  @Inject
+  GitRepoMetricsCache(MetricMaker metricMaker, GitRepoMetricsConfig config) {
+    this(metricMaker, config, Clock.systemDefaultZone());
   }
 
   public Map<String, Long> getMetrics() {
     return metrics;
   }
 
-  public void setMetrics(Map<String, Long> metrics) {
+  public void setMetrics(Map<String, Long> metrics, String projectName) {
+    this.collectedAt.put(projectName, clock.millis());
     this.metrics = metrics;
   }
 
+  @VisibleForTesting
+  public Map<String, Long> getCollectedAt() {
+    return collectedAt;
+  }
+
   public void initCache() {
     metricsNames.forEach(
         gitRepoMetric -> {
@@ -84,6 +104,16 @@
   }
 
   public boolean shouldCollectStats(String projectName) {
+    long lastCollectionTime = collectedAt.getOrDefault(projectName, 0L);
+    long currentTimeMs = System.currentTimeMillis();
+    boolean doCollectStats = lastCollectionTime + gracePeriodMs <= currentTimeMs;
+    if (!doCollectStats) {
+      logger.atFine().log(
+          "Skip stats collection for %s (grace period: %d, last collection time: %d, current time: %d",
+          projectName, gracePeriodMs, lastCollectionTime, currentTimeMs);
+      return false;
+    }
+
     return projects.stream().anyMatch(p -> p.equals(projectName));
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsConfig.java b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsConfig.java
index a9937cf..a588a73 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsConfig.java
@@ -22,6 +22,7 @@
 import com.google.inject.Singleton;
 import java.util.Arrays;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.Config;
 
 @Singleton
@@ -38,4 +39,8 @@
   public List<String> getRepositoryNames() {
     return Arrays.stream(config.getStringList(pluginName, null, "project")).collect(toList());
   }
+
+  public Long getGracePeriodMs() {
+    return config.getTimeUnit("git-repo-metrics", null, "gracePeriod", 0L, TimeUnit.MILLISECONDS);
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoUpdateListener.java b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoUpdateListener.java
index 1411c61..e9e4d98 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoUpdateListener.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoUpdateListener.java
@@ -23,7 +23,7 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
   private final ExecutorService executor;
   private final UpdateGitMetricsTask.Factory updateGitMetricsTaskFactory;
-  private GitRepoMetricsCache gitRepoMetricsCache;
+  private final GitRepoMetricsCache gitRepoMetricsCache;
 
   @Inject
   GitRepoUpdateListener(
@@ -40,11 +40,9 @@
     String projectName = event.getProjectName();
     logger.atFine().log("Got an update for project %s", projectName);
 
-    if (!gitRepoMetricsCache.shouldCollectStats(projectName)) {
-      return;
+    if (gitRepoMetricsCache.shouldCollectStats(projectName)) {
+      UpdateGitMetricsTask updateGitMetricsTask = updateGitMetricsTaskFactory.create(projectName);
+      executor.execute(updateGitMetricsTask);
     }
-
-    UpdateGitMetricsTask updateGitMetricsTask = updateGitMetricsTaskFactory.create(projectName);
-    executor.execute(updateGitMetricsTask);
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/UpdateGitMetricsTask.java b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/UpdateGitMetricsTask.java
index 2196a06..ed120e5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/UpdateGitMetricsTask.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/UpdateGitMetricsTask.java
@@ -61,7 +61,7 @@
       Map<String, Long> newMetrics = gitStats.get();
       logger.atInfo().log(
           "Here all the metrics for %s - %s", project.getName(), getStringFromMap(newMetrics));
-      gitRepoMetricsCache.setMetrics(newMetrics);
+      gitRepoMetricsCache.setMetrics(newMetrics, projectName);
     } catch (RepositoryNotFoundException e) {
       logger.atSevere().withCause(e).log("Cannot find repository for %s", projectName);
     } catch (IOException e) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/collectors/GitStats.java b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/collectors/GitStats.java
index 5a32a5b..2eab2f9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/collectors/GitStats.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/gitrepometrics/collectors/GitStats.java
@@ -81,4 +81,8 @@
   private void putMetric(Map<String, Long> metrics, String metricName, long value) {
     metrics.put(GitRepoMetricsCache.getMetricName(metricName, p.getName()), value);
   }
+
+  public String getStatName() {
+    return "git-statistics";
+  }
 }
diff --git a/src/resources/Documentation/config.md b/src/resources/Documentation/config.md
index 3ba47a9..26d162e 100644
--- a/src/resources/Documentation/config.md
+++ b/src/resources/Documentation/config.md
@@ -30,4 +30,8 @@
 [git-repo-metrics]
   project = test-project
   project = another-repo
-```
\ No newline at end of file
+  gracePeriod = 5m
+```
+
+_git-repo-metrics.gracePeriod_: Grace period between samples collection. Used to avoid aggressive
+metrics collection. By default, 0.
\ No newline at end of file
diff --git a/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/ConfigSetupUtils.java b/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/ConfigSetupUtils.java
index fc74cef..2872ea9 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/ConfigSetupUtils.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/ConfigSetupUtils.java
@@ -27,15 +27,21 @@
 import org.eclipse.jgit.lib.Config;
 
 public class ConfigSetupUtils {
-  final static String pluginName = "git-repo-metrics";
+  static final String pluginName = "git-repo-metrics";
   private final Path basePath;
   private final Path gitBasePath;
   private final List<String> projects;
+  private final String gracePeriod;
 
   public ConfigSetupUtils(List<String> projects) throws IOException {
+    this(projects, "0");
+  }
+
+  public ConfigSetupUtils(List<String> projects, String gracePeriod) throws IOException {
     this.basePath = Files.createTempDirectory("git_repo_metrics_");
     this.gitBasePath = new File(basePath.toFile(), "git").toPath();
     this.projects = projects;
+    this.gracePeriod = gracePeriod;
   }
 
   public GitRepoMetricsConfig getGitRepoMetricsConfig() {
@@ -50,6 +56,7 @@
     Config c = new Config();
 
     c.setStringList(pluginName, null, "project", projects);
+    c.setString(pluginName, null, "gracePeriod", gracePeriod);
     c.setString("gerrit", null, "basePath", gitBasePath.toString());
     return c;
   }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsCacheTest.java b/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsCacheTest.java
index 7a3cf5a..70c2dd9 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsCacheTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/gitrepometrics/GitRepoMetricsCacheTest.java
@@ -16,17 +16,21 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.metrics.CallbackMetric0;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.DisabledMetricMaker;
 import com.googlesource.gerrit.plugins.gitrepometrics.collectors.GitStats;
 import java.io.IOException;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 import java.util.Collections;
 import org.junit.Before;
 import org.junit.Test;
 
 public class GitRepoMetricsCacheTest {
-  GitRepoMetricsCache gitRepoMetricsCacheModule;
+  GitRepoMetricsCache gitRepoMetricsCache;
   GitRepoMetricsConfig gitRepoMetricsConfig;
   FakeMetricMaker fakeMetricMaker;
   private ConfigSetupUtils configSetupUtils;
@@ -41,28 +45,81 @@
   public void shouldRegisterMetrics() throws IOException {
     gitRepoMetricsConfig = configSetupUtils.getGitRepoMetricsConfig();
     fakeMetricMaker = new FakeMetricMaker();
-    gitRepoMetricsCacheModule = new GitRepoMetricsCache(fakeMetricMaker, gitRepoMetricsConfig);
-    gitRepoMetricsCacheModule.initCache();
+    gitRepoMetricsCache = new GitRepoMetricsCache(fakeMetricMaker, gitRepoMetricsConfig);
+    gitRepoMetricsCache.initCache();
     assertThat(fakeMetricMaker.callsCounter).isEqualTo(GitStats.availableMetrics().size());
   }
 
   @Test
   public void shouldCollectStatsForEnabledRepo() throws IOException {
     gitRepoMetricsConfig = configSetupUtils.getGitRepoMetricsConfig();
-    gitRepoMetricsCacheModule =
-        new GitRepoMetricsCache(new FakeMetricMaker(), gitRepoMetricsConfig);
+    gitRepoMetricsCache = new GitRepoMetricsCache(new FakeMetricMaker(), gitRepoMetricsConfig);
 
-    assertThat(gitRepoMetricsCacheModule.shouldCollectStats(enabledRepo)).isTrue();
+    assertThat(gitRepoMetricsCache.shouldCollectStats(enabledRepo)).isTrue();
   }
 
   @Test
   public void shouldNotCollectStatsForDisabledRepo() throws IOException {
     String disabledRepo = "disabledRepo";
     gitRepoMetricsConfig = configSetupUtils.getGitRepoMetricsConfig();
-    gitRepoMetricsCacheModule =
-        new GitRepoMetricsCache(new FakeMetricMaker(), gitRepoMetricsConfig);
+    gitRepoMetricsCache = new GitRepoMetricsCache(new FakeMetricMaker(), gitRepoMetricsConfig);
 
-    assertThat(gitRepoMetricsCacheModule.shouldCollectStats(disabledRepo)).isFalse();
+    assertThat(gitRepoMetricsCache.shouldCollectStats(disabledRepo)).isFalse();
+  }
+
+  @Test
+  public void shouldCollectStatsWhenGracePeriodNotSet() throws IOException {
+    ConfigSetupUtils configSetupUtils =
+        new ConfigSetupUtils(Collections.singletonList(enabledRepo));
+    gitRepoMetricsConfig = configSetupUtils.getGitRepoMetricsConfig();
+    gitRepoMetricsCache = new GitRepoMetricsCache(new FakeMetricMaker(), gitRepoMetricsConfig);
+
+    gitRepoMetricsCache.setMetrics(ImmutableMap.of("anyMetric", 0L), enabledRepo);
+
+    assertThat(gitRepoMetricsCache.shouldCollectStats(enabledRepo)).isTrue();
+  }
+
+  @Test
+  public void shouldSkipCollectionWhenGracePeriodIsNotExpired() throws IOException {
+    ConfigSetupUtils configSetupUtils =
+        new ConfigSetupUtils(Collections.singletonList(enabledRepo), "5 m");
+    gitRepoMetricsConfig = configSetupUtils.getGitRepoMetricsConfig();
+    gitRepoMetricsCache = new GitRepoMetricsCache(new FakeMetricMaker(), gitRepoMetricsConfig);
+
+    gitRepoMetricsCache.setMetrics(ImmutableMap.of("anyMetric", 0L), enabledRepo);
+
+    assertThat(gitRepoMetricsCache.shouldCollectStats(enabledRepo)).isFalse();
+  }
+
+  @Test
+  public void shouldCollectStatsWhenGracePeriodIsExpired() throws IOException {
+    ConfigSetupUtils configSetupUtils =
+        new ConfigSetupUtils(Collections.singletonList(enabledRepo), "1 s");
+    gitRepoMetricsConfig = configSetupUtils.getGitRepoMetricsConfig();
+    gitRepoMetricsCache =
+        new GitRepoMetricsCache(
+            new FakeMetricMaker(),
+            gitRepoMetricsConfig,
+            Clock.fixed(
+                Instant.now().minus(2, ChronoUnit.SECONDS), Clock.systemDefaultZone().getZone()));
+
+    gitRepoMetricsCache.setMetrics(ImmutableMap.of("anyMetric", 0L), enabledRepo);
+
+    assertThat(gitRepoMetricsCache.shouldCollectStats(enabledRepo)).isTrue();
+  }
+
+  @Test
+  public void shouldSetCollectionTime() throws IOException {
+    ConfigSetupUtils configSetupUtils =
+        new ConfigSetupUtils(Collections.singletonList(enabledRepo));
+    gitRepoMetricsConfig = configSetupUtils.getGitRepoMetricsConfig();
+    gitRepoMetricsCache = new GitRepoMetricsCache(new FakeMetricMaker(), gitRepoMetricsConfig);
+
+    long currentTimeStamp = System.currentTimeMillis();
+
+    gitRepoMetricsCache.setMetrics(ImmutableMap.of("anyMetric", 0L), enabledRepo);
+
+    assertThat(gitRepoMetricsCache.getCollectedAt().get(enabledRepo)).isAtLeast(currentTimeStamp);
   }
 
   private class FakeMetricMaker extends DisabledMetricMaker {