From 5db076096966170cde636fa2c41d7777abca193e Mon Sep 17 00:00:00 2001 From: =?utf8?q?S=C3=A9bastien=20Lesaint?= Date: Fri, 8 Jan 2016 18:01:57 +0100 Subject: [PATCH] SONAR-7168 fix stop during restart not working lifeCycle transition from RESTARTING to STOPPING should actually _not_ be allowed because it can occur when restarting child processes (the WatcherThreads detects that stop and tries to shutdown all other processes), this fixes the issue by adding a HARD_STOPPING state, representing a not gracefull stop, to which transition from RESTARTING is allowed adds class AllProcessesCommands which implements access to sharedMemory for any process and is now used as the underlying implementation of DefaultProcessCommands. This class allows using a single IO to access sharedMemory from App --- .../sonar/process/monitor/JavaCommand.java | 11 - .../process/monitor/JavaProcessLauncher.java | 30 ++- .../org/sonar/process/monitor/Monitor.java | 78 +++++- .../process/monitor/JavaCommandTest.java | 3 - .../monitor/JavaProcessLauncherTest.java | 12 +- .../sonar/process/monitor/MonitorTest.java | 92 ++++--- .../sonar/process/AllProcessesCommands.java | 224 ++++++++++++++++++ .../sonar/process/DefaultProcessCommands.java | 127 ++-------- .../java/org/sonar/process/Lifecycle.java | 10 +- .../org/sonar/process/ProcessCommands.java | 2 +- .../process/AllProcessesCommandsTest.java | 152 ++++++++++++ .../process/DefaultProcessCommandsTest.java | 51 ++-- .../java/org/sonar/process/LifecycleTest.java | 6 +- .../org/sonar/process/test/HttpProcess.java | 6 +- .../server/platform/ws/RestartAction.java | 15 +- .../main/java/org/sonar/application/App.java | 37 +-- .../java/org/sonar/application/AppTest.java | 24 -- 17 files changed, 603 insertions(+), 277 deletions(-) create mode 100644 server/sonar-process/src/main/java/org/sonar/process/AllProcessesCommands.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/AllProcessesCommandsTest.java diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaCommand.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaCommand.java index 9187e0305ea..8a5551039d5 100644 --- a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaCommand.java +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaCommand.java @@ -52,8 +52,6 @@ public class JavaCommand { private final Map envVariables = new HashMap<>(System.getenv()); - private File tempDir = null; - private int processIndex = -1; public JavaCommand(String key) { @@ -78,15 +76,6 @@ public class JavaCommand { return this; } - public File getTempDir() { - return tempDir; - } - - public JavaCommand setTempDir(File tempDir) { - this.tempDir = tempDir; - return this; - } - public List getJavaOptions() { return javaOptions; } diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaProcessLauncher.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaProcessLauncher.java index 6a2eb83bb44..9bf969cd944 100644 --- a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaProcessLauncher.java +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaProcessLauncher.java @@ -19,13 +19,6 @@ */ package org.sonar.process.monitor; -import org.apache.commons.lang.StringUtils; -import org.slf4j.LoggerFactory; -import org.sonar.process.DefaultProcessCommands; -import org.sonar.process.ProcessCommands; -import org.sonar.process.ProcessEntryPoint; -import org.sonar.process.ProcessUtils; - import java.io.File; import java.io.FileOutputStream; import java.io.OutputStream; @@ -33,20 +26,33 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Properties; +import org.apache.commons.lang.StringUtils; +import org.slf4j.LoggerFactory; +import org.sonar.process.AllProcessesCommands; +import org.sonar.process.ProcessCommands; +import org.sonar.process.ProcessEntryPoint; +import org.sonar.process.ProcessUtils; public class JavaProcessLauncher { private final Timeouts timeouts; + private final File tempDir; + private final AllProcessesCommands allProcessesCommands; - public JavaProcessLauncher(Timeouts timeouts) { + public JavaProcessLauncher(Timeouts timeouts, File tempDir, AllProcessesCommands allProcessesCommands) { this.timeouts = timeouts; + this.tempDir = tempDir; + this.allProcessesCommands = allProcessesCommands; + } + + public void close() { + allProcessesCommands.close(); } ProcessRef launch(JavaCommand command) { Process process = null; try { - // cleanup existing monitor files - ProcessCommands commands = new DefaultProcessCommands(command.getTempDir(), command.getProcessIndex()); + ProcessCommands commands = allProcessesCommands.getProcessCommand(command.getProcessIndex(), true); ProcessBuilder processBuilder = create(command); LoggerFactory.getLogger(getClass()).info("Launch process[{}]: {}", @@ -69,7 +75,7 @@ public class JavaProcessLauncher { commands.add(buildJavaPath()); commands.addAll(javaCommand.getJavaOptions()); // TODO warning - does it work if temp dir contains a whitespace ? - commands.add(String.format("-Djava.io.tmpdir=%s", javaCommand.getTempDir().getAbsolutePath())); + commands.add(String.format("-Djava.io.tmpdir=%s", tempDir.getAbsolutePath())); commands.addAll(buildClasspath(javaCommand)); commands.add(javaCommand.getClassName()); commands.add(buildPropertiesFile(javaCommand).getAbsolutePath()); @@ -101,7 +107,7 @@ public class JavaProcessLauncher { props.setProperty(ProcessEntryPoint.PROPERTY_PROCESS_KEY, javaCommand.getKey()); props.setProperty(ProcessEntryPoint.PROPERTY_PROCESS_INDEX, Integer.toString(javaCommand.getProcessIndex())); props.setProperty(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, String.valueOf(timeouts.getTerminationTimeout())); - props.setProperty(ProcessEntryPoint.PROPERTY_SHARED_PATH, javaCommand.getTempDir().getAbsolutePath()); + props.setProperty(ProcessEntryPoint.PROPERTY_SHARED_PATH, tempDir.getAbsolutePath()); OutputStream out = new FileOutputStream(propertiesFile); props.store(out, String.format("Temporary properties file for command [%s]", javaCommand.getKey())); out.close(); 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 749389d1054..7dcf8ede0d0 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,6 +19,7 @@ */ package org.sonar.process.monitor; +import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -27,6 +28,7 @@ import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.sonar.process.AllProcessesCommands; import org.sonar.process.Lifecycle; import org.sonar.process.Lifecycle.State; import org.sonar.process.ProcessCommands; @@ -37,9 +39,11 @@ public class Monitor { private static final Logger LOG = LoggerFactory.getLogger(Monitor.class); private static final Timeouts TIMEOUTS = new Timeouts(); private static final long WATCH_DELAY_MS = 500L; + private static final int CURRENT_PROCESS_NUMBER = 0; private static int restartorInstanceCounter = 0; + private final AllProcessesCommands allProcessesCommands; private final JavaProcessLauncher launcher; private final SystemExit systemExit; @@ -53,15 +57,18 @@ public class Monitor { private List javaCommands; @CheckForNull private RestartorThread restartor; + @CheckForNull + HardStopWatcherThread hardStopWatcher; static int nextProcessId = 1; - Monitor(JavaProcessLauncher launcher, SystemExit exit) { - this.launcher = launcher; + Monitor(File tempDir, SystemExit exit) { + this.allProcessesCommands = new AllProcessesCommands(tempDir); + this.launcher = new JavaProcessLauncher(TIMEOUTS, tempDir, allProcessesCommands); this.systemExit = exit; } - public static Monitor create() { - return new Monitor(new JavaProcessLauncher(TIMEOUTS), new SystemExit()); + public static Monitor create(File tempDir) { + return new Monitor(tempDir, new SystemExit()); } /** @@ -108,6 +115,12 @@ public class Monitor { } } + public void watchForHardStop() { + ProcessCommands processCommand = this.allProcessesCommands.getProcessCommand(CURRENT_PROCESS_NUMBER, true); + this.hardStopWatcher = new HardStopWatcherThread(processCommand); + this.hardStopWatcher.start(); + } + private void monitor(ProcessRef processRef) { // physically watch if process is alive WatcherThread watcherThread = new WatcherThread(processRef, this); @@ -145,7 +158,9 @@ public class Monitor { boolean waitForOneRestart() { boolean restartRequested = awaitChildProcessesTermination(); + trace("finished waiting, restartRequested=" + restartRequested); if (restartRequested) { + trace("awaitTermination(restartor)=" + restartor); awaitTermination(restartor); } return restartRequested; @@ -176,8 +191,8 @@ public class Monitor { * Blocks until all processes are terminated. */ public void stop() { - trace("start stop async..."); - stopAsync(); + trace("start hard stop async..."); + stopAsync(State.HARD_STOPPING); trace("await termination of terminator..."); awaitTermination(terminator); cleanAfterTermination(); @@ -187,17 +202,18 @@ public class Monitor { private void cleanAfterTermination() { trace("go to STOPPED..."); - // safeguard if TerminatorThread is buggy and stop restartWatcher if (lifecycle.tryToMoveTo(State.STOPPED)) { trace("await termination of restartWatcher..."); - // wait for restartWatcher to cleanly stop - awaitTermination(restartWatcher); + // wait for restartWatcher and hardStopWatcher to cleanly stop + awaitTermination(restartWatcher, hardStopWatcher); trace("restartWatcher done"); // removing shutdown hook to avoid called stop() unnecessarily unless already in shutdownHook if (!systemExit.isInShutdownHook()) { trace("removing shutdown hook..."); Runtime.getRuntime().removeShutdownHook(shutdownHook); } + // cleanly close JavaLauncher + launcher.close(); } } @@ -207,7 +223,11 @@ public class Monitor { * this call will be blocking until the previous request finishes. */ public void stopAsync() { - if (lifecycle.tryToMoveTo(State.STOPPING)) { + stopAsync(State.STOPPING); + } + + private void stopAsync(State stoppingState) { + if (lifecycle.tryToMoveTo(stoppingState)) { terminator.start(); } } @@ -288,12 +308,38 @@ public class Monitor { } + public class HardStopWatcherThread extends Thread { + private final ProcessCommands processCommands; + + public HardStopWatcherThread(ProcessCommands processCommands) { + super("Hard stop watcher"); + this.processCommands = processCommands; + } + + @Override + public void run() { + while (lifecycle.getState() != Lifecycle.State.STOPPED) { + if (processCommands.askedForStop()) { + trace("Stopping process"); + Monitor.this.stop(); + } else { + try { + Thread.sleep(WATCH_DELAY_MS); + } catch (InterruptedException ignored) { + // keep watching + } + } + } + } + + } + private void stopProcesses() { - List watcherThreads = new ArrayList<>(this.watcherThreads); + List watcherThreadsCopy = new ArrayList<>(this.watcherThreads); // create a copy and reverse it to terminate in reverse order of startup (dependency order) - Collections.reverse(watcherThreads); + Collections.reverse(watcherThreadsCopy); - for (WatcherThread watcherThread : watcherThreads) { + for (WatcherThread watcherThread : watcherThreadsCopy) { ProcessRef ref = watcherThread.getProcessRef(); if (!ref.isStopped()) { LOG.info("{} is stopping", ref); @@ -339,6 +385,12 @@ public class Monitor { } } + private static void awaitTermination(Thread... threads) { + for (Thread thread : threads) { + awaitTermination(thread); + } + } + private static void awaitTermination(@Nullable Thread t) { if (t == null) { return; diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaCommandTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaCommandTest.java index cc376db2d35..5e36ef6c797 100644 --- a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaCommandTest.java +++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaCommandTest.java @@ -44,8 +44,6 @@ public class JavaCommandTest { command.setClassName("org.sonar.ElasticSearch"); command.setEnvVariable("JAVA_COMMAND_TEST", "1000"); - File tempDir = temp.newFolder(); - command.setTempDir(tempDir); File workDir = temp.newFolder(); command.setWorkDir(workDir); command.addClasspath("lib/*.jar"); @@ -56,7 +54,6 @@ public class JavaCommandTest { assertThat(command.getClasspath()).containsOnly("lib/*.jar", "conf/*.xml"); assertThat(command.getJavaOptions()).containsOnly("-Xmx128m"); assertThat(command.getWorkDir()).isSameAs(workDir); - assertThat(command.getTempDir()).isSameAs(tempDir); assertThat(command.getClassName()).isEqualTo("org.sonar.ElasticSearch"); // copy current env variables diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaProcessLauncherTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaProcessLauncherTest.java index a4c2c82ca64..cdd723c6fb4 100644 --- a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaProcessLauncherTest.java +++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaProcessLauncherTest.java @@ -19,17 +19,25 @@ */ package org.sonar.process.monitor; +import java.io.File; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.process.AllProcessesCommands; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; public class JavaProcessLauncherTest { + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + @Test - public void fail_to_launch() { + public void fail_to_launch() throws Exception { + File tempDir = temp.newFolder(); JavaCommand command = new JavaCommand("test"); - JavaProcessLauncher launcher = new JavaProcessLauncher(new Timeouts()); + JavaProcessLauncher launcher = new JavaProcessLauncher(new Timeouts(), tempDir, new AllProcessesCommands(tempDir)); try { // command is not correct (missing options), java.lang.ProcessBuilder#start() // throws an exception 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 a9937311e1e..038d9aa64e1 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 @@ -32,6 +32,7 @@ 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.Before; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; @@ -90,6 +91,14 @@ public class MonitorTest { @Rule public TemporaryFolder temp = new TemporaryFolder(); + private File tempDir; + + @Before + public void setUp() throws Exception { + tempDir = temp.newFolder(); + + } + /** * Safeguard */ @@ -104,8 +113,8 @@ public class MonitorTest { } @Test - public void fail_to_start_if_no_commands() { - monitor = newDefaultMonitor(); + public void fail_to_start_if_no_commands() throws Exception { + monitor = newDefaultMonitor(tempDir); try { monitor.start(Collections.emptyList()); fail(); @@ -116,7 +125,7 @@ public class MonitorTest { @Test public void fail_to_start_multiple_times() throws Exception { - monitor = newDefaultMonitor(); + monitor = newDefaultMonitor(tempDir); monitor.start(Arrays.asList(newStandardProcessCommand())); boolean failed = false; try { @@ -130,8 +139,8 @@ public class MonitorTest { @Test public void start_then_stop_gracefully() throws Exception { - monitor = newDefaultMonitor(); - HttpProcessClient client = new HttpProcessClient("test"); + monitor = newDefaultMonitor(tempDir); + HttpProcessClient client = new HttpProcessClient(tempDir, "test"); // blocks until started monitor.start(Arrays.asList(client.newCommand())); @@ -148,9 +157,9 @@ public class MonitorTest { @Test public void start_then_stop_sequence_of_commands() throws Exception { - monitor = newDefaultMonitor(); - HttpProcessClient p1 = new HttpProcessClient("p1"); - HttpProcessClient p2 = new HttpProcessClient("p2"); + monitor = newDefaultMonitor(tempDir); + HttpProcessClient p1 = new HttpProcessClient(tempDir, "p1"); + HttpProcessClient p2 = new HttpProcessClient(tempDir, "p2"); monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand())); // start p2 when p1 is fully started (ready) @@ -173,9 +182,9 @@ public class MonitorTest { @Test public void stop_all_processes_if_monitor_shutdowns() throws Exception { - monitor = newDefaultMonitor(); - HttpProcessClient p1 = new HttpProcessClient("p1"); - HttpProcessClient p2 = new HttpProcessClient("p2"); + monitor = newDefaultMonitor(tempDir); + HttpProcessClient p1 = new HttpProcessClient(tempDir, "p1"); + HttpProcessClient p2 = new HttpProcessClient(tempDir, "p2"); monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand())); assertThat(p1).isReady(); assertThat(p2).isReady(); @@ -190,9 +199,9 @@ public class MonitorTest { @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 = newDefaultMonitor(tempDir); + HttpProcessClient p1 = new HttpProcessClient(tempDir, "p1"); + HttpProcessClient p2 = new HttpProcessClient(tempDir, "p2"); monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand())); assertThat(p1).isReady(); @@ -221,9 +230,9 @@ public class MonitorTest { @Test public void stop_all_processes_if_one_shutdowns() throws Exception { - monitor = newDefaultMonitor(); - HttpProcessClient p1 = new HttpProcessClient("p1"); - HttpProcessClient p2 = new HttpProcessClient("p2"); + monitor = newDefaultMonitor(tempDir); + HttpProcessClient p1 = new HttpProcessClient(tempDir, "p1"); + HttpProcessClient p2 = new HttpProcessClient(tempDir, "p2"); monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand())); assertThat(p1.isReady()).isTrue(); assertThat(p2.isReady()).isTrue(); @@ -242,9 +251,9 @@ public class MonitorTest { @Test public void stop_all_processes_if_one_fails_to_start() throws Exception { - monitor = newDefaultMonitor(); - HttpProcessClient p1 = new HttpProcessClient("p1"); - HttpProcessClient p2 = new HttpProcessClient("p2", -1); + monitor = newDefaultMonitor(tempDir); + HttpProcessClient p1 = new HttpProcessClient(tempDir, "p1"); + HttpProcessClient p2 = new HttpProcessClient(tempDir, "p2", -1); try { monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand())); fail(); @@ -260,11 +269,11 @@ public class MonitorTest { } @Test - public void test_too_many_processes() { + public void test_too_many_processes() throws Exception { while (Monitor.getNextProcessId() < ProcessCommands.MAX_PROCESSES - 1) { } try { - newDefaultMonitor(); + newDefaultMonitor(tempDir); } catch (IllegalStateException e) { assertThat(e).hasMessageStartingWith("The maximum number of processes launched has been reached "); } finally { @@ -274,11 +283,10 @@ public class MonitorTest { @Test public void fail_to_start_if_bad_class_name() throws Exception { - monitor = newDefaultMonitor(); + monitor = newDefaultMonitor(tempDir); JavaCommand command = new JavaCommand("test") .addClasspath(testJar.getAbsolutePath()) - .setClassName("org.sonar.process.test.Unknown") - .setTempDir(temp.newFolder()); + .setClassName("org.sonar.process.test.Unknown"); try { monitor.start(Arrays.asList(command)); @@ -289,9 +297,19 @@ public class MonitorTest { } } - private Monitor newDefaultMonitor() { - Timeouts timeouts = new Timeouts(); - return new Monitor(new JavaProcessLauncher(timeouts), exit); + @Test + public void watchForHardStop_adds_a_hardStopWatcher_thread_and_starts_it() throws Exception { + Monitor monitor = newDefaultMonitor(tempDir); + assertThat(monitor.hardStopWatcher).isNull(); + + monitor.watchForHardStop(); + + assertThat(monitor.hardStopWatcher).isNotNull(); + assertThat(monitor.hardStopWatcher.isAlive()).isTrue(); + } + + private Monitor newDefaultMonitor(File tempDir) throws IOException { + return new Monitor(tempDir, exit); } /** @@ -302,16 +320,16 @@ public class MonitorTest { private final String commandKey; private final File tempDir; - private HttpProcessClient(String commandKey) throws IOException { - this(commandKey, NetworkUtils.freePort()); + private HttpProcessClient(File tempDir, String commandKey) throws IOException { + this(tempDir, commandKey, NetworkUtils.freePort()); } /** * Use httpPort=-1 to make server fail to start */ - private HttpProcessClient(String commandKey, int httpPort) throws IOException { + private HttpProcessClient(File tempDir, String commandKey, int httpPort) throws IOException { + this.tempDir = tempDir; this.commandKey = commandKey; - this.tempDir = temp.newFolder(commandKey); this.httpPort = httpPort; } @@ -319,8 +337,7 @@ public class MonitorTest { return new JavaCommand(commandKey) .addClasspath(testJar.getAbsolutePath()) .setClassName("org.sonar.process.test.HttpProcess") - .setArgument("httpPort", String.valueOf(httpPort)) - .setTempDir(tempDir); + .setArgument("httpPort", String.valueOf(httpPort)); } /** @@ -386,7 +403,7 @@ public class MonitorTest { private List readTimeFromFile(String filename) { try { - File file = new File(tempDir, filename); + File file = new File(tempDir, httpPort + "-" + filename); if (file.isFile() && file.exists()) { String[] split = StringUtils.split(FileUtils.readFileToString(file), ','); List res = new ArrayList<>(split.length); @@ -402,7 +419,7 @@ public class MonitorTest { } private boolean fileExists(String filename) { - File file = new File(tempDir, filename); + File file = new File(tempDir, httpPort + "-" + filename); return file.isFile() && file.exists(); } } @@ -542,8 +559,7 @@ public class MonitorTest { private JavaCommand newStandardProcessCommand() throws IOException { return new JavaCommand("standard") .addClasspath(testJar.getAbsolutePath()) - .setClassName("org.sonar.process.test.StandardProcess") - .setTempDir(temp.newFolder()); + .setClassName("org.sonar.process.test.StandardProcess"); } } diff --git a/server/sonar-process/src/main/java/org/sonar/process/AllProcessesCommands.java b/server/sonar-process/src/main/java/org/sonar/process/AllProcessesCommands.java new file mode 100644 index 00000000000..d6c1ee4666d --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/AllProcessesCommands.java @@ -0,0 +1,224 @@ +/* + * SonarQube :: Process + * 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; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import org.apache.commons.io.IOUtils; + +import static org.sonar.process.ProcessCommands.MAX_PROCESSES; + +/** + * Process inter-communication to : + *
    + *
  • share status of child process
  • + *
  • stop child process
  • + *
+ * + *

+ * It relies on files shared by both processes. Following alternatives were considered but not selected : + *

    + *
  • JMX beans over RMI: network issues (mostly because of Java reverse-DNS) + requires to configure and open a new port
  • + *
  • simple socket protocol: same drawbacks are RMI connection
  • + *
  • java.lang.Process#destroy(): shutdown hooks are not executed on some OS (mostly MSWindows)
  • + *
  • execute OS-specific commands (for instance kill on *nix): OS-specific, so hell to support. Moreover how to get identify a process ?
  • + *
+ */ +public class AllProcessesCommands { + + /** + * The ByteBuffer will contains : + *
    + *
  • First byte will contains 0x00 until stop command is issued = 0xFF
  • + *
  • Then each 10 bytes will be reserved for each process
  • + *
+ * + * Description of ten bytes of each process : + *
    + *
  • First byte will contains the state 0x00 until READY 0x01
  • + *
  • The second byte will contains the request for stopping 0x00 or STOP (0xFF)
  • + *
  • The second byte will contains the request for restarting 0x00 or RESTART (0xAA)
  • + *
  • The next 8 bytes contains a long (System.currentTimeInMillis for ping)
  • + *
+ */ + final MappedByteBuffer mappedByteBuffer; + private final RandomAccessFile sharedMemory; + private static final int BYTE_LENGTH_FOR_ONE_PROCESS = 1 + 1 + 1 + 8; + + // With this shared memory we can handle up to MAX_PROCESSES processes + private static final int MAX_SHARED_MEMORY = BYTE_LENGTH_FOR_ONE_PROCESS * MAX_PROCESSES; + + public static final byte STOP = (byte) 0xFF; + public static final byte RESTART = (byte) 0xAA; + public static final byte READY = (byte) 0x01; + public static final byte EMPTY = (byte) 0x00; + + public AllProcessesCommands(File directory) { + if (!directory.isDirectory() || !directory.exists()) { + throw new IllegalArgumentException("Not a valid directory: " + directory); + } + + try { + sharedMemory = new RandomAccessFile(new File(directory, "sharedmemory"), "rw"); + mappedByteBuffer = sharedMemory.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, MAX_SHARED_MEMORY); + } catch (IOException e) { + throw new IllegalArgumentException("Unable to create shared memory : ", e); + } + } + + public ProcessCommands getProcessCommand(int processNumber, boolean clean) { + checkProcessNumber(processNumber); + ProcessCommands processCommands = new ProcessCommandsImpl(processNumber); + if (clean) { + cleanData(processNumber); + } + return processCommands; + } + + boolean isReady(int processNumber) { + return mappedByteBuffer.get(offset(processNumber)) == READY; + } + + /** + * To be executed by child process to declare that it's ready + */ + void setReady(int processNumber) { + mappedByteBuffer.put(offset(processNumber), READY); + } + + void ping(int processNumber) { + mappedByteBuffer.putLong(2 + offset(processNumber), System.currentTimeMillis()); + } + + long getLastPing(int processNumber) { + return mappedByteBuffer.getLong(2 + offset(processNumber)); + } + + /** + * To be executed by monitor process to ask for child process termination + */ + void askForStop(int processNumber) { + mappedByteBuffer.put(offset(processNumber) + 1, STOP); + } + + boolean askedForStop(int processNumber) { + return mappedByteBuffer.get(offset(processNumber) + 1) == STOP; + } + + void askForRestart(int processNumber) { + mappedByteBuffer.put(offset(processNumber) + 3, RESTART); + } + + boolean askedForRestart(int processNumber) { + return mappedByteBuffer.get(offset(processNumber) + 3) == RESTART; + } + + void acknowledgeAskForRestart(int processNumber) { + mappedByteBuffer.put(offset(processNumber) + 3, EMPTY); + } + + public void close() { + IOUtils.closeQuietly(sharedMemory); + } + + int offset(int processNumber) { + return BYTE_LENGTH_FOR_ONE_PROCESS * processNumber; + } + + public void checkProcessNumber(int processNumber) { + boolean result = processNumber >= 0 && processNumber < MAX_PROCESSES; + if (!result) { + throw new IllegalArgumentException(String.format("Process number %s is not valid", processNumber)); + } + } + + private void cleanData(int processNumber) { + for (int i = 0; i < BYTE_LENGTH_FOR_ONE_PROCESS; i++) { + mappedByteBuffer.put(offset(processNumber) + i, EMPTY); + } + } + + private class ProcessCommandsImpl implements ProcessCommands { + + private final int processNumber; + + public ProcessCommandsImpl(int processNumber) { + this.processNumber = processNumber; + } + + @Override + public boolean isReady() { + return AllProcessesCommands.this.isReady(processNumber); + } + + @Override + public void setReady() { + AllProcessesCommands.this.setReady(processNumber); + } + + @Override + public void ping() { + AllProcessesCommands.this.ping(processNumber); + } + + @Override + public long getLastPing() { + return AllProcessesCommands.this.getLastPing(processNumber); + } + + @Override + public void askForStop() { + AllProcessesCommands.this.askForStop(processNumber); + } + + @Override + public boolean askedForStop() { + return AllProcessesCommands.this.askedForStop(processNumber); + } + + @Override + public void askForRestart() { + AllProcessesCommands.this.askForRestart(processNumber); + } + + @Override + public boolean askedForRestart() { + return AllProcessesCommands.this.askedForRestart(processNumber); + } + + @Override + public void acknowledgeAskForRestart() { + AllProcessesCommands.this.acknowledgeAskForRestart(processNumber); + } + + @Override + public void endWatch() { + throw new UnsupportedOperationException("ProcessCommands created from AllProcessesCommands can not be closed directly. Close AllProcessesCommands instead"); + } + + @Override + public void close() throws Exception { + endWatch(); + } + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/DefaultProcessCommands.java b/server/sonar-process/src/main/java/org/sonar/process/DefaultProcessCommands.java index b00a9bb91fa..6f8286b5d7d 100644 --- a/server/sonar-process/src/main/java/org/sonar/process/DefaultProcessCommands.java +++ b/server/sonar-process/src/main/java/org/sonar/process/DefaultProcessCommands.java @@ -20,164 +20,81 @@ package org.sonar.process; import java.io.File; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; -import org.apache.commons.io.IOUtils; import org.slf4j.LoggerFactory; /** - * Process inter-communication to : - *
    - *
  • share status of child process
  • - *
  • stop child process
  • - *
- * - *

- * It relies on files shared by both processes. Following alternatives were considered but not selected : - *

    - *
  • JMX beans over RMI: network issues (mostly because of Java reverse-DNS) + requires to configure and open a new port
  • - *
  • simple socket protocol: same drawbacks are RMI connection
  • - *
  • java.lang.Process#destroy(): shutdown hooks are not executed on some OS (mostly MSWindows)
  • - *
  • execute OS-specific commands (for instance kill on *nix): OS-specific, so hell to support. Moreover how to get identify a process ?
  • - *
+ * Default implementation of {@link ProcessCommands} based on a {@link AllProcessesCommands} of which will request a + * single {@link ProcessCommands} to use as delegate for the specified processNumber. */ public class DefaultProcessCommands implements ProcessCommands { - - /** - * The ByteBuffer will contains : - *
    - *
  • First byte will contains 0x00 until stop command is issued = 0xFF
  • - *
  • Then each 10 bytes will be reserved for each process
  • - *
- * - * Description of ten bytes of each process : - *
    - *
  • First byte will contains the state 0x00 until READY 0x01
  • - *
  • The second byte will contains the request for stopping 0x00 or STOP (0xFF)
  • - *
  • The second byte will contains the request for restarting 0x00 or RESTART (0xAA)
  • - *
  • The next 8 bytes contains a long (System.currentTimeInMillis for ping)
  • - *
- */ - final MappedByteBuffer mappedByteBuffer; - private final RandomAccessFile sharedMemory; - private static final int BYTE_LENGTH_FOR_ONE_PROCESS = 1 + 1 + 1 + 8; - - // With this shared memory we can handle up to MAX_PROCESSES processes - private static final int MAX_SHARED_MEMORY = BYTE_LENGTH_FOR_ONE_PROCESS * MAX_PROCESSES; - - public static final byte STOP = (byte) 0xFF; - public static final byte RESTART = (byte) 0xAA; - public static final byte READY = (byte) 0x01; - public static final byte EMPTY = (byte) 0x00; - - private int processNumber; + private final AllProcessesCommands allProcessesCommands; + private final ProcessCommands delegate; public DefaultProcessCommands(File directory, int processNumber) { this(directory, processNumber, true); } public DefaultProcessCommands(File directory, int processNumber, boolean clean) { - // processNumber should not excess MAX_PROCESSES and must not be below -1 - assert processNumber <= MAX_PROCESSES : "Incorrect process number"; - assert processNumber >= -1 : "Incorrect process number"; - - this.processNumber = processNumber; - if (!directory.isDirectory() || !directory.exists()) { - throw new IllegalArgumentException("Not a valid directory: " + directory); - } - - try { - sharedMemory = new RandomAccessFile(new File(directory, "sharedmemory"), "rw"); - mappedByteBuffer = sharedMemory.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, MAX_SHARED_MEMORY); - if (clean) { - cleanData(); - } - } catch (IOException e) { - throw new IllegalArgumentException("Unable to create shared memory : ", e); - } + this.allProcessesCommands = new AllProcessesCommands(directory); + this.delegate = allProcessesCommands.getProcessCommand(processNumber, clean); } @Override public boolean isReady() { - return canBeMonitored() && mappedByteBuffer.get(offset()) == READY; + return delegate.isReady(); } - /** - * To be executed by child process to declare that it's ready - */ @Override public void setReady() { - if (canBeMonitored()) { - mappedByteBuffer.put(offset(), READY); - } + delegate.setReady(); } @Override public void ping() { - if (canBeMonitored()) { - mappedByteBuffer.putLong(2 + offset(), System.currentTimeMillis()); - } + delegate.ping(); } @Override public long getLastPing() { - if (canBeMonitored()) { - return mappedByteBuffer.getLong(2 + offset()); - } else { - return -1; - } + return delegate.getLastPing(); } - /** - * To be executed by monitor process to ask for child process termination - */ @Override public void askForStop() { - mappedByteBuffer.put(offset() + 1, STOP); + delegate.askForStop(); } @Override public boolean askedForStop() { - return mappedByteBuffer.get(offset() + 1) == STOP; + return delegate.askedForStop(); } @Override public void askForRestart() { - mappedByteBuffer.put(offset() + 3, RESTART); + delegate.askForRestart(); } @Override public boolean askedForRestart() { - return mappedByteBuffer.get(offset() + 3) == RESTART; + return delegate.askedForRestart(); } @Override public void acknowledgeAskForRestart() { - mappedByteBuffer.put(offset() + 3, EMPTY); + delegate.acknowledgeAskForRestart(); } @Override public void endWatch() { - IOUtils.closeQuietly(sharedMemory); - } - - int offset() { - return BYTE_LENGTH_FOR_ONE_PROCESS * processNumber; - } - - private boolean canBeMonitored() { - boolean result = processNumber >= 0 && processNumber < MAX_PROCESSES; - if (!result) { - LoggerFactory.getLogger(getClass()).info("This process cannot be monitored. Process Id : [{}]", processNumber); + try { + close(); + } catch (Exception e) { + LoggerFactory.getLogger(getClass()).error("Failed to close DefaultProcessCommands", e); } - return result; } - private void cleanData() { - for (int i = 0; i < BYTE_LENGTH_FOR_ONE_PROCESS; i++) { - mappedByteBuffer.put(offset() + i, EMPTY); - } + @Override + public void close() throws Exception { + allProcessesCommands.close(); } } diff --git a/server/sonar-process/src/main/java/org/sonar/process/Lifecycle.java b/server/sonar-process/src/main/java/org/sonar/process/Lifecycle.java index 6aa8e1d932b..18a747e14e4 100644 --- a/server/sonar-process/src/main/java/org/sonar/process/Lifecycle.java +++ b/server/sonar-process/src/main/java/org/sonar/process/Lifecycle.java @@ -28,6 +28,7 @@ import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.sonar.process.Lifecycle.State.HARD_STOPPING; import static org.sonar.process.Lifecycle.State.INIT; import static org.sonar.process.Lifecycle.State.RESTARTING; import static org.sonar.process.Lifecycle.State.STARTED; @@ -39,7 +40,7 @@ public class Lifecycle { private static final Logger LOG = LoggerFactory.getLogger(Lifecycle.class); public enum State { - INIT, STARTING, STARTED, RESTARTING, STOPPING, STOPPED + INIT, STARTING, STARTED, RESTARTING, STOPPING, HARD_STOPPING, STOPPED } private static final Map> TRANSITIONS = buildTransitions(); @@ -47,10 +48,11 @@ public class Lifecycle { private static Map> buildTransitions() { Map> res = new EnumMap<>(State.class); res.put(INIT, toSet(STARTING)); - res.put(STARTING, toSet(STARTED, STOPPING)); - res.put(STARTED, toSet(RESTARTING, STOPPING)); - res.put(RESTARTING, toSet(STARTING, STOPPING)); + res.put(STARTING, toSet(STARTED, STOPPING, HARD_STOPPING)); + res.put(STARTED, toSet(RESTARTING, STOPPING, HARD_STOPPING)); + res.put(RESTARTING, toSet(STARTING, HARD_STOPPING)); res.put(STOPPING, toSet(STOPPED)); + res.put(HARD_STOPPING, toSet(STOPPED)); res.put(STOPPED, toSet()); return res; } diff --git a/server/sonar-process/src/main/java/org/sonar/process/ProcessCommands.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessCommands.java index 789a4b8cefc..8c0fe5ed008 100644 --- a/server/sonar-process/src/main/java/org/sonar/process/ProcessCommands.java +++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessCommands.java @@ -36,7 +36,7 @@ package org.sonar.process; *
  • execute OS-specific commands (for instance kill on *nix): OS-specific, so hell to support. Moreover how to get identify a process ?
  • * */ -public interface ProcessCommands { +public interface ProcessCommands extends AutoCloseable { int MAX_PROCESSES = 50; diff --git a/server/sonar-process/src/test/java/org/sonar/process/AllProcessesCommandsTest.java b/server/sonar-process/src/test/java/org/sonar/process/AllProcessesCommandsTest.java new file mode 100644 index 00000000000..97a8a6d19da --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/AllProcessesCommandsTest.java @@ -0,0 +1,152 @@ +/* + * SonarQube :: Process + * 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; + +import java.io.File; +import org.apache.commons.io.FileUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; +import static org.sonar.process.ProcessCommands.MAX_PROCESSES; + +public class AllProcessesCommandsTest { + + private static final int PROCESS_NUMBER = 1; + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void fail_to_init_if_dir_does_not_exist() throws Exception { + File dir = temp.newFolder(); + FileUtils.deleteQuietly(dir); + + try { + new AllProcessesCommands(dir); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e).hasMessage("Not a valid directory: " + dir.getAbsolutePath()); + } + } + + @Test + public void child_process_update_the_mapped_memory() throws Exception { + File dir = temp.newFolder(); + + AllProcessesCommands commands = new AllProcessesCommands(dir); + assertThat(commands.isReady(PROCESS_NUMBER)).isFalse(); + assertThat(commands.mappedByteBuffer.get(commands.offset(PROCESS_NUMBER))).isEqualTo(AllProcessesCommands.EMPTY); + assertThat(commands.mappedByteBuffer.getLong(2 + commands.offset(PROCESS_NUMBER))).isEqualTo(0L); + + commands.setReady(PROCESS_NUMBER); + assertThat(commands.isReady(PROCESS_NUMBER)).isTrue(); + assertThat(commands.mappedByteBuffer.get(commands.offset(PROCESS_NUMBER))).isEqualTo(AllProcessesCommands.READY); + + long currentTime = System.currentTimeMillis(); + commands.ping(PROCESS_NUMBER); + assertThat(commands.mappedByteBuffer.getLong(2 + commands.offset(PROCESS_NUMBER))).isGreaterThanOrEqualTo(currentTime); + } + + @Test + public void ask_for_stop() throws Exception { + File dir = temp.newFolder(); + + AllProcessesCommands commands = new AllProcessesCommands(dir); + assertThat(commands.mappedByteBuffer.get(commands.offset(PROCESS_NUMBER) + PROCESS_NUMBER)).isNotEqualTo(AllProcessesCommands.STOP); + assertThat(commands.askedForStop(PROCESS_NUMBER)).isFalse(); + + commands.askForStop(PROCESS_NUMBER); + assertThat(commands.askedForStop(PROCESS_NUMBER)).isTrue(); + assertThat(commands.mappedByteBuffer.get(commands.offset(PROCESS_NUMBER) + PROCESS_NUMBER)).isEqualTo(AllProcessesCommands.STOP); + } + + @Test + public void ask_for_restart() throws Exception { + File dir = temp.newFolder(); + + AllProcessesCommands commands = new AllProcessesCommands(dir); + assertThat(commands.mappedByteBuffer.get(commands.offset(PROCESS_NUMBER) + 3)).isNotEqualTo(AllProcessesCommands.RESTART); + assertThat(commands.askedForRestart(PROCESS_NUMBER)).isFalse(); + + commands.askForRestart(PROCESS_NUMBER); + assertThat(commands.askedForRestart(PROCESS_NUMBER)).isTrue(); + assertThat(commands.mappedByteBuffer.get(commands.offset(PROCESS_NUMBER) + 3)).isEqualTo(AllProcessesCommands.RESTART); + } + + @Test + public void acknowledgeAskForRestart_has_no_effect_when_no_restart_asked() throws Exception { + File dir = temp.newFolder(); + + AllProcessesCommands commands = new AllProcessesCommands(dir); + assertThat(commands.mappedByteBuffer.get(commands.offset(PROCESS_NUMBER) + 3)).isNotEqualTo(AllProcessesCommands.RESTART); + assertThat(commands.askedForRestart(PROCESS_NUMBER)).isFalse(); + + commands.acknowledgeAskForRestart(PROCESS_NUMBER); + assertThat(commands.mappedByteBuffer.get(commands.offset(PROCESS_NUMBER) + 3)).isNotEqualTo(AllProcessesCommands.RESTART); + assertThat(commands.askedForRestart(PROCESS_NUMBER)).isFalse(); + } + + @Test + public void acknowledgeAskForRestart_resets_askForRestart_has_no_effect_when_no_restart_asked() throws Exception { + File dir = temp.newFolder(); + + AllProcessesCommands commands = new AllProcessesCommands(dir); + + commands.askForRestart(PROCESS_NUMBER); + assertThat(commands.askedForRestart(PROCESS_NUMBER)).isTrue(); + assertThat(commands.mappedByteBuffer.get(commands.offset(PROCESS_NUMBER) + 3)).isEqualTo(AllProcessesCommands.RESTART); + + commands.acknowledgeAskForRestart(PROCESS_NUMBER); + assertThat(commands.mappedByteBuffer.get(commands.offset(PROCESS_NUMBER) + 3)).isNotEqualTo(AllProcessesCommands.RESTART); + assertThat(commands.askedForRestart(PROCESS_NUMBER)).isFalse(); + } + + @Test + public void getProcessCommands_fails_if_processNumber_is_less_than_0() throws Exception { + File dir = temp.newFolder(); + int processNumber = -2; + + AllProcessesCommands allProcessesCommands = new AllProcessesCommands(dir); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Process number " + processNumber + " is not valid"); + + allProcessesCommands.getProcessCommand(processNumber, true); + } + + @Test + public void getProcessCommands_fails_if_processNumber_is_higher_than_MAX_PROCESSES() throws Exception { + File dir = temp.newFolder(); + int processNumber = MAX_PROCESSES + 1; + + AllProcessesCommands allProcessesCommands = new AllProcessesCommands(dir); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Process number " + processNumber + " is not valid"); + + allProcessesCommands.getProcessCommand(processNumber, true); + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/DefaultProcessCommandsTest.java b/server/sonar-process/src/test/java/org/sonar/process/DefaultProcessCommandsTest.java index ee5f2f21243..2da77889377 100644 --- a/server/sonar-process/src/test/java/org/sonar/process/DefaultProcessCommandsTest.java +++ b/server/sonar-process/src/test/java/org/sonar/process/DefaultProcessCommandsTest.java @@ -23,11 +23,12 @@ import java.io.File; import org.apache.commons.io.FileUtils; import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; import static org.junit.Assert.fail; +import static org.sonar.process.ProcessCommands.MAX_PROCESSES; public class DefaultProcessCommandsTest { @@ -35,6 +36,8 @@ public class DefaultProcessCommandsTest { @Rule public TemporaryFolder temp = new TemporaryFolder(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); @Test public void fail_to_init_if_dir_does_not_exist() throws Exception { @@ -55,16 +58,9 @@ public class DefaultProcessCommandsTest { DefaultProcessCommands commands = new DefaultProcessCommands(dir, PROCESS_NUMBER); assertThat(commands.isReady()).isFalse(); - assertThat(commands.mappedByteBuffer.get(commands.offset())).isEqualTo(DefaultProcessCommands.EMPTY); - assertThat(commands.mappedByteBuffer.getLong(2 + commands.offset())).isEqualTo(0L); commands.setReady(); assertThat(commands.isReady()).isTrue(); - assertThat(commands.mappedByteBuffer.get(commands.offset())).isEqualTo(DefaultProcessCommands.READY); - - long currentTime = System.currentTimeMillis(); - commands.ping(); - assertThat(commands.mappedByteBuffer.getLong(2 + commands.offset())).isGreaterThanOrEqualTo(currentTime); } @Test @@ -72,12 +68,10 @@ public class DefaultProcessCommandsTest { File dir = temp.newFolder(); DefaultProcessCommands commands = new DefaultProcessCommands(dir, PROCESS_NUMBER); - assertThat(commands.mappedByteBuffer.get(commands.offset() + PROCESS_NUMBER)).isNotEqualTo(DefaultProcessCommands.STOP); assertThat(commands.askedForStop()).isFalse(); commands.askForStop(); assertThat(commands.askedForStop()).isTrue(); - assertThat(commands.mappedByteBuffer.get(commands.offset() + PROCESS_NUMBER)).isEqualTo(DefaultProcessCommands.STOP); } @Test @@ -85,12 +79,10 @@ public class DefaultProcessCommandsTest { File dir = temp.newFolder(); DefaultProcessCommands commands = new DefaultProcessCommands(dir, PROCESS_NUMBER); - assertThat(commands.mappedByteBuffer.get(commands.offset() + 3)).isNotEqualTo(DefaultProcessCommands.RESTART); assertThat(commands.askedForRestart()).isFalse(); commands.askForRestart(); assertThat(commands.askedForRestart()).isTrue(); - assertThat(commands.mappedByteBuffer.get(commands.offset() + 3)).isEqualTo(DefaultProcessCommands.RESTART); } @Test @@ -98,11 +90,9 @@ public class DefaultProcessCommandsTest { File dir = temp.newFolder(); DefaultProcessCommands commands = new DefaultProcessCommands(dir, PROCESS_NUMBER); - assertThat(commands.mappedByteBuffer.get(commands.offset() + 3)).isNotEqualTo(DefaultProcessCommands.RESTART); assertThat(commands.askedForRestart()).isFalse(); commands.acknowledgeAskForRestart(); - assertThat(commands.mappedByteBuffer.get(commands.offset() + 3)).isNotEqualTo(DefaultProcessCommands.RESTART); assertThat(commands.askedForRestart()).isFalse(); } @@ -114,27 +104,30 @@ public class DefaultProcessCommandsTest { commands.askForRestart(); assertThat(commands.askedForRestart()).isTrue(); - assertThat(commands.mappedByteBuffer.get(commands.offset() + 3)).isEqualTo(DefaultProcessCommands.RESTART); commands.acknowledgeAskForRestart(); - assertThat(commands.mappedByteBuffer.get(commands.offset() + 3)).isNotEqualTo(DefaultProcessCommands.RESTART); assertThat(commands.askedForRestart()).isFalse(); } @Test - public void test_max_processes() throws Exception { + public void constructor_fails_if_processNumber_is_less_than_0() throws Exception { File dir = temp.newFolder(); - try { - new DefaultProcessCommands(dir, -2); - failBecauseExceptionWasNotThrown(AssertionError.class); - } catch (AssertionError e) { - assertThat(e).hasMessage("Incorrect process number"); - } - try { - new DefaultProcessCommands(dir, ProcessCommands.MAX_PROCESSES + 1); - failBecauseExceptionWasNotThrown(AssertionError.class); - } catch (AssertionError e) { - assertThat(e).hasMessage("Incorrect process number"); - } + int processNumber = -2; + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Process number " + processNumber + " is not valid"); + + new DefaultProcessCommands(dir, processNumber); + } + + @Test + public void getProcessCommands_fails_if_processNumber_is_higher_than_MAX_PROCESSES() throws Exception { + File dir = temp.newFolder(); + int processNumber = MAX_PROCESSES + 1; + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Process number " + processNumber + " is not valid"); + + new DefaultProcessCommands(dir, processNumber); } } diff --git a/server/sonar-process/src/test/java/org/sonar/process/LifecycleTest.java b/server/sonar-process/src/test/java/org/sonar/process/LifecycleTest.java index 6337fe60e74..b91c84754ef 100644 --- a/server/sonar-process/src/test/java/org/sonar/process/LifecycleTest.java +++ b/server/sonar-process/src/test/java/org/sonar/process/LifecycleTest.java @@ -62,10 +62,10 @@ public class LifecycleTest { } @Test - public void can_move_to_STOPPING_from_any_state_but_STARTING_and_STARTED_only() { + public void can_move_to_STOPPING_from_STARTING_and_STARTED_only() { for (State state : values()) { boolean tryToMoveTo = newLifeCycle(state).tryToMoveTo(STOPPING); - if (state == STARTING || state == STARTED || state == RESTARTING) { + if (state == STARTING || state == STARTED) { assertThat(tryToMoveTo).describedAs("from state " + state).isTrue(); } else { assertThat(tryToMoveTo).describedAs("from state " + state).isFalse(); @@ -90,6 +90,8 @@ public class LifecycleTest { return newLifeCycle(STARTED, state); case STOPPING: return newLifeCycle(STARTED, state); + case HARD_STOPPING: + return newLifeCycle(STARTING, state); case STOPPED: return newLifeCycle(STOPPING, state); default: 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 86f4ccb4b37..73a6b27a788 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 @@ -43,6 +43,7 @@ import java.io.IOException; */ public class HttpProcess implements Monitored { + private final int httpPort; private final Server server; private boolean ready = false; // temp dir is specific to this process @@ -50,8 +51,9 @@ public class HttpProcess implements Monitored { private final ProcessCommands processCommands; public HttpProcess(int httpPort, ProcessCommands processCommands) { + this.httpPort = httpPort; this.processCommands = processCommands; - server = new Server(httpPort); + this.server = new Server(httpPort); } @Override @@ -120,7 +122,7 @@ public class HttpProcess implements Monitored { private void writeTimeToFile(String filename) { try { - FileUtils.write(new File(tempDir, filename), System.currentTimeMillis() + ",", true); + FileUtils.write(new File(tempDir, httpPort + "-" + filename), System.currentTimeMillis() + ",", true); } catch (IOException e) { throw new IllegalStateException(e); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/RestartAction.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/RestartAction.java index 0d395e89199..d50a751725c 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/RestartAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/RestartAction.java @@ -68,15 +68,20 @@ public class RestartAction implements SystemWsAction { @Override public void handle(Request request, Response response) { if (settings.getBoolean("sonar.web.dev")) { - LOGGER.info("Restart server"); + LOGGER.info("Fast restarting WebServer..."); platform.restart(); - LOGGER.info("Server restarted"); + LOGGER.info("WebServer restarted"); } else { LOGGER.info("Requesting SonarQube restart"); userSession.checkPermission(UserRole.ADMIN); - ProcessCommands commands = new DefaultProcessCommands( - nonNullValueAsFile(PROPERTY_SHARED_PATH), nonNullAsInt(PROPERTY_PROCESS_INDEX), false); - commands.askForRestart(); + + File shareDir = nonNullValueAsFile(PROPERTY_SHARED_PATH); + int processNumber = nonNullAsInt(PROPERTY_PROCESS_INDEX); + try (ProcessCommands commands = new DefaultProcessCommands(shareDir, processNumber, false)) { + commands.askForRestart(); + } catch (Exception e) { + LOGGER.warn("Failed to close ProcessCommands", e); + } } response.noContent(); } diff --git a/sonar-application/src/main/java/org/sonar/application/App.java b/sonar-application/src/main/java/org/sonar/application/App.java index db78e1bf0c8..f704fcc41a7 100644 --- a/sonar-application/src/main/java/org/sonar/application/App.java +++ b/sonar-application/src/main/java/org/sonar/application/App.java @@ -19,35 +19,28 @@ */ package org.sonar.application; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang.StringUtils; -import org.sonar.process.DefaultProcessCommands; import org.sonar.process.MinimumViableSystem; -import org.sonar.process.ProcessCommands; import org.sonar.process.ProcessProperties; import org.sonar.process.Props; -import org.sonar.process.StopWatcher; import org.sonar.process.Stoppable; import org.sonar.process.monitor.JavaCommand; import org.sonar.process.monitor.Monitor; -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - /** * Entry-point of process that starts and monitors elasticsearch and web servers */ public class App implements Stoppable { - private static final int APP_PROCESS_NUMBER = 0; - private final Monitor monitor; - private StopWatcher stopWatcher = null; - public App() { - this(Monitor.create()); + public App(File tempDir) { + this(Monitor.create(tempDir)); } App(Monitor monitor) { @@ -56,10 +49,7 @@ public class App implements Stoppable { public void start(Props props) { if (props.valueAsBoolean(ProcessProperties.ENABLE_STOP_COMMAND, false)) { - File tempDir = props.nonNullValueAsFile(ProcessProperties.PATH_TEMP); - ProcessCommands commands = new DefaultProcessCommands(tempDir, APP_PROCESS_NUMBER); - stopWatcher = new StopWatcher(commands, this); - stopWatcher.start(); + monitor.watchForHardStop(); } monitor.start(createCommands(props)); monitor.awaitTermination(); @@ -68,14 +58,12 @@ public class App implements Stoppable { private List createCommands(Props props) { List commands = new ArrayList<>(); File homeDir = props.nonNullValueAsFile(ProcessProperties.PATH_HOME); - File tempDir = props.nonNullValueAsFile(ProcessProperties.PATH_TEMP); JavaCommand elasticsearch = new JavaCommand("search"); elasticsearch .setWorkDir(homeDir) .addJavaOptions("-Djava.awt.headless=true") .addJavaOptions(props.nonNullValue(ProcessProperties.SEARCH_JAVA_OPTS)) .addJavaOptions(props.nonNullValue(ProcessProperties.SEARCH_JAVA_ADDITIONAL_OPTS)) - .setTempDir(tempDir.getAbsoluteFile()) .setClassName("org.sonar.search.SearchServer") .setArguments(props.rawProperties()) .addClasspath("./lib/common/*") @@ -89,7 +77,6 @@ public class App implements Stoppable { .addJavaOptions(ProcessProperties.WEB_ENFORCED_JVM_ARGS) .addJavaOptions(props.nonNullValue(ProcessProperties.WEB_JAVA_OPTS)) .addJavaOptions(props.nonNullValue(ProcessProperties.WEB_JAVA_ADDITIONAL_OPTS)) - .setTempDir(tempDir.getAbsoluteFile()) // required for logback tomcat valve .setEnvVariable(ProcessProperties.PATH_LOGS, props.nonNullValue(ProcessProperties.PATH_LOGS)) .setClassName("org.sonar.server.app.WebServer") @@ -118,16 +105,14 @@ public class App implements Stoppable { AppLogging logging = new AppLogging(); logging.configure(props); - App app = new App(); + File tempDir = props.nonNullValueAsFile(ProcessProperties.PATH_TEMP); + App app = new App(tempDir); app.start(props); } - StopWatcher getStopWatcher() { - return stopWatcher; - } - @Override public void stopAsync() { - monitor.stopAsync(); + monitor.stop(); } + } diff --git a/sonar-application/src/test/java/org/sonar/application/AppTest.java b/sonar-application/src/test/java/org/sonar/application/AppTest.java index 8b468bb2b52..03b33e8111b 100644 --- a/sonar-application/src/test/java/org/sonar/application/AppTest.java +++ b/sonar-application/src/test/java/org/sonar/application/AppTest.java @@ -52,30 +52,6 @@ public class AppTest { .startsWith(FilenameUtils.normalize(homeDir.getAbsolutePath(), true)); } - @Test - public void do_not_watch_stop_file_by_default() throws Exception { - Monitor monitor = mock(Monitor.class); - App app = new App(monitor); - app.start(initDefaultProps()); - - assertThat(app.getStopWatcher()).isNull(); - } - - @Test - public void watch_stop_file() throws Exception { - Monitor monitor = mock(Monitor.class); - App app = new App(monitor); - Props props = initDefaultProps(); - props.set("sonar.enableStopCommand", "true"); - app.start(props); - - assertThat(app.getStopWatcher()).isNotNull(); - assertThat(app.getStopWatcher().isAlive()).isTrue(); - - app.getStopWatcher().stopWatching(); - app.getStopWatcher().interrupt(); - } - @Test public void start_elasticsearch_and_tomcat_by_default() throws Exception { Monitor monitor = mock(Monitor.class); -- 2.39.5