]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7168 rework threads and restart implementation
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Thu, 7 Jan 2016 17:20:00 +0000 (18:20 +0100)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Wed, 13 Jan 2016 12:42:43 +0000 (13:42 +0100)
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Monitor.java
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/RestartRequestWatcherThread.java [deleted file]
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/TerminatorThread.java [deleted file]
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/WatcherThread.java
server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/RestartRequestWatcherThreadTest.java [deleted file]
server/sonar-process/src/main/java/org/sonar/process/Lifecycle.java
server/sonar-process/src/test/java/org/sonar/process/LifecycleTest.java
sonar-application/src/main/java/org/sonar/application/App.java

index 2655bb013490ac165d15cb028fda8674fc2cfc13..98498c0e17eec68ea02f9716299eed6cd89e6474 100644 (file)
 package org.sonar.process.monitor;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
 import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.sonar.process.Lifecycle;
 import org.sonar.process.Lifecycle.State;
@@ -30,24 +33,26 @@ import org.sonar.process.ProcessCommands;
 import org.sonar.process.SystemExit;
 
 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 int restartorInstanceCounter = 0;
 
-  private final List<ProcessRef> processes = new CopyOnWriteArrayList<>();
   private final JavaProcessLauncher launcher;
 
   private final SystemExit systemExit;
-  private Thread shutdownHook = new Thread(new MonitorShutdownHook(), "Monitor Shutdown Hook");
+  private final Thread shutdownHook = new Thread(new MonitorShutdownHook(), "Monitor Shutdown Hook");
 
-  // used by awaitStop() to block until all processes are shutdown
-  private List<WatcherThread> watcherThreads = new CopyOnWriteArrayList<>();
+  private final List<WatcherThread> watcherThreads = new CopyOnWriteArrayList<>();
+  private final Lifecycle lifecycle = new Lifecycle();
+  private final TerminatorThread terminator = new TerminatorThread();
+  private final RestartRequestWatcherThread restartWatcher = new RestartRequestWatcherThread();
   @CheckForNull
   private List<JavaCommand> javaCommands;
   @CheckForNull
-  private Lifecycle lifecycle;
-  @CheckForNull
-  private RestartRequestWatcherThread restartWatcher;
-  @CheckForNull
-  private TerminatorThread terminator;
+  private RestartorThread restartor;
   static int nextProcessId = 1;
 
   Monitor(JavaProcessLauncher launcher, SystemExit exit) {
@@ -70,43 +75,37 @@ public class Monitor {
       throw new IllegalArgumentException("At least one command is required");
     }
 
-    if (lifecycle != null) {
+    if (lifecycle.getState() != State.INIT) {
       throw new IllegalStateException("Can not start multiple times");
     }
 
     // intercepts CTRL-C
     Runtime.getRuntime().addShutdownHook(shutdownHook);
+    this.restartWatcher.start();
 
     this.javaCommands = commands;
-    start();
-  }
-
-  private void start() {
-    resetState();
-    List<ProcessRef> processRefs = startAndMonitorProcesses();
-    startWatchingForRestartRequests(processRefs);
+    startProcesses();
   }
 
-  private void resetState() {
-    this.lifecycle = new Lifecycle();
-    lifecycle.tryToMoveTo(State.STARTING);
-    this.watcherThreads.clear();
+  private void startProcesses() {
+    // do no start any child process if not in state INIT or RESTARTING (a stop could be in progress too)
+    if (lifecycle.tryToMoveTo(State.STARTING)) {
+      startAndMonitorProcesses();
+      stopIfAnyProcessDidNotStart();
+    }
   }
 
-  private List<ProcessRef> startAndMonitorProcesses() {
-    List<ProcessRef> processRefs = new ArrayList<>(javaCommands.size());
+  private void startAndMonitorProcesses() {
     for (JavaCommand command : javaCommands) {
       try {
         ProcessRef processRef = launcher.launch(command);
         monitor(processRef);
-        processRefs.add(processRef);
       } catch (RuntimeException e) {
         // fail to start or to monitor
         stop();
         throw e;
       }
     }
-    return processRefs;
   }
 
   private void monitor(ProcessRef processRef) {
@@ -115,19 +114,14 @@ public class Monitor {
     watcherThread.start();
     watcherThreads.add(watcherThread);
 
-    processes.add(processRef);
-
     // wait for process to be ready (accept requests or so on)
     processRef.waitForReady();
 
-    LoggerFactory.getLogger(getClass()).info(String.format("%s is up", processRef));
+    LOG.info("{} is up", processRef);
   }
 
-  private void startWatchingForRestartRequests(List<ProcessRef> processRefs) {
-    if (lifecycle.tryToMoveTo(State.STARTED)) {
-      stopRestartWatcher();
-      startRestartWatcher(processRefs);
-    } else {
+  private void stopIfAnyProcessDidNotStart() {
+    if (!lifecycle.tryToMoveTo(State.STARTED)) {
       // stopping or stopped during startup, for instance :
       // 1. A is started
       // 2. B starts
@@ -138,60 +132,43 @@ public class Monitor {
     }
   }
 
-  private void stopRestartWatcher() {
-    if (this.restartWatcher != null) {
-      this.restartWatcher.stopWatching();
-      try {
-        this.restartWatcher.join();
-      } catch (InterruptedException e) {
-        // failed to cleanly stop (very unlikely), ignore and proceed
-      }
-    }
-  }
-
-  private void startRestartWatcher(List<ProcessRef> processRefs) {
-    this.restartWatcher = new RestartRequestWatcherThread(this, processRefs);
-    this.restartWatcher.start();
-  }
-
   /**
    * Blocks until all processes are terminated
    */
   public void awaitTermination() {
-    while (awaitTerminationImpl()) {
-      LoggerFactory.getLogger(RestartRequestWatcherThread.class).info("Restarting SQ...");
-      start();
+    while (awaitChildProcessesTermination()) {
+      trace("await termination of restartor...");
+      awaitTermination(restartor);
     }
-    stopRestartWatcher();
+    cleanAfterTermination();
   }
 
   boolean waitForOneRestart() {
-    boolean restartRequested = awaitTerminationImpl();
+    boolean restartRequested = awaitChildProcessesTermination();
     if (restartRequested) {
-      start();
+      awaitTermination(restartor);
     }
     return restartRequested;
   }
 
-  private boolean awaitTerminationImpl() {
-    for (WatcherThread watcherThread : watcherThreads) {
-      while (watcherThread.isAlive()) {
-        try {
-          watcherThread.join();
-        } catch (InterruptedException ignored) {
-          // ignore, stop blocking
-        }
-      }
+  private boolean awaitChildProcessesTermination() {
+    trace("await termination of child processes...");
+    List<WatcherThread> watcherThreadsCopy = new ArrayList<>(this.watcherThreads);
+    for (WatcherThread watcherThread : watcherThreadsCopy) {
+      awaitTermination(watcherThread);
     }
-    return hasRestartBeenRequested();
+    trace("all child processes done");
+    return hasRestartBeenRequested(watcherThreadsCopy);
   }
 
-  private boolean hasRestartBeenRequested() {
+  private static boolean hasRestartBeenRequested(List<WatcherThread> watcherThreads) {
     for (WatcherThread watcherThread : watcherThreads) {
       if (watcherThread.isAskedForRestart()) {
+        trace("one child process requested restart");
         return true;
       }
     }
+    trace("no child process requested restart");
     return false;
   }
 
@@ -199,19 +176,30 @@ public class Monitor {
    * Blocks until all processes are terminated.
    */
   public void stop() {
+    trace("start stop async...");
     stopAsync();
-    try {
-      terminator.join();
-    } catch (InterruptedException ignored) {
-      // stop blocking and exiting
-    }
-    // safeguard if TerminatorThread is buggy
-    lifecycle.tryToMoveTo(State.STOPPED);
-    // cleanly stop restart watcher
-    stopRestartWatcher();
+    trace("await termination of terminator...");
+    awaitTermination(terminator);
+    cleanAfterTermination();
+    trace("exit...");
     systemExit.exit(0);
   }
 
+  private void cleanAfterTermination() {
+    trace("go to STOPPED...");
+    // safeguard if TerminatorThread is buggy and stop restartWatcher
+    lifecycle.tryToMoveTo(State.STOPPED);
+    trace("await termination of restartWatcher...");
+    // wait for restartWatcher to cleanly stop
+    awaitTermination(restartWatcher);
+    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);
+    }
+  }
+
   /**
    * Asks for processes termination and returns without blocking until termination.
    * However, if a termination request is already under way (it's not supposed to happen, but, technically, it can occur),
@@ -219,17 +207,117 @@ public class Monitor {
    */
   public void stopAsync() {
     if (lifecycle.tryToMoveTo(State.STOPPING)) {
-      if (terminator != null) {
+      terminator.start();
+    }
+  }
+
+  public void restartAsync() {
+    if (lifecycle.tryToMoveTo(State.RESTARTING)) {
+      restartor = new RestartorThread();
+      restartor.start();
+    }
+  }
+
+  /**
+   * Runs every time a restart request is detected.
+   */
+  private class RestartorThread extends Thread {
+
+    private RestartorThread() {
+      super("Restartor " + (restartorInstanceCounter++));
+    }
+
+    @Override
+    public void run() {
+      stopProcesses();
+      startProcesses();
+    }
+  }
+
+  /**
+   * Runs only once
+   */
+  private class TerminatorThread extends Thread {
+
+    private TerminatorThread() {
+      super("Terminator");
+    }
+
+    @Override
+    public void run() {
+      stopProcesses();
+    }
+  }
+
+  /**
+   * Watches for any child process requesting a restart of all children processes.
+   * It runs once and as long as {@link #lifecycle} hasn't reached {@link Lifecycle.State#STOPPED} and holds its checks
+   * when {@link #lifecycle} is not in state {@link Lifecycle.State#STARTED} to avoid taking the same request into account
+   * twice.
+   */
+  public class RestartRequestWatcherThread extends Thread {
+    public RestartRequestWatcherThread() {
+      super("Restart watcher");
+    }
+
+    @Override
+    public void run() {
+      while (lifecycle.getState() != Lifecycle.State.STOPPED) {
+        if (lifecycle.getState() == Lifecycle.State.STARTED && didAnyProcessRequestRestart()) {
+          restartAsync();
+        }
         try {
-          terminator.join();
-        } catch (InterruptedException e) {
-          // stop waiting for thread to complete and continue with creating a new one
+          Thread.sleep(WATCH_DELAY_MS);
+        } catch (InterruptedException ignored) {
+          // keep watching
         }
       }
-      terminator = new TerminatorThread(TIMEOUTS);
-      terminator.setProcesses(processes);
-      terminator.start();
     }
+
+    private boolean didAnyProcessRequestRestart() {
+      for (WatcherThread watcherThread : watcherThreads) {
+        ProcessRef processRef = watcherThread.getProcessRef();
+        if (processRef.getCommands().askedForRestart()) {
+          LOG.info("Process [{}] requested restart", processRef.getKey());
+          return true;
+        }
+      }
+      return false;
+    }
+
+  }
+
+  private void stopProcesses() {
+    ArrayList<WatcherThread> watcherThreads = new ArrayList<>(this.watcherThreads);
+    // create a copy and reverse it to terminate in reverse order of startup (dependency order)
+    Collections.reverse(watcherThreads);
+
+    for (WatcherThread watcherThread : watcherThreads) {
+      ProcessRef ref = watcherThread.getProcessRef();
+      if (!ref.isStopped()) {
+        LOG.info("{} is stopping", ref);
+        ref.askForGracefulAsyncStop();
+
+        long killAt = System.currentTimeMillis() + TIMEOUTS.getTerminationTimeout();
+        while (!ref.isStopped() && System.currentTimeMillis() < killAt) {
+          try {
+            Thread.sleep(10L);
+          } catch (InterruptedException e) {
+            // stop asking for graceful stops, Monitor will hardly kill all processes
+            break;
+          }
+        }
+        if (!ref.isStopped()) {
+          LOG.info("{} failed to stop in a timely fashion. Killing it.", ref);
+        }
+        ref.stop();
+        LOG.info("{} is stopped", ref);
+      }
+    }
+
+    // all processes are stopped, no need to keep references to these WatcherThread anymore
+    trace("all processes stopped, clean list of watcherThreads...");
+    this.watcherThreads.clear();
   }
 
   public State getState() {
@@ -240,19 +328,34 @@ public class Monitor {
     return shutdownHook;
   }
 
-  public void restartAsync() {
-    stopAsync();
-  }
-
   private class MonitorShutdownHook implements Runnable {
     @Override
     public void run() {
       systemExit.setInShutdownHook();
+      trace("calling stop from MonitorShutdownHook...");
       // blocks until everything is corrected terminated
       stop();
     }
   }
 
+  private static void awaitTermination(@Nullable Thread t) {
+    if (t == null) {
+      return;
+    }
+
+    while (t.isAlive()) {
+      try {
+        t.join();
+      } catch (InterruptedException e) {
+        // ignore and stop blocking
+      }
+    }
+  }
+
+  private static void trace(String s) {
+    System.err.println("APP: " + s);
+  }
+
   public static int getNextProcessId() {
     if (nextProcessId >= ProcessCommands.MAX_PROCESSES) {
       throw new IllegalStateException("The maximum number of processes launched has been reached " + ProcessCommands.MAX_PROCESSES);
diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/RestartRequestWatcherThread.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/RestartRequestWatcherThread.java
deleted file mode 100644 (file)
index 394b477..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * SonarQube :: Process Monitor
- * 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.monitor;
-
-import java.util.List;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import static java.util.Objects.requireNonNull;
-
-public class RestartRequestWatcherThread extends Thread {
-  private static final Logger LOG = LoggerFactory.getLogger(RestartRequestWatcherThread.class);
-  private static int instanceCounter = 0;
-
-  private final Monitor monitor;
-  private final List<ProcessRef> processes;
-  private final long delayMs;
-
-  private boolean watching = true;
-
-  public RestartRequestWatcherThread(Monitor monitor, List<ProcessRef> processes) {
-    this(monitor, processes, 500);
-  }
-
-  public RestartRequestWatcherThread(Monitor monitor, List<ProcessRef> processes, long delayMs) {
-    super("Restart watcher " + (instanceCounter++));
-    this.monitor = requireNonNull(monitor, "monitor can not be null");
-    this.processes = requireNonNull(processes, "processes can not be null");
-    this.delayMs = delayMs;
-  }
-
-  @Override
-  public void run() {
-    while (watching) {
-      for (ProcessRef processCommands : processes) {
-        if (processCommands.getCommands().askedForRestart()) {
-          LOG.info("Process [{}] requested restart", processCommands.getKey());
-          monitor.restartAsync();
-          watching = false;
-        } else {
-          try {
-            Thread.sleep(delayMs);
-          } catch (InterruptedException ignored) {
-            // keep watching
-          }
-        }
-      }
-    }
-  }
-
-  public void stopWatching() {
-    this.watching = false;
-  }
-
-  public boolean isWatching() {
-    return watching;
-  }
-}
diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/TerminatorThread.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/TerminatorThread.java
deleted file mode 100644 (file)
index ab0a08d..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * SonarQube :: Process Monitor
- * 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.monitor;
-
-import org.slf4j.LoggerFactory;
-
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Terminates all monitored processes. Tries to gracefully terminate each process,
- * then kill if timeout expires. Ping monitoring is disabled so process auto kills (self graceful termination, else self kill)
- * if it does not receive the termination request.
- */
-class TerminatorThread extends Thread {
-  private static int instanceCounter = 0;
-
-  private final Timeouts timeouts;
-  private List<ProcessRef> processes = Collections.emptyList();
-
-  TerminatorThread(Timeouts timeouts) {
-    super("Terminator " + (instanceCounter++));
-    this.timeouts = timeouts;
-  }
-
-  /**
-   * To be called before {@link #run()}
-   */
-  void setProcesses(List<ProcessRef> l) {
-    this.processes = l;
-  }
-
-  @Override
-  public void run() {
-    // terminate in reverse order of startup (dependency order)
-    for (int index = processes.size() - 1; index >= 0; index--) {
-      ProcessRef ref = processes.get(index);
-      if (!ref.isStopped()) {
-        LoggerFactory.getLogger(getClass()).info(String.format("%s is stopping", ref));
-        ref.askForGracefulAsyncStop();
-
-        long killAt = System.currentTimeMillis() + timeouts.getTerminationTimeout();
-        while (!ref.isStopped() && System.currentTimeMillis() < killAt) {
-          try {
-            Thread.sleep(100L);
-          } catch (InterruptedException e) {
-            // stop asking for graceful stops, Monitor will hardly kill all processes
-            return;
-          }
-        }
-        if (!ref.isStopped()) {
-          LoggerFactory.getLogger(getClass()).info(String.format("%s failed to stop in a timely fashion. Killing it.", ref));
-        }
-        ref.stop();
-        LoggerFactory.getLogger(getClass()).info(String.format("%s is stopped", ref));
-      }
-    }
-  }
-}
index d6e6da4965123d2ba82248e1e9db9059a2936be0..86bf067b6f120707c86e859039d268fba997f9b0 100644 (file)
@@ -64,6 +64,10 @@ class WatcherThread extends Thread {
     }
   }
 
+  public ProcessRef getProcessRef() {
+    return processRef;
+  }
+
   public boolean isAskedForRestart() {
     return askedForRestart;
   }
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/RestartRequestWatcherThreadTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/RestartRequestWatcherThreadTest.java
deleted file mode 100644 (file)
index c87f0ba..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * SonarQube :: Process Monitor
- * 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.monitor;
-
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Random;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.TemporaryFolder;
-import org.sonar.process.DefaultProcessCommands;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-
-public class RestartRequestWatcherThreadTest {
-  private static final long TEST_DELAYS_MS = 5L;
-
-  @Rule
-  public ExpectedException expectedException = ExpectedException.none();
-  @Rule
-  public TemporaryFolder temp = new TemporaryFolder();
-
-  private Monitor monitor = mock(Monitor.class);
-
-  @Test
-  public void constructor_throws_NPE_if_monitor_arg_is_null() {
-    expectedException.expect(NullPointerException.class);
-    expectedException.expectMessage("monitor can not be null");
-
-    new RestartRequestWatcherThread(null, Collections.<ProcessRef>emptyList());
-  }
-
-  @Test
-  public void constructor_throws_NPE_if_processes_arg_is_null() {
-    expectedException.expect(NullPointerException.class);
-    expectedException.expectMessage("processes can not be null");
-
-    new RestartRequestWatcherThread(monitor, null);
-  }
-
-  @Test
-  public void each_RestartRequestWatcherThread_instance_get_a_unique_thread_name() {
-    assertThat(newSingleProcessRefRestartWatcher().getName())
-        .isNotEqualTo(newSingleProcessRefRestartWatcher().getName());
-  }
-
-  @Test
-  public void does_not_stop_watching_when_no_processRef_requests_restart() throws Exception {
-    RestartRequestWatcherThread underTest = newSingleProcessRefRestartWatcher();
-
-    underTest.start();
-
-    Thread.sleep(200L);
-
-    assertThat(underTest.isWatching()).isTrue();
-    assertThat(underTest.isAlive()).isTrue();
-  }
-
-  @Test(timeout = 500L)
-  public void stops_watching_when_any_processRef_requests_restart() throws Exception {
-    ProcessRef processRef1 = newProcessRef(1);
-    ProcessRef processRef2 = newProcessRef(2);
-    RestartRequestWatcherThread underTest = newSingleProcessRefRestartWatcher(processRef1, processRef2);
-
-    underTest.start();
-
-    Thread.sleep(123L);
-
-    if (new Random().nextInt() % 2 == 1) {
-      processRef1.getCommands().askForRestart();
-    } else {
-      processRef2.getCommands().askForRestart();
-    }
-
-    underTest.join();
-
-    assertThat(underTest.isWatching()).isFalse();
-    verify(monitor).restartAsync();
-  }
-
-  private RestartRequestWatcherThread newSingleProcessRefRestartWatcher(ProcessRef... processRefs) {
-    return new RestartRequestWatcherThread(monitor, Arrays.asList(processRefs), TEST_DELAYS_MS);
-  }
-
-  private ProcessRef newProcessRef(int id) {
-    try {
-      return new ProcessRef(String.valueOf(id), new DefaultProcessCommands(temp.newFolder(), id), mock(Process.class), mock(StreamGobbler.class));
-    } catch (IOException e) {
-      throw new RuntimeException(e);
-    }
-  }
-}
index 9d6c93df39266e56ce96200957ac12cde4b2f9f3..18c74daabaccd49e6855e9884bde7df72b7d4974 100644 (file)
  */
 package org.sonar.process;
 
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 public class Lifecycle {
+  private static final Logger LOG = LoggerFactory.getLogger(Lifecycle.class);
 
   public enum State {
-    INIT, STARTING, STARTED, STOPPING, STOPPED
+    INIT, STARTING, STARTED, RESTARTING, STOPPING, STOPPED
+  }
+
+  private static final Map<State, Set<State>> TRANSITIONS = buildTransitions();
+
+  private static Map<State, Set<State>> buildTransitions() {
+    Map<State, Set<State>> res = new HashMap<>(State.values().length);
+    res.put(State.INIT, toSet(State.STARTING));
+    res.put(State.STARTING, toSet(State.STARTED, State.STOPPING));
+    res.put(State.STARTED, toSet(State.RESTARTING, State.STOPPING));
+    res.put(State.RESTARTING, toSet(State.STARTING));
+    res.put(State.STOPPING, toSet(State.STOPPED));
+    res.put(State.STOPPED, toSet());
+    return res;
+  }
+
+  private static Set<State> toSet(State... states) {
+    if (states.length == 0) {
+      return Collections.emptySet();
+    }
+    if (states.length == 1) {
+      return Collections.singleton(states[0]);
+    }
+    Set<State> res = new HashSet<>(states.length);
+    Collections.addAll(res, states);
+    return res;
   }
 
   private State state = State.INIT;
@@ -32,15 +67,18 @@ public class Lifecycle {
   }
 
   public synchronized boolean tryToMoveTo(State to) {
-    if (state.ordinal() < to.ordinal()) {
-      state = to;
-      return true;
+    boolean res = false;
+    State currentState = state;
+    if (TRANSITIONS.get(currentState).contains(to)) {
+      this.state = to;
+      res = true;
     }
-    return false;
+    LOG.trace("tryToMoveTo from {} to {} => {}", currentState, to, res);
+    return res;
   }
 
   @Override
-  public boolean equals(Object o) {
+  public boolean equals(@Nullable Object o) {
     if (this == o) {
       return true;
     }
index 7dd9f2a43bfe58cc94dae566fb10a223b4732e63..fa9d925d15932625be7a468aea2f695aa504fa27 100644 (file)
@@ -37,19 +37,69 @@ public class LifecycleTest {
 
     // different state
     Lifecycle stopping = new Lifecycle();
-    stopping.tryToMoveTo(State.STOPPING);
+    stopping.tryToMoveTo(State.STARTING);
     assertThat(stopping).isNotEqualTo(init);
   }
 
   @Test
-  public void try_to_move() {
+  public void try_to_move_does_not_support_jumping_states() {
     Lifecycle lifecycle = new Lifecycle();
     assertThat(lifecycle.getState()).isEqualTo(State.INIT);
 
-    assertThat(lifecycle.tryToMoveTo(State.STARTED)).isTrue();
-    assertThat(lifecycle.getState()).isEqualTo(State.STARTED);
+    assertThat(lifecycle.tryToMoveTo(State.STARTED)).isFalse();
+    assertThat(lifecycle.getState()).isEqualTo(State.INIT);
+
+    assertThat(lifecycle.tryToMoveTo(State.STARTING)).isTrue();
+    assertThat(lifecycle.getState()).isEqualTo(State.STARTING);
+  }
+
+  @Test
+  public void no_state_can_not_move_to_itself() {
+    for (State state : State.values()) {
+      assertThat(newLifeCycle(state).tryToMoveTo(state)).isFalse();
+    }
+  }
+
+  @Test
+  public void can_move_to_STOPPING_from_any_state_but_STARTING_and_STARTED_only() {
+    for (State state : State.values()) {
+      boolean tryToMoveTo = newLifeCycle(state).tryToMoveTo(State.STOPPING);
+      if (state == State.STARTING || state == State.STARTED) {
+        assertThat(tryToMoveTo).describedAs("from state " + state).isTrue();
+      } else {
+        assertThat(tryToMoveTo).describedAs("from state " + state).isFalse();
+      }
+    }
+  }
+
+  @Test
+  public void can_move_to_STARTING_from_RESTARTING() {
+    assertThat(newLifeCycle(State.RESTARTING).tryToMoveTo(State.STARTING)).isTrue();
+  }
+
+  private static Lifecycle newLifeCycle(State state) {
+    switch (state) {
+      case INIT:
+        return new Lifecycle();
+      case STARTING:
+        return newLifeCycle(State.INIT, state);
+      case STARTED:
+        return newLifeCycle(State.STARTING, state);
+      case RESTARTING:
+        return newLifeCycle(State.STARTED, state);
+      case STOPPING:
+        return newLifeCycle(State.STARTED, state);
+      case STOPPED:
+        return newLifeCycle(State.STOPPING, state);
+      default:
+        throw new IllegalArgumentException("Unsupported state " + state);
+    }
+  }
 
-    assertThat(lifecycle.tryToMoveTo(State.STARTING)).isFalse();
-    assertThat(lifecycle.getState()).isEqualTo(State.STARTED);
+  private static Lifecycle newLifeCycle(State from, State to) {
+    Lifecycle lifecycle;
+    lifecycle = newLifeCycle(from);
+    assertThat(lifecycle.tryToMoveTo(to)).isTrue();
+    return lifecycle;
   }
 }
index 3699304b84f18019edace3cbb0b4d444e3b8c75b..db78e1bf0c8ef64f26d6196708cd70a7459c1340 100644 (file)
@@ -41,6 +41,8 @@ import java.util.Properties;
  */
 public class App implements Stoppable {
 
+  private static final int APP_PROCESS_NUMBER = 0;
+
   private final Monitor monitor;
   private StopWatcher stopWatcher = null;
 
@@ -55,7 +57,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, 0);
+      ProcessCommands commands = new DefaultProcessCommands(tempDir, APP_PROCESS_NUMBER);
       stopWatcher = new StopWatcher(commands, this);
       stopWatcher.start();
     }
@@ -63,7 +65,7 @@ public class App implements Stoppable {
     monitor.awaitTermination();
   }
 
-  List<JavaCommand> createCommands(Props props) {
+  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);