Execute checks asynchronously with timeout

Do not block during the execution of the health-checks. Wait up to
500 msec and fail for timeout if not completed yet.

Change-Id: I1b282e9de339ace631262bd16b6e13cc1ffa35e4
diff --git a/src/main/java/com/googlesource/gerrit/plugins/healthcheck/HealthCheckModule.java b/src/main/java/com/googlesource/gerrit/plugins/healthcheck/HealthCheckModule.java
index 6971282..0fa02f2 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/healthcheck/HealthCheckModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/healthcheck/HealthCheckModule.java
@@ -14,14 +14,21 @@
 
 package com.googlesource.gerrit.plugins.healthcheck;
 
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.inject.AbstractModule;
 import com.googlesource.gerrit.plugins.healthcheck.check.HealthCheck;
+import java.util.concurrent.Executors;
 
 public class HealthCheckModule extends AbstractModule {
+  public static final int CHECK_THREADS_DEFAULT = 10;
 
   @Override
   protected void configure() {
+    bind(ListeningExecutorService.class)
+        .toInstance(
+            MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(CHECK_THREADS_DEFAULT)));
     DynamicSet.setOf(binder(), HealthCheck.class);
   }
 }
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 f186092..5c8d410 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
@@ -14,14 +14,22 @@
 
 package com.googlesource.gerrit.plugins.healthcheck.check;
 
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 public abstract class AbstractHealthCheck implements HealthCheck {
   private static final Logger log = LoggerFactory.getLogger(AbstractHealthCheck.class);
+  public static final long CHECK_TIMEOUT = 500L;
   private final String name;
+  private final ListeningExecutorService executor;
 
-  protected AbstractHealthCheck(String name) {
+  protected AbstractHealthCheck(ListeningExecutorService executor, String name) {
+    this.executor = executor;
     this.name = name;
   }
 
@@ -32,15 +40,29 @@
 
   @Override
   public Status run() {
-    Result healthy;
-    long ts = System.currentTimeMillis();
+    final long ts = System.currentTimeMillis();
+    ListenableFuture<Status> resultFuture =
+        executor.submit(
+            () -> {
+              Result healthy;
+              try {
+                healthy = doCheck();
+              } catch (Exception e) {
+                log.warn("Check {} failed", name, e);
+                healthy = Result.FAILED;
+              }
+              return new Status(healthy, ts, System.currentTimeMillis() - ts);
+            });
+
     try {
-      healthy = doCheck();
-    } catch (Exception e) {
-      log.warn("Check {} failed", name, e);
-      healthy = Result.FAILED;
+      return resultFuture.get(CHECK_TIMEOUT, TimeUnit.MILLISECONDS);
+    } catch (TimeoutException e) {
+      log.warn("Check {} timed out", name, e);
+      return new Status(Result.TIMEOUT, ts, System.currentTimeMillis() - ts);
+    } catch (InterruptedException | ExecutionException e) {
+      log.warn("Check {} failed while waiting for its future result", name, e);
+      return new Status(Result.FAILED, ts, System.currentTimeMillis() - ts);
     }
-    return new Status(healthy, ts, System.currentTimeMillis() - ts);
   }
 
   protected abstract Result doCheck() throws Exception;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/healthcheck/check/JGitHealthCheck.java b/src/main/java/com/googlesource/gerrit/plugins/healthcheck/check/JGitHealthCheck.java
index be0a373..a23e0d1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/healthcheck/check/JGitHealthCheck.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/healthcheck/check/JGitHealthCheck.java
@@ -16,6 +16,7 @@
 
 import static com.googlesource.gerrit.plugins.healthcheck.check.HealthCheckNames.JGIT;
 
+import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
@@ -27,8 +28,11 @@
   private final AllProjectsName allProjectsName;
 
   @Inject
-  public JGitHealthCheck(GitRepositoryManager repositoryManager, AllProjectsName allProjectsName) {
-    super(JGIT);
+  public JGitHealthCheck(
+      ListeningExecutorService executor,
+      GitRepositoryManager repositoryManager,
+      AllProjectsName allProjectsName) {
+    super(executor, JGIT);
     this.repositoryManager = repositoryManager;
     this.allProjectsName = allProjectsName;
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/healthcheck/check/ProjectsListHealthCheck.java b/src/main/java/com/googlesource/gerrit/plugins/healthcheck/check/ProjectsListHealthCheck.java
index 669ae62..580f3a0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/healthcheck/check/ProjectsListHealthCheck.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/healthcheck/check/ProjectsListHealthCheck.java
@@ -16,6 +16,7 @@
 
 import static com.googlesource.gerrit.plugins.healthcheck.check.HealthCheckNames.PROJECTSLIST;
 
+import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.server.project.ListProjects;
 import com.google.inject.Inject;
@@ -29,8 +30,8 @@
   private final ListProjects listProjects;
 
   @Inject
-  public ProjectsListHealthCheck(ListProjects listProjects) {
-    super(PROJECTSLIST);
+  public ProjectsListHealthCheck(ListeningExecutorService executor, ListProjects listProjects) {
+    super(executor, PROJECTSLIST);
     this.listProjects = listProjects;
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/healthcheck/check/ReviewDbHealthCheck.java b/src/main/java/com/googlesource/gerrit/plugins/healthcheck/check/ReviewDbHealthCheck.java
index 10aad07..138997b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/healthcheck/check/ReviewDbHealthCheck.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/healthcheck/check/ReviewDbHealthCheck.java
@@ -16,27 +16,24 @@
 
 import static com.googlesource.gerrit.plugins.healthcheck.check.HealthCheckNames.REVIEWDB;
 
+import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class ReviewDbHealthCheck extends AbstractHealthCheck {
-  private static final Logger log = LoggerFactory.getLogger(ReviewDbHealthCheck.class);
-
-  private final Provider<ReviewDb> reviewDb;
+  private final SchemaFactory<ReviewDb> reviewDb;
 
   @Inject
-  public ReviewDbHealthCheck(Provider<ReviewDb> reviewDb) {
-    super(REVIEWDB);
+  public ReviewDbHealthCheck(ListeningExecutorService executor, SchemaFactory<ReviewDb> reviewDb) {
+    super(executor, REVIEWDB);
     this.reviewDb = reviewDb;
   }
 
   @Override
   protected Result doCheck() throws Exception {
-    try (ReviewDb db = reviewDb.get()) {
+    try (ReviewDb db = reviewDb.open()) {
       db.schemaVersion().get(new CurrentSchemaVersion.Key());
       return Result.PASSED;
     }
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 f5c6036..86a0965 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/healthcheck/HealthCheckIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/healthcheck/HealthCheckIT.java
@@ -21,6 +21,7 @@
 
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestPlugin;
 import com.google.gson.Gson;
 import com.google.gson.JsonObject;
@@ -28,6 +29,7 @@
 import org.junit.Test;
 
 @TestPlugin(name = "healthcheck", sysModule = "com.googlesource.gerrit.plugins.healthcheck.Module")
+@Sandboxed
 public class HealthCheckIT extends LightweightPluginDaemonTest {
   Gson gson = new Gson();
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/healthcheck/JGitHealthCheckTest.java b/src/test/java/com/googlesource/gerrit/plugins/healthcheck/JGitHealthCheckTest.java
index 677d22a..990b4cb 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/healthcheck/JGitHealthCheckTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/healthcheck/JGitHealthCheckTest.java
@@ -17,11 +17,14 @@
 import static com.google.common.truth.Truth.assertThat;
 import static org.eclipse.jgit.lib.RefUpdate.Result.NEW;
 
+import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.RepositoryCaseMismatchException;
 import com.google.gerrit.testutil.InMemoryRepositoryManager;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.healthcheck.check.HealthCheck.Result;
 import com.googlesource.gerrit.plugins.healthcheck.check.JGitHealthCheck;
 import java.io.IOException;
@@ -41,10 +44,14 @@
 public class JGitHealthCheckTest {
   private AllProjectsName allProjectsName = new AllProjectsName("All-Projects");
   private InMemoryRepositoryManager inMemoryRepositoryManager = new InMemoryRepositoryManager();
-  PersonIdent personIdent = new PersonIdent("Gerrit Rietveld", "gerrit@rietveld.nl");
+  private PersonIdent personIdent = new PersonIdent("Gerrit Rietveld", "gerrit@rietveld.nl");
+
+  @Inject private ListeningExecutorService executor;
 
   @Before
   public void setupAllProjects() throws Exception {
+    Guice.createInjector(new HealthCheckModule()).injectMembers(this);
+
     InMemoryRepositoryManager.Repo repo =
         inMemoryRepositoryManager.createRepository(allProjectsName);
     createCommit(repo, "refs/meta/config");
@@ -53,14 +60,14 @@
   @Test
   public void shouldBeHealthyWhenJGitIsWorking() {
     JGitHealthCheck reviewDbCheck =
-        new JGitHealthCheck(getWorkingRepositoryManager(), allProjectsName);
+        new JGitHealthCheck(executor, getWorkingRepositoryManager(), allProjectsName);
     assertThat(reviewDbCheck.run().result).isEqualTo(Result.PASSED);
   }
 
   @Test
   public void shouldBeUnhealthyWhenJGitIsFailing() {
     JGitHealthCheck jGitHealthCheck =
-        new JGitHealthCheck(getFailingGitRepositoryManager(), allProjectsName);
+        new JGitHealthCheck(executor, getFailingGitRepositoryManager(), allProjectsName);
     assertThat(jGitHealthCheck.run().result).isEqualTo(Result.FAILED);
   }
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/healthcheck/ProjectsListHealthCheckTest.java b/src/test/java/com/googlesource/gerrit/plugins/healthcheck/ProjectsListHealthCheckTest.java
index 668dbdf..a45ea32 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/healthcheck/ProjectsListHealthCheckTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/healthcheck/ProjectsListHealthCheckTest.java
@@ -16,29 +16,50 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.server.project.ListProjects;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.healthcheck.check.AbstractHealthCheck;
 import com.googlesource.gerrit.plugins.healthcheck.check.HealthCheck.Result;
 import com.googlesource.gerrit.plugins.healthcheck.check.ProjectsListHealthCheck;
 import java.util.SortedMap;
 import java.util.TreeMap;
+import org.junit.Before;
 import org.junit.Test;
 
 public class ProjectsListHealthCheckTest {
+  @Inject private ListeningExecutorService executor;
+
+  @Before
+  public void setUp() {
+    Guice.createInjector(new HealthCheckModule()).injectMembers(this);
+  }
 
   @Test
   public void shouldBeHealthyWhenListProjectsWorks() {
-    ProjectsListHealthCheck jGitHealthCheck = new ProjectsListHealthCheck(getWorkingProjectList());
+    ProjectsListHealthCheck jGitHealthCheck =
+        new ProjectsListHealthCheck(executor, getWorkingProjectList(0));
     assertThat(jGitHealthCheck.run().result).isEqualTo(Result.PASSED);
   }
 
   @Test
   public void shouldBeUnhealthyWhenListProjectsIsFailing() {
-    ProjectsListHealthCheck jGitHealthCheck = new ProjectsListHealthCheck(getFailingProjectList());
+    ProjectsListHealthCheck jGitHealthCheck =
+        new ProjectsListHealthCheck(executor, getFailingProjectList());
     assertThat(jGitHealthCheck.run().result).isEqualTo(Result.FAILED);
   }
 
+  @Test
+  public void shouldBeUnhealthyWhenListProjectsIsTimingOut() {
+    ProjectsListHealthCheck jGitHealthCheck =
+        new ProjectsListHealthCheck(
+            executor, getWorkingProjectList(AbstractHealthCheck.CHECK_TIMEOUT * 2));
+    assertThat(jGitHealthCheck.run().result).isEqualTo(Result.TIMEOUT);
+  }
+
   private ListProjects getFailingProjectList() {
     return new ListProjects(null, null, null, null, null, null, null, null) {
       @Override
@@ -48,12 +69,17 @@
     };
   }
 
-  private ListProjects getWorkingProjectList() {
+  private ListProjects getWorkingProjectList(long execTime) {
     return new ListProjects(null, null, null, null, null, null, null, null) {
       @Override
       public SortedMap<String, ProjectInfo> apply() throws BadRequestException {
         SortedMap<String, ProjectInfo> projects = new TreeMap<>();
         projects.put("testproject", new ProjectInfo());
+        try {
+          Thread.sleep(execTime);
+        } catch (InterruptedException e) {
+          throw new IllegalStateException(e);
+        }
         return projects;
       }
     };
diff --git a/src/test/java/com/googlesource/gerrit/plugins/healthcheck/ReviewDbHealthCheckTest.java b/src/test/java/com/googlesource/gerrit/plugins/healthcheck/ReviewDbHealthCheckTest.java
index aae2082..1e5d712 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/healthcheck/ReviewDbHealthCheckTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/healthcheck/ReviewDbHealthCheckTest.java
@@ -16,52 +16,49 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.testutil.DisabledReviewDb;
 import com.google.gerrit.testutil.InMemoryDatabase;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Provider;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.healthcheck.check.HealthCheck;
 import com.googlesource.gerrit.plugins.healthcheck.check.ReviewDbHealthCheck;
+import org.junit.Before;
 import org.junit.Test;
 
 public class ReviewDbHealthCheckTest {
+  private SchemaFactory<ReviewDb> workingReviewDbFactory;
+
+  @Inject private ListeningExecutorService executor;
+
+  @Before
+  public void setUp() throws Exception {
+    Guice.createInjector(new HealthCheckModule()).injectMembers(this);
+    workingReviewDbFactory = InMemoryDatabase.newDatabase(new LifecycleManager()).create();
+  }
 
   @Test
-  public void shouldBeHealthyWhenReviewDbIsWorking() throws OrmException {
-    ReviewDbHealthCheck reviewDbCheck = new ReviewDbHealthCheck(getWorkingReviewDbProvider());
+  public void shouldBeHealthyWhenReviewDbIsWorking() {
+    ReviewDbHealthCheck reviewDbCheck = new ReviewDbHealthCheck(executor, workingReviewDbFactory);
     assertThat(reviewDbCheck.run().result).isEqualTo(HealthCheck.Result.PASSED);
   }
 
   @Test
   public void shouldBeUnhealthyWhenReviewDbIsFailing() {
-    ReviewDbHealthCheck reviewDbCheck = new ReviewDbHealthCheck(getFailingReviewDbProvider());
+    ReviewDbHealthCheck reviewDbCheck =
+        new ReviewDbHealthCheck(executor, getFailingReviewDbProvider());
     assertThat(reviewDbCheck.run().result).isEqualTo(HealthCheck.Result.FAILED);
   }
 
-  private Provider<ReviewDb> getFailingReviewDbProvider() {
-    return new Provider<ReviewDb>() {
+  private SchemaFactory<ReviewDb> getFailingReviewDbProvider() {
+    return new SchemaFactory<ReviewDb>() {
       @Override
-      public ReviewDb get() {
+      public ReviewDb open() {
         return new DisabledReviewDb();
       }
     };
   }
-
-  private Provider<ReviewDb> getWorkingReviewDbProvider() throws OrmException {
-    InMemoryDatabase inMemoryDatabase =
-        InMemoryDatabase.newDatabase(new LifecycleManager()).create();
-    return new Provider<ReviewDb>() {
-      @Override
-      public ReviewDb get() {
-        try {
-          return inMemoryDatabase.open();
-        } catch (OrmException e) {
-          e.printStackTrace();
-          return null;
-        }
-      }
-    };
-  }
 }