Merge branch 'stable-3.10'

* stable-3.10:
  Short-circuit on failed healthchecks
  Simplify construction of map of check results
  Simplify check for any failures
  Run checks in parallel

Change-Id: I30c16e082fca33af3ae82d610d8e5191fdaf584d
diff --git a/src/main/java/com/googlesource/gerrit/plugins/healthcheck/check/GlobalHealthCheck.java b/src/main/java/com/googlesource/gerrit/plugins/healthcheck/check/GlobalHealthCheck.java
index fa5f14a..4e4cef7 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/healthcheck/check/GlobalHealthCheck.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/healthcheck/check/GlobalHealthCheck.java
@@ -22,9 +22,11 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.healthcheck.HealthCheckConfig;
-import java.util.Arrays;
+import java.util.Collections;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import java.util.stream.StreamSupport;
 
@@ -33,6 +35,31 @@
 
   private final DynamicSet<HealthCheck> healthChecks;
 
+  public static class MemoizedStatusSummary implements Supplier<StatusSummary> {
+    private final AtomicReference<StatusSummary> result = new AtomicReference<>();
+    private final HealthCheck check;
+
+    MemoizedStatusSummary(HealthCheck check) {
+      this.check = check;
+    }
+
+    @Override
+    public StatusSummary get() {
+      if (result.get() == null) {
+        result.set(check.run());
+      }
+      return result.get();
+    }
+
+    public StatusSummary getIfCompleted() {
+      StatusSummary completedResult = result.get();
+      return completedResult == null
+          ? new StatusSummary(
+              Result.NOT_RUN, System.currentTimeMillis(), 0L, Collections.emptyMap())
+          : completedResult;
+    }
+  }
+
   @Inject
   public GlobalHealthCheck(
       DynamicSet<HealthCheck> healthChecks,
@@ -47,17 +74,16 @@
   public HealthCheck.StatusSummary run() {
     Iterable<HealthCheck> iterable = () -> healthChecks.iterator();
     long ts = System.currentTimeMillis();
-    Map<String, Object> checkToResults =
+    Map<String, MemoizedStatusSummary> checkToResults =
         StreamSupport.stream(iterable.spliterator(), false)
-            .map(check -> Arrays.asList(check.name(), check.run()))
-            .collect(Collectors.toMap(k -> (String) k.get(0), v -> v.get(1)));
+            .collect(Collectors.toMap(HealthCheck::name, MemoizedStatusSummary::new));
     long elapsed = System.currentTimeMillis() - ts;
+    Result checkResult = hasAnyFailureOnResults(checkToResults) ? Result.FAILED : Result.PASSED;
+    Map<String, Object> reportedResults =
+        checkToResults.entrySet().stream()
+            .collect(Collectors.toMap(Map.Entry::getKey, v -> v.getValue().getIfCompleted()));
     StatusSummary globalStatus =
-        new HealthCheck.StatusSummary(
-            hasAnyFailureOnResults(checkToResults) ? Result.FAILED : Result.PASSED,
-            ts,
-            elapsed,
-            checkToResults);
+        new HealthCheck.StatusSummary(checkResult, ts, elapsed, reportedResults);
     if (globalStatus.isFailure()) {
       failureCounterMetric.increment();
     }
@@ -70,13 +96,7 @@
     return run().result;
   }
 
-  public static boolean hasAnyFailureOnResults(Map<String, Object> results) {
-    return results.values().stream()
-        .filter(
-            res ->
-                res instanceof HealthCheck.StatusSummary
-                    && ((HealthCheck.StatusSummary) res).isFailure())
-        .findAny()
-        .isPresent();
+  public static boolean hasAnyFailureOnResults(Map<String, MemoizedStatusSummary> results) {
+    return results.values().stream().parallel().anyMatch(res -> res.get().isFailure());
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/healthcheck/check/HealthCheck.java b/src/main/java/com/googlesource/gerrit/plugins/healthcheck/check/HealthCheck.java
index 7c4f8a1..db07320 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/healthcheck/check/HealthCheck.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/healthcheck/check/HealthCheck.java
@@ -30,6 +30,8 @@
     FAILED,
     @SerializedName("timeout")
     TIMEOUT,
+    @SerializedName("not_run")
+    NOT_RUN,
     @SerializedName("disabled")
     DISABLED;
   }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/healthcheck/AbstractHealthCheckIntegrationTest.java b/src/test/java/com/googlesource/gerrit/plugins/healthcheck/AbstractHealthCheckIntegrationTest.java
index 56f96fe..1b1cd70 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/healthcheck/AbstractHealthCheckIntegrationTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/healthcheck/AbstractHealthCheckIntegrationTest.java
@@ -16,13 +16,20 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.metrics.MetricMaker;
 import com.google.gson.Gson;
 import com.google.gson.JsonObject;
 import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
 import com.google.inject.Key;
+import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.healthcheck.check.AbstractHealthCheck;
+import com.googlesource.gerrit.plugins.healthcheck.check.HealthCheck;
 import com.googlesource.gerrit.plugins.healthcheck.check.HealthCheckNames;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -34,6 +41,33 @@
     protected void configure() {
       install(new HealthCheckExtensionApiModule());
       install(new Module());
+      DynamicSet.bind(binder(), HealthCheck.class).to(FakeHealthCheck.class);
+    }
+  }
+
+  @Singleton
+  public static class FakeHealthCheck extends AbstractHealthCheck {
+    private long sleep = 0L;
+    private Result result = Result.PASSED;
+
+    @Inject
+    public FakeHealthCheck(
+        ListeningExecutorService executor, HealthCheckConfig config, MetricMaker metricMaker) {
+      super(executor, config, "fake-check", metricMaker);
+    }
+
+    public void setSleep(long sleep) {
+      this.sleep = sleep;
+    }
+
+    public void setResult(Result result) {
+      this.result = result;
+    }
+
+    @Override
+    protected Result doCheck() throws Exception {
+      Thread.sleep(sleep);
+      return result;
     }
   }
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/healthcheck/HealthCheckIT.java b/src/test/java/com/googlesource/gerrit/plugins/healthcheck/HealthCheckIT.java
index 86ce328..621ba17 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/healthcheck/HealthCheckIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/healthcheck/HealthCheckIT.java
@@ -26,9 +26,15 @@
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestPlugin;
 import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
+import com.googlesource.gerrit.plugins.healthcheck.check.HealthCheck;
 import java.io.File;
 import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 import javax.servlet.http.HttpServletResponse;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.junit.Before;
@@ -114,6 +120,25 @@
   }
 
   @Test
+  public void shouldNotRunChecksWhenOneFails() throws Exception {
+    FakeHealthCheck fakeHealthCheck = plugin.getSysInjector().getInstance(FakeHealthCheck.class);
+    fakeHealthCheck.setResult(HealthCheck.Result.FAILED);
+
+    RestResponse resp = getHealthCheckStatus();
+    resp.assertStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    JsonObject respJson = getResponseJson(resp);
+    Set<Map.Entry<String, JsonElement>> checkResults = respJson.entrySet();
+    List<String> results =
+        checkResults.stream()
+            .map(Map.Entry::getValue)
+            .filter(JsonElement::isJsonObject)
+            .map(j -> j.getAsJsonObject().get("result"))
+            .map(JsonElement::getAsString)
+            .collect(Collectors.toList());
+    assertThat(results).contains("not_run");
+  }
+
+  @Test
   public void shouldReturnAuthCheckAsDisabled() throws Exception {
     disableCheck(AUTH);
     RestResponse resp = getHealthCheckStatus();