*/
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;
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());
}
/**
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) {
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 {
}
}
}
+ return hasRestartBeenRequested();
+ }
+
+ private boolean hasRestartBeenRequested() {
+ for (WatcherThread watcherThread : watcherThreads) {
+ if (watcherThread.isAskedForRestart()) {
+ return true;
+ }
+ }
+ return false;
}
/**
}
// 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();
}
return shutdownHook;
}
+ public void restartAsync() {
+ stopAsync();
+ }
+
private class MonitorShutdownHook implements Runnable {
@Override
public void run() {
return process;
}
+ public ProcessCommands getCommands() {
+ return commands;
+ }
+
void waitForReady() {
boolean ready = false;
while (!ready) {
--- /dev/null
+/*
+ * 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;
+ }
+}
* 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;
}
*/
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
return terminationTimeout;
}
- /**
- * @see #getTerminationTimeout()
- */
- void setTerminationTimeout(long l) {
- this.terminationTimeout = l;
- }
-
}
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
while (!stopped) {
try {
processRef.getProcess().waitFor();
+ askedForRestart = processRef.getCommands().askedForRestart();
+ processRef.getCommands().acknowledgeAskForRestart();
// finalize status of ProcessRef
processRef.stop();
}
}
}
+
+ public boolean isAskedForRestart() {
+ return askedForRestart;
+ }
}
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;
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 {
// 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);
}
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
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
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
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();
}
}
private Monitor newDefaultMonitor() {
Timeouts timeouts = new Timeouts();
- return new Monitor(new JavaProcessLauncher(timeouts), exit, new TerminatorThread(timeouts));
+ return new Monitor(new JavaProcessLauncher(timeouts), exit);
}
/**
*/
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) {
*/
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
}
}
+ 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
*/
return fileExists("terminatedAt");
}
- long wasStartingAt() throws IOException {
+ List<Long> wasStartingAt() {
return readTimeFromFile("startingAt");
}
- long wasGracefullyTerminatedAt() throws IOException {
+ List<Long> wasGracefullyTerminatedAt() {
return readTimeFromFile("terminatedAt");
}
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");
}
}
}
+ 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())
--- /dev/null
+/*
+ * 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);
+ }
+ }
+}
@Test
public void test_values() throws Exception {
- Timeouts timeouts = new Timeouts();
- timeouts.setTerminationTimeout(3L);
+ Timeouts timeouts = new Timeouts(3L);
assertThat(timeouts.getTerminationTimeout()).isEqualTo(3L);
}
}
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;
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)
*/
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);
}
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);
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);
}
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()));
}
}