From: Sébastien Lesaint Date: Thu, 7 Jan 2016 11:17:32 +0000 (+0100) Subject: SONAR-7168 add support for restart requested by child processes X-Git-Tag: 5.4-M5~23 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=73e02b6b9650e3af36e4fdb4aef243b9b92823ea;p=sonarqube.git SONAR-7168 add support for restart requested by child processes --- diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Monitor.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Monitor.java index e89c577b848..c86748780f7 100644 --- a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Monitor.java +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Monitor.java @@ -19,8 +19,10 @@ */ 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 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 watcherThreads = new CopyOnWriteArrayList<>(); + private List watcherThreads = new CopyOnWriteArrayList<>(); + @CheckForNull + private List 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 processRefs = startAndMonitorProcesses(); + startWatchingForRestartRequests(processRefs); + } + + private void resetState() { + this.lifecycle = new Lifecycle(); + lifecycle.tryToMoveTo(State.STARTING); + this.watcherThreads.clear(); + } + + private List startAndMonitorProcesses() { + List 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 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 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() { diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/ProcessRef.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/ProcessRef.java index d8ea32822d5..f1541284766 100644 --- a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/ProcessRef.java +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/ProcessRef.java @@ -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 index 00000000000..394b477cb38 --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/RestartRequestWatcherThread.java @@ -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 processes; + private final long delayMs; + + private boolean watching = true; + + public RestartRequestWatcherThread(Monitor monitor, List processes) { + this(monitor, processes, 500); + } + + public RestartRequestWatcherThread(Monitor monitor, List 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; + } +} diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/TerminatorThread.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/TerminatorThread.java index c0c1d692cdc..ab0a08df27f 100644 --- a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/TerminatorThread.java +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/TerminatorThread.java @@ -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 processes = Collections.emptyList(); TerminatorThread(Timeouts timeouts) { - super("Terminator"); + super("Terminator " + (instanceCounter++)); this.timeouts = timeouts; } diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Timeouts.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Timeouts.java index e943b421048..5d19e5c52d7 100644 --- a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Timeouts.java +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Timeouts.java @@ -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; - } - } diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/WatcherThread.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/WatcherThread.java index 7011b4383a9..d6e6da49651 100644 --- a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/WatcherThread.java +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/WatcherThread.java @@ -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; + } } diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/MonitorTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/MonitorTest.java index 6141a9aa66c..a9937311e1e 100644 --- a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/MonitorTest.java +++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/MonitorTest.java @@ -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 wasStartingAt() { return readTimeFromFile("startingAt"); } - long wasGracefullyTerminatedAt() throws IOException { + List wasGracefullyTerminatedAt() { return readTimeFromFile("terminatedAt"); } @@ -323,14 +380,23 @@ public class MonitorTest { return fileExists("readyAt"); } - long wasReadyAt() throws IOException { + List 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 readTimeFromFile(String filename) { + try { + File file = new File(tempDir, filename); + if (file.isFile() && file.exists()) { + String[] split = StringUtils.split(FileUtils.readFileToString(file), ','); + List 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 { + 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 startingAt = actual.wasStartingAt(); + longs.assertEqual(info, startingAt.size(), times); + + return this; + } + + public HttpProcessClientAssert wasStartedBefore(long date) { + isNotNull(); + + List 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 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 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 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 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 index 00000000000..c87f0bae734 --- /dev/null +++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/RestartRequestWatcherThreadTest.java @@ -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.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); + } + } +} diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TimeoutsTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TimeoutsTest.java index 43a2ff40cf3..f3bf1a93a03 100644 --- a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TimeoutsTest.java +++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TimeoutsTest.java @@ -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); } } diff --git a/server/sonar-process/src/test/java/org/sonar/process/test/HttpProcess.java b/server/sonar-process/src/test/java/org/sonar/process/test/HttpProcess.java index 78e1a9b37c4..86f4ccb4b37 100644 --- a/server/sonar-process/src/test/java/org/sonar/process/test/HttpProcess.java +++ b/server/sonar-process/src/test/java/org/sonar/process/test/HttpProcess.java @@ -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())); } }