Merge branch 'stable-3.2' into stable-3.3

* stable-3.2:
  Make GlobalHealthCheck reentrant

Change-Id: Idbb21bc86bf2e0bfc021f80f1a77a4c43b512409
diff --git a/src/main/java/com/googlesource/gerrit/plugins/healthcheck/HealthCheckMetrics.java b/src/main/java/com/googlesource/gerrit/plugins/healthcheck/HealthCheckMetrics.java
index fb298fd..cea2530 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/healthcheck/HealthCheckMetrics.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/healthcheck/HealthCheckMetrics.java
@@ -15,6 +15,7 @@
 package com.googlesource.gerrit.plugins.healthcheck;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Sets;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
@@ -54,7 +55,10 @@
   @Override
   public void start() {
 
-    for (HealthCheck healthCheck : healthChecks) {
+    Set<HealthCheck> allChecks = Sets.newHashSet(healthChecks);
+    allChecks.add(globalHealthCheck);
+
+    for (HealthCheck healthCheck : allChecks) {
       String name = healthCheck.name();
 
       Counter0 failureMetric = getFailureMetric(name);
@@ -65,21 +69,6 @@
       registeredMetrics.add(metricMaker.newTrigger(latencyMetric, metricCallBack));
       triggers.add(metricCallBack);
     }
-
-    Counter0 globalFailureMetric = getFailureMetric("global");
-    CallbackMetric0<Long> globalLatencyMetric = getLatencyMetric("global");
-    Runnable globalMetricCallBack =
-        () -> {
-          HealthCheck.StatusSummary status = globalHealthCheck.getGlobalStatusSummary();
-          globalLatencyMetric.set(status.elapsed);
-          if (status.isFailure()) {
-            globalFailureMetric.increment();
-          }
-        };
-
-    registeredMetrics.add(globalFailureMetric);
-    registeredMetrics.add(metricMaker.newTrigger(globalLatencyMetric, globalMetricCallBack));
-    triggers.add(globalMetricCallBack);
   }
 
   private Runnable getCallbackMetric(
diff --git a/src/main/java/com/googlesource/gerrit/plugins/healthcheck/api/HealthCheckStatusEndpoint.java b/src/main/java/com/googlesource/gerrit/plugins/healthcheck/api/HealthCheckStatusEndpoint.java
index 9e0a607..2d5f7de 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/healthcheck/api/HealthCheckStatusEndpoint.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/healthcheck/api/HealthCheckStatusEndpoint.java
@@ -20,6 +20,7 @@
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.healthcheck.check.GlobalHealthCheck;
 import com.googlesource.gerrit.plugins.healthcheck.check.HealthCheck;
+import com.googlesource.gerrit.plugins.healthcheck.check.HealthCheck.Result;
 import java.util.Map;
 import javax.servlet.http.HttpServletResponse;
 
@@ -36,15 +37,16 @@
   @Override
   public Response<Map<String, Object>> apply(ConfigResource resource)
       throws AuthException, BadRequestException, ResourceConflictException, Exception {
-    Map<String, Object> result = healthChecks.run();
+    HealthCheck.StatusSummary globalHealthCheckStatus = healthChecks.run();
 
-    result.put("ts", healthChecks.getGlobalStatusSummary().ts);
-    result.put("elapsed", healthChecks.getGlobalStatusSummary().elapsed);
-    return Response.withStatusCode(getHTTPResultCode(result), result);
+    Map<String, Object> result = globalHealthCheckStatus.subChecks;
+    result.put("ts", globalHealthCheckStatus.ts);
+    result.put("elapsed", globalHealthCheckStatus.elapsed);
+    return Response.withStatusCode(getHTTPResultCode(globalHealthCheckStatus), result);
   }
 
-  private int getHTTPResultCode(Map<String, Object> result) {
-    return healthChecks.getResultStatus(result) == HealthCheck.Result.FAILED
+  private int getHTTPResultCode(HealthCheck.StatusSummary checkStatus) {
+    return checkStatus.result == Result.FAILED
         ? HttpServletResponse.SC_INTERNAL_SERVER_ERROR
         : HttpServletResponse.SC_OK;
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/healthcheck/check/AbstractHealthCheck.java b/src/main/java/com/googlesource/gerrit/plugins/healthcheck/check/AbstractHealthCheck.java
index a6eae79..d0181ab 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/healthcheck/check/AbstractHealthCheck.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/healthcheck/check/AbstractHealthCheck.java
@@ -17,6 +17,7 @@
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.googlesource.gerrit.plugins.healthcheck.HealthCheckConfig;
+import java.util.Collections;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
@@ -28,7 +29,7 @@
   private final long timeout;
   private final String name;
   private final ListeningExecutorService executor;
-  protected StatusSummary latestStatus;
+  protected volatile StatusSummary latestStatus;
   protected HealthCheckConfig config;
 
   protected AbstractHealthCheck(
@@ -47,6 +48,7 @@
 
   @Override
   public StatusSummary run() {
+    StatusSummary checkStatusSummary;
     boolean enabled = config.healthCheckEnabled(name);
     final long ts = System.currentTimeMillis();
     ListenableFuture<StatusSummary> resultFuture =
@@ -59,18 +61,24 @@
                 log.warn("Check {} failed", name, e);
                 healthy = Result.FAILED;
               }
-              return new StatusSummary(healthy, ts, System.currentTimeMillis() - ts);
+              return new StatusSummary(
+                  healthy, ts, System.currentTimeMillis() - ts, Collections.emptyMap());
             });
     try {
-      latestStatus = resultFuture.get(timeout, TimeUnit.MILLISECONDS);
+      checkStatusSummary = resultFuture.get(timeout, TimeUnit.MILLISECONDS);
     } catch (TimeoutException e) {
       log.warn("Check {} timed out", name, e);
-      latestStatus = new StatusSummary(Result.TIMEOUT, ts, System.currentTimeMillis() - ts);
+      checkStatusSummary =
+          new StatusSummary(
+              Result.TIMEOUT, ts, System.currentTimeMillis() - ts, Collections.emptyMap());
     } catch (InterruptedException | ExecutionException e) {
       log.warn("Check {} failed while waiting for its future result", name, e);
-      latestStatus = new StatusSummary(Result.FAILED, ts, System.currentTimeMillis() - ts);
+      checkStatusSummary =
+          new StatusSummary(
+              Result.FAILED, ts, System.currentTimeMillis() - ts, Collections.emptyMap());
     }
-    return latestStatus;
+    latestStatus = checkStatusSummary.shallowCopy();
+    return checkStatusSummary;
   }
 
   protected abstract Result doCheck() throws Exception;
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 c9066c5..c464a9e 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
@@ -23,18 +23,18 @@
 import java.util.stream.StreamSupport;
 
 @Singleton
-public class GlobalHealthCheck {
+public class GlobalHealthCheck implements HealthCheck {
 
   private final DynamicSet<HealthCheck> healthChecks;
-  private HealthCheck.StatusSummary globalStatusSummary;
+  private volatile StatusSummary latestStatus = StatusSummary.INITIAL_STATUS;
 
   @Inject
   public GlobalHealthCheck(DynamicSet<HealthCheck> healthChecks) {
     this.healthChecks = healthChecks;
-    this.globalStatusSummary = HealthCheck.StatusSummary.INITIAL_STATUS;
   }
 
-  public Map<String, Object> run() {
+  @Override
+  public HealthCheck.StatusSummary run() {
     Iterable<HealthCheck> iterable = () -> healthChecks.iterator();
     long ts = System.currentTimeMillis();
     Map<String, Object> checkToResults =
@@ -42,25 +42,33 @@
             .map(check -> Arrays.asList(check.name(), check.run()))
             .collect(Collectors.toMap(k -> (String) k.get(0), v -> v.get(1)));
     long elapsed = System.currentTimeMillis() - ts;
-    globalStatusSummary =
-        new HealthCheck.StatusSummary(getResultStatus(checkToResults), ts, elapsed);
-    return checkToResults;
+    StatusSummary globalStatus =
+        new HealthCheck.StatusSummary(
+            hasAnyFailureOnResults(checkToResults) ? Result.FAILED : Result.PASSED,
+            ts,
+            elapsed,
+            checkToResults);
+    latestStatus = globalStatus.shallowCopy();
+    return globalStatus;
   }
 
-  public HealthCheck.StatusSummary getGlobalStatusSummary() {
-    return this.globalStatusSummary;
-  }
-
-  public HealthCheck.Result getResultStatus(Map<String, Object> result) {
-    if (result.values().stream()
+  public static boolean hasAnyFailureOnResults(Map<String, Object> results) {
+    return results.values().stream()
         .filter(
             res ->
                 res instanceof HealthCheck.StatusSummary
                     && ((HealthCheck.StatusSummary) res).isFailure())
-        .findFirst()
-        .isPresent()) {
-      return HealthCheck.Result.FAILED;
-    }
-    return HealthCheck.Result.PASSED;
+        .findAny()
+        .isPresent();
+  }
+
+  @Override
+  public String name() {
+    return "global";
+  }
+
+  @Override
+  public StatusSummary getLatestStatus() {
+    return latestStatus;
   }
 }
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 de571d1..46acdde 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
@@ -16,7 +16,9 @@
 
 import com.google.gson.annotations.SerializedName;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
 
 public interface HealthCheck {
@@ -34,22 +36,29 @@
 
   public class StatusSummary {
     public static final StatusSummary INITIAL_STATUS =
-        new StatusSummary(Result.PASSED, System.currentTimeMillis(), 0L);
+        new StatusSummary(Result.PASSED, System.currentTimeMillis(), 0L, Collections.emptyMap());
     public final Result result;
     public final long ts;
     public final long elapsed;
+    public final transient Map<String, Object> subChecks;
+
     public static final Set<Result> failingResults =
         new HashSet<>(Arrays.asList(Result.FAILED, Result.TIMEOUT));
 
-    public StatusSummary(Result result, long ts, long elapsed) {
+    public StatusSummary(Result result, long ts, long elapsed, Map<String, Object> subChecks) {
       this.result = result;
       this.ts = ts;
       this.elapsed = elapsed;
+      this.subChecks = subChecks;
     }
 
     public Boolean isFailure() {
       return failingResults.contains(this.result);
     }
+
+    public StatusSummary shallowCopy() {
+      return new StatusSummary(result, ts, elapsed, Collections.emptyMap());
+    }
   }
 
   StatusSummary run();
diff --git a/src/test/java/com/googlesource/gerrit/plugins/healthcheck/GlobalHealthCheckTest.java b/src/test/java/com/googlesource/gerrit/plugins/healthcheck/GlobalHealthCheckTest.java
deleted file mode 100644
index a1c792e..0000000
--- a/src/test/java/com/googlesource/gerrit/plugins/healthcheck/GlobalHealthCheckTest.java
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.googlesource.gerrit.plugins.healthcheck;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.inject.Inject;
-import com.googlesource.gerrit.plugins.healthcheck.check.GlobalHealthCheck;
-import com.googlesource.gerrit.plugins.healthcheck.check.HealthCheck;
-import java.util.HashMap;
-import org.junit.Test;
-
-public class GlobalHealthCheckTest {
-
-  @Inject private DynamicSet<HealthCheck> healthChecks;
-
-  GlobalHealthCheck globalHealthCheck = new GlobalHealthCheck(healthChecks);
-
-  @Test
-  public void shouldFailIfOneCheckFails() {
-
-    HashMap<String, Object> checks =
-        new HashMap<String, Object>() {
-          {
-            put("failingCheck", new HealthCheck.StatusSummary(HealthCheck.Result.FAILED, 0L, 0L));
-            put("passingCheck", new HealthCheck.StatusSummary(HealthCheck.Result.PASSED, 0L, 0L));
-          }
-        };
-    assertThat(globalHealthCheck.getResultStatus(checks)).isEqualTo(HealthCheck.Result.FAILED);
-  }
-
-  @Test
-  public void shouldPassIfAllChecksPass() {
-    HashMap<String, Object> checks =
-        new HashMap<String, Object>() {
-          {
-            put("passingCheck", new HealthCheck.StatusSummary(HealthCheck.Result.PASSED, 0L, 0L));
-          }
-        };
-    assertThat(globalHealthCheck.getResultStatus(checks)).isEqualTo(HealthCheck.Result.PASSED);
-  }
-}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/healthcheck/HealthCheckMetricsTest.java b/src/test/java/com/googlesource/gerrit/plugins/healthcheck/HealthCheckMetricsTest.java
index fb64048..1c01950 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/healthcheck/HealthCheckMetricsTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/healthcheck/HealthCheckMetricsTest.java
@@ -33,6 +33,7 @@
 import com.googlesource.gerrit.plugins.healthcheck.check.HealthCheck;
 import com.googlesource.gerrit.plugins.healthcheck.check.HealthCheck.Result;
 import com.googlesource.gerrit.plugins.healthcheck.check.HealthCheck.StatusSummary;
+import java.util.Collections;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -69,7 +70,7 @@
   @Test
   public void shouldSendCounterWhenStatusSummaryFailed() {
     Long elapsed = 100L;
-    setWithStatusSummary(new StatusSummary(Result.FAILED, 1L, elapsed));
+    setWithStatusSummary(new StatusSummary(Result.FAILED, 1L, elapsed, Collections.emptyMap()));
 
     assertThat(testMetrics.getFailures()).isEqualTo(1);
     assertThat(testMetrics.getLatency()).isEqualTo(elapsed);
@@ -78,7 +79,7 @@
   @Test
   public void shouldSendCounterWhenStatusSummaryTimeout() {
     Long elapsed = 100L;
-    setWithStatusSummary(new StatusSummary(Result.TIMEOUT, 1L, elapsed));
+    setWithStatusSummary(new StatusSummary(Result.TIMEOUT, 1L, elapsed, Collections.emptyMap()));
 
     assertThat(testMetrics.getFailures()).isEqualTo(1);
     assertThat(testMetrics.getLatency()).isEqualTo(elapsed);
@@ -87,7 +88,7 @@
   @Test
   public void shouldNOTSendCounterWhenStatusSummarySuccess() {
     Long elapsed = 100L;
-    setWithStatusSummary(new StatusSummary(Result.PASSED, 1L, elapsed));
+    setWithStatusSummary(new StatusSummary(Result.PASSED, 1L, elapsed, Collections.emptyMap()));
 
     assertThat(testMetrics.failures).isEqualTo(0L);
     assertThat(testMetrics.getLatency()).isEqualTo(elapsed);