]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7168 add support for restart requested by child processes
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Thu, 7 Jan 2016 11:17:32 +0000 (12:17 +0100)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Wed, 13 Jan 2016 10:45:51 +0000 (11:45 +0100)
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Monitor.java
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/ProcessRef.java
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/RestartRequestWatcherThread.java [new file with mode: 0644]
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/TerminatorThread.java
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Timeouts.java
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/WatcherThread.java
server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/MonitorTest.java
server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/RestartRequestWatcherThreadTest.java [new file with mode: 0644]
server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TimeoutsTest.java
server/sonar-process/src/test/java/org/sonar/process/test/HttpProcess.java

index e89c577b848a335173e2d6cde36cf1a242cbb8a0..c86748780f7023b4cf7100ee3bd1b8f730e17793 100644 (file)
  */
 package org.sonar.process.monitor;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
+import javax.annotation.CheckForNull;
 import org.slf4j.LoggerFactory;
 import org.sonar.process.Lifecycle;
 import org.sonar.process.Lifecycle.State;
@@ -28,28 +30,33 @@ import org.sonar.process.ProcessCommands;
 import org.sonar.process.SystemExit;
 
 public class Monitor {
+  private static final Timeouts TIMEOUTS = new Timeouts();
 
   private final List<ProcessRef> processes = new CopyOnWriteArrayList<>();
-  private final TerminatorThread terminator;
   private final JavaProcessLauncher launcher;
-  private final Lifecycle lifecycle = new Lifecycle();
 
   private final SystemExit systemExit;
   private Thread shutdownHook = new Thread(new MonitorShutdownHook(), "Monitor Shutdown Hook");
 
   // used by awaitStop() to block until all processes are shutdown
-  private final List<WatcherThread> watcherThreads = new CopyOnWriteArrayList<>();
+  private List<WatcherThread> watcherThreads = new CopyOnWriteArrayList<>();
+  @CheckForNull
+  private List<JavaCommand> javaCommands;
+  @CheckForNull
+  private Lifecycle lifecycle;
+  @CheckForNull
+  private RestartRequestWatcherThread restartWatcher;
+  @CheckForNull
+  private TerminatorThread terminator;
   static int nextProcessId = 0;
 
-  Monitor(JavaProcessLauncher launcher, SystemExit exit, TerminatorThread terminator) {
+  Monitor(JavaProcessLauncher launcher, SystemExit exit) {
     this.launcher = launcher;
-    this.terminator = terminator;
     this.systemExit = exit;
   }
 
   public static Monitor create() {
-    Timeouts timeouts = new Timeouts();
-    return new Monitor(new JavaProcessLauncher(timeouts), new SystemExit(), new TerminatorThread(timeouts));
+    return new Monitor(new JavaProcessLauncher(TIMEOUTS), new SystemExit());
   }
 
   /**
@@ -63,33 +70,43 @@ public class Monitor {
       throw new IllegalArgumentException("At least one command is required");
     }
 
-    if (!lifecycle.tryToMoveTo(State.STARTING)) {
+    if (lifecycle != null) {
       throw new IllegalStateException("Can not start multiple times");
     }
 
     // intercepts CTRL-C
     Runtime.getRuntime().addShutdownHook(shutdownHook);
 
-    for (JavaCommand command : commands) {
+    this.javaCommands = commands;
+    start();
+  }
+
+  private void start() {
+    resetState();
+    List<ProcessRef> processRefs = startAndMonitorProcesses();
+    startWatchingForRestartRequests(processRefs);
+  }
+
+  private void resetState() {
+    this.lifecycle = new Lifecycle();
+    lifecycle.tryToMoveTo(State.STARTING);
+    this.watcherThreads.clear();
+  }
+
+  private List<ProcessRef> startAndMonitorProcesses() {
+    List<ProcessRef> processRefs = new ArrayList<>(javaCommands.size());
+    for (JavaCommand command : javaCommands) {
       try {
         ProcessRef processRef = launcher.launch(command);
         monitor(processRef);
+        processRefs.add(processRef);
       } catch (RuntimeException e) {
         // fail to start or to monitor
         stop();
         throw e;
       }
     }
-
-    if (!lifecycle.tryToMoveTo(State.STARTED)) {
-      // stopping or stopped during startup, for instance :
-      // 1. A is started
-      // 2. B starts
-      // 3. A crashes while B is starting
-      // 4. if B was not monitored during Terminator execution, then it's an alive orphan
-      stop();
-      throw new IllegalStateException("Stopped during startup");
-    }
+    return processRefs;
   }
 
   private void monitor(ProcessRef processRef) {
@@ -106,10 +123,57 @@ public class Monitor {
     LoggerFactory.getLogger(getClass()).info(String.format("%s is up", processRef));
   }
 
+  private void startWatchingForRestartRequests(List<ProcessRef> processRefs) {
+    if (lifecycle.tryToMoveTo(State.STARTED)) {
+      stopRestartWatcher();
+      startRestartWatcher(processRefs);
+    } else {
+      // stopping or stopped during startup, for instance :
+      // 1. A is started
+      // 2. B starts
+      // 3. A crashes while B is starting
+      // 4. if B was not monitored during Terminator execution, then it's an alive orphan
+      stop();
+      throw new IllegalStateException("Stopped during startup");
+    }
+  }
+
+  private void stopRestartWatcher() {
+    if (this.restartWatcher != null) {
+      this.restartWatcher.stopWatching();
+      try {
+        this.restartWatcher.join();
+      } catch (InterruptedException e) {
+        // failed to cleanly stop (very unlikely), ignore and proceed
+      }
+    }
+  }
+
+  private void startRestartWatcher(List<ProcessRef> processRefs) {
+    this.restartWatcher = new RestartRequestWatcherThread(this, processRefs);
+    this.restartWatcher.start();
+  }
+
   /**
    * Blocks until all processes are terminated
    */
   public void awaitTermination() {
+    while (awaitTerminationImpl()) {
+      LoggerFactory.getLogger(RestartRequestWatcherThread.class).info("Restarting SQ...");
+      start();
+    }
+    stopRestartWatcher();
+  }
+
+  boolean waitForOneRestart() {
+    boolean restartRequested = awaitTerminationImpl();
+    if (restartRequested) {
+      start();
+    }
+    return restartRequested;
+  }
+
+  private boolean awaitTerminationImpl() {
     for (WatcherThread watcherThread : watcherThreads) {
       while (watcherThread.isAlive()) {
         try {
@@ -119,6 +183,16 @@ public class Monitor {
         }
       }
     }
+    return hasRestartBeenRequested();
+  }
+
+  private boolean hasRestartBeenRequested() {
+    for (WatcherThread watcherThread : watcherThreads) {
+      if (watcherThread.isAskedForRestart()) {
+        return true;
+      }
+    }
+    return false;
   }
 
   /**
@@ -133,14 +207,26 @@ public class Monitor {
     }
     // safeguard if TerminatorThread is buggy
     lifecycle.tryToMoveTo(State.STOPPED);
+    // cleanly stop restart watcher
+    stopRestartWatcher();
     systemExit.exit(0);
   }
 
   /**
    * Asks for processes termination and returns without blocking until termination.
+   * However, if a termination request is already under way (it's not supposed to happen, but, technically, it can occur),
+   * this call will be blocking until the previous request finishes.
    */
   public void stopAsync() {
     if (lifecycle.tryToMoveTo(State.STOPPING)) {
+      if (terminator != null) {
+        try {
+          terminator.join();
+        } catch (InterruptedException e) {
+          // stop waiting for thread to complete and continue with creating a new one
+        }
+      }
+      terminator = new TerminatorThread(TIMEOUTS);
       terminator.setProcesses(processes);
       terminator.start();
     }
@@ -154,6 +240,10 @@ public class Monitor {
     return shutdownHook;
   }
 
+  public void restartAsync() {
+    stopAsync();
+  }
+
   private class MonitorShutdownHook implements Runnable {
     @Override
     public void run() {
index d8ea32822d553360c037ef12f0b481f9a1a5534f..f1541284766e01f315bd43fd0da5716a28d0d510 100644 (file)
@@ -54,6 +54,10 @@ class ProcessRef {
     return process;
   }
 
+  public ProcessCommands getCommands() {
+    return commands;
+  }
+
   void waitForReady() {
     boolean ready = false;
     while (!ready) {
diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/RestartRequestWatcherThread.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/RestartRequestWatcherThread.java
new file mode 100644 (file)
index 0000000..394b477
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * SonarQube :: Process Monitor
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.process.monitor;
+
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static java.util.Objects.requireNonNull;
+
+public class RestartRequestWatcherThread extends Thread {
+  private static final Logger LOG = LoggerFactory.getLogger(RestartRequestWatcherThread.class);
+  private static int instanceCounter = 0;
+
+  private final Monitor monitor;
+  private final List<ProcessRef> processes;
+  private final long delayMs;
+
+  private boolean watching = true;
+
+  public RestartRequestWatcherThread(Monitor monitor, List<ProcessRef> processes) {
+    this(monitor, processes, 500);
+  }
+
+  public RestartRequestWatcherThread(Monitor monitor, List<ProcessRef> processes, long delayMs) {
+    super("Restart watcher " + (instanceCounter++));
+    this.monitor = requireNonNull(monitor, "monitor can not be null");
+    this.processes = requireNonNull(processes, "processes can not be null");
+    this.delayMs = delayMs;
+  }
+
+  @Override
+  public void run() {
+    while (watching) {
+      for (ProcessRef processCommands : processes) {
+        if (processCommands.getCommands().askedForRestart()) {
+          LOG.info("Process [{}] requested restart", processCommands.getKey());
+          monitor.restartAsync();
+          watching = false;
+        } else {
+          try {
+            Thread.sleep(delayMs);
+          } catch (InterruptedException ignored) {
+            // keep watching
+          }
+        }
+      }
+    }
+  }
+
+  public void stopWatching() {
+    this.watching = false;
+  }
+
+  public boolean isWatching() {
+    return watching;
+  }
+}
index c0c1d692cdc7145c06f172784087f8c46a2e7a34..ab0a08df27f74c03d548e94eb8b60bc3eb464954 100644 (file)
@@ -30,12 +30,13 @@ import java.util.List;
  * if it does not receive the termination request.
  */
 class TerminatorThread extends Thread {
+  private static int instanceCounter = 0;
 
   private final Timeouts timeouts;
   private List<ProcessRef> processes = Collections.emptyList();
 
   TerminatorThread(Timeouts timeouts) {
-    super("Terminator");
+    super("Terminator " + (instanceCounter++));
     this.timeouts = timeouts;
   }
 
index e943b4210482fda90c3ee244b8dd55efc2d53890..5d19e5c52d7a355f66258428a0531c47d660013f 100644 (file)
@@ -24,7 +24,15 @@ package org.sonar.process.monitor;
  */
 class Timeouts {
 
-  private long terminationTimeout = 60000L;
+  private final long terminationTimeout;
+
+  Timeouts(long terminationTimeout) {
+    this.terminationTimeout = terminationTimeout;
+  }
+
+  public Timeouts() {
+    this(60000L);
+  }
 
   /**
    * [both monitor and monitored process] timeout of graceful termination before hard killing
@@ -33,11 +41,4 @@ class Timeouts {
     return terminationTimeout;
   }
 
-  /**
-   * @see #getTerminationTimeout()
-   */
-  void setTerminationTimeout(long l) {
-    this.terminationTimeout = l;
-  }
-
 }
index 7011b4383a9fc65cf446d6b5fcd96d7b8c5b57b2..d6e6da4965123d2ba82248e1e9db9059a2936be0 100644 (file)
@@ -32,6 +32,7 @@ class WatcherThread extends Thread {
 
   private final ProcessRef processRef;
   private final Monitor monitor;
+  private boolean askedForRestart = false;
 
   WatcherThread(ProcessRef processRef, Monitor monitor) {
     // this name is different than Thread#toString(), which includes name, priority
@@ -48,6 +49,8 @@ class WatcherThread extends Thread {
     while (!stopped) {
       try {
         processRef.getProcess().waitFor();
+        askedForRestart = processRef.getCommands().askedForRestart();
+        processRef.getCommands().acknowledgeAskForRestart();
 
         // finalize status of ProcessRef
         processRef.stop();
@@ -60,4 +63,8 @@ class WatcherThread extends Thread {
       }
     }
   }
+
+  public boolean isAskedForRestart() {
+    return askedForRestart;
+  }
 }
index 6141a9aa66c084aff53b6eb6a21a8062b0933c3e..a9937311e1e6476973c74b58c04bfc19560551bc 100644 (file)
@@ -22,10 +22,15 @@ package org.sonar.process.monitor;
 import com.github.kevinsawicki.http.HttpRequest;
 import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.List;
 import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.StringUtils;
+import org.assertj.core.api.AbstractAssert;
+import org.assertj.core.internal.Longs;
 import org.junit.After;
 import org.junit.BeforeClass;
 import org.junit.Rule;
@@ -42,6 +47,7 @@ import org.sonar.process.SystemExit;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.mock;
+import static org.sonar.process.monitor.MonitorTest.HttpProcessClientAssert.assertThat;
 
 public class MonitorTest {
 
@@ -129,13 +135,14 @@ public class MonitorTest {
     // blocks until started
     monitor.start(Arrays.asList(client.newCommand()));
 
-    assertThat(client.isReady()).isTrue();
-    assertThat(client.wasReadyAt()).isLessThanOrEqualTo(System.currentTimeMillis());
+    assertThat(client).isReady()
+      .wasStartedBefore(System.currentTimeMillis());
 
     // blocks until stopped
     monitor.stop();
-    assertThat(client.isReady()).isFalse();
-    assertThat(client.wasGracefullyTerminated()).isTrue();
+    assertThat(client)
+      .isNotReady()
+      .wasGracefullyTerminated();
     assertThat(monitor.getState()).isEqualTo(State.STOPPED);
   }
 
@@ -147,18 +154,21 @@ public class MonitorTest {
     monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand()));
 
     // start p2 when p1 is fully started (ready)
-    assertThat(p1.isReady()).isTrue();
-    assertThat(p2.isReady()).isTrue();
-    assertThat(p2.wasStartingAt()).isGreaterThanOrEqualTo(p1.wasReadyAt());
+    assertThat(p1)
+      .isReady()
+      .wasStartedBefore(p2);
+    assertThat(p2)
+        .isReady();
 
     monitor.stop();
 
     // stop in inverse order
-    assertThat(p1.isReady()).isFalse();
-    assertThat(p2.isReady()).isFalse();
-    assertThat(p1.wasGracefullyTerminated()).isTrue();
-    assertThat(p2.wasGracefullyTerminated()).isTrue();
-    assertThat(p2.wasGracefullyTerminatedAt()).isLessThanOrEqualTo(p1.wasGracefullyTerminatedAt());
+    assertThat(p1)
+      .isNotReady()
+      .wasGracefullyTerminated();
+    assertThat(p2)
+      .isNotReady()
+      .wasGracefullyTerminatedBefore(p1);
   }
 
   @Test
@@ -167,15 +177,46 @@ public class MonitorTest {
     HttpProcessClient p1 = new HttpProcessClient("p1");
     HttpProcessClient p2 = new HttpProcessClient("p2");
     monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand()));
-    assertThat(p1.isReady()).isTrue();
-    assertThat(p2.isReady()).isTrue();
+    assertThat(p1).isReady();
+    assertThat(p2).isReady();
 
     // emulate CTRL-C
     monitor.getShutdownHook().run();
     monitor.getShutdownHook().join();
 
-    assertThat(p1.wasGracefullyTerminated()).isTrue();
-    assertThat(p2.wasGracefullyTerminated()).isTrue();
+    assertThat(p1).wasGracefullyTerminated();
+    assertThat(p2).wasGracefullyTerminated();
+  }
+
+  @Test
+  public void restart_all_processes_if_one_asks_for_restart() throws Exception {
+    monitor = newDefaultMonitor();
+    HttpProcessClient p1 = new HttpProcessClient("p1");
+    HttpProcessClient p2 = new HttpProcessClient("p2");
+    monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand()));
+
+    assertThat(p1).isReady();
+    assertThat(p2).isReady();
+
+    p2.restart();
+
+    assertThat(monitor.waitForOneRestart()).isTrue();
+
+    assertThat(p1)
+        .wasStarted(2)
+        .wasGracefullyTerminated(1);
+    assertThat(p2)
+        .wasStarted(2)
+        .wasGracefullyTerminated(1);
+
+    monitor.stop();
+
+    assertThat(p1)
+        .wasStarted(2)
+        .wasGracefullyTerminated(2);
+    assertThat(p2)
+        .wasStarted(2)
+        .wasGracefullyTerminated(2);
   }
 
   @Test
@@ -191,10 +232,12 @@ public class MonitorTest {
     p1.kill();
     monitor.awaitTermination();
 
-    assertThat(p1.isReady()).isFalse();
-    assertThat(p2.isReady()).isFalse();
-    assertThat(p1.wasGracefullyTerminated()).isFalse();
-    assertThat(p2.wasGracefullyTerminated()).isTrue();
+    assertThat(p1)
+      .isNotReady()
+      .wasNotGracefullyTerminated();
+    assertThat(p2)
+      .isNotReady()
+      .wasGracefullyTerminated();
   }
 
   @Test
@@ -206,11 +249,13 @@ public class MonitorTest {
       monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand()));
       fail();
     } catch (Exception expected) {
-      assertThat(p1.wasReady()).isTrue();
-      assertThat(p2.wasReady()).isFalse();
-      assertThat(p1.wasGracefullyTerminated()).isTrue();
-      // self "gracefully terminated", even if startup went bad
-      assertThat(p2.wasGracefullyTerminated()).isTrue();
+      assertThat(p1)
+        .hasBeenReady()
+        .wasGracefullyTerminated();
+      assertThat(p2)
+        .hasNotBeenReady()
+        // self "gracefully terminated", even if startup went bad
+        .wasGracefullyTerminated();
     }
   }
 
@@ -246,7 +291,7 @@ public class MonitorTest {
 
   private Monitor newDefaultMonitor() {
     Timeouts timeouts = new Timeouts();
-    return new Monitor(new JavaProcessLauncher(timeouts), exit, new TerminatorThread(timeouts));
+    return new Monitor(new JavaProcessLauncher(timeouts), exit);
   }
 
   /**
@@ -283,7 +328,7 @@ public class MonitorTest {
      */
     boolean isReady() {
       try {
-        HttpRequest httpRequest = HttpRequest.get("http://localhost:" + httpPort + "/ping")
+        HttpRequest httpRequest = HttpRequest.get("http://localhost:" + httpPort + "/" + "ping")
           .readTimeout(2000).connectTimeout(2000);
         return httpRequest.ok() && httpRequest.body().equals("ping");
       } catch (HttpRequest.HttpRequestException e) {
@@ -296,7 +341,7 @@ public class MonitorTest {
      */
     void kill() {
       try {
-        HttpRequest.post("http://localhost:" + httpPort + "/kill")
+        HttpRequest.post("http://localhost:" + httpPort + "/" + "kill")
           .readTimeout(5000).connectTimeout(5000).ok();
       } catch (Exception e) {
         // HTTP request can't be fully processed, as web server hardly
@@ -304,6 +349,18 @@ public class MonitorTest {
       }
     }
 
+    public void restart() {
+      try {
+        HttpRequest httpRequest = HttpRequest.post("http://localhost:" + httpPort + "/" + "restart")
+            .readTimeout(5000).connectTimeout(5000);
+        if (!httpRequest.ok() || !"ok".equals(httpRequest.body())) {
+          throw new IllegalStateException("Wrong response calling restart");
+        }
+      } catch (Exception e) {
+        throw new IllegalStateException("Failed to call restart", e);
+      }
+    }
+
     /**
      * @see org.sonar.process.test.HttpProcess
      */
@@ -311,11 +368,11 @@ public class MonitorTest {
       return fileExists("terminatedAt");
     }
 
-    long wasStartingAt() throws IOException {
+    List<Long> wasStartingAt() {
       return readTimeFromFile("startingAt");
     }
 
-    long wasGracefullyTerminatedAt() throws IOException {
+    List<Long> wasGracefullyTerminatedAt() {
       return readTimeFromFile("terminatedAt");
     }
 
@@ -323,14 +380,23 @@ public class MonitorTest {
       return fileExists("readyAt");
     }
 
-    long wasReadyAt() throws IOException {
+    List<Long> wasReadyAt() {
       return readTimeFromFile("readyAt");
     }
 
-    private long readTimeFromFile(String filename) throws IOException {
-      File file = new File(tempDir, filename);
-      if (file.isFile() && file.exists()) {
-        return Long.parseLong(FileUtils.readFileToString(file));
+    private List<Long> readTimeFromFile(String filename) {
+      try {
+        File file = new File(tempDir, filename);
+        if (file.isFile() && file.exists()) {
+          String[] split = StringUtils.split(FileUtils.readFileToString(file), ',');
+          List<Long> res = new ArrayList<>(split.length);
+          for (String s : split) {
+            res.add(Long.parseLong(s));
+          }
+          return res;
+        }
+      } catch (IOException e) {
+        return Collections.emptyList();
       }
       throw new IllegalStateException("File does not exist");
     }
@@ -341,6 +407,138 @@ public class MonitorTest {
     }
   }
 
+  public static class HttpProcessClientAssert extends AbstractAssert<HttpProcessClientAssert, HttpProcessClient> {
+    Longs longs = Longs.instance();
+
+    protected HttpProcessClientAssert(HttpProcessClient actual) {
+      super(actual, HttpProcessClientAssert.class);
+    }
+
+    public static HttpProcessClientAssert assertThat(HttpProcessClient actual) {
+      return new HttpProcessClientAssert(actual);
+    }
+
+    public HttpProcessClientAssert wasStarted(int times) {
+      isNotNull();
+
+      List<Long> startingAt = actual.wasStartingAt();
+      longs.assertEqual(info, startingAt.size(), times);
+
+      return this;
+    }
+
+    public HttpProcessClientAssert wasStartedBefore(long date) {
+      isNotNull();
+
+      List<Long> startingAt = actual.wasStartingAt();
+      longs.assertEqual(info, startingAt.size(), 1);
+      longs.assertLessThanOrEqualTo(info, startingAt.iterator().next(), date);
+
+      return this;
+    }
+
+    public HttpProcessClientAssert wasStartedBefore(HttpProcessClient client) {
+      isNotNull();
+
+      List<Long> startingAt = actual.wasStartingAt();
+      longs.assertEqual(info, startingAt.size(), 1);
+      longs.assertLessThanOrEqualTo(info, startingAt.iterator().next(), client.wasStartingAt().iterator().next());
+
+      return this;
+    }
+
+    public HttpProcessClientAssert wasTerminated(int times) {
+      isNotNull();
+
+      List<Long> terminatedAt = actual.wasGracefullyTerminatedAt();
+      longs.assertEqual(info, terminatedAt.size(), 2);
+
+      return this;
+    }
+
+    public HttpProcessClientAssert wasGracefullyTerminated() {
+      isNotNull();
+
+      if (!actual.wasGracefullyTerminated()) {
+        failWithMessage("HttpClient %s should have been gracefully terminated", actual.commandKey);
+      }
+
+      return this;
+    }
+
+    public HttpProcessClientAssert wasNotGracefullyTerminated() {
+      isNotNull();
+
+      if (actual.wasGracefullyTerminated()) {
+        failWithMessage("HttpClient %s should not have been gracefully terminated", actual.commandKey);
+      }
+
+      return this;
+    }
+
+    public HttpProcessClientAssert wasGracefullyTerminatedBefore(HttpProcessClient p1) {
+      isNotNull();
+
+      List<Long> wasGracefullyTerminatedAt = actual.wasGracefullyTerminatedAt();
+      longs.assertEqual(info, wasGracefullyTerminatedAt.size(), 1);
+      longs.assertLessThanOrEqualTo(info, wasGracefullyTerminatedAt.iterator().next(), p1.wasGracefullyTerminatedAt().iterator().next());
+
+      return this;
+    }
+
+    public HttpProcessClientAssert wasGracefullyTerminated(int times) {
+      isNotNull();
+
+      List<Long> wasGracefullyTerminatedAt = actual.wasGracefullyTerminatedAt();
+      longs.assertEqual(info, wasGracefullyTerminatedAt.size(), times);
+
+      return this;
+    }
+
+    public HttpProcessClientAssert isReady() {
+      isNotNull();
+
+      // check condition
+      if (!actual.isReady()) {
+        failWithMessage("HttpClient %s should be ready", actual.commandKey);
+      }
+
+      return this;
+    }
+
+    public HttpProcessClientAssert isNotReady() {
+      isNotNull();
+
+      if (actual.isReady()) {
+        failWithMessage("HttpClient %s should not be ready", actual.commandKey);
+      }
+
+      return this;
+    }
+
+    public HttpProcessClientAssert hasBeenReady() {
+      isNotNull();
+
+      // check condition
+      if (!actual.wasReady()) {
+        failWithMessage("HttpClient %s should been ready at least once", actual.commandKey);
+      }
+
+      return this;
+    }
+
+    public HttpProcessClientAssert hasNotBeenReady() {
+      isNotNull();
+
+      // check condition
+      if (actual.wasReady()) {
+        failWithMessage("HttpClient %s should never been ready", actual.commandKey);
+      }
+
+      return this;
+    }
+  }
+
   private JavaCommand newStandardProcessCommand() throws IOException {
     return new JavaCommand("standard")
       .addClasspath(testJar.getAbsolutePath())
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/RestartRequestWatcherThreadTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/RestartRequestWatcherThreadTest.java
new file mode 100644 (file)
index 0000000..c87f0ba
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * SonarQube :: Process Monitor
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.process.monitor;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Random;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.process.DefaultProcessCommands;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class RestartRequestWatcherThreadTest {
+  private static final long TEST_DELAYS_MS = 5L;
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+  @Rule
+  public TemporaryFolder temp = new TemporaryFolder();
+
+  private Monitor monitor = mock(Monitor.class);
+
+  @Test
+  public void constructor_throws_NPE_if_monitor_arg_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("monitor can not be null");
+
+    new RestartRequestWatcherThread(null, Collections.<ProcessRef>emptyList());
+  }
+
+  @Test
+  public void constructor_throws_NPE_if_processes_arg_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("processes can not be null");
+
+    new RestartRequestWatcherThread(monitor, null);
+  }
+
+  @Test
+  public void each_RestartRequestWatcherThread_instance_get_a_unique_thread_name() {
+    assertThat(newSingleProcessRefRestartWatcher().getName())
+        .isNotEqualTo(newSingleProcessRefRestartWatcher().getName());
+  }
+
+  @Test
+  public void does_not_stop_watching_when_no_processRef_requests_restart() throws Exception {
+    RestartRequestWatcherThread underTest = newSingleProcessRefRestartWatcher();
+
+    underTest.start();
+
+    Thread.sleep(200L);
+
+    assertThat(underTest.isWatching()).isTrue();
+    assertThat(underTest.isAlive()).isTrue();
+  }
+
+  @Test(timeout = 500L)
+  public void stops_watching_when_any_processRef_requests_restart() throws Exception {
+    ProcessRef processRef1 = newProcessRef(1);
+    ProcessRef processRef2 = newProcessRef(2);
+    RestartRequestWatcherThread underTest = newSingleProcessRefRestartWatcher(processRef1, processRef2);
+
+    underTest.start();
+
+    Thread.sleep(123L);
+
+    if (new Random().nextInt() % 2 == 1) {
+      processRef1.getCommands().askForRestart();
+    } else {
+      processRef2.getCommands().askForRestart();
+    }
+
+    underTest.join();
+
+    assertThat(underTest.isWatching()).isFalse();
+    verify(monitor).restartAsync();
+  }
+
+  private RestartRequestWatcherThread newSingleProcessRefRestartWatcher(ProcessRef... processRefs) {
+    return new RestartRequestWatcherThread(monitor, Arrays.asList(processRefs), TEST_DELAYS_MS);
+  }
+
+  private ProcessRef newProcessRef(int id) {
+    try {
+      return new ProcessRef(String.valueOf(id), new DefaultProcessCommands(temp.newFolder(), id), mock(Process.class), mock(StreamGobbler.class));
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+}
index 43a2ff40cf37f2792a8729f8e45fdbdb6ebd2cc4..f3bf1a93a039b8da68fcaea84cae07d1560f51fb 100644 (file)
@@ -33,8 +33,7 @@ public class TimeoutsTest {
 
   @Test
   public void test_values() throws Exception {
-    Timeouts timeouts = new Timeouts();
-    timeouts.setTerminationTimeout(3L);
+    Timeouts timeouts = new Timeouts(3L);
     assertThat(timeouts.getTerminationTimeout()).isEqualTo(3L);
   }
 }
index 78e1a9b37c470d61e3be008a0034251ddb53dc62..86f4ccb4b3775698326f7086c749482dfc5e89b8 100644 (file)
@@ -25,6 +25,7 @@ import org.eclipse.jetty.server.Server;
 import org.eclipse.jetty.server.handler.AbstractHandler;
 import org.eclipse.jetty.server.handler.ContextHandler;
 import org.sonar.process.Monitored;
+import org.sonar.process.ProcessCommands;
 import org.sonar.process.ProcessEntryPoint;
 
 import javax.servlet.ServletException;
@@ -35,7 +36,8 @@ import java.io.File;
 import java.io.IOException;
 
 /**
- * Http server used for testing (see MonitorTest). It accepts HTTP commands /ping and /kill to hardly exit.
+ * Http server used for testing (see MonitorTest).
+ * It accepts HTTP commands /ping, /restart to request restart of all child processes and /kill to hardly exit.
  * It also pushes status to temp files, so test can verify what was really done (when server went ready state and
  * if it was gracefully terminated)
  */
@@ -45,8 +47,10 @@ public class HttpProcess implements Monitored {
   private boolean ready = false;
   // temp dir is specific to this process
   private final File tempDir = new File(System.getProperty("java.io.tmpdir"));
+  private final ProcessCommands processCommands;
 
-  public HttpProcess(int httpPort) {
+  public HttpProcess(int httpPort, ProcessCommands processCommands) {
+    this.processCommands = processCommands;
     server = new Server(httpPort);
   }
 
@@ -63,6 +67,11 @@ public class HttpProcess implements Monitored {
         if ("/ping".equals(target)) {
           request.setHandled(true);
           httpServletResponse.getWriter().print("ping");
+        } else if ("/restart".equals(target)) {
+          writeTimeToFile("restartAskedAt");
+          request.setHandled(true);
+          processCommands.askForRestart();
+          httpServletResponse.getWriter().print("ok");
         } else if ("/kill".equals(target)) {
           writeTimeToFile("killedAt");
           System.exit(0);
@@ -111,7 +120,7 @@ public class HttpProcess implements Monitored {
 
   private void writeTimeToFile(String filename) {
     try {
-      FileUtils.write(new File(tempDir, filename), String.valueOf(System.currentTimeMillis()));
+      FileUtils.write(new File(tempDir, filename), System.currentTimeMillis() + ",", true);
     } catch (IOException e) {
       throw new IllegalStateException(e);
     }
@@ -119,6 +128,6 @@ public class HttpProcess implements Monitored {
 
   public static void main(String[] args) {
     ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(args);
-    entryPoint.launch(new HttpProcess(entryPoint.getProps().valueAsInt("httpPort")));
+    entryPoint.launch(new HttpProcess(entryPoint.getProps().valueAsInt("httpPort"), entryPoint.getCommands()));
   }
 }