]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6036 use IPC (Inter Process Communication) for monitoring 111/head
authorEric Hartmann <hartmann.eric@gmail.com>
Thu, 19 Feb 2015 17:58:59 +0000 (18:58 +0100)
committerSimon Brandhof <simon.brandhof@sonarsource.com>
Fri, 20 Feb 2015 18:07:28 +0000 (19:07 +0100)
processes

14 files changed:
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaCommand.java
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaProcessLauncher.java
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/KnownJavaCommand.java [new file with mode: 0644]
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Monitor.java
server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/MonitorTest.java
server/sonar-process/src/main/java/org/sonar/process/ProcessCommands.java
server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java
server/sonar-process/src/main/java/org/sonar/process/StopWatcher.java
server/sonar-process/src/main/java/org/sonar/process/StopperThread.java
server/sonar-process/src/test/java/org/sonar/process/ProcessCommandsTest.java
server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java
server/sonar-process/src/test/java/org/sonar/process/StopperThreadTest.java
sonar-application/src/main/assembly/conf/wrapper.conf
sonar-application/src/main/java/org/sonar/application/App.java

index 813ea1d6ea930e97ca9e6468f63ce2381abcd61a..f4593308b9b3750485f8c683c4b729368b2798f3 100644 (file)
@@ -54,14 +54,24 @@ public class JavaCommand {
 
   private File tempDir = null;
 
+  private int processIndex = -1;
+
   public JavaCommand(String key) {
     this.key = key;
+    processIndex = KnownJavaCommand.lookIndexFor(key);
+    if (processIndex == -1) {
+      processIndex = Monitor.getNextProcessId();
+    }
   }
 
   public String getKey() {
     return key;
   }
 
+  public int getProcessIndex() {
+    return processIndex;
+  }
+
   public File getWorkDir() {
     return workDir;
   }
index 3cedc1c9c84cab1c14a0aee165bb6f1147ecea76..dd67a10489dead04652de6f01b95b39198ecd0e7 100644 (file)
@@ -45,8 +45,7 @@ public class JavaProcessLauncher {
     Process process = null;
     try {
       // cleanup existing monitor files
-      ProcessCommands commands = new ProcessCommands(command.getTempDir(), command.getKey());
-      commands.prepare();
+      ProcessCommands commands = new ProcessCommands(command.getTempDir(), command.getProcessIndex());
 
       ProcessBuilder processBuilder = create(command);
       LoggerFactory.getLogger(getClass()).info("Launch process[{}]: {}",
@@ -99,6 +98,7 @@ public class JavaProcessLauncher {
       Properties props = new Properties();
       props.putAll(javaCommand.getArguments());
       props.setProperty(ProcessEntryPoint.PROPERTY_PROCESS_KEY, javaCommand.getKey());
+      props.setProperty(ProcessEntryPoint.PROPERTY_PROCESS_INDEX, "" + javaCommand.getProcessIndex());
       props.setProperty(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, String.valueOf(timeouts.getTerminationTimeout()));
       props.setProperty(ProcessEntryPoint.PROPERTY_SHARED_PATH, javaCommand.getTempDir().getAbsolutePath());
       OutputStream out = new FileOutputStream(propertiesFile);
diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/KnownJavaCommand.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/KnownJavaCommand.java
new file mode 100644 (file)
index 0000000..ce9a49a
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * 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.monitor;
+
+/**
+ * Created by eric on 20/02/15.
+ */
+public enum KnownJavaCommand {
+  APP("app", 0), WEB("web", 1), ELASTIC_SEARCH("search", 2), UNKNOWN("unknown", -1);
+
+  private String key;
+  private int index;
+
+  KnownJavaCommand(String key, int index) {
+    this.key = key;
+    this.index = index;
+  }
+
+  public String getKey() {
+    return key;
+  }
+
+  public int getIndex() {
+    return index;
+  }
+
+  public static KnownJavaCommand lookFor(String key) {
+    for (KnownJavaCommand knownJavaCommand : KnownJavaCommand.values()) {
+      if (knownJavaCommand.getKey().equals(key)) {
+        return knownJavaCommand;
+      }
+    }
+    return KnownJavaCommand.UNKNOWN;
+  }
+
+  public static int lookIndexFor(String key) {
+    return lookFor(key).getIndex();
+  }
+
+  public static int getFirstIndexAvailable() {
+    int result = 0;
+    for (KnownJavaCommand knownJavaCommand : KnownJavaCommand.values()) {
+      result = knownJavaCommand.getIndex() >= result ? knownJavaCommand.getIndex() + 1 : result;
+    }
+    return result;
+  }
+}
index 4af3a08a1c2c605e59771ebb6eb3366fb8405f9e..ccbeb558db017e84e1d70e82e91d28d89ecb3d49 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.process.monitor;
 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;
@@ -39,6 +40,7 @@ public class Monitor {
 
   // used by awaitStop() to block until all processes are shutdown
   private final List<WatcherThread> watcherThreads = new CopyOnWriteArrayList<WatcherThread>();
+  static int nextProcessId = KnownJavaCommand.getFirstIndexAvailable();
 
   Monitor(JavaProcessLauncher launcher, SystemExit exit, TerminatorThread terminator) {
     this.launcher = launcher;
@@ -161,4 +163,11 @@ public class Monitor {
       stop();
     }
   }
+
+  public static int getNextProcessId() {
+    if (nextProcessId >= ProcessCommands.getMaxProcesses()) {
+      throw new IllegalStateException("The maximum number of processes launched has been reached " + ProcessCommands.getMaxProcesses());
+    }
+    return nextProcessId++;
+  }
 }
index 46200f45b7b4b9d4678004cd141354afd5b76e22..4a27d50ed6239052aec30ab59f1b223b9a6ddbc1 100644 (file)
@@ -29,6 +29,7 @@ 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.ProcessCommands;
 import org.sonar.process.SystemExit;
 
 import java.io.File;
@@ -208,6 +209,18 @@ public class MonitorTest {
     }
   }
 
+  @Test
+  public void test_too_many_processes() {
+    while (Monitor.getNextProcessId() < ProcessCommands.getMaxProcesses() - 1) {}
+    try {
+      newDefaultMonitor();
+    } catch (IllegalStateException e) {
+      assertThat(e).hasMessageStartingWith("The maximum number of processes launched has been reached ");
+    } finally {
+      Monitor.nextProcessId = KnownJavaCommand.getFirstIndexAvailable();
+    }
+  }
+
   @Test
   public void force_stop_if_too_long() throws Exception {
     // TODO
index 1c7af0c004eff49ae9914b6b1b0324d2f9ca446a..0e7edb39af82bfa9a1be3f17820b8cd3670659e4 100644 (file)
  */
 package org.sonar.process;
 
-import org.apache.commons.io.FileUtils;
+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 :
@@ -42,75 +45,99 @@ import java.io.IOException;
  */
 public class ProcessCommands {
 
-  private final File readyFile, stopFile;
-
-  public ProcessCommands(File directory, String processKey) {
+  /**
+   * 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 static final int MAX_PROCESSES = 50;
+  private static final int BYTE_LENGTH_FOR_ONE_PROCESS = 1 + 1 + 8;
+  private static final int MAX_SHARED_MEMORY = BYTE_LENGTH_FOR_ONE_PROCESS * MAX_PROCESSES; // With this shared memory we can handle up to MAX_PROCESSES 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 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);
     }
-    this.readyFile = new File(directory, processKey + ".ready");
-    this.stopFile = new File(directory, processKey + ".stop");
-  }
-
-  // visible for tests
-  ProcessCommands(File readyFile, File stopFile) {
-    this.readyFile = readyFile;
-    this.stopFile = stopFile;
-  }
 
-  public void prepare() {
-    deleteFile(readyFile);
-    deleteFile(stopFile);
-  }
-
-  public void endWatch() {
-    // do not fail if files can't be deleted
-    FileUtils.deleteQuietly(readyFile);
-    FileUtils.deleteQuietly(stopFile);
+    try {
+      RandomAccessFile 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 boolean isReady() {
-    return readyFile.exists();
+    return canBeMonitored() && mappedByteBuffer.get(offset()) == READY;
   }
 
   /**
    * To be executed by child process to declare that it's ready
    */
   public void setReady() {
-    createFile(readyFile);
+    if (canBeMonitored()) {
+      mappedByteBuffer.put(offset(), READY);
+    }
+  }
+
+  public void ping() {
+    if (canBeMonitored()) {
+      mappedByteBuffer.putLong(2 + offset(), System.currentTimeMillis());
+    }
+  }
+
+  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
    */
   public void askForStop() {
-    createFile(stopFile);
+    mappedByteBuffer.put(offset() + 1, STOP);
   }
 
   public boolean askedForStop() {
-    return stopFile.exists();
+    return mappedByteBuffer.get(offset() + 1) == STOP;
   }
 
-  File getReadyFile() {
-    return readyFile;
+  int offset() {
+    return BYTE_LENGTH_FOR_ONE_PROCESS * processNumber;
   }
 
-  File getStopFile() {
-    return stopFile;
-  }
-
-  private void createFile(File file) {
-    try {
-      FileUtils.touch(file);
-    } catch (IOException e) {
-      throw new IllegalStateException(String.format("Fail to create file %s", file), e);
+  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 deleteFile(File file) {
-    if (file.exists() && !file.delete()) {
-      throw new MessageException(String.format(
-        "Fail to delete file %s. Please check that no SonarQube process is alive", file));
-    }
+  public static final int getMaxProcesses() {
+    return MAX_PROCESSES;
   }
-}
+}
\ No newline at end of file
index 69327b8115ae253c8df1e7e4b64a589f8f0aa223..0687b94b74296b3c8694e3040196e6416227add2 100644 (file)
@@ -24,6 +24,7 @@ import org.slf4j.LoggerFactory;
 public class ProcessEntryPoint implements Stoppable {
 
   public static final String PROPERTY_PROCESS_KEY = "process.key";
+  public static final String PROPERTY_PROCESS_INDEX = "process.index";
   public static final String PROPERTY_TERMINATION_TIMEOUT = "process.terminationTimeout";
   public static final String PROPERTY_SHARED_PATH = "process.sharedDir";
 
@@ -66,7 +67,6 @@ public class ProcessEntryPoint implements Stoppable {
     if (!lifecycle.tryToMoveTo(Lifecycle.State.STARTING)) {
       throw new IllegalStateException("Already started");
     }
-    commands.prepare();
     monitored = mp;
 
     try {
@@ -135,7 +135,7 @@ public class ProcessEntryPoint implements Stoppable {
   public static ProcessEntryPoint createForArguments(String[] args) {
     Props props = ConfigurationUtils.loadPropsFromCommandLineArgs(args);
     ProcessCommands commands = new ProcessCommands(
-      props.nonNullValueAsFile(PROPERTY_SHARED_PATH), props.nonNullValue(PROPERTY_PROCESS_KEY));
+      props.nonNullValueAsFile(PROPERTY_SHARED_PATH), Integer.parseInt(props.nonNullValue(PROPERTY_PROCESS_INDEX)));
     return new ProcessEntryPoint(props, new SystemExit(), commands);
   }
 }
index 4ba726a2d85bda4c5c8eb644a748279b158083c4..65ffb533c7865bb8d6fe854d0fbd1926c363090c 100644 (file)
@@ -45,23 +45,18 @@ public class StopWatcher extends Thread {
 
   @Override
   public void run() {
-    commands.prepare();
-    try {
-      while (watching) {
-        if (commands.askedForStop()) {
-          LoggerFactory.getLogger(getClass()).info("Stopping process");
-          stoppable.stopAsync();
+    while (watching) {
+      if (commands.askedForStop()) {
+        LoggerFactory.getLogger(getClass()).info("Stopping process");
+        stoppable.stopAsync();
+        watching = false;
+      } else {
+        try {
+          Thread.sleep(delayMs);
+        } catch (InterruptedException ignored) {
           watching = false;
-        } else {
-          try {
-            Thread.sleep(delayMs);
-          } catch (InterruptedException ignored) {
-            watching = false;
-          }
         }
       }
-    } finally {
-      commands.endWatch();
     }
   }
 
index 764d551a93c623a7e9b1202814fd7ad987f683af..23819212cfc67df8cdebdfe29a9dffa762c90a76 100644 (file)
@@ -57,6 +57,5 @@ class StopperThread extends Thread {
       LoggerFactory.getLogger(getClass()).error(String.format("Can not stop in %dms", terminationTimeout), e);
     }
     executor.shutdownNow();
-    commands.endWatch();
   }
 }
index 0e163a8f1e31dccadd7f792af991ecd21239ecb5..aba88feb0162a47e2ecfd5ccb5c772d2677ead56 100644 (file)
@@ -27,9 +27,8 @@ 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;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
 
 public class ProcessCommandsTest {
 
@@ -42,7 +41,7 @@ public class ProcessCommandsTest {
     FileUtils.deleteQuietly(dir);
 
     try {
-      new ProcessCommands(dir, "web");
+      new ProcessCommands(dir, 1);
       fail();
     } catch (IllegalArgumentException e) {
       assertThat(e).hasMessage("Not a valid directory: " + dir.getAbsolutePath());
@@ -50,62 +49,50 @@ public class ProcessCommandsTest {
   }
 
   @Test
-  public void delete_files_on_prepare() throws Exception {
+  public void child_process_update_the_mapped_memory() throws Exception {
     File dir = temp.newFolder();
-    assertThat(dir).exists();
-    FileUtils.touch(new File(dir, "web.ready"));
-    FileUtils.touch(new File(dir, "web.stop"));
 
-    ProcessCommands commands = new ProcessCommands(dir, "web");
-    commands.prepare();
-
-    assertThat(commands.getReadyFile()).doesNotExist();
-    assertThat(commands.getStopFile()).doesNotExist();
-  }
-
-  @Test
-  public void fail_to_prepare_if_file_is_locked() throws Exception {
-    File readyFile = mock(File.class);
-    when(readyFile.exists()).thenReturn(true);
-    when(readyFile.delete()).thenReturn(false);
-
-    ProcessCommands commands = new ProcessCommands(readyFile, temp.newFile());
-    try {
-      commands.prepare();
-      fail();
-    } catch (MessageException e) {
-      // ok
-    }
-  }
-
-  @Test
-  public void child_process_create_file_when_ready() throws Exception {
-    File dir = temp.newFolder();
-
-    ProcessCommands commands = new ProcessCommands(dir, "web");
-    commands.prepare();
+    ProcessCommands commands = new ProcessCommands(dir, 1);
     assertThat(commands.isReady()).isFalse();
-    assertThat(commands.getReadyFile()).doesNotExist();
+    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.getReadyFile()).exists().isFile();
+    assertThat(commands.mappedByteBuffer.get(commands.offset())).isEqualTo(ProcessCommands.READY);
 
-    commands.endWatch();
-    assertThat(commands.getReadyFile()).doesNotExist();
+    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, "web");
+    ProcessCommands commands = new ProcessCommands(dir, 1);
+    assertThat(commands.mappedByteBuffer.get(commands.offset() + 1)).isNotEqualTo(ProcessCommands.STOP);
     assertThat(commands.askedForStop()).isFalse();
-    assertThat(commands.getStopFile()).doesNotExist();
 
     commands.askForStop();
     assertThat(commands.askedForStop()).isTrue();
-    assertThat(commands.getStopFile()).exists().isFile();
-    assertThat(commands.getStopFile().getName()).isEqualTo("web.stop");
+    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");
+    }
   }
 }
index 1857b7cc93b6b4189347b9e19289f5746beee507..bfca7cb63bafd92a5d65b4ad93d7803fb63628f5 100644 (file)
@@ -52,7 +52,7 @@ public class ProcessEntryPointTest {
   @Test
   public void load_properties_from_file() throws Exception {
     File propsFile = temp.newFile();
-    FileUtils.write(propsFile, "sonar.foo=bar\nprocess.key=web\nprocess.sharedDir=" + temp.newFolder().getAbsolutePath().replaceAll("\\\\", "/"));
+    FileUtils.write(propsFile, "sonar.foo=bar\nprocess.key=web\nprocess.index=1\nprocess.sharedDir=" + temp.newFolder().getAbsolutePath().replaceAll("\\\\", "/"));
 
     ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(new String[] {propsFile.getAbsolutePath()});
     assertThat(entryPoint.getProps().value("sonar.foo")).isEqualTo("bar");
index 0973a06277bc1b45c766efdbb752ccecf7b43c8f..2f6260f9d7857abb898d7d0783d1dd54dfb72a8b 100644 (file)
@@ -44,7 +44,6 @@ public class StopperThreadTest {
     stopper.join();
 
     verify(monitored).stop();
-    verify(commands).endWatch();
   }
 
   @Test(timeout = 3000L)
@@ -65,7 +64,5 @@ public class StopperThreadTest {
     stopper.join();
 
     verify(monitored).stop();
-    // even if stopper was interrupted, stop watching process
-    verify(commands).endWatch();
   }
 }
index 86834b8a80b03dcd2952af78e6dca33f65e19cfd..530e560b7873ce78eb4620952ed0a4d579e5016f 100644 (file)
@@ -86,3 +86,4 @@ wrapper.ntservice.interactive=false
 wrapper.disable_restarts=TRUE
 wrapper.ping.timeout=0
 wrapper.shutdown.timeout=3000
+wrapper.jvm_exit.timeout=3000
index e70c34df4feeb6c2f5c045c75ed5e06c99573bb8..f5418d42ab86e816297c8cc53dd31e96caefd7bd 100644 (file)
@@ -28,6 +28,7 @@ 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.KnownJavaCommand;
 import org.sonar.process.monitor.Monitor;
 
 import java.io.File;
@@ -53,9 +54,8 @@ public class App implements Stoppable {
 
   public void start(Props props) {
     if (props.valueAsBoolean(ProcessConstants.ENABLE_STOP_COMMAND, false)) {
-      // stop application when file <temp>/app.stop is created
       File tempDir = props.nonNullValueAsFile(ProcessConstants.PATH_TEMP);
-      ProcessCommands commands = new ProcessCommands(tempDir, "app");
+      ProcessCommands commands = new ProcessCommands(tempDir, KnownJavaCommand.APP.getIndex());
       stopWatcher = new StopWatcher(commands, this);
       stopWatcher.start();
     }