From 1283bbf0857434b3438c77ffe8d781284d9a7df7 Mon Sep 17 00:00:00 2001 From: Sébastien Lesaint Date: Mon, 6 May 2019 09:30:40 +0200 Subject: SONAR-12043 main process supports graceful and hard stop --- server/sonar-process/build.gradle | 1 + .../org/sonar/process/AbstractStopWatcher.java | 65 ++++++++++ .../org/sonar/process/AbstractStopperThread.java | 61 +++++++++ .../java/org/sonar/process/HardStopWatcher.java | 68 ---------- .../java/org/sonar/process/HardStopperThread.java | 53 -------- .../src/main/java/org/sonar/process/Monitored.java | 5 + .../java/org/sonar/process/ProcessEntryPoint.java | 144 ++++++++++++++++++--- .../src/main/java/org/sonar/process/Stoppable.java | 2 + .../org/sonar/process/AbstractStopWatcherTest.java | 75 +++++++++++ .../sonar/process/AbstractStopperThreadTest.java | 78 +++++++++++ .../org/sonar/process/HardStopWatcherTest.java | 69 ---------- .../org/sonar/process/HardStopperThreadTest.java | 60 --------- .../org/sonar/process/ProcessEntryPointTest.java | 44 ++++++- .../org/sonar/process/test/StandardProcess.java | 15 ++- 14 files changed, 465 insertions(+), 275 deletions(-) create mode 100644 server/sonar-process/src/main/java/org/sonar/process/AbstractStopWatcher.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/AbstractStopperThread.java delete mode 100644 server/sonar-process/src/main/java/org/sonar/process/HardStopWatcher.java delete mode 100644 server/sonar-process/src/main/java/org/sonar/process/HardStopperThread.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/AbstractStopWatcherTest.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/AbstractStopperThreadTest.java delete mode 100644 server/sonar-process/src/test/java/org/sonar/process/HardStopWatcherTest.java delete mode 100644 server/sonar-process/src/test/java/org/sonar/process/HardStopperThreadTest.java (limited to 'server/sonar-process') diff --git a/server/sonar-process/build.gradle b/server/sonar-process/build.gradle index d458ec793f0..ee525939cb9 100644 --- a/server/sonar-process/build.gradle +++ b/server/sonar-process/build.gradle @@ -27,5 +27,6 @@ dependencies { testCompile 'org.assertj:assertj-core' testCompile 'org.eclipse.jetty:jetty-server' testCompile 'org.mockito:mockito-core' + testCompile 'org.awaitility:awaitility' testCompile project(':sonar-testing-harness') } diff --git a/server/sonar-process/src/main/java/org/sonar/process/AbstractStopWatcher.java b/server/sonar-process/src/main/java/org/sonar/process/AbstractStopWatcher.java new file mode 100644 index 00000000000..50e4e73940a --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/AbstractStopWatcher.java @@ -0,0 +1,65 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info 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; + +import com.google.common.annotations.VisibleForTesting; +import java.util.function.BooleanSupplier; + +abstract class AbstractStopWatcher extends Thread { + private final Runnable stopCommand; + private final BooleanSupplier shouldStopTest; + private final long delayMs; + private volatile boolean watching = true; + + public AbstractStopWatcher(String threadName, Runnable stopCommand, BooleanSupplier shouldStopTest) { + this(threadName, stopCommand, shouldStopTest, 500L); + } + + @VisibleForTesting + AbstractStopWatcher(String threadName, Runnable stopCommand, BooleanSupplier shouldStopTest, long delayMs) { + super(threadName); + this.stopCommand = stopCommand; + this.shouldStopTest = shouldStopTest; + this.delayMs = delayMs; + } + + @Override + public void run() { + while (watching) { + if (shouldStopTest.getAsBoolean()) { + stopCommand.run(); + watching = false; + } else { + try { + Thread.sleep(delayMs); + } catch (InterruptedException ignored) { + watching = false; + // restore interrupted flag + Thread.currentThread().interrupt(); + } + } + } + } + + public void stopWatching() { + super.interrupt(); + watching = false; + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/AbstractStopperThread.java b/server/sonar-process/src/main/java/org/sonar/process/AbstractStopperThread.java new file mode 100644 index 00000000000..ae0c85d0018 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/AbstractStopperThread.java @@ -0,0 +1,61 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info 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; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.slf4j.LoggerFactory; + +/** + * Stops process in a short time fashion + */ +abstract class AbstractStopperThread extends Thread { + + private final Runnable stopCode; + private final long terminationTimeoutMs; + private boolean stop = false; + + AbstractStopperThread(String threadName, Runnable stopCode, long terminationTimeoutMs) { + super(threadName); + this.stopCode = stopCode; + this.terminationTimeoutMs = terminationTimeoutMs; + } + + @Override + public void run() { + ExecutorService executor = Executors.newSingleThreadExecutor(); + try { + Future future = executor.submit(stopCode); + future.get(terminationTimeoutMs, TimeUnit.MILLISECONDS); + } catch (Exception e) { + if (!stop) { + LoggerFactory.getLogger(getClass()).error("Can not stop in {}ms", terminationTimeoutMs, e); + } + } + executor.shutdownNow(); + } + + public void stopIt() { + this.stop = true; + super.interrupt(); + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/HardStopWatcher.java b/server/sonar-process/src/main/java/org/sonar/process/HardStopWatcher.java deleted file mode 100644 index 7286d64259a..00000000000 --- a/server/sonar-process/src/main/java/org/sonar/process/HardStopWatcher.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2019 SonarSource SA - * mailto:info 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; - -import org.slf4j.LoggerFactory; -import org.sonar.process.sharedmemoryfile.ProcessCommands; - -/** - * This watchdog is looking for hard stop to be requested via {@link ProcessCommands#askedForHardStop()}. - */ -public class HardStopWatcher extends Thread { - - private final ProcessCommands commands; - private final Stoppable stoppable; - private final long delayMs; - private boolean watching = true; - - public HardStopWatcher(ProcessCommands commands, Stoppable stoppable) { - this(commands, stoppable, 500L); - } - - HardStopWatcher(ProcessCommands commands, Stoppable stoppable, long delayMs) { - super("HardStop Watcher"); - this.commands = commands; - this.stoppable = stoppable; - this.delayMs = delayMs; - } - - @Override - public void run() { - while (watching) { - if (commands.askedForHardStop()) { - LoggerFactory.getLogger(getClass()).info("Hard stopping process"); - stoppable.hardStopAsync(); - watching = false; - } else { - try { - Thread.sleep(delayMs); - } catch (InterruptedException ignored) { - watching = false; - // restore interrupted flag - Thread.currentThread().interrupt(); - } - } - } - } - - public void stopWatching() { - watching = false; - } -} diff --git a/server/sonar-process/src/main/java/org/sonar/process/HardStopperThread.java b/server/sonar-process/src/main/java/org/sonar/process/HardStopperThread.java deleted file mode 100644 index c2414dd17ba..00000000000 --- a/server/sonar-process/src/main/java/org/sonar/process/HardStopperThread.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2019 SonarSource SA - * mailto:info 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; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import org.slf4j.LoggerFactory; - -/** - * Stops process in a short time fashion - */ -class HardStopperThread extends Thread { - - private final Monitored monitored; - private final long terminationTimeoutMs; - - HardStopperThread(Monitored monitored, long terminationTimeoutMs) { - super("HardStopper"); - this.monitored = monitored; - this.terminationTimeoutMs = terminationTimeoutMs; - } - - @Override - public void run() { - ExecutorService executor = Executors.newSingleThreadExecutor(); - try { - Future future = executor.submit(monitored::hardStop); - future.get(terminationTimeoutMs, TimeUnit.MILLISECONDS); - } catch (Exception e) { - LoggerFactory.getLogger(getClass()).error("Can not stop in {}ms", terminationTimeoutMs, e); - } - executor.shutdownNow(); - } -} diff --git a/server/sonar-process/src/main/java/org/sonar/process/Monitored.java b/server/sonar-process/src/main/java/org/sonar/process/Monitored.java index 56d6b249ce0..653f5408220 100644 --- a/server/sonar-process/src/main/java/org/sonar/process/Monitored.java +++ b/server/sonar-process/src/main/java/org/sonar/process/Monitored.java @@ -46,6 +46,11 @@ public interface Monitored { */ void awaitStop(); + /** + * Ask process to gracefully stop and wait until then. + */ + void stop(); + /** * Ask process to quickly stop and wait until then. */ diff --git a/server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java index ae6b4376557..1976f5699b4 100644 --- a/server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java +++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java @@ -20,12 +20,17 @@ package org.sonar.process; import java.io.File; +import java.util.Optional; +import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.process.sharedmemoryfile.DefaultProcessCommands; import org.sonar.process.sharedmemoryfile.ProcessCommands; -public class ProcessEntryPoint implements Stoppable { +import static java.util.Optional.of; +import static java.util.Optional.ofNullable; + +public class ProcessEntryPoint { public static final String PROPERTY_PROCESS_KEY = "process.key"; public static final String PROPERTY_PROCESS_INDEX = "process.index"; @@ -39,18 +44,19 @@ public class ProcessEntryPoint implements Stoppable { private final Lifecycle lifecycle = new Lifecycle(); private final ProcessCommands commands; private final SystemExit exit; + private final StopWatcher stopWatcher; private final HardStopWatcher hardStopWatcher; - private volatile Monitored monitored; - private volatile HardStopperThread hardStopperThread; - // new Runnable() is important to avoid conflict of call to ProcessEntryPoint#stop() with Thread#stop() - private Thread shutdownHook = new Thread(new Runnable() { + private final Thread shutdownHook = new Thread(new Runnable() { @Override public void run() { exit.setInShutdownHook(); - hardStop(); + stop(); } }); + private volatile Monitored monitored; + private volatile StopperThread stopperThread; + private volatile HardStopperThread hardStopperThread; ProcessEntryPoint(Props props, SystemExit exit, ProcessCommands commands) { this(props, getProcessNumber(props), getSharedDir(props), exit, commands); @@ -63,6 +69,7 @@ public class ProcessEntryPoint implements Stoppable { this.sharedDir = sharedDir; this.exit = exit; this.commands = commands; + this.stopWatcher = new StopWatcher(commands, this); this.hardStopWatcher = new HardStopWatcher(commands, this); } @@ -108,6 +115,7 @@ public class ProcessEntryPoint implements Stoppable { private void launch(Logger logger) throws InterruptedException { logger.info("Starting {}", getKey()); Runtime.getRuntime().addShutdownHook(shutdownHook); + stopWatcher.start(); hardStopWatcher.start(); monitored.start(); @@ -152,31 +160,63 @@ public class ProcessEntryPoint implements Stoppable { return lifecycle.getState() == Lifecycle.State.STARTED; } + void stop() { + stopAsync() + .ifPresent(stoppingThread -> { + try { + // join() does nothing if thread already finished + stoppingThread.join(); + lifecycle.tryToMoveTo(Lifecycle.State.STOPPED); + commands.endWatch(); + exit.exit(0); + } catch (InterruptedException e) { + // stop can be aborted by a hard stop + Thread.currentThread().interrupt(); + } + }); + } + + private Optional stopAsync() { + if (lifecycle.tryToMoveTo(Lifecycle.State.STOPPING)) { + long terminationTimeoutMs = Long.parseLong(props.nonNullValue(PROPERTY_TERMINATION_TIMEOUT_MS)); + stopperThread = new StopperThread(monitored, terminationTimeoutMs); + stopperThread.start(); + stopWatcher.stopWatching(); + return of(stopperThread); + } + // stopperThread could already exist + return ofNullable(stopperThread); + } + /** * Blocks until stopped in a timely fashion (see {@link HardStopperThread}) */ void hardStop() { - hardStopAsync(); - try { - // hardStopperThread is not null for sure - // join() does nothing if thread already finished - hardStopperThread.join(); - lifecycle.tryToMoveTo(Lifecycle.State.STOPPED); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - commands.endWatch(); - exit.exit(0); + hardStopAsync() + .ifPresent(stoppingThread -> { + try { + // join() does nothing if thread already finished + stoppingThread.join(); + lifecycle.tryToMoveTo(Lifecycle.State.STOPPED); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + commands.endWatch(); + exit.exit(0); + }); } - @Override - public void hardStopAsync() { + private Optional hardStopAsync() { if (lifecycle.tryToMoveTo(Lifecycle.State.HARD_STOPPING)) { long terminationTimeoutMs = Long.parseLong(props.nonNullValue(PROPERTY_TERMINATION_TIMEOUT_MS)); - hardStopperThread = new HardStopperThread(monitored, terminationTimeoutMs); + hardStopperThread = new HardStopperThread(monitored, terminationTimeoutMs, stopperThread); hardStopperThread.start(); hardStopWatcher.stopWatching(); + stopWatcher.stopWatching(); + return of(hardStopperThread); } + // hardStopperThread could already exist + return ofNullable(hardStopperThread); } Lifecycle.State getState() { @@ -202,4 +242,68 @@ public class ProcessEntryPoint implements Stoppable { private static File getSharedDir(Props props) { return props.nonNullValueAsFile(PROPERTY_SHARED_PATH); } + + /** + * This watchdog is looking for hard stop to be requested via {@link ProcessCommands#askedForHardStop()}. + */ + private static class HardStopWatcher extends AbstractStopWatcher { + + private HardStopWatcher(ProcessCommands commands, ProcessEntryPoint processEntryPoint) { + super( + "HardStop Watcher", + () -> { + LoggerFactory.getLogger(HardStopWatcher.class).info("Hard stopping process"); + processEntryPoint.hardStopAsync(); + }, + commands::askedForHardStop); + } + + } + + /** + * This watchdog is looking for graceful stop to be requested via {@link ProcessCommands#askedForStop()} ()}. + */ + private static class StopWatcher extends AbstractStopWatcher { + + private StopWatcher(ProcessCommands commands, ProcessEntryPoint processEntryPoint) { + super( + "Stop Watcher", + () -> { + LoggerFactory.getLogger(StopWatcher.class).info("Stopping process"); + processEntryPoint.stopAsync(); + }, + commands::askedForStop); + } + + } + + /** + * Stops process in a graceful fashion + */ + private static class StopperThread extends AbstractStopperThread { + + private StopperThread(Monitored monitored, long terminationTimeoutMs) { + super("Stopper", monitored::stop, terminationTimeoutMs); + } + + } + + /** + * Stops process in a short time fashion + */ + private static class HardStopperThread extends AbstractStopperThread { + + private HardStopperThread(Monitored monitored, long terminationTimeoutMs, @Nullable StopperThread stopperThread) { + super( + "HardStopper", + () -> { + if (stopperThread != null) { + stopperThread.stopIt(); + } + monitored.hardStop(); + }, + terminationTimeoutMs); + } + + } } diff --git a/server/sonar-process/src/main/java/org/sonar/process/Stoppable.java b/server/sonar-process/src/main/java/org/sonar/process/Stoppable.java index 7e189583aef..cadc0ba58b2 100644 --- a/server/sonar-process/src/main/java/org/sonar/process/Stoppable.java +++ b/server/sonar-process/src/main/java/org/sonar/process/Stoppable.java @@ -21,6 +21,8 @@ package org.sonar.process; public interface Stoppable { + void stopAsync(); + void hardStopAsync(); } diff --git a/server/sonar-process/src/test/java/org/sonar/process/AbstractStopWatcherTest.java b/server/sonar-process/src/test/java/org/sonar/process/AbstractStopWatcherTest.java new file mode 100644 index 00000000000..8fbc9b2cefd --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/AbstractStopWatcherTest.java @@ -0,0 +1,75 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info 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; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.DisableOnDebug; +import org.junit.rules.TestRule; +import org.junit.rules.Timeout; +import org.sonar.process.sharedmemoryfile.ProcessCommands; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class AbstractStopWatcherTest { + + @Rule + public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60)); + + @Test + public void stop_if_receive_command() throws Exception { + + ProcessCommands commands = mock(ProcessCommands.class); + when(commands.askedForHardStop()).thenReturn(false, true); + Stoppable stoppable = mock(Stoppable.class); + + AbstractStopWatcher underTest = new AbstractStopWatcher("TheThreadName", + stoppable::hardStopAsync, commands::askedForHardStop, 1L) {}; + underTest.start(); + + while (underTest.isAlive()) { + Thread.sleep(1L); + } + verify(stoppable).hardStopAsync(); + assertThat(underTest.getName()).isEqualTo("TheThreadName"); + } + + @Test + public void stop_watching_on_interruption() throws Exception { + ProcessCommands commands = mock(ProcessCommands.class); + when(commands.askedForHardStop()).thenReturn(false); + Stoppable stoppable = mock(Stoppable.class); + + AbstractStopWatcher underTest = new AbstractStopWatcher("TheThreadName", + stoppable::hardStopAsync, commands::askedForHardStop, 1L) {}; + underTest.start(); + underTest.interrupt(); + + while (underTest.isAlive()) { + Thread.sleep(1L); + } + verify(stoppable, never()).hardStopAsync(); + assertThat(underTest.getName()).isEqualTo("TheThreadName"); + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/AbstractStopperThreadTest.java b/server/sonar-process/src/test/java/org/sonar/process/AbstractStopperThreadTest.java new file mode 100644 index 00000000000..a3e4050a14d --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/AbstractStopperThreadTest.java @@ -0,0 +1,78 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info 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; + +import java.util.concurrent.TimeUnit; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +public class AbstractStopperThreadTest { + private Monitored monitored = mock(Monitored.class); + + @Test + public void stop_in_a_timely_fashion() { + // max stop timeout is 5 seconds, but test fails after 3 seconds + // -> guarantees that stop is immediate + AbstractStopperThread stopper = new AbstractStopperThread("theThreadName", () -> monitored.hardStop(), 5000L){}; + stopper.start(); + + verify(monitored, timeout(3000)).hardStop(); + assertThat(stopper.getName()).isEqualTo("theThreadName"); + } + + @Test + public void stop_timeout() { + doAnswer(invocationOnMock -> { + await().atMost(10, TimeUnit.SECONDS).until(() -> false); + return null; + }).when(monitored).hardStop(); + + // max stop timeout is 100 milliseconds + AbstractStopperThread stopper = new AbstractStopperThread("theThreadName", () -> monitored.hardStop(), 5000L){}; + stopper.start(); + + verify(monitored, timeout(3000)).hardStop(); + assertThat(stopper.getName()).isEqualTo("theThreadName"); + } + + @Test + public void stopIt_interrupts_worker() { + doAnswer(invocationOnMock -> { + await().atMost(10, TimeUnit.SECONDS).until(() -> false); + return null; + }).when(monitored).hardStop(); + + // max stop timeout is 100 milliseconds + AbstractStopperThread stopper = new AbstractStopperThread("theThreadName", () -> monitored.hardStop(), 5000L){}; + stopper.start(); + + verify(monitored, timeout(3_000)).hardStop(); + + stopper.stopIt(); + await().atMost(3, TimeUnit.SECONDS).until(() -> !stopper.isAlive()); + assertThat(stopper.isAlive()).isFalse(); + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/HardStopWatcherTest.java b/server/sonar-process/src/test/java/org/sonar/process/HardStopWatcherTest.java deleted file mode 100644 index c8806d05f21..00000000000 --- a/server/sonar-process/src/test/java/org/sonar/process/HardStopWatcherTest.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2019 SonarSource SA - * mailto:info 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; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.DisableOnDebug; -import org.junit.rules.TestRule; -import org.junit.rules.Timeout; -import org.sonar.process.sharedmemoryfile.ProcessCommands; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class HardStopWatcherTest { - - @Rule - public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60)); - - @Test - public void stop_if_receive_command() throws Exception { - ProcessCommands commands = mock(ProcessCommands.class); - when(commands.askedForHardStop()).thenReturn(false, true); - Stoppable stoppable = mock(Stoppable.class); - - HardStopWatcher underTest = new HardStopWatcher(commands, stoppable, 1L); - underTest.start(); - - while (underTest.isAlive()) { - Thread.sleep(1L); - } - verify(stoppable).hardStopAsync(); - } - - @Test - public void stop_watching_on_interruption() throws Exception { - ProcessCommands commands = mock(ProcessCommands.class); - when(commands.askedForHardStop()).thenReturn(false); - Stoppable stoppable = mock(Stoppable.class); - - HardStopWatcher underTest = new HardStopWatcher(commands, stoppable, 1L); - underTest.start(); - underTest.interrupt(); - - while (underTest.isAlive()) { - Thread.sleep(1L); - } - verify(stoppable, never()).hardStopAsync(); - } -} diff --git a/server/sonar-process/src/test/java/org/sonar/process/HardStopperThreadTest.java b/server/sonar-process/src/test/java/org/sonar/process/HardStopperThreadTest.java deleted file mode 100644 index a53b8f8a23c..00000000000 --- a/server/sonar-process/src/test/java/org/sonar/process/HardStopperThreadTest.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2019 SonarSource SA - * mailto:info 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; - -import org.junit.Test; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.verify; - -public class HardStopperThreadTest { - private Monitored monitored = mock(Monitored.class); - - @Test - public void stop_in_a_timely_fashion() { - // max stop timeout is 5 seconds, but test fails after 3 seconds - // -> guarantees that stop is immediate - HardStopperThread stopper = new HardStopperThread(monitored, 5000L); - stopper.start(); - - verify(monitored, timeout(3000)).hardStop(); - } - - @Test - public void stop_timeout() { - doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocationOnMock) throws Throwable { - Thread.sleep(10000L); - return null; - } - }).when(monitored).hardStop(); - - // max stop timeout is 100 milliseconds - HardStopperThread stopper = new HardStopperThread(monitored, 100L); - stopper.start(); - - verify(monitored, timeout(3000)).hardStop(); - } -} diff --git a/server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java b/server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java index bd0689d3ed1..2cbfe3715a7 100644 --- a/server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java +++ b/server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java @@ -19,7 +19,9 @@ */ package org.sonar.process; +import java.io.File; import java.io.IOException; +import java.util.Properties; import org.apache.commons.io.FileUtils; import org.junit.Rule; import org.junit.Test; @@ -31,9 +33,6 @@ import org.sonar.process.Lifecycle.State; import org.sonar.process.sharedmemoryfile.ProcessCommands; import org.sonar.process.test.StandardProcess; -import java.io.File; -import java.util.Properties; - import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; @@ -89,7 +88,7 @@ public class ProcessEntryPointTest { } @Test - public void launch_then_request_hard_stop() throws Exception { + public void launch_then_request_graceful_stop() throws Exception { Props props = createProps(); final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit, commands); final StandardProcess process = new StandardProcess(); @@ -109,9 +108,36 @@ public class ProcessEntryPointTest { // requests for graceful stop -> waits until down // Should terminate before the timeout of 30s + entryPoint.stop(); + + assertThat(process.getState()).isEqualTo(State.STOPPED); + assertThat(process.wasHardStopped()).isEqualTo(false); + } + + @Test + public void launch_then_request_hard_stop() throws Exception { + Props props = createProps(); + final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit, commands); + final StandardProcess process = new StandardProcess(); + + Thread runner = new Thread() { + @Override + public void run() { + // starts and waits until terminated + entryPoint.launch(process); + } + }; + runner.start(); + + while (process.getState() != State.STARTED) { + Thread.sleep(10L); + } + + // requests for stop hardly waiting entryPoint.hardStop(); assertThat(process.getState()).isEqualTo(State.STOPPED); + assertThat(process.wasHardStopped()).isEqualTo(true); } @Test @@ -180,6 +206,11 @@ public class ProcessEntryPointTest { } + @Override + public void stop() { + + } + @Override public void hardStop() { @@ -203,6 +234,11 @@ public class ProcessEntryPointTest { } + @Override + public void stop() { + + } + @Override public void hardStop() { diff --git a/server/sonar-process/src/test/java/org/sonar/process/test/StandardProcess.java b/server/sonar-process/src/test/java/org/sonar/process/test/StandardProcess.java index 7db02eff2f1..ef85fd90227 100644 --- a/server/sonar-process/src/test/java/org/sonar/process/test/StandardProcess.java +++ b/server/sonar-process/src/test/java/org/sonar/process/test/StandardProcess.java @@ -26,6 +26,7 @@ import org.sonar.process.Lifecycle.State; public class StandardProcess implements Monitored { private State state = State.INIT; + private boolean hardStopped = false; private final Thread daemon = new Thread() { @Override @@ -64,13 +65,21 @@ public class StandardProcess implements Monitored { } } + @Override + public void stop() { + state = State.STOPPING; + daemon.interrupt(); + state = State.STOPPED; + } + /** * Blocks until stopped */ @Override public void hardStop() { - state = State.STOPPING; + state = State.HARD_STOPPING; daemon.interrupt(); + hardStopped = true; state = State.STOPPED; } @@ -78,6 +87,10 @@ public class StandardProcess implements Monitored { return state; } + public boolean wasHardStopped() { + return hardStopped; + } + public static void main(String[] args) { ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(args); entryPoint.launch(new StandardProcess()); -- cgit v1.2.3