diff options
17 files changed, 603 insertions, 277 deletions
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<String, String> 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<String> 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<JavaCommand> 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<WatcherThread> watcherThreads = new ArrayList<>(this.watcherThreads); + List<WatcherThread> 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.<JavaCommand>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<Long> 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<Long> 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 : + * <ul> + * <li>share status of child process</li> + * <li>stop child process</li> + * </ul> + * + * <p/> + * It relies on files shared by both processes. Following alternatives were considered but not selected : + * <ul> + * <li>JMX beans over RMI: network issues (mostly because of Java reverse-DNS) + requires to configure and open a new port</li> + * <li>simple socket protocol: same drawbacks are RMI connection</li> + * <li>java.lang.Process#destroy(): shutdown hooks are not executed on some OS (mostly MSWindows)</li> + * <li>execute OS-specific commands (for instance kill on *nix): OS-specific, so hell to support. Moreover how to get identify a process ?</li> + * </ul> + */ +public class AllProcessesCommands { + + /** + * The ByteBuffer will contains : + * <ul> + * <li>First byte will contains 0x00 until stop command is issued = 0xFF</li> + * <li>Then each 10 bytes will be reserved for each process</li> + * </ul> + * + * Description of ten bytes of each process : + * <ul> + * <li>First byte will contains the state 0x00 until READY 0x01</li> + * <li>The second byte will contains the request for stopping 0x00 or STOP (0xFF)</li> + * <li>The second byte will contains the request for restarting 0x00 or RESTART (0xAA)</li> + * <li>The next 8 bytes contains a long (System.currentTimeInMillis for ping)</li> + * </ul> + */ + 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 : - * <ul> - * <li>share status of child process</li> - * <li>stop child process</li> - * </ul> - * - * <p/> - * It relies on files shared by both processes. Following alternatives were considered but not selected : - * <ul> - * <li>JMX beans over RMI: network issues (mostly because of Java reverse-DNS) + requires to configure and open a new port</li> - * <li>simple socket protocol: same drawbacks are RMI connection</li> - * <li>java.lang.Process#destroy(): shutdown hooks are not executed on some OS (mostly MSWindows)</li> - * <li>execute OS-specific commands (for instance kill on *nix): OS-specific, so hell to support. Moreover how to get identify a process ?</li> - * </ul> + * 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 : - * <ul> - * <li>First byte will contains 0x00 until stop command is issued = 0xFF</li> - * <li>Then each 10 bytes will be reserved for each process</li> - * </ul> - * - * Description of ten bytes of each process : - * <ul> - * <li>First byte will contains the state 0x00 until READY 0x01</li> - * <li>The second byte will contains the request for stopping 0x00 or STOP (0xFF)</li> - * <li>The second byte will contains the request for restarting 0x00 or RESTART (0xAA)</li> - * <li>The next 8 bytes contains a long (System.currentTimeInMillis for ping)</li> - * </ul> - */ - 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<State, Set<State>> TRANSITIONS = buildTransitions(); @@ -47,10 +48,11 @@ public class Lifecycle { private static Map<State, Set<State>> buildTransitions() { Map<State, Set<State>> 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; * <li>execute OS-specific commands (for instance kill on *nix): OS-specific, so hell to support. Moreover how to get identify a process ?</li> * </ul> */ -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<JavaCommand> createCommands(Props props) { List<JavaCommand> 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 @@ -53,30 +53,6 @@ public class AppTest { } @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); App app = new App(monitor); |