Try to stabilize StopWatcherTest when machine is slow or under pressure. More than 1 second could
be spent in the creation of ProcessCommands mock.
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;
Process process = null;
try {
// cleanup existing monitor files
- ProcessCommands commands = new ProcessCommands(command.getTempDir(), command.getProcessIndex());
+ ProcessCommands commands = new DefaultProcessCommands(command.getTempDir(), command.getProcessIndex());
ProcessBuilder processBuilder = create(command);
LoggerFactory.getLogger(getClass()).info("Launch process[{}]: {}",
*/
package org.sonar.process.monitor;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
import org.slf4j.LoggerFactory;
import org.sonar.process.Lifecycle;
import org.sonar.process.Lifecycle.State;
import org.sonar.process.ProcessCommands;
import org.sonar.process.SystemExit;
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-
public class Monitor {
private final List<ProcessRef> processes = new CopyOnWriteArrayList<>();
}
public static int getNextProcessId() {
- if (nextProcessId >= ProcessCommands.getMaxProcesses()) {
- throw new IllegalStateException("The maximum number of processes launched has been reached " + ProcessCommands.getMaxProcesses());
+ if (nextProcessId >= ProcessCommands.MAX_PROCESSES) {
+ throw new IllegalStateException("The maximum number of processes launched has been reached " + ProcessCommands.MAX_PROCESSES);
}
return nextProcessId++;
}
package org.sonar.process.monitor;
import com.github.kevinsawicki.http.HttpRequest;
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
import org.apache.commons.io.FileUtils;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.rules.Timeout;
-import org.sonar.process.NetworkUtils;
import org.sonar.process.Lifecycle.State;
+import org.sonar.process.NetworkUtils;
import org.sonar.process.ProcessCommands;
import org.sonar.process.SystemExit;
-import java.io.File;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
@Test
public void test_too_many_processes() {
- while (Monitor.getNextProcessId() < ProcessCommands.getMaxProcesses() - 1) {}
+ while (Monitor.getNextProcessId() < ProcessCommands.MAX_PROCESSES - 1) {
+ }
try {
newDefaultMonitor();
} catch (IllegalStateException e) {
--- /dev/null
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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 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>
+ */
+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 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 + 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 READY = (byte) 0x01;
+ public static final byte EMPTY = (byte) 0x00;
+
+ private int processNumber;
+
+ public DefaultProcessCommands(File directory, int processNumber) {
+ // 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);
+ cleanData();
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Unable to create shared memory : ", e);
+ }
+ }
+
+ @Override
+ public boolean isReady() {
+ return canBeMonitored() && mappedByteBuffer.get(offset()) == READY;
+ }
+
+ /**
+ * To be executed by child process to declare that it's ready
+ */
+ @Override
+ public void setReady() {
+ if (canBeMonitored()) {
+ mappedByteBuffer.put(offset(), READY);
+ }
+ }
+
+ @Override
+ public void ping() {
+ if (canBeMonitored()) {
+ mappedByteBuffer.putLong(2 + offset(), System.currentTimeMillis());
+ }
+ }
+
+ @Override
+ public long getLastPing() {
+ if (canBeMonitored()) {
+ return mappedByteBuffer.getLong(2 + offset());
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * To be executed by monitor process to ask for child process termination
+ */
+ @Override
+ public void askForStop() {
+ mappedByteBuffer.put(offset() + 1, STOP);
+ }
+
+ @Override
+ public boolean askedForStop() {
+ return mappedByteBuffer.get(offset() + 1) == STOP;
+ }
+
+ @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);
+ }
+ return result;
+ }
+
+ private void cleanData() {
+ for (int i = 0; i < BYTE_LENGTH_FOR_ONE_PROCESS; i++) {
+ mappedByteBuffer.put(offset() + i, EMPTY);
+ }
+ }
+}
*/
package org.sonar.process;
-import org.apache.commons.io.IOUtils;
-import org.slf4j.LoggerFactory;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.RandomAccessFile;
-import java.nio.MappedByteBuffer;
-import java.nio.channels.FileChannel;
-
/**
* Process inter-communication to :
* <ul>
* <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 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 next 8 bytes contains a long (System.currentTimeInMillis for ping)</li>
- * </ul>
- */
- final MappedByteBuffer mappedByteBuffer;
- private final RandomAccessFile sharedMemory;
- private static final int MAX_PROCESSES = 50;
- private static final int BYTE_LENGTH_FOR_ONE_PROCESS = 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 READY = (byte) 0x01;
- public static final byte EMPTY = (byte) 0x00;
+public interface ProcessCommands {
- private int processNumber;
+ int MAX_PROCESSES = 50;
- public ProcessCommands(File directory, int processNumber) {
- // 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);
- cleanData();
- } catch (IOException e) {
- throw new IllegalArgumentException("Unable to create shared memory : ", e);
- }
- }
-
- public boolean isReady() {
- return canBeMonitored() && mappedByteBuffer.get(offset()) == READY;
- }
+ boolean isReady();
/**
* To be executed by child process to declare that it's ready
*/
- public void setReady() {
- if (canBeMonitored()) {
- mappedByteBuffer.put(offset(), READY);
- }
- }
+ void setReady();
- public void ping() {
- if (canBeMonitored()) {
- mappedByteBuffer.putLong(2 + offset(), System.currentTimeMillis());
- }
- }
+ void ping();
- public long getLastPing() {
- if (canBeMonitored()) {
- return mappedByteBuffer.getLong(2 + offset());
- } else {
- return -1;
- }
- }
+ long getLastPing();
/**
* To be executed by monitor process to ask for child process termination
*/
- public void askForStop() {
- mappedByteBuffer.put(offset() + 1, STOP);
- }
-
- public boolean askedForStop() {
- return mappedByteBuffer.get(offset() + 1) == STOP;
- }
-
- 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);
- }
- return result;
- }
+ void askForStop();
- private void cleanData() {
- for (int i = 0; i < BYTE_LENGTH_FOR_ONE_PROCESS; i++) {
- mappedByteBuffer.put(offset() + i, EMPTY);
- }
- }
+ boolean askedForStop();
- public static int getMaxProcesses() {
- return MAX_PROCESSES;
- }
+ void endWatch();
}
public static ProcessEntryPoint createForArguments(String[] args) {
Props props = ConfigurationUtils.loadPropsFromCommandLineArgs(args);
- ProcessCommands commands = new ProcessCommands(
+ ProcessCommands commands = new DefaultProcessCommands(
props.nonNullValueAsFile(PROPERTY_SHARED_PATH), Integer.parseInt(props.nonNullValue(PROPERTY_PROCESS_INDEX)));
return new ProcessEntryPoint(props, new SystemExit(), commands);
}
--- /dev/null
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.TemporaryFolder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;
+import static org.junit.Assert.fail;
+
+public class DefaultProcessCommandsTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Test
+ public void fail_to_init_if_dir_does_not_exist() throws Exception {
+ File dir = temp.newFolder();
+ FileUtils.deleteQuietly(dir);
+
+ try {
+ new DefaultProcessCommands(dir, 1);
+ 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();
+
+ DefaultProcessCommands commands = new DefaultProcessCommands(dir, 1);
+ 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
+ public void ask_for_stop() throws Exception {
+ File dir = temp.newFolder();
+
+ DefaultProcessCommands commands = new DefaultProcessCommands(dir, 1);
+ assertThat(commands.mappedByteBuffer.get(commands.offset() + 1)).isNotEqualTo(DefaultProcessCommands.STOP);
+ assertThat(commands.askedForStop()).isFalse();
+
+ commands.askForStop();
+ assertThat(commands.askedForStop()).isTrue();
+ assertThat(commands.mappedByteBuffer.get(commands.offset() + 1)).isEqualTo(DefaultProcessCommands.STOP);
+ }
+
+ @Test
+ public void test_max_processes() 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");
+ }
+ }
+}
+++ /dev/null
-/*
- * SonarQube, open source software quality management tool.
- * Copyright (C) 2008-2014 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * SonarQube 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.
- *
- * SonarQube is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.process;
-
-import org.apache.commons.io.FileUtils;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-
-import java.io.File;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;
-import static org.junit.Assert.fail;
-
-public class ProcessCommandsTest {
-
- @Rule
- public TemporaryFolder temp = new TemporaryFolder();
-
- @Test
- public void fail_to_init_if_dir_does_not_exist() throws Exception {
- File dir = temp.newFolder();
- FileUtils.deleteQuietly(dir);
-
- try {
- new ProcessCommands(dir, 1);
- 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();
-
- ProcessCommands commands = new ProcessCommands(dir, 1);
- assertThat(commands.isReady()).isFalse();
- assertThat(commands.mappedByteBuffer.get(commands.offset())).isEqualTo(ProcessCommands.EMPTY);
- assertThat(commands.mappedByteBuffer.getLong(2 + commands.offset())).isEqualTo(0L);
-
- commands.setReady();
- assertThat(commands.isReady()).isTrue();
- assertThat(commands.mappedByteBuffer.get(commands.offset())).isEqualTo(ProcessCommands.READY);
-
- long currentTime = System.currentTimeMillis();
- commands.ping();
- assertThat(commands.mappedByteBuffer.getLong(2 + commands.offset())).isGreaterThanOrEqualTo(currentTime);
- }
-
- @Test
- public void ask_for_stop() throws Exception {
- File dir = temp.newFolder();
-
- ProcessCommands commands = new ProcessCommands(dir, 1);
- assertThat(commands.mappedByteBuffer.get(commands.offset() + 1)).isNotEqualTo(ProcessCommands.STOP);
- assertThat(commands.askedForStop()).isFalse();
-
- commands.askForStop();
- assertThat(commands.askedForStop()).isTrue();
- assertThat(commands.mappedByteBuffer.get(commands.offset() + 1)).isEqualTo(ProcessCommands.STOP);
- }
-
- @Test
- public void test_max_processes() throws Exception {
- File dir = temp.newFolder();
- try {
- new ProcessCommands(dir, -2);
- failBecauseExceptionWasNotThrown(AssertionError.class);
- } catch (AssertionError e) {
- assertThat(e).hasMessage("Incorrect process number");
- }
- try {
- new ProcessCommands(dir, ProcessCommands.getMaxProcesses() + 1);
- failBecauseExceptionWasNotThrown(AssertionError.class);
- } catch (AssertionError e) {
- assertThat(e).hasMessage("Incorrect process number");
- }
- }
-}
*/
package org.sonar.process;
+import java.util.concurrent.TimeUnit;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.Timeout;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
public class StopWatcherTest {
- @Test(timeout = 1000L)
+ @Rule
+ public Timeout timeout = new Timeout(1000, TimeUnit.MILLISECONDS);
+
+ @Test
public void stop_if_receive_command() throws InterruptedException {
ProcessCommands commands = mock(ProcessCommands.class);
- when(commands.askedForStop()).thenReturn(false).thenReturn(true);
+ when(commands.askedForStop()).thenReturn(false, true);
Stoppable stoppable = mock(Stoppable.class);
StopWatcher watcher = new StopWatcher(commands, stoppable, 1L);
verify(stoppable).stopAsync();
}
- @Test(timeout = 1000L)
+ @Test
public void stop_watching_on_interruption() throws InterruptedException {
ProcessCommands commands = mock(ProcessCommands.class);
when(commands.askedForStop()).thenReturn(false);
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;
public void start(Props props) {
if (props.valueAsBoolean(ProcessProperties.ENABLE_STOP_COMMAND, false)) {
File tempDir = props.nonNullValueAsFile(ProcessProperties.PATH_TEMP);
- ProcessCommands commands = new ProcessCommands(tempDir, 0);
+ ProcessCommands commands = new DefaultProcessCommands(tempDir, 0);
stopWatcher = new StopWatcher(commands, this);
stopWatcher.start();
}