@@ -37,9 +37,6 @@ public class JavaCommand { | |||
private File workDir; | |||
// any available port by default | |||
private int jmxPort = -1; | |||
// for example -Xmx1G | |||
private final List<String> javaOptions = new ArrayList<String>(); | |||
@@ -54,6 +51,8 @@ public class JavaCommand { | |||
private final Map<String, String> envVariables = new HashMap<String, String>(System.getenv()); | |||
private File tempDir = null; | |||
public JavaCommand(String key) { | |||
this.key = key; | |||
} | |||
@@ -71,24 +70,20 @@ public class JavaCommand { | |||
return this; | |||
} | |||
/** | |||
* Shortcut to set the java option -Djava.io.tmpdir | |||
*/ | |||
public JavaCommand setTempDir(File tempDir) { | |||
this.javaOptions.add("-Djava.io.tmpdir=" + tempDir.getAbsolutePath()); | |||
return this; | |||
public File getTempDir() { | |||
return tempDir; | |||
} | |||
public int getJmxPort() { | |||
return jmxPort; | |||
public JavaCommand setTempDir(File tempDir) { | |||
this.tempDir = tempDir; | |||
return this; | |||
} | |||
/** | |||
* Current mandatory to be set | |||
*/ | |||
public JavaCommand setJmxPort(int jmxPort) { | |||
this.jmxPort = jmxPort; | |||
return this; | |||
public File getReadyFile() { | |||
if (tempDir == null) { | |||
throw new IllegalStateException("Temp directory not set"); | |||
} | |||
return new File(tempDir, key + ".ready"); | |||
} | |||
public List<String> getJavaOptions() { | |||
@@ -156,20 +151,10 @@ public class JavaCommand { | |||
return this; | |||
} | |||
public boolean isDebugMode() { | |||
for (String javaOption : javaOptions) { | |||
if (javaOption.contains("-agentlib:jdwp")) { | |||
return true; | |||
} | |||
} | |||
return false; | |||
} | |||
@Override | |||
public String toString() { | |||
final StringBuilder sb = new StringBuilder("JavaCommand{"); | |||
sb.append("workDir=").append(workDir); | |||
sb.append(", jmxPort=").append(jmxPort); | |||
sb.append(", javaOptions=").append(javaOptions); | |||
sb.append(", className='").append(className).append('\''); | |||
sb.append(", classpath=").append(classpath); |
@@ -21,9 +21,9 @@ package org.sonar.process.monitor; | |||
import org.apache.commons.lang.StringUtils; | |||
import org.slf4j.LoggerFactory; | |||
import org.sonar.process.LoopbackAddress; | |||
import org.sonar.process.ProcessEntryPoint; | |||
import org.sonar.process.ProcessUtils; | |||
import org.sonar.process.SharedStatus; | |||
import java.io.File; | |||
import java.io.FileOutputStream; | |||
@@ -44,20 +44,27 @@ public class JavaProcessLauncher { | |||
ProcessRef launch(JavaCommand command) { | |||
Process process = null; | |||
try { | |||
// cleanup existing monitor file. Child process creates it when ready. | |||
// TODO fail if impossible to delete | |||
SharedStatus sharedStatus = new SharedStatus(command.getReadyFile()); | |||
sharedStatus.prepare(); | |||
ProcessBuilder processBuilder = create(command); | |||
LoggerFactory.getLogger(getClass()).info("Launch {}: {}", | |||
command.getKey(), StringUtils.join(processBuilder.command(), " ")); | |||
long startedAt = System.currentTimeMillis(); | |||
process = processBuilder.start(); | |||
StreamGobbler errorGobbler = new StreamGobbler(process.getErrorStream(), command.getKey()); | |||
StreamGobbler inputGobbler = new StreamGobbler(process.getInputStream(), command.getKey()); | |||
inputGobbler.start(); | |||
errorGobbler.start(); | |||
return new ProcessRef(command.getKey(), process, errorGobbler, inputGobbler); | |||
ProcessRef ref = new ProcessRef(command.getKey(), sharedStatus, process, inputGobbler); | |||
ref.setLaunchedAt(startedAt); | |||
return ref; | |||
} catch (Exception e) { | |||
// just in case | |||
ProcessUtils.destroyQuietly(process); | |||
ProcessUtils.sendKillSignal(process); | |||
throw new IllegalStateException("Fail to launch " + command.getKey(), e); | |||
} | |||
} | |||
@@ -66,17 +73,17 @@ public class JavaProcessLauncher { | |||
List<String> commands = new ArrayList<String>(); | |||
commands.add(buildJavaPath()); | |||
commands.addAll(javaCommand.getJavaOptions()); | |||
commands.addAll(buildJmxOptions(javaCommand)); | |||
// TODO warning - does it work if temp dir contains a whitespace ? | |||
commands.add(String.format("-Djava.io.tmpdir=%s", javaCommand.getTempDir().getAbsolutePath())); | |||
commands.addAll(buildClasspath(javaCommand)); | |||
commands.add(javaCommand.getClassName()); | |||
// TODO warning - does it work if temp dir contains a whitespace ? | |||
commands.add(buildPropertiesFile(javaCommand).getAbsolutePath()); | |||
ProcessBuilder processBuilder = new ProcessBuilder(); | |||
processBuilder.command(commands); | |||
processBuilder.directory(javaCommand.getWorkDir()); | |||
processBuilder.environment().putAll(javaCommand.getEnvVariables()); | |||
processBuilder.redirectErrorStream(true); | |||
return processBuilder; | |||
} | |||
@@ -86,18 +93,6 @@ public class JavaProcessLauncher { | |||
"bin" + separator + "java").getAbsolutePath(); | |||
} | |||
private List<String> buildJmxOptions(JavaCommand javaCommand) { | |||
if (javaCommand.getJmxPort() < 1) { | |||
throw new IllegalStateException("JMX port is not set"); | |||
} | |||
return Arrays.asList( | |||
"-Dcom.sun.management.jmxremote", | |||
"-Dcom.sun.management.jmxremote.port=" + javaCommand.getJmxPort(), | |||
"-Dcom.sun.management.jmxremote.authenticate=false", | |||
"-Dcom.sun.management.jmxremote.ssl=false", | |||
"-Djava.rmi.server.hostname=" + LoopbackAddress.get().getHostAddress()); | |||
} | |||
private List<String> buildClasspath(JavaCommand javaCommand) { | |||
return Arrays.asList("-cp", StringUtils.join(javaCommand.getClasspath(), System.getProperty("path.separator"))); | |||
} | |||
@@ -105,14 +100,12 @@ public class JavaProcessLauncher { | |||
private File buildPropertiesFile(JavaCommand javaCommand) { | |||
File propertiesFile = null; | |||
try { | |||
propertiesFile = File.createTempFile("sq-conf", "properties"); | |||
propertiesFile = File.createTempFile("sq-process", "properties"); | |||
Properties props = new Properties(); | |||
props.putAll(javaCommand.getArguments()); | |||
props.setProperty(ProcessEntryPoint.PROPERTY_PROCESS_KEY, javaCommand.getKey()); | |||
props.setProperty(ProcessEntryPoint.PROPERTY_AUTOKILL_DISABLED, String.valueOf(javaCommand.isDebugMode())); | |||
props.setProperty(ProcessEntryPoint.PROPERTY_AUTOKILL_PING_TIMEOUT, String.valueOf(timeouts.getAutokillPingTimeout())); | |||
props.setProperty(ProcessEntryPoint.PROPERTY_AUTOKILL_PING_INTERVAL, String.valueOf(timeouts.getAutokillPingInterval())); | |||
props.setProperty(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, String.valueOf(timeouts.getTerminationTimeout())); | |||
props.setProperty(ProcessEntryPoint.PROPERTY_STATUS_PATH, javaCommand.getReadyFile().getAbsolutePath()); | |||
OutputStream out = new FileOutputStream(propertiesFile); | |||
props.store(out, String.format("Temporary properties file for command [%s]", javaCommand.getKey())); | |||
out.close(); |
@@ -1,44 +0,0 @@ | |||
/* | |||
* 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; | |||
/** | |||
* Interactions with monitored process | |||
*/ | |||
public interface JmxConnector { | |||
/** | |||
* Throws an exception if timeout reached | |||
*/ | |||
void connect(JavaCommand command, ProcessRef processRef, long timeoutMs); | |||
void ping(ProcessRef process); | |||
/** | |||
* Throws an exception if timeout reached | |||
*/ | |||
boolean isReady(ProcessRef process, long timeoutMs); | |||
/** | |||
* Throws an exception if timeout reached | |||
*/ | |||
void terminate(ProcessRef process, long timeoutMs); | |||
} |
@@ -21,8 +21,7 @@ package org.sonar.process.monitor; | |||
import org.slf4j.LoggerFactory; | |||
import org.sonar.process.Lifecycle; | |||
import org.sonar.process.MessageException; | |||
import org.sonar.process.State; | |||
import org.sonar.process.Lifecycle.State; | |||
import org.sonar.process.SystemExit; | |||
import java.util.List; | |||
@@ -33,28 +32,23 @@ public class Monitor { | |||
private final List<ProcessRef> processes = new CopyOnWriteArrayList<ProcessRef>(); | |||
private final TerminatorThread terminator; | |||
private final JavaProcessLauncher launcher; | |||
private final JmxConnector jmxConnector; | |||
private final Lifecycle lifecycle = new Lifecycle(); | |||
private final Timeouts timeouts; | |||
private final SystemExit systemExit; | |||
private Thread shutdownHook = new Thread(new MonitorShutdownHook(), "Monitor Shutdown Hook"); | |||
// used by awaitTermination() to block until all processes are shutdown | |||
// used by awaitStop() to block until all processes are shutdown | |||
private final List<WatcherThread> watcherThreads = new CopyOnWriteArrayList<WatcherThread>(); | |||
Monitor(JavaProcessLauncher launcher, JmxConnector jmxConnector, Timeouts timeouts, SystemExit exit) { | |||
Monitor(JavaProcessLauncher launcher, SystemExit exit) { | |||
this.launcher = launcher; | |||
this.jmxConnector = jmxConnector; | |||
this.timeouts = timeouts; | |||
this.terminator = new TerminatorThread(processes, jmxConnector, timeouts); | |||
this.terminator = new TerminatorThread(processes); | |||
this.systemExit = exit; | |||
} | |||
public static Monitor create() { | |||
Timeouts timeouts = new Timeouts(); | |||
return new Monitor(new JavaProcessLauncher(timeouts), new RmiJmxConnector(), | |||
timeouts, new SystemExit()); | |||
return new Monitor(new JavaProcessLauncher(timeouts), new SystemExit()); | |||
} | |||
/** | |||
@@ -103,44 +97,14 @@ public class Monitor { | |||
watcherThread.start(); | |||
watcherThreads.add(watcherThread); | |||
// add to list of monitored processes only when successfully connected to it | |||
jmxConnector.connect(command, processRef, timeouts.getJmxConnectionTimeout()); | |||
processes.add(processRef); | |||
// ping process on a regular basis | |||
processRef.setPingEnabled(!command.isDebugMode()); | |||
if (processRef.isPingEnabled()) { | |||
PingerThread.startPinging(processRef, jmxConnector, timeouts); | |||
} | |||
// wait for process to be ready (accept requests or so on) | |||
waitForReady(processRef); | |||
processRef.waitForReady(); | |||
LoggerFactory.getLogger(getClass()).info(String.format("%s is up", processRef)); | |||
} | |||
private void waitForReady(ProcessRef processRef) { | |||
boolean ready = false; | |||
while (!ready) { | |||
if (processRef.isTerminated()) { | |||
throw new MessageException(String.format("%s failed to start", processRef)); | |||
} | |||
try { | |||
ready = jmxConnector.isReady(processRef, timeouts.getMonitorIsReadyTimeout()); | |||
} catch (Exception ignored) { | |||
// failed to send request, probably because RMI server is still not alive | |||
// trying again, as long as process is alive | |||
// TODO could be improved to have a STARTING timeout (to be implemented in monitor or | |||
// in child process ?) | |||
} | |||
try { | |||
Thread.sleep(300L); | |||
} catch (InterruptedException e) { | |||
throw new IllegalStateException("Interrupted while waiting for " + processRef + " to be ready", e); | |||
} | |||
} | |||
} | |||
/** | |||
* Blocks until all processes are terminated | |||
*/ | |||
@@ -160,14 +124,13 @@ public class Monitor { | |||
* Blocks until all processes are terminated. | |||
*/ | |||
public void stop() { | |||
terminateAsync(); | |||
stopAsync(); | |||
try { | |||
terminator.join(); | |||
} catch (InterruptedException ignored) { | |||
// ignore, stop blocking | |||
// stop blocking and exiting | |||
} | |||
// safeguard if TerminatorThread is buggy | |||
hardKillAll(); | |||
lifecycle.tryToMoveTo(State.STOPPED); | |||
systemExit.exit(0); | |||
} | |||
@@ -176,7 +139,7 @@ public class Monitor { | |||
* Asks for processes termination and returns without blocking until termination. | |||
* @return true if termination was requested, false if it was already being terminated | |||
*/ | |||
boolean terminateAsync() { | |||
boolean stopAsync() { | |||
boolean requested = false; | |||
if (lifecycle.tryToMoveTo(State.STOPPING)) { | |||
requested = true; | |||
@@ -185,13 +148,6 @@ public class Monitor { | |||
return requested; | |||
} | |||
private void hardKillAll() { | |||
// no specific order, kill'em all!!! | |||
for (ProcessRef process : processes) { | |||
process.hardKill(); | |||
} | |||
} | |||
public State getState() { | |||
return lifecycle.getState(); | |||
} |
@@ -1,60 +0,0 @@ | |||
/* | |||
* 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; | |||
import java.util.concurrent.Executors; | |||
import java.util.concurrent.ScheduledExecutorService; | |||
import java.util.concurrent.TimeUnit; | |||
/** | |||
* This thread pings a process - through RMI - at fixed delay | |||
*/ | |||
class PingerThread extends Thread { | |||
private final ProcessRef processRef; | |||
private final JmxConnector jmxConnector; | |||
private PingerThread(ProcessRef process, JmxConnector jmxConnector) { | |||
// it's important to give a name for traceability in profiling tools like visualVM | |||
super(String.format("Ping[%s]", process.getKey())); | |||
setDaemon(true); | |||
this.processRef = process; | |||
this.jmxConnector = jmxConnector; | |||
} | |||
@Override | |||
public void run() { | |||
if (!processRef.isTerminated() && processRef.isPingEnabled()) { | |||
try { | |||
jmxConnector.ping(processRef); | |||
} catch (Exception ignored) { | |||
// failed to ping | |||
} | |||
} else { | |||
interrupt(); | |||
} | |||
} | |||
static void startPinging(ProcessRef processRef, JmxConnector jmxConnector, Timeouts timeouts) { | |||
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); | |||
PingerThread pinger = new PingerThread(processRef, jmxConnector); | |||
scheduler.scheduleAtFixedRate(pinger, 0L, timeouts.getMonitorPingInterval(), TimeUnit.MILLISECONDS); | |||
} | |||
} |
@@ -19,21 +19,26 @@ | |||
*/ | |||
package org.sonar.process.monitor; | |||
import org.slf4j.LoggerFactory; | |||
import org.sonar.process.MessageException; | |||
import org.sonar.process.ProcessUtils; | |||
import org.sonar.process.SharedStatus; | |||
class ProcessRef { | |||
private final String key; | |||
private final SharedStatus sharedStatus; | |||
private final Process process; | |||
private final StreamGobbler[] gobblers; | |||
private volatile boolean terminated = false; | |||
private volatile boolean pingEnabled = true; | |||
private final StreamGobbler gobbler; | |||
private long launchedAt; | |||
private volatile boolean stopped = false; | |||
ProcessRef(String key, Process process, StreamGobbler... gobblers) { | |||
ProcessRef(String key, SharedStatus sharedStatus, Process process, StreamGobbler gobbler) { | |||
this.key = key; | |||
this.sharedStatus = sharedStatus; | |||
this.process = process; | |||
this.terminated = !ProcessUtils.isAlive(process); | |||
this.gobblers = gobblers; | |||
this.stopped = !ProcessUtils.isAlive(process); | |||
this.gobbler = gobbler; | |||
} | |||
/** | |||
@@ -50,45 +55,53 @@ class ProcessRef { | |||
return process; | |||
} | |||
/** | |||
* Almost real-time status | |||
*/ | |||
boolean isTerminated() { | |||
return terminated; | |||
void waitForReady() { | |||
boolean ready = false; | |||
while (!ready) { | |||
if (isStopped()) { | |||
throw new MessageException(String.format("%s failed to start", this)); | |||
} | |||
ready = sharedStatus.wasStartedAfter(launchedAt); | |||
try { | |||
Thread.sleep(200L); | |||
} catch (InterruptedException e) { | |||
throw new IllegalStateException(String.format("Interrupted while waiting for %s to be ready", this), e); | |||
} | |||
} | |||
} | |||
/** | |||
* Sending pings can be disabled when requesting for termination or when process is on debug mode (JDWP) | |||
*/ | |||
void setPingEnabled(boolean b) { | |||
this.pingEnabled = b; | |||
void setLaunchedAt(long launchedAt) { | |||
this.launchedAt = launchedAt; | |||
} | |||
boolean isPingEnabled() { | |||
return pingEnabled; | |||
/** | |||
* Almost real-time status | |||
*/ | |||
boolean isStopped() { | |||
return stopped; | |||
} | |||
/** | |||
* Destroy the process without gracefully asking it to terminate (kill -9). | |||
* @return true if the process was killed, false if process is already terminated | |||
* Sends kill signal and awaits termination. | |||
*/ | |||
boolean hardKill() { | |||
boolean killed = false; | |||
terminated = true; | |||
pingEnabled = false; | |||
void kill() { | |||
if (ProcessUtils.isAlive(process)) { | |||
ProcessUtils.destroyQuietly(process); | |||
killed = true; | |||
} | |||
for (StreamGobbler gobbler : gobblers) { | |||
StreamGobbler.waitUntilFinish(gobbler); | |||
LoggerFactory.getLogger(getClass()).info(String.format("%s is stopping", this)); | |||
ProcessUtils.sendKillSignal(process); | |||
try { | |||
// signal is sent, waiting for shutdown hooks to be executed | |||
process.waitFor(); | |||
StreamGobbler.waitUntilFinish(gobbler); | |||
ProcessUtils.closeStreams(process); | |||
} catch (InterruptedException ignored) { | |||
// can't wait for the termination of process. Let's assume it's down. | |||
} | |||
} | |||
ProcessUtils.closeStreams(process); | |||
return killed; | |||
stopped = true; | |||
} | |||
void setTerminated(boolean b) { | |||
this.terminated = b; | |||
void setStopped(boolean b) { | |||
this.stopped = b; | |||
} | |||
@Override |
@@ -1,145 +0,0 @@ | |||
/* | |||
* 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; | |||
import org.slf4j.LoggerFactory; | |||
import org.sonar.process.JmxUtils; | |||
import org.sonar.process.LoopbackAddress; | |||
import org.sonar.process.ProcessMXBean; | |||
import org.sonar.process.ProcessUtils; | |||
import javax.annotation.CheckForNull; | |||
import javax.management.JMX; | |||
import javax.management.MBeanServerConnection; | |||
import javax.management.remote.JMXConnector; | |||
import javax.management.remote.JMXConnectorFactory; | |||
import javax.management.remote.JMXServiceURL; | |||
import java.util.IdentityHashMap; | |||
import java.util.Map; | |||
import java.util.concurrent.Callable; | |||
import java.util.concurrent.ExecutorService; | |||
import java.util.concurrent.Executors; | |||
import java.util.concurrent.Future; | |||
import java.util.concurrent.TimeUnit; | |||
class RmiJmxConnector implements JmxConnector { | |||
static { | |||
/* | |||
* Prevents such warnings : | |||
* | |||
* WARNING: Failed to restart: java.io.IOException: Failed to get a RMI stub: javax.naming.ServiceUnavailableException [Root exception | |||
* is java.rmi.ConnectException: Connection refused to host: 127.0.0.1; nested exception is: | |||
* java.net.ConnectException: Connection refused] | |||
* Sep 11, 2014 7:32:32 PM RMIConnector RMIClientCommunicatorAdmin-doStop | |||
* WARNING: Failed to call the method close():java.rmi.ConnectException: Connection refused to host: 127.0.0.1; nested exception is: | |||
* java.net.ConnectException: Connection refused | |||
* Sep 11, 2014 7:32:32 PM ClientCommunicatorAdmin Checker-run | |||
* WARNING: Failed to check connection: java.net.ConnectException: Connection refused | |||
* Sep 11, 2014 7:32:32 PM ClientCommunicatorAdmin Checker-run | |||
* WARNING: stopping | |||
*/ | |||
System.setProperty("sun.rmi.transport.tcp.logLevel", "SEVERE"); | |||
} | |||
private final Map<ProcessRef, ProcessMXBean> mbeans = new IdentityHashMap<ProcessRef, ProcessMXBean>(); | |||
@Override | |||
public synchronized void connect(final JavaCommand command, ProcessRef processRef, long timeoutMs) { | |||
ConnectorCallable callable = new ConnectorCallable(command, processRef.getProcess()); | |||
ProcessMXBean mxBean = execute(callable, timeoutMs); | |||
if (mxBean != null) { | |||
register(processRef, mxBean); | |||
} | |||
} | |||
@Override | |||
public void ping(ProcessRef processRef) { | |||
mbeans.get(processRef).ping(); | |||
} | |||
@Override | |||
public boolean isReady(final ProcessRef processRef, long timeoutMs) { | |||
return execute(new Callable<Boolean>() { | |||
@Override | |||
public Boolean call() throws Exception { | |||
return mbeans.get(processRef).isReady(); | |||
} | |||
}, timeoutMs); | |||
} | |||
@Override | |||
public void terminate(final ProcessRef processRef, long timeoutMs) { | |||
execute(new Callable() { | |||
@Override | |||
public Void call() throws Exception { | |||
LoggerFactory.getLogger(getClass()).info("Request termination of " + processRef); | |||
mbeans.get(processRef).terminate(); | |||
return null; | |||
} | |||
}, timeoutMs); | |||
} | |||
void register(ProcessRef processRef, ProcessMXBean mxBean) { | |||
mbeans.put(processRef, mxBean); | |||
} | |||
private <T> T execute(Callable<T> callable, long timeoutMs) { | |||
ExecutorService executor = Executors.newSingleThreadExecutor(); | |||
try { | |||
Future<T> future = executor.submit(callable); | |||
return future.get(timeoutMs, TimeUnit.MILLISECONDS); | |||
} catch (Exception e) { | |||
throw new IllegalStateException("Fail send JMX request", e); | |||
} finally { | |||
executor.shutdownNow(); | |||
} | |||
} | |||
private static class ConnectorCallable implements Callable<ProcessMXBean> { | |||
private final JavaCommand command; | |||
private final Process process; | |||
private ConnectorCallable(JavaCommand command, Process process) { | |||
this.command = command; | |||
this.process = process; | |||
} | |||
@Override | |||
@CheckForNull | |||
public ProcessMXBean call() throws Exception { | |||
JMXServiceURL jmxUrl = JmxUtils.serviceUrl(LoopbackAddress.get(), command.getJmxPort()); | |||
while (ProcessUtils.isAlive(process)) { | |||
try { | |||
JMXConnector jmxConnector = JMXConnectorFactory.connect(jmxUrl, null); | |||
MBeanServerConnection mBeanServer = jmxConnector.getMBeanServerConnection(); | |||
return JMX.newMBeanProxy(mBeanServer, JmxUtils.objectName(command.getKey()), ProcessMXBean.class); | |||
} catch (Exception ignored) { | |||
// ignored, RMI server is probably not started yet | |||
} | |||
Thread.sleep(300L); | |||
} | |||
// process went down, no need to connect | |||
return null; | |||
} | |||
} | |||
} |
@@ -55,8 +55,8 @@ class StreamGobbler extends Thread { | |||
while ((line = br.readLine()) != null) { | |||
logger.info(line); | |||
} | |||
} catch (Exception e) { | |||
logger.error("Fail to read process logs", e); | |||
} catch (Exception ignored) { | |||
// ignored | |||
} finally { | |||
IOUtils.closeQuietly(br); | |||
} |
@@ -19,8 +19,6 @@ | |||
*/ | |||
package org.sonar.process.monitor; | |||
import org.slf4j.LoggerFactory; | |||
import java.util.List; | |||
/** | |||
@@ -31,33 +29,18 @@ import java.util.List; | |||
class TerminatorThread extends Thread { | |||
private final List<ProcessRef> processes; | |||
private final JmxConnector jmxConnector; | |||
private final Timeouts timeouts; | |||
TerminatorThread(List<ProcessRef> processes, JmxConnector jmxConnector, Timeouts timeouts) { | |||
TerminatorThread(List<ProcessRef> processes) { | |||
super("Terminator"); | |||
this.processes = processes; | |||
this.jmxConnector = jmxConnector; | |||
this.timeouts = timeouts; | |||
} | |||
@Override | |||
public void run() { | |||
// terminate in reverse order of startup (dependency order) | |||
for (int index = processes.size() - 1; index >= 0; index--) { | |||
final ProcessRef processRef = processes.get(index); | |||
if (!processRef.isTerminated()) { | |||
processRef.setPingEnabled(false); | |||
try { | |||
jmxConnector.terminate(processRef, timeouts.getTerminationTimeout()); | |||
} catch (Exception ignored) { | |||
// failed to gracefully stop in a timely fashion | |||
LoggerFactory.getLogger(getClass()).info(String.format("Kill %s", processRef)); | |||
} finally { | |||
// kill even if graceful termination was done, just to be sure that physical process is really down | |||
processRef.hardKill(); | |||
} | |||
} | |||
ProcessRef processRef = processes.get(index); | |||
processRef.kill(); | |||
} | |||
} | |||
} |
@@ -25,81 +25,6 @@ package org.sonar.process.monitor; | |||
class Timeouts { | |||
private long terminationTimeout = 120000L; | |||
private long jmxConnectionTimeout = 15000L; | |||
private long monitorPingInterval = 3000L; | |||
private long monitorIsReadyTimeout = 10000L; | |||
private long autokillPingTimeout = 60000L; | |||
private long autokillPingInterval = 3000L; | |||
/** | |||
* [monitor] Timeout to get connected to RMI MXBean while process is alive | |||
*/ | |||
long getJmxConnectionTimeout() { | |||
return jmxConnectionTimeout; | |||
} | |||
/** | |||
* @see #getJmxConnectionTimeout() | |||
*/ | |||
void setJmxConnectionTimeout(long l) { | |||
this.jmxConnectionTimeout = l; | |||
} | |||
/** | |||
* [monitor] Delay between each ping request | |||
*/ | |||
long getMonitorPingInterval() { | |||
return monitorPingInterval; | |||
} | |||
/** | |||
* @see #getMonitorPingInterval() | |||
*/ | |||
void setMonitorPingInterval(long l) { | |||
this.monitorPingInterval = l; | |||
} | |||
/** | |||
* [monitor] Timeout of isReady request | |||
*/ | |||
long getMonitorIsReadyTimeout() { | |||
return monitorIsReadyTimeout; | |||
} | |||
/** | |||
* @see #getMonitorIsReadyTimeout() | |||
*/ | |||
void setMonitorIsReadyTimeout(long l) { | |||
this.monitorIsReadyTimeout = l; | |||
} | |||
/** | |||
* [monitored process] maximum age of last received ping before process autokills | |||
*/ | |||
long getAutokillPingTimeout() { | |||
return autokillPingTimeout; | |||
} | |||
/** | |||
* @see #getAutokillPingTimeout() | |||
*/ | |||
void setAutokillPingTimeout(long l) { | |||
this.autokillPingTimeout = l; | |||
} | |||
/** | |||
* [monitored process] delay between checks of freshness of received pings | |||
*/ | |||
long getAutokillPingInterval() { | |||
return autokillPingInterval; | |||
} | |||
/** | |||
* @see #getAutokillPingInterval() | |||
*/ | |||
void setAutokillPingInterval(long l) { | |||
this.autokillPingInterval = l; | |||
} | |||
/** | |||
* [both monitor and monitored process] timeout of graceful termination before hard killing |
@@ -46,16 +46,16 @@ class WatcherThread extends Thread { | |||
@Override | |||
public void run() { | |||
boolean alive = true; | |||
while (alive) { | |||
boolean stopped = false; | |||
while (!stopped) { | |||
try { | |||
processRef.getProcess().waitFor(); | |||
processRef.setTerminated(true); | |||
alive = false; | |||
LoggerFactory.getLogger(getClass()).info(String.format("%s is down", processRef)); | |||
processRef.setStopped(true); | |||
stopped = true; | |||
LoggerFactory.getLogger(getClass()).info(String.format("%s is stopped", processRef)); | |||
// terminate all other processes, but in another thread | |||
monitor.stop(); | |||
monitor.stopAsync(); | |||
} catch (InterruptedException ignored) { | |||
// continue to watch process | |||
} |
@@ -1,34 +0,0 @@ | |||
/* | |||
* 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; | |||
/** | |||
* Used to verify that pings were sent or not. | |||
*/ | |||
public class CallVerifierJmxConnector extends RmiJmxConnector { | |||
boolean askedPing = false; | |||
@Override | |||
public void ping(ProcessRef process) { | |||
askedPing = true; | |||
super.ping(process); | |||
} | |||
} |
@@ -1,42 +0,0 @@ | |||
/* | |||
* 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; | |||
public class ImpossibleToConnectJmxConnector implements JmxConnector { | |||
@Override | |||
public void connect(JavaCommand command, ProcessRef processRef, long timeoutMs) { | |||
throw new IllegalStateException("Test - Impossible to connect to JMX"); | |||
} | |||
@Override | |||
public void ping(ProcessRef process) { | |||
} | |||
@Override | |||
public boolean isReady(ProcessRef process, long timeoutMs) { | |||
return false; | |||
} | |||
@Override | |||
public void terminate(ProcessRef process, long timeoutMs) { | |||
} | |||
} |
@@ -42,7 +42,6 @@ public class JavaCommandTest { | |||
args.setProperty("second_arg", "val2"); | |||
command.setArguments(args); | |||
command.setJmxPort(1234); | |||
command.setClassName("org.sonar.ElasticSearch"); | |||
command.setEnvVariable("JAVA_COMMAND_TEST", "1000"); | |||
File tempDir = temp.newFolder(); | |||
@@ -55,9 +54,9 @@ public class JavaCommandTest { | |||
assertThat(command.toString()).isNotNull(); | |||
assertThat(command.getClasspath()).containsOnly("lib/*.jar", "conf/*.xml"); | |||
assertThat(command.getJavaOptions()).containsOnly("-Xmx128m", "-Djava.io.tmpdir=" + tempDir.getAbsolutePath()); | |||
assertThat(command.getJavaOptions()).containsOnly("-Xmx128m"); | |||
assertThat(command.getWorkDir()).isSameAs(workDir); | |||
assertThat(command.getJmxPort()).isEqualTo(1234); | |||
assertThat(command.getTempDir()).isSameAs(tempDir); | |||
assertThat(command.getClassName()).isEqualTo("org.sonar.ElasticSearch"); | |||
// copy current env variables | |||
@@ -65,18 +64,6 @@ public class JavaCommandTest { | |||
assertThat(command.getEnvVariables().size()).isEqualTo(System.getenv().size() + 1); | |||
} | |||
@Test | |||
public void test_debug_mode() throws Exception { | |||
JavaCommand command = new JavaCommand("es"); | |||
assertThat(command.isDebugMode()).isFalse(); | |||
command.addJavaOption("-Xmx512m"); | |||
assertThat(command.isDebugMode()).isFalse(); | |||
command.addJavaOption("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"); | |||
assertThat(command.isDebugMode()).isTrue(); | |||
} | |||
@Test | |||
public void split_java_options() throws Exception { | |||
JavaCommand command = new JavaCommand("foo"); |
@@ -20,7 +20,6 @@ | |||
package org.sonar.process.monitor; | |||
import org.junit.Test; | |||
import org.sonar.process.NetworkUtils; | |||
import static org.fest.assertions.Assertions.assertThat; | |||
import static org.fest.assertions.Fail.fail; | |||
@@ -29,7 +28,7 @@ public class JavaProcessLauncherTest { | |||
@Test | |||
public void fail_to_launch() throws Exception { | |||
JavaCommand command = new JavaCommand("test").setJmxPort(NetworkUtils.freePort()); | |||
JavaCommand command = new JavaCommand("test"); | |||
JavaProcessLauncher launcher = new JavaProcessLauncher(new Timeouts()); | |||
try { | |||
// command is not correct (missing options), java.lang.ProcessBuilder#start() |
@@ -28,7 +28,7 @@ import org.junit.Test; | |||
import org.junit.rules.TemporaryFolder; | |||
import org.junit.rules.Timeout; | |||
import org.sonar.process.NetworkUtils; | |||
import org.sonar.process.State; | |||
import org.sonar.process.Lifecycle.State; | |||
import org.sonar.process.SystemExit; | |||
import java.io.File; | |||
@@ -159,24 +159,7 @@ public class MonitorTest { | |||
} | |||
@Test | |||
public void fail_to_connect_to_jmx() throws Exception { | |||
Timeouts timeouts = new Timeouts(); | |||
monitor = new Monitor(new JavaProcessLauncher(timeouts), | |||
new ImpossibleToConnectJmxConnector(), timeouts, exit); | |||
HttpProcessClient p1 = new HttpProcessClient("p1"); | |||
try { | |||
monitor.start(Arrays.asList(p1.newCommand())); | |||
fail(); | |||
} catch (Exception e) { | |||
// process was correctly launched, but there was a problem with RMI | |||
assertThat(p1.isReady()).isFalse(); | |||
assertThat(p1.wasGracefullyTerminated()).isFalse(); | |||
} | |||
} | |||
@Test | |||
public void terminate_all_processes_if_monitor_shutdowns() throws Exception { | |||
public void stop_all_processes_if_monitor_shutdowns() throws Exception { | |||
monitor = newDefaultMonitor(); | |||
HttpProcessClient p1 = new HttpProcessClient("p1"), p2 = new HttpProcessClient("p2"); | |||
monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand())); | |||
@@ -192,7 +175,7 @@ public class MonitorTest { | |||
} | |||
@Test | |||
public void terminate_all_processes_if_one_monitored_process_shutdowns() throws Exception { | |||
public void stop_all_processes_if_one_shutdowns() throws Exception { | |||
monitor = newDefaultMonitor(); | |||
HttpProcessClient p1 = new HttpProcessClient("p1"), p2 = new HttpProcessClient("p2"); | |||
monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand())); | |||
@@ -210,9 +193,9 @@ public class MonitorTest { | |||
} | |||
@Test | |||
public void terminate_all_processes_if_one_fails_to_start() throws Exception { | |||
public void stop_all_processes_if_one_fails_to_start() throws Exception { | |||
monitor = newDefaultMonitor(); | |||
HttpProcessClient p1 = new HttpProcessClient("p1"), p2 = new HttpProcessClient("p2", -1, NetworkUtils.freePort()); | |||
HttpProcessClient p1 = new HttpProcessClient("p1"), p2 = new HttpProcessClient("p2", -1); | |||
try { | |||
monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand())); | |||
fail(); | |||
@@ -226,18 +209,8 @@ public class MonitorTest { | |||
} | |||
@Test | |||
public void kill_process_if_fail_to_request_gracefully_termination() throws Exception { | |||
Timeouts timeouts = new Timeouts(); | |||
timeouts.setTerminationTimeout(100L); | |||
monitor = new Monitor(new JavaProcessLauncher(timeouts), | |||
new TerminationFailureRmiConnector(), timeouts, exit); | |||
HttpProcessClient p1 = new HttpProcessClient("p1"); | |||
monitor.start(Arrays.asList(p1.newCommand())); | |||
assertThat(p1.isReady()).isTrue(); | |||
monitor.stop(); | |||
assertThat(p1.isReady()).isFalse(); | |||
public void force_stop_if_too_long() throws Exception { | |||
// TODO | |||
} | |||
@Test | |||
@@ -246,7 +219,6 @@ public class MonitorTest { | |||
JavaCommand command = new JavaCommand("test") | |||
.addClasspath(testJar.getAbsolutePath()) | |||
.setClassName("org.sonar.process.test.Unknown") | |||
.setJmxPort(NetworkUtils.freePort()) | |||
.setTempDir(temp.newFolder()); | |||
try { | |||
@@ -258,63 +230,9 @@ public class MonitorTest { | |||
} | |||
} | |||
@Test | |||
public void terminate_all_if_one_monitored_process_shutdowns() throws Exception { | |||
monitor = newDefaultMonitor(); | |||
HttpProcessClient client = new HttpProcessClient("test"); | |||
// blocks until started | |||
monitor.start(Arrays.asList(client.newCommand())); | |||
assertThat(client.isReady()).isTrue(); | |||
client.kill(); | |||
assertThat(client.isReady()).isFalse(); | |||
// does not wait, already terminated | |||
monitor.awaitTermination(); | |||
// TODO check logs | |||
} | |||
@Test | |||
public void fail_if_jmx_port_is_not_available() throws Exception { | |||
monitor = newDefaultMonitor(); | |||
// c1 and c2 have same JMX port | |||
int jmxPort = NetworkUtils.freePort(); | |||
HttpProcessClient p1 = new HttpProcessClient("p1", NetworkUtils.freePort(), jmxPort); | |||
HttpProcessClient p2 = new HttpProcessClient("p2", NetworkUtils.freePort(), jmxPort); | |||
try { | |||
monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand())); | |||
fail(); | |||
} catch (Exception expected) { | |||
assertThat(p1.wasReady()).isTrue(); | |||
assertThat(p2.wasReady()).isFalse(); | |||
assertThat(p1.isReady()).isFalse(); | |||
assertThat(p2.isReady()).isFalse(); | |||
} | |||
} | |||
@Test | |||
public void disable_autokill_on_jvm_debug_mode() throws Exception { | |||
Timeouts timeouts = new Timeouts(); | |||
timeouts.setMonitorPingInterval(10L); | |||
timeouts.setAutokillPingInterval(10L); | |||
timeouts.setAutokillPingTimeout(10L); | |||
CallVerifierJmxConnector jmxConnector = new CallVerifierJmxConnector(); | |||
monitor = new Monitor(new JavaProcessLauncher(timeouts), jmxConnector, timeouts, exit); | |||
JavaCommand command = newStandardProcessCommand() | |||
.addJavaOption("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=" + NetworkUtils.freePort()); | |||
monitor.start(Arrays.asList(command)); | |||
Thread.sleep(20L); | |||
assertThat(jmxConnector.askedPing).isFalse(); | |||
monitor.stop(); | |||
} | |||
private Monitor newDefaultMonitor() { | |||
Timeouts timeouts = new Timeouts(); | |||
return new Monitor(new JavaProcessLauncher(timeouts), new RmiJmxConnector(), timeouts, exit); | |||
return new Monitor(new JavaProcessLauncher(timeouts), exit); | |||
} | |||
/** | |||
@@ -324,27 +242,24 @@ public class MonitorTest { | |||
private final int httpPort; | |||
private final String commandKey; | |||
private final File tempDir; | |||
private int jmxPort; | |||
private HttpProcessClient(String commandKey) throws IOException { | |||
this(commandKey, NetworkUtils.freePort(), NetworkUtils.freePort()); | |||
this(commandKey, NetworkUtils.freePort()); | |||
} | |||
/** | |||
* Use httpPort=-1 to make server fail to start | |||
*/ | |||
private HttpProcessClient(String commandKey, int httpPort, int jmxPort) throws IOException { | |||
private HttpProcessClient(String commandKey, int httpPort) throws IOException { | |||
this.commandKey = commandKey; | |||
this.tempDir = temp.newFolder(commandKey); | |||
this.httpPort = httpPort; | |||
this.jmxPort = jmxPort; | |||
} | |||
JavaCommand newCommand() throws IOException { | |||
return new JavaCommand(commandKey) | |||
.addClasspath(testJar.getAbsolutePath()) | |||
.setClassName("org.sonar.process.test.HttpProcess") | |||
.setJmxPort(jmxPort) | |||
.setArgument("httpPort", String.valueOf(httpPort)) | |||
.setTempDir(tempDir); | |||
} | |||
@@ -416,7 +331,6 @@ public class MonitorTest { | |||
return new JavaCommand("standard") | |||
.addClasspath(testJar.getAbsolutePath()) | |||
.setClassName("org.sonar.process.test.StandardProcess") | |||
.setJmxPort(NetworkUtils.freePort()) | |||
.setTempDir(temp.newFolder()); | |||
} | |||
@@ -1,71 +0,0 @@ | |||
/* | |||
* 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; | |||
import org.junit.Test; | |||
import org.mockito.invocation.InvocationOnMock; | |||
import org.mockito.stubbing.Answer; | |||
import org.sonar.process.NetworkUtils; | |||
import org.sonar.process.ProcessMXBean; | |||
import static org.fest.assertions.Assertions.assertThat; | |||
import static org.fest.assertions.Fail.fail; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
public class RmiJmxConnectorTest { | |||
@Test | |||
public void fail_to_connect_in_timely_fashion() throws Exception { | |||
RmiJmxConnector connector = new RmiJmxConnector(); | |||
ProcessRef ref = mock(ProcessRef.class); | |||
JavaCommand command = new JavaCommand("foo").setJmxPort(NetworkUtils.freePort()); | |||
try { | |||
connector.connect(command, ref, 0L); | |||
fail(); | |||
} catch (IllegalStateException e) { | |||
// ok | |||
} | |||
} | |||
@Test | |||
public void throw_exception_on_timeout() throws Exception { | |||
RmiJmxConnector connector = new RmiJmxConnector(); | |||
ProcessRef ref = mock(ProcessRef.class); | |||
ProcessMXBean mxBean = mock(ProcessMXBean.class); | |||
connector.register(ref, mxBean); | |||
when(mxBean.isReady()).thenAnswer(new Answer<Boolean>() { | |||
@Override | |||
public Boolean answer(InvocationOnMock invocationOnMock) throws Throwable { | |||
Thread.sleep(Long.MAX_VALUE); | |||
return null; | |||
} | |||
}); | |||
try { | |||
connector.isReady(ref, 5L); | |||
fail(); | |||
} catch (IllegalStateException e) { | |||
assertThat(e).hasMessage("Fail send JMX request"); | |||
} | |||
} | |||
} |
@@ -1,27 +0,0 @@ | |||
/* | |||
* 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; | |||
public class TerminationFailureRmiConnector extends RmiJmxConnector { | |||
@Override | |||
public void terminate(ProcessRef processRef, long timeoutMs) { | |||
throw new IllegalStateException("Test - fail to send termination request"); | |||
} | |||
} |
@@ -28,29 +28,13 @@ public class TimeoutsTest { | |||
@Test | |||
public void test_default_values() throws Exception { | |||
Timeouts timeouts = new Timeouts(); | |||
assertThat(timeouts.getMonitorPingInterval()).isGreaterThan(1000L); | |||
assertThat(timeouts.getAutokillPingInterval()).isGreaterThan(1000L); | |||
assertThat(timeouts.getAutokillPingTimeout()).isGreaterThan(1000L); | |||
assertThat(timeouts.getTerminationTimeout()).isGreaterThan(1000L); | |||
assertThat(timeouts.getJmxConnectionTimeout()).isGreaterThan(1000L); | |||
assertThat(timeouts.getMonitorIsReadyTimeout()).isGreaterThan(1000L); | |||
} | |||
@Test | |||
public void test_values() throws Exception { | |||
Timeouts timeouts = new Timeouts(); | |||
timeouts.setAutokillPingInterval(1L); | |||
timeouts.setAutokillPingTimeout(2L); | |||
timeouts.setTerminationTimeout(3L); | |||
timeouts.setJmxConnectionTimeout(4L); | |||
timeouts.setMonitorPingInterval(5L); | |||
timeouts.setMonitorIsReadyTimeout(6L); | |||
assertThat(timeouts.getAutokillPingInterval()).isEqualTo(1L); | |||
assertThat(timeouts.getAutokillPingTimeout()).isEqualTo(2L); | |||
assertThat(timeouts.getTerminationTimeout()).isEqualTo(3L); | |||
assertThat(timeouts.getJmxConnectionTimeout()).isEqualTo(4L); | |||
assertThat(timeouts.getMonitorPingInterval()).isEqualTo(5L); | |||
assertThat(timeouts.getMonitorIsReadyTimeout()).isEqualTo(6L); | |||
} | |||
} |
@@ -34,6 +34,6 @@ public class WatcherThreadTest { | |||
WatcherThread watcher = new WatcherThread(ref, monitor); | |||
watcher.start(); | |||
watcher.join(); | |||
verify(monitor).stop(); | |||
verify(monitor).stopAsync(); | |||
} | |||
} |
@@ -1,81 +0,0 @@ | |||
/* | |||
* 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 javax.management.MBeanServer; | |||
import javax.management.MalformedObjectNameException; | |||
import javax.management.ObjectName; | |||
import javax.management.remote.JMXServiceURL; | |||
import java.lang.management.ManagementFactory; | |||
import java.net.Inet6Address; | |||
import java.net.InetAddress; | |||
import java.net.MalformedURLException; | |||
public class JmxUtils { | |||
private JmxUtils() { | |||
// only static stuff | |||
} | |||
public static final String DOMAIN = "org.sonar"; | |||
public static final String NAME_PROPERTY = "name"; | |||
public static final String WEB_SERVER_NAME = "web"; | |||
public static final String SEARCH_SERVER_NAME = "search"; | |||
public static ObjectName objectName(String name) { | |||
try { | |||
return new ObjectName(DOMAIN, NAME_PROPERTY, name); | |||
} catch (MalformedObjectNameException e) { | |||
throw new IllegalStateException("Cannot create ObjectName for " + name, e); | |||
} | |||
} | |||
public static void registerMBean(Object mbean, String name) { | |||
try { | |||
MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer(); | |||
ObjectName oName = objectName(name); | |||
// Check if already registered in JVM (might run multiple instance in JUnits) | |||
if (mbeanServer.isRegistered(oName)) { | |||
mbeanServer.unregisterMBean(oName); | |||
} | |||
mbeanServer.registerMBean(mbean, oName); | |||
} catch (RuntimeException re) { | |||
throw re; | |||
} catch (Exception e) { | |||
throw new IllegalStateException("Fail to register JMX MBean named " + name, e); | |||
} | |||
} | |||
public static JMXServiceURL serviceUrl(InetAddress host, int port) { | |||
String address = host.getHostAddress(); | |||
if (host instanceof Inet6Address) { | |||
// See http://docs.oracle.com/javase/7/docs/api/javax/management/remote/JMXServiceURL.html | |||
// "The host is a host name, an IPv4 numeric host address, or an IPv6 numeric address enclosed in square brackets." | |||
address = String.format("[%s]", address); | |||
} | |||
try { | |||
return new JMXServiceURL("rmi", address, port, String.format("/jndi/rmi://%s:%d/jmxrmi", address, port)); | |||
} catch (MalformedURLException e) { | |||
throw new IllegalStateException("JMX url does not look well formed", e); | |||
} | |||
} | |||
} |
@@ -21,6 +21,10 @@ package org.sonar.process; | |||
public class Lifecycle { | |||
public static enum State { | |||
INIT, STARTING, STARTED, STOPPING, STOPPED | |||
} | |||
private State state = State.INIT; | |||
public State getState() { | |||
@@ -35,7 +39,6 @@ public class Lifecycle { | |||
return false; | |||
} | |||
@Override | |||
public boolean equals(Object o) { | |||
if (this == o) { | |||
@@ -44,7 +47,6 @@ public class Lifecycle { | |||
if (o == null || getClass() != o.getClass()) { | |||
return false; | |||
} | |||
Lifecycle lifecycle = (Lifecycle) o; | |||
return state == lifecycle.state; | |||
} |
@@ -19,6 +19,9 @@ | |||
*/ | |||
package org.sonar.process; | |||
/** | |||
* Unchecked exception that is displayed without stacktrace | |||
*/ | |||
public class MessageException extends RuntimeException { | |||
public MessageException(String message) { | |||
super(message); |
@@ -19,7 +19,7 @@ | |||
*/ | |||
package org.sonar.process; | |||
public interface MonitoredProcess extends Terminable { | |||
public interface Monitored { | |||
/** | |||
* Starts process. No need to block until fully started and operational. | |||
@@ -36,6 +36,7 @@ public interface MonitoredProcess extends Terminable { | |||
/** | |||
* Blocks until the process is terminated | |||
*/ | |||
void awaitTermination(); | |||
void awaitStop(); | |||
void stop(); | |||
} |
@@ -19,6 +19,8 @@ | |||
*/ | |||
package org.sonar.process; | |||
import org.apache.commons.io.IOUtils; | |||
import java.net.ServerSocket; | |||
public class NetworkUtils { | |||
@@ -27,14 +29,18 @@ public class NetworkUtils { | |||
// only static stuff | |||
} | |||
/** | |||
* Get an unused port | |||
*/ | |||
public static int freePort() { | |||
ServerSocket s = null; | |||
try { | |||
ServerSocket s = new ServerSocket(0); | |||
int port = s.getLocalPort(); | |||
s.close(); | |||
return port; | |||
s = new ServerSocket(0); | |||
return s.getLocalPort(); | |||
} catch (Exception e) { | |||
throw new IllegalStateException("Can not find an open network port", e); | |||
} finally { | |||
IOUtils.closeQuietly(s); | |||
} | |||
} | |||
} |
@@ -21,24 +21,19 @@ package org.sonar.process; | |||
import org.slf4j.LoggerFactory; | |||
import java.util.concurrent.Executors; | |||
import java.util.concurrent.ScheduledExecutorService; | |||
import java.util.concurrent.TimeUnit; | |||
public class ProcessEntryPoint implements ProcessMXBean { | |||
public class ProcessEntryPoint { | |||
public static final String PROPERTY_PROCESS_KEY = "process.key"; | |||
public static final String PROPERTY_AUTOKILL_DISABLED = "process.autokill.disabled"; | |||
public static final String PROPERTY_AUTOKILL_PING_TIMEOUT = "process.autokill.pingTimeout"; | |||
public static final String PROPERTY_AUTOKILL_PING_INTERVAL = "process.autokill.pingInterval"; | |||
public static final String PROPERTY_TERMINATION_TIMEOUT = "process.terminationTimeout"; | |||
public static final String PROPERTY_STATUS_PATH = "process.statusPath"; | |||
private final Props props; | |||
private final Lifecycle lifecycle = new Lifecycle(); | |||
private volatile MonitoredProcess monitoredProcess; | |||
private volatile long lastPing = 0L; | |||
private final SharedStatus sharedStatus; | |||
private volatile Monitored monitored; | |||
private volatile StopperThread stopperThread; | |||
private final SystemExit exit; | |||
private Thread shutdownHook = new Thread(new Runnable() { | |||
@Override | |||
public void run() { | |||
@@ -47,9 +42,10 @@ public class ProcessEntryPoint implements ProcessMXBean { | |||
} | |||
}); | |||
ProcessEntryPoint(Props props, SystemExit exit) { | |||
ProcessEntryPoint(Props props, SystemExit exit, SharedStatus sharedStatus) { | |||
this.props = props; | |||
this.exit = exit; | |||
this.sharedStatus = sharedStatus; | |||
} | |||
public Props getProps() { | |||
@@ -59,29 +55,25 @@ public class ProcessEntryPoint implements ProcessMXBean { | |||
/** | |||
* Launch process and waits until it's down | |||
*/ | |||
public void launch(MonitoredProcess mp) { | |||
if (!lifecycle.tryToMoveTo(State.STARTING)) { | |||
public void launch(Monitored mp) { | |||
if (!lifecycle.tryToMoveTo(Lifecycle.State.STARTING)) { | |||
throw new IllegalStateException("Already started"); | |||
} | |||
monitoredProcess = mp; | |||
// TODO check if these properties are available in System Info | |||
JmxUtils.registerMBean(this, props.nonNullValue(PROPERTY_PROCESS_KEY)); | |||
Runtime.getRuntime().addShutdownHook(shutdownHook); | |||
if (!props.valueAsBoolean(PROPERTY_AUTOKILL_DISABLED, false)) { | |||
// mainly for Java Debugger | |||
scheduleAutokill(); | |||
} | |||
monitored = mp; | |||
try { | |||
monitoredProcess.start(); | |||
Runtime.getRuntime().addShutdownHook(shutdownHook); | |||
monitored.start(); | |||
boolean ready = false; | |||
while (!ready) { | |||
ready = monitoredProcess.isReady(); | |||
ready = monitored.isReady(); | |||
Thread.sleep(200L); | |||
} | |||
if (lifecycle.tryToMoveTo(State.STARTED)) { | |||
monitoredProcess.awaitTermination(); | |||
sharedStatus.setReady(); | |||
if (lifecycle.tryToMoveTo(Lifecycle.State.STARTED)) { | |||
monitored.awaitStop(); | |||
} | |||
} catch (Exception e) { | |||
LoggerFactory.getLogger(getClass()).warn("Fail to start", e); | |||
@@ -91,56 +83,30 @@ public class ProcessEntryPoint implements ProcessMXBean { | |||
} | |||
} | |||
@Override | |||
public boolean isReady() { | |||
return lifecycle.getState() == State.STARTED; | |||
} | |||
@Override | |||
public void ping() { | |||
lastPing = System.currentTimeMillis(); | |||
boolean isStarted() { | |||
return lifecycle.getState() == Lifecycle.State.STARTED; | |||
} | |||
/** | |||
* Blocks until stopped in a timely fashion (see {@link org.sonar.process.StopperThread}) | |||
*/ | |||
@Override | |||
public void terminate() { | |||
if (lifecycle.tryToMoveTo(State.STOPPING)) { | |||
stopperThread = new StopperThread(monitoredProcess, Long.parseLong(props.nonNullValue(PROPERTY_TERMINATION_TIMEOUT))); | |||
void terminate() { | |||
if (lifecycle.tryToMoveTo(Lifecycle.State.STOPPING)) { | |||
stopperThread = new StopperThread(monitored, sharedStatus, Long.parseLong(props.nonNullValue(PROPERTY_TERMINATION_TIMEOUT))); | |||
stopperThread.start(); | |||
} | |||
try { | |||
// stopperThread is not null for sure | |||
// join() does nothing if thread already finished | |||
stopperThread.join(); | |||
lifecycle.tryToMoveTo(State.STOPPED); | |||
lifecycle.tryToMoveTo(Lifecycle.State.STOPPED); | |||
} catch (InterruptedException e) { | |||
// nothing to do, the process is going to be exited | |||
} | |||
exit.exit(0); | |||
} | |||
private void scheduleAutokill() { | |||
final long autokillPingTimeoutMs = props.valueAsInt(PROPERTY_AUTOKILL_PING_TIMEOUT); | |||
long autokillPingIntervalMs = props.valueAsInt(PROPERTY_AUTOKILL_PING_INTERVAL); | |||
Runnable autokiller = new Runnable() { | |||
@Override | |||
public void run() { | |||
long time = System.currentTimeMillis(); | |||
if (time - lastPing > autokillPingTimeoutMs) { | |||
LoggerFactory.getLogger(getClass()).info(String.format( | |||
"Did not receive any ping during %d seconds. Shutting down.", autokillPingTimeoutMs / 1000)); | |||
terminate(); | |||
} | |||
} | |||
}; | |||
lastPing = System.currentTimeMillis(); | |||
ScheduledExecutorService monitor = Executors.newScheduledThreadPool(1); | |||
monitor.scheduleWithFixedDelay(autokiller, autokillPingIntervalMs, autokillPingIntervalMs, TimeUnit.MILLISECONDS); | |||
} | |||
State getState() { | |||
Lifecycle.State getState() { | |||
return lifecycle.getState(); | |||
} | |||
@@ -150,6 +116,6 @@ public class ProcessEntryPoint implements ProcessMXBean { | |||
public static ProcessEntryPoint createForArguments(String[] args) { | |||
Props props = ConfigurationUtils.loadPropsFromCommandLineArgs(args); | |||
return new ProcessEntryPoint(props, new SystemExit()); | |||
return new ProcessEntryPoint(props, new SystemExit(), new SharedStatus(props.nonNullValueAsFile(PROPERTY_STATUS_PATH))); | |||
} | |||
} |
@@ -41,7 +41,6 @@ public class ProcessLogging { | |||
// StatusPrinter will handle this | |||
} | |||
StatusPrinter.printInCaseOfErrorsOrWarnings(context); | |||
} | |||
/** |
@@ -1,28 +0,0 @@ | |||
/* | |||
* 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; | |||
public interface ProcessMXBean extends Terminable { | |||
boolean isReady(); | |||
void ping(); | |||
} |
@@ -47,28 +47,25 @@ public class ProcessUtils { | |||
} | |||
/** | |||
* Destroys process (equivalent to kill -9) if alive | |||
* @return true if the process was destroyed, false if process is null or already destroyed. | |||
* Send kill signal to stop process. Shutdown hooks are executed. It's the equivalent of SIGTERM on Linux. | |||
* Correctly tested on Java 6 and 7 on both Mac/MSWindows | |||
* @return true if the signal is sent, false if process is already down | |||
*/ | |||
public static boolean destroyQuietly(@Nullable Process process) { | |||
boolean destroyed = false; | |||
public static boolean sendKillSignal(@Nullable Process process) { | |||
boolean sentSignal = false; | |||
if (isAlive(process)) { | |||
try { | |||
process.destroy(); | |||
while (isAlive(process)) { | |||
// destroy() sends the signal, it does not wait for the process to be down | |||
Thread.sleep(100L); | |||
} | |||
destroyed = true; | |||
sentSignal = true; | |||
} catch (Exception e) { | |||
LoggerFactory.getLogger(ProcessUtils.class).error("Fail to destroy " + process); | |||
LoggerFactory.getLogger(ProcessUtils.class).error("Fail to kill " + process); | |||
} | |||
} | |||
return destroyed; | |||
return sentSignal; | |||
} | |||
public static void closeStreams(@Nullable Process process) { | |||
if (process != null) { | |||
if (process!=null) { | |||
IOUtils.closeQuietly(process.getInputStream()); | |||
IOUtils.closeQuietly(process.getOutputStream()); | |||
IOUtils.closeQuietly(process.getErrorStream()); |
@@ -77,7 +77,7 @@ public class Props { | |||
public File nonNullValueAsFile(String key) { | |||
String s = value(key); | |||
if (s == null) { | |||
throw new IllegalArgumentException("Property " + key + " is missing"); | |||
throw new IllegalArgumentException("Property " + key + " is not set"); | |||
} | |||
return new File(s); | |||
} |
@@ -20,40 +20,44 @@ | |||
package org.sonar.process; | |||
import org.apache.commons.io.FileUtils; | |||
import org.junit.After; | |||
import org.junit.Before; | |||
import org.junit.Rule; | |||
import org.junit.rules.TemporaryFolder; | |||
import java.io.File; | |||
import java.io.IOException; | |||
import java.net.ServerSocket; | |||
public abstract class BaseProcessTest { | |||
public class SharedStatus { | |||
@Rule | |||
public TemporaryFolder temp = new TemporaryFolder(); | |||
private final File file; | |||
public static final String DUMMY_OK_APP = "org.sonar.application.DummyOkProcess"; | |||
int freePort; | |||
File dummyAppJar; | |||
Process proc; | |||
public SharedStatus(File file) { | |||
this.file = file; | |||
} | |||
@Before | |||
public void setup() throws IOException { | |||
ServerSocket socket = new ServerSocket(0); | |||
freePort = socket.getLocalPort(); | |||
socket.close(); | |||
/** | |||
* Executed by monitor - remove existing shared file before starting child process | |||
*/ | |||
public void prepare() { | |||
if (file.exists()) { | |||
if (!file.delete()) { | |||
throw new MessageException(String.format( | |||
"Fail to delete file %s. Please check that no SonarQube process is alive", file)); | |||
} | |||
} | |||
} | |||
dummyAppJar = FileUtils.toFile(getClass().getResource("/sonar-dummy-app.jar")); | |||
public boolean wasStartedAfter(long launchedAt) { | |||
// File#lastModified() can have second precision on some OS | |||
return file.exists() && file.lastModified() / 1000 >= launchedAt / 1000; | |||
} | |||
@After | |||
public void tearDown() { | |||
if (proc != null) { | |||
ProcessUtils.destroyQuietly(proc); | |||
public void setReady() { | |||
try { | |||
FileUtils.touch(file); | |||
} catch (IOException e) { | |||
throw new IllegalStateException("Fail to create file " + file, e); | |||
} | |||
} | |||
public void setStopped() { | |||
FileUtils.deleteQuietly(file); | |||
} | |||
} |
@@ -1,26 +0,0 @@ | |||
/* | |||
* 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; | |||
public enum State { | |||
INIT, STARTING, STARTED, STOPPING, STOPPED | |||
} |
@@ -27,17 +27,19 @@ import java.util.concurrent.Future; | |||
import java.util.concurrent.TimeUnit; | |||
/** | |||
* Gracefully stops process, but exits JVM if too long | |||
* Gracefully stops process in a timely fashion | |||
*/ | |||
class StopperThread extends Thread { | |||
private final Terminable terminable; | |||
private final Monitored monitored; | |||
private final long terminationTimeout; | |||
private final SharedStatus sharedStatus; | |||
StopperThread(Terminable terminable, long terminationTimeout) { | |||
StopperThread(Monitored monitored, SharedStatus sharedStatus, long terminationTimeout) { | |||
super("Stopper"); | |||
this.terminable = terminable; | |||
this.monitored = monitored; | |||
this.terminationTimeout = terminationTimeout; | |||
this.sharedStatus = sharedStatus; | |||
} | |||
@Override | |||
@@ -46,15 +48,15 @@ class StopperThread extends Thread { | |||
Future future = executor.submit(new Runnable() { | |||
@Override | |||
public void run() { | |||
terminable.terminate(); | |||
monitored.stop(); | |||
} | |||
}); | |||
try { | |||
future.get(terminationTimeout, TimeUnit.MILLISECONDS); | |||
} catch (Exception e) { | |||
LoggerFactory.getLogger(getClass()).error("Can not terminate in " + terminationTimeout + "ms", e); | |||
} finally { | |||
executor.shutdownNow(); | |||
LoggerFactory.getLogger(getClass()).error(String.format("Can not stop in %dms", terminationTimeout), e); | |||
} | |||
executor.shutdownNow(); | |||
sharedStatus.setStopped(); | |||
} | |||
} |
@@ -1,28 +0,0 @@ | |||
/* | |||
* 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; | |||
/** | |||
* This term "terminate" is used in order to not conflict with {@link Thread#stop()}. | |||
*/ | |||
public interface Terminable { | |||
void terminate(); | |||
} |
@@ -1,127 +0,0 @@ | |||
/* | |||
* 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.junit.Test; | |||
import javax.management.MBeanServer; | |||
import javax.management.ObjectName; | |||
import javax.management.remote.JMXServiceURL; | |||
import java.lang.management.ManagementFactory; | |||
import java.net.Inet4Address; | |||
import java.net.Inet6Address; | |||
import java.net.InetAddress; | |||
import java.net.NetworkInterface; | |||
import java.net.SocketException; | |||
import java.util.Enumeration; | |||
import static org.fest.assertions.Assertions.assertThat; | |||
import static org.fest.assertions.Fail.fail; | |||
public class JmxUtilsTest { | |||
class MyBean implements ProcessMXBean { | |||
@Override | |||
public void terminate() { | |||
} | |||
@Override | |||
public void ping() { | |||
} | |||
@Override | |||
public boolean isReady() { | |||
return true; | |||
} | |||
} | |||
@Test | |||
public void construct_jmx_objectName() throws Exception { | |||
MyBean mxBean = new MyBean(); | |||
ObjectName objectName = JmxUtils.objectName(mxBean.getClass().getSimpleName()); | |||
assertThat(objectName).isNotNull(); | |||
assertThat(objectName.getDomain()).isEqualTo(JmxUtils.DOMAIN); | |||
assertThat(objectName.getKeyProperty(JmxUtils.NAME_PROPERTY)).isEqualTo(mxBean.getClass().getSimpleName()); | |||
} | |||
@Test | |||
public void fail_jmx_objectName() throws Exception { | |||
try { | |||
JmxUtils.objectName(":"); | |||
fail(); | |||
} catch (Exception e) { | |||
assertThat(e.getMessage()).isEqualTo("Cannot create ObjectName for :"); | |||
} | |||
} | |||
@Test | |||
public void register_mbean() throws Exception { | |||
// 0 Get mbServer and create out test MXBean | |||
MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer(); | |||
MyBean mxBean = new MyBean(); | |||
ObjectName objectName = JmxUtils.objectName(mxBean.getClass().getSimpleName()); | |||
// 1 assert that mxBean gets registered | |||
assertThat(mbeanServer.isRegistered(objectName)).isFalse(); | |||
JmxUtils.registerMBean(mxBean, mxBean.getClass().getSimpleName()); | |||
assertThat(mbeanServer.isRegistered(objectName)).isTrue(); | |||
try { | |||
JmxUtils.registerMBean(new Object(), ""); | |||
fail(); | |||
} catch (IllegalStateException e) { | |||
// ok | |||
} | |||
} | |||
@Test | |||
public void serviceUrl_ipv4() throws Exception { | |||
JMXServiceURL url = JmxUtils.serviceUrl(ip(Inet4Address.class), 1234); | |||
assertThat(url).isNotNull(); | |||
assertThat(url.getPort()).isEqualTo(1234); | |||
} | |||
@Test | |||
public void serviceUrl_ipv6() throws Exception { | |||
JMXServiceURL url = JmxUtils.serviceUrl(ip(Inet6Address.class), 1234); | |||
assertThat(url).isNotNull(); | |||
assertThat(url.getPort()).isEqualTo(1234); | |||
} | |||
private static InetAddress ip(Class inetAddressClass) throws SocketException { | |||
Enumeration<NetworkInterface> ifaces = NetworkInterface.getNetworkInterfaces(); | |||
while (ifaces.hasMoreElements()) { | |||
NetworkInterface iface = ifaces.nextElement(); | |||
Enumeration<InetAddress> addresses = iface.getInetAddresses(); | |||
while (addresses.hasMoreElements()) { | |||
InetAddress addr = addresses.nextElement(); | |||
if (addr.getClass().isAssignableFrom(inetAddressClass)) { | |||
return addr; | |||
} | |||
} | |||
} | |||
throw new IllegalStateException("no ipv4 address"); | |||
} | |||
} |
@@ -22,6 +22,7 @@ package org.sonar.process; | |||
import org.junit.Test; | |||
import static org.fest.assertions.Assertions.assertThat; | |||
import static org.sonar.process.Lifecycle.State; | |||
public class LifecycleTest { | |||
@@ -39,4 +40,16 @@ public class LifecycleTest { | |||
stopping.tryToMoveTo(State.STOPPING); | |||
assertThat(stopping).isNotEqualTo(init); | |||
} | |||
@Test | |||
public void try_to_move() throws Exception { | |||
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.STARTING)).isFalse(); | |||
assertThat(lifecycle.getState()).isEqualTo(State.STARTED); | |||
} | |||
} |
@@ -27,11 +27,10 @@ import static org.fest.assertions.Assertions.assertThat; | |||
public class NetworkUtilsTest { | |||
@Test | |||
public void find_free_port() throws Exception { | |||
int port = NetworkUtils.freePort(); | |||
assertThat(port).isGreaterThan(1024); | |||
assertThat(port).isGreaterThan(0); | |||
} | |||
@Test | |||
@@ -58,4 +57,4 @@ public class NetworkUtilsTest { | |||
assertThat(port1).isNotSameAs(port2); | |||
} | |||
} | |||
} |
@@ -32,6 +32,7 @@ import java.util.Properties; | |||
import static org.fest.assertions.Assertions.assertThat; | |||
import static org.fest.assertions.Fail.fail; | |||
import static org.mockito.Mockito.mock; | |||
import static org.sonar.process.Lifecycle.State; | |||
public class ProcessEntryPointTest { | |||
@@ -49,47 +50,29 @@ public class ProcessEntryPointTest { | |||
@Test | |||
public void load_properties_from_file() throws Exception { | |||
File propsFile = temp.newFile(); | |||
FileUtils.write(propsFile, "sonar.foo=bar"); | |||
FileUtils.write(propsFile, "sonar.foo=bar\nprocess.key=web\nprocess.statusPath=status.temp"); | |||
ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(new String[]{propsFile.getAbsolutePath()}); | |||
ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(new String[] {propsFile.getAbsolutePath()}); | |||
assertThat(entryPoint.getProps().value("sonar.foo")).isEqualTo("bar"); | |||
assertThat(entryPoint.getProps().value("process.key")).isEqualTo("web"); | |||
} | |||
@Test | |||
public void test_initial_state() throws Exception { | |||
Props props = new Props(new Properties()); | |||
ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit); | |||
ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit, mock(SharedStatus.class)); | |||
assertThat(entryPoint.getProps()).isSameAs(props); | |||
assertThat(entryPoint.isReady()).isFalse(); | |||
assertThat(entryPoint.isStarted()).isFalse(); | |||
assertThat(entryPoint.getState()).isEqualTo(State.INIT); | |||
// do not fail | |||
entryPoint.ping(); | |||
} | |||
@Test | |||
public void fail_to_launch_if_missing_monitor_properties() throws Exception { | |||
Props props = new Props(new Properties()); | |||
ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit); | |||
StandardProcess process = new StandardProcess(); | |||
try { | |||
entryPoint.launch(process); | |||
fail(); | |||
} catch (IllegalArgumentException e) { | |||
assertThat(e).hasMessage("Missing property: process.key"); | |||
assertThat(process.getState()).isEqualTo(State.INIT); | |||
} | |||
} | |||
@Test | |||
public void fail_to_launch_multiple_times() throws Exception { | |||
Props props = new Props(new Properties()); | |||
props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "test"); | |||
props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_DISABLED, "true"); | |||
props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000"); | |||
ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit); | |||
ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit, mock(SharedStatus.class)); | |||
entryPoint.launch(new NoopProcess()); | |||
try { | |||
@@ -104,9 +87,8 @@ public class ProcessEntryPointTest { | |||
public void launch_then_request_graceful_termination() throws Exception { | |||
Props props = new Props(new Properties()); | |||
props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "test"); | |||
props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_DISABLED, "true"); | |||
props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000"); | |||
final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit); | |||
final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit, mock(SharedStatus.class)); | |||
final StandardProcess process = new StandardProcess(); | |||
Thread runner = new Thread() { | |||
@@ -129,28 +111,12 @@ public class ProcessEntryPointTest { | |||
assertThat(process.getState()).isEqualTo(State.STOPPED); | |||
} | |||
@Test | |||
public void autokill_if_no_pings() throws Exception { | |||
Props props = new Props(new Properties()); | |||
props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "test"); | |||
props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000"); | |||
props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_PING_INTERVAL, "5"); | |||
props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_PING_TIMEOUT, "1"); | |||
final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit); | |||
final StandardProcess process = new StandardProcess(); | |||
entryPoint.launch(process); | |||
assertThat(process.getState()).isEqualTo(State.STOPPED); | |||
} | |||
@Test | |||
public void terminate_if_unexpected_shutdown() throws Exception { | |||
Props props = new Props(new Properties()); | |||
props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "foo"); | |||
props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_DISABLED, "true"); | |||
props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000"); | |||
final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit); | |||
final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit, mock(SharedStatus.class)); | |||
final StandardProcess process = new StandardProcess(); | |||
Thread runner = new Thread() { | |||
@@ -177,16 +143,15 @@ public class ProcessEntryPointTest { | |||
public void terminate_if_startup_error() throws Exception { | |||
Props props = new Props(new Properties()); | |||
props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "foo"); | |||
props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_DISABLED, "true"); | |||
props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000"); | |||
final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit); | |||
final MonitoredProcess process = new StartupErrorProcess(); | |||
final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit, mock(SharedStatus.class)); | |||
final Monitored process = new StartupErrorProcess(); | |||
entryPoint.launch(process); | |||
assertThat(entryPoint.getState()).isEqualTo(State.STOPPED); | |||
} | |||
private static class NoopProcess implements MonitoredProcess { | |||
private static class NoopProcess implements Monitored { | |||
@Override | |||
public void start() { | |||
@@ -199,17 +164,17 @@ public class ProcessEntryPointTest { | |||
} | |||
@Override | |||
public void awaitTermination() { | |||
public void awaitStop() { | |||
} | |||
@Override | |||
public void terminate() { | |||
public void stop() { | |||
} | |||
} | |||
private static class StartupErrorProcess implements MonitoredProcess { | |||
private static class StartupErrorProcess implements Monitored { | |||
@Override | |||
public void start() { | |||
@@ -222,12 +187,12 @@ public class ProcessEntryPointTest { | |||
} | |||
@Override | |||
public void awaitTermination() { | |||
public void awaitStop() { | |||
} | |||
@Override | |||
public void terminate() { | |||
public void stop() { | |||
} | |||
} |
@@ -1,28 +0,0 @@ | |||
/* | |||
* 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.junit.Test; | |||
import static org.fest.assertions.Assertions.assertThat; | |||
public class ProcessUtilsTest { | |||
} |
@@ -0,0 +1,101 @@ | |||
/* | |||
* 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.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.TemporaryFolder; | |||
import java.io.File; | |||
import static org.fest.assertions.Assertions.assertThat; | |||
import static org.fest.assertions.Fail.fail; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
public class SharedStatusTest { | |||
@Rule | |||
public TemporaryFolder temp = new TemporaryFolder(); | |||
@Test | |||
public void prepare() throws Exception { | |||
File file = temp.newFile(); | |||
assertThat(file).exists(); | |||
SharedStatus sharedStatus = new SharedStatus(file); | |||
sharedStatus.prepare(); | |||
assertThat(file).doesNotExist(); | |||
} | |||
@Test | |||
public void fail_to_prepare_if_file_is_locked() throws Exception { | |||
File file = mock(File.class); | |||
when(file.exists()).thenReturn(true); | |||
when(file.delete()).thenReturn(false); | |||
SharedStatus sharedStatus = new SharedStatus(file); | |||
try { | |||
sharedStatus.prepare(); | |||
fail(); | |||
} catch (MessageException e) { | |||
// ok | |||
} | |||
} | |||
@Test | |||
public void create_file_when_ready_then_delete_when_stopped() throws Exception { | |||
File file = new File(temp.newFolder(), "foo.txt"); | |||
assertThat(file).doesNotExist(); | |||
SharedStatus sharedStatus = new SharedStatus(file); | |||
sharedStatus.setReady(); | |||
assertThat(file).exists(); | |||
sharedStatus.setStopped(); | |||
assertThat(file).doesNotExist(); | |||
} | |||
@Test | |||
public void was_started_after() throws Exception { | |||
File file = mock(File.class); | |||
SharedStatus sharedStatus = new SharedStatus(file); | |||
// does not exist | |||
when(file.exists()).thenReturn(false); | |||
when(file.lastModified()).thenReturn(123456L); | |||
assertThat(sharedStatus.wasStartedAfter(122000L)).isFalse(); | |||
// file created before | |||
when(file.exists()).thenReturn(true); | |||
when(file.lastModified()).thenReturn(123456L); | |||
assertThat(sharedStatus.wasStartedAfter(124000L)).isFalse(); | |||
// file created after | |||
when(file.exists()).thenReturn(true); | |||
when(file.lastModified()).thenReturn(123456L); | |||
assertThat(sharedStatus.wasStartedAfter(123123L)).isTrue(); | |||
// file created after, but can be truncated to second on some OS | |||
when(file.exists()).thenReturn(true); | |||
when(file.lastModified()).thenReturn(123000L); | |||
assertThat(sharedStatus.wasStartedAfter(123456L)).isTrue(); | |||
} | |||
} |
@@ -0,0 +1,79 @@ | |||
/* | |||
* 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.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.TemporaryFolder; | |||
import org.mockito.invocation.InvocationOnMock; | |||
import org.mockito.stubbing.Answer; | |||
import java.io.File; | |||
import static org.fest.assertions.Assertions.assertThat; | |||
import static org.mockito.Mockito.doAnswer; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.verify; | |||
public class StopperThreadTest { | |||
@Rule | |||
public TemporaryFolder temp = new TemporaryFolder(); | |||
@Test(timeout = 3000L) | |||
public void stop_in_a_timely_fashion() throws Exception { | |||
File file = temp.newFile(); | |||
SharedStatus sharedStatus = new SharedStatus(file); | |||
assertThat(file).exists(); | |||
Monitored monitored = mock(Monitored.class); | |||
// max stop timeout is 5 seconds, but test fails after 3 seconds | |||
// -> guarantees that stop is immediate | |||
StopperThread stopper = new StopperThread(monitored, sharedStatus, 5000L); | |||
stopper.start(); | |||
stopper.join(); | |||
verify(monitored).stop(); | |||
assertThat(file).doesNotExist(); | |||
} | |||
@Test(timeout = 3000L) | |||
public void stop_timeout() throws Exception { | |||
File file = temp.newFile(); | |||
SharedStatus sharedStatus = new SharedStatus(file); | |||
assertThat(file).exists(); | |||
Monitored monitored = mock(Monitored.class); | |||
doAnswer(new Answer() { | |||
@Override | |||
public Object answer(InvocationOnMock invocationOnMock) throws Throwable { | |||
Thread.sleep(10000L); | |||
return null; | |||
} | |||
}).when(monitored).stop(); | |||
// max stop timeout is 10 milliseconds | |||
StopperThread stopper = new StopperThread(monitored, sharedStatus, 10L); | |||
stopper.start(); | |||
stopper.join(); | |||
verify(monitored).stop(); | |||
assertThat(file).doesNotExist(); | |||
} | |||
} |
@@ -24,7 +24,7 @@ import org.eclipse.jetty.server.Request; | |||
import org.eclipse.jetty.server.Server; | |||
import org.eclipse.jetty.server.handler.AbstractHandler; | |||
import org.eclipse.jetty.server.handler.ContextHandler; | |||
import org.sonar.process.MonitoredProcess; | |||
import org.sonar.process.Monitored; | |||
import org.sonar.process.ProcessEntryPoint; | |||
import javax.servlet.ServletException; | |||
@@ -39,7 +39,7 @@ import java.io.IOException; | |||
* It also pushes status to temp files, so test can verify what was really done (when server went ready state and | |||
* if it was gracefully terminated) | |||
*/ | |||
public class HttpProcess implements MonitoredProcess { | |||
public class HttpProcess implements Monitored { | |||
private final Server server; | |||
private boolean ready = false; | |||
@@ -78,18 +78,22 @@ public class HttpProcess implements MonitoredProcess { | |||
@Override | |||
public boolean isReady() { | |||
System.out.println("received isReady()"); | |||
if (ready) { | |||
return true; | |||
} | |||
System.out.println("checking server.isStarted()"); | |||
if (server.isStarted()) { | |||
System.out.println("moving to ready"); | |||
ready = true; | |||
writeTimeToFile("readyAt"); | |||
} | |||
return false; | |||
System.out.println("ready: " + ready); | |||
return ready; | |||
} | |||
@Override | |||
public void awaitTermination() { | |||
public void awaitStop() { | |||
try { | |||
server.join(); | |||
} catch (InterruptedException ignore) { | |||
@@ -98,9 +102,10 @@ public class HttpProcess implements MonitoredProcess { | |||
} | |||
@Override | |||
public void terminate() { | |||
public void stop() { | |||
try { | |||
if (!server.isStopped()) { | |||
System.out.println("HttpProcess stopping"); | |||
server.stop(); | |||
writeTimeToFile("terminatedAt"); | |||
} |
@@ -0,0 +1,85 @@ | |||
/* | |||
* 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.test; | |||
import org.sonar.process.Monitored; | |||
import org.sonar.process.ProcessEntryPoint; | |||
import org.sonar.process.Lifecycle.State; | |||
public class InfiniteTerminationProcess implements Monitored { | |||
private State state = State.INIT; | |||
private final Thread daemon = new Thread() { | |||
@Override | |||
public void run() { | |||
try { | |||
while (true) { | |||
Thread.sleep(100L); | |||
} | |||
} catch (InterruptedException e) { | |||
// return | |||
} | |||
} | |||
}; | |||
/** | |||
* Blocks until started() | |||
*/ | |||
@Override | |||
public void start() { | |||
state = State.STARTING; | |||
daemon.start(); | |||
state = State.STARTED; | |||
} | |||
@Override | |||
public boolean isReady() { | |||
return state == State.STARTED; | |||
} | |||
@Override | |||
public void awaitStop() { | |||
try { | |||
daemon.join(); | |||
} catch (InterruptedException e) { | |||
// interrupted by call to terminate() | |||
} | |||
} | |||
/** | |||
* Blocks until stopped | |||
*/ | |||
@Override | |||
public void stop() { | |||
state = State.STOPPING; | |||
try { | |||
daemon.join(); | |||
} catch (InterruptedException e) { | |||
e.printStackTrace(); | |||
} | |||
state = State.STOPPED; | |||
} | |||
public static void main(String[] args) { | |||
ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(args); | |||
entryPoint.launch(new InfiniteTerminationProcess()); | |||
} | |||
} |
@@ -19,11 +19,11 @@ | |||
*/ | |||
package org.sonar.process.test; | |||
import org.sonar.process.MonitoredProcess; | |||
import org.sonar.process.Monitored; | |||
import org.sonar.process.ProcessEntryPoint; | |||
import org.sonar.process.State; | |||
import org.sonar.process.Lifecycle.State; | |||
public class StandardProcess implements MonitoredProcess { | |||
public class StandardProcess implements Monitored { | |||
private State state = State.INIT; | |||
@@ -56,7 +56,7 @@ public class StandardProcess implements MonitoredProcess { | |||
} | |||
@Override | |||
public void awaitTermination() { | |||
public void awaitStop() { | |||
try { | |||
daemon.join(); | |||
} catch (InterruptedException e) { | |||
@@ -68,7 +68,7 @@ public class StandardProcess implements MonitoredProcess { | |||
* Blocks until stopped | |||
*/ | |||
@Override | |||
public void terminate() { | |||
public void stop() { | |||
state = State.STOPPING; | |||
daemon.interrupt(); | |||
state = State.STOPPED; |
@@ -27,7 +27,7 @@ import org.elasticsearch.node.Node; | |||
import org.elasticsearch.node.NodeBuilder; | |||
import org.slf4j.LoggerFactory; | |||
import org.sonar.process.MinimumViableSystem; | |||
import org.sonar.process.MonitoredProcess; | |||
import org.sonar.process.Monitored; | |||
import org.sonar.process.ProcessEntryPoint; | |||
import org.sonar.process.ProcessLogging; | |||
import org.sonar.process.Props; | |||
@@ -39,7 +39,7 @@ import java.util.Collections; | |||
import java.util.HashSet; | |||
import java.util.Set; | |||
public class SearchServer implements MonitoredProcess { | |||
public class SearchServer implements Monitored { | |||
public static final String SONAR_NODE_NAME = "sonar.node.name"; | |||
public static final String ES_PORT_PROPERTY = "sonar.search.port"; | |||
@@ -55,7 +55,6 @@ public class SearchServer implements MonitoredProcess { | |||
private final Set<String> nodes = new HashSet<String>(); | |||
private final Props props; | |||
private final Object lock = new Object(); | |||
private Node node; | |||
@@ -71,79 +70,77 @@ public class SearchServer implements MonitoredProcess { | |||
@Override | |||
public void start() { | |||
synchronized (lock) { | |||
Integer port = props.valueAsInt(ES_PORT_PROPERTY); | |||
String clusterName = props.value(ES_CLUSTER_PROPERTY); | |||
LoggerFactory.getLogger(SearchServer.class).info("Starting ES[{}] on port: {}", clusterName, port); | |||
ImmutableSettings.Builder esSettings = ImmutableSettings.settingsBuilder() | |||
// Disable MCast | |||
.put("discovery.zen.ping.multicast.enabled", "false") | |||
// Index storage policies | |||
.put("index.merge.policy.max_merge_at_once", "200") | |||
.put("index.merge.policy.segments_per_tier", "200") | |||
.put("index.number_of_shards", "1") | |||
.put("index.number_of_replicas", MINIMUM_INDEX_REPLICATION) | |||
.put("index.store.type", "mmapfs") | |||
.put("indices.store.throttle.type", "merge") | |||
.put("indices.store.throttle.max_bytes_per_sec", "200mb") | |||
// Install our own listUpdate scripts | |||
.put("script.default_lang", "native") | |||
.put("script.native." + ListUpdate.NAME + ".type", ListUpdate.UpdateListScriptFactory.class.getName()) | |||
// Node is pure transport | |||
.put("transport.tcp.port", port) | |||
.put("http.enabled", false) | |||
// Setting up ES paths | |||
.put("path.data", esDataDir().getAbsolutePath()) | |||
.put("path.work", esWorkDir().getAbsolutePath()) | |||
.put("path.logs", esLogDir().getAbsolutePath()); | |||
if (!nodes.isEmpty()) { | |||
LoggerFactory.getLogger(SearchServer.class).info("Joining ES cluster with master: {}", nodes); | |||
esSettings.put("discovery.zen.ping.unicast.hosts", StringUtils.join(nodes, ",")); | |||
esSettings.put("node.master", false); | |||
// Enforce a N/2+1 number of masters in cluster | |||
esSettings.put("discovery.zen.minimum_master_nodes", 1); | |||
// Change master pool requirement when in distributed mode | |||
// esSettings.put("discovery.zen.minimum_master_nodes", (int) Math.floor(nodes.size() / 2.0) + 1); | |||
} | |||
Integer port = props.valueAsInt(ES_PORT_PROPERTY); | |||
String clusterName = props.value(ES_CLUSTER_PROPERTY); | |||
LoggerFactory.getLogger(SearchServer.class).info("Starting ES[{}] on port: {}", clusterName, port); | |||
ImmutableSettings.Builder esSettings = ImmutableSettings.settingsBuilder() | |||
// Disable MCast | |||
.put("discovery.zen.ping.multicast.enabled", "false") | |||
// Index storage policies | |||
.put("index.merge.policy.max_merge_at_once", "200") | |||
.put("index.merge.policy.segments_per_tier", "200") | |||
.put("index.number_of_shards", "1") | |||
.put("index.number_of_replicas", MINIMUM_INDEX_REPLICATION) | |||
.put("index.store.type", "mmapfs") | |||
.put("indices.store.throttle.type", "merge") | |||
.put("indices.store.throttle.max_bytes_per_sec", "200mb") | |||
// Install our own listUpdate scripts | |||
.put("script.default_lang", "native") | |||
.put("script.native." + ListUpdate.NAME + ".type", ListUpdate.UpdateListScriptFactory.class.getName()) | |||
// Node is pure transport | |||
.put("transport.tcp.port", port) | |||
.put("http.enabled", false) | |||
// Setting up ES paths | |||
.put("path.data", esDataDir().getAbsolutePath()) | |||
.put("path.work", esWorkDir().getAbsolutePath()) | |||
.put("path.logs", esLogDir().getAbsolutePath()); | |||
if (!nodes.isEmpty()) { | |||
LoggerFactory.getLogger(SearchServer.class).info("Joining ES cluster with master: {}", nodes); | |||
esSettings.put("discovery.zen.ping.unicast.hosts", StringUtils.join(nodes, ",")); | |||
esSettings.put("node.master", false); | |||
// Enforce a N/2+1 number of masters in cluster | |||
esSettings.put("discovery.zen.minimum_master_nodes", 1); | |||
// Change master pool requirement when in distributed mode | |||
// esSettings.put("discovery.zen.minimum_master_nodes", (int) Math.floor(nodes.size() / 2.0) + 1); | |||
} | |||
// Set cluster coordinates | |||
esSettings.put("cluster.name", clusterName); | |||
esSettings.put("node.rack_id", props.value(SONAR_NODE_NAME, "unknown")); | |||
esSettings.put("cluster.routing.allocation.awareness.attributes", "rack_id"); | |||
if (props.contains(SONAR_NODE_NAME)) { | |||
esSettings.put("node.name", props.value(SONAR_NODE_NAME)); | |||
} else { | |||
try { | |||
esSettings.put("node.name", InetAddress.getLocalHost().getHostName()); | |||
} catch (Exception e) { | |||
LoggerFactory.getLogger(SearchServer.class).warn("Could not determine hostname", e); | |||
esSettings.put("node.name", "sq-" + System.currentTimeMillis()); | |||
} | |||
// Set cluster coordinates | |||
esSettings.put("cluster.name", clusterName); | |||
esSettings.put("node.rack_id", props.value(SONAR_NODE_NAME, "unknown")); | |||
esSettings.put("cluster.routing.allocation.awareness.attributes", "rack_id"); | |||
if (props.contains(SONAR_NODE_NAME)) { | |||
esSettings.put("node.name", props.value(SONAR_NODE_NAME)); | |||
} else { | |||
try { | |||
esSettings.put("node.name", InetAddress.getLocalHost().getHostName()); | |||
} catch (Exception e) { | |||
LoggerFactory.getLogger(SearchServer.class).warn("Could not determine hostname", e); | |||
esSettings.put("node.name", "sq-" + System.currentTimeMillis()); | |||
} | |||
} | |||
// Make sure the index settings are up to date. | |||
initAnalysis(esSettings); | |||
// Make sure the index settings are up to date. | |||
initAnalysis(esSettings); | |||
// And building our ES Node | |||
node = NodeBuilder.nodeBuilder() | |||
.settings(esSettings) | |||
.build().start(); | |||
// And building our ES Node | |||
node = NodeBuilder.nodeBuilder() | |||
.settings(esSettings) | |||
.build().start(); | |||
node.client().admin().indices() | |||
.preparePutTemplate("default") | |||
.setTemplate("*") | |||
.addMapping("_default_", "{\"dynamic\": \"strict\"}") | |||
.get(); | |||
} | |||
node.client().admin().indices() | |||
.preparePutTemplate("default") | |||
.setTemplate("*") | |||
.addMapping("_default_", "{\"dynamic\": \"strict\"}") | |||
.get(); | |||
} | |||
@Override | |||
@@ -156,7 +153,7 @@ public class SearchServer implements MonitoredProcess { | |||
} | |||
@Override | |||
public void awaitTermination() { | |||
public void awaitStop() { | |||
while (node != null && !node.isClosed()) { | |||
try { | |||
Thread.sleep(200L); | |||
@@ -251,11 +248,9 @@ public class SearchServer implements MonitoredProcess { | |||
} | |||
@Override | |||
public void terminate() { | |||
synchronized (lock) { | |||
if (!node.isClosed()) { | |||
node.close(); | |||
} | |||
public synchronized void stop() { | |||
if (!node.isClosed()) { | |||
node.close(); | |||
} | |||
} | |||
@@ -85,10 +85,15 @@ class EmbeddedTomcat { | |||
} | |||
void terminate() { | |||
LoggerFactory.getLogger(getClass()).info("--- RECEIVED TERMINATE TOMCAT"); | |||
if (tomcat.getServer().getState().isAvailable()) { | |||
try { | |||
LoggerFactory.getLogger(getClass()).info("--- STOP TOMCAT"); | |||
tomcat.stop(); | |||
tomcat.destroy(); | |||
LoggerFactory.getLogger(getClass()).info("--- TOMCAT TERMINATION - WAIT 10s"); | |||
Thread.sleep(10000L); | |||
LoggerFactory.getLogger(getClass()).info("--- TOMCAT TERMINATED AFTER 10s"); | |||
} catch (Exception e) { | |||
LoggerFactory.getLogger(EmbeddedTomcat.class).error("Fail to stop web server", e); | |||
} |
@@ -20,11 +20,11 @@ | |||
package org.sonar.server.app; | |||
import org.sonar.process.MinimumViableSystem; | |||
import org.sonar.process.MonitoredProcess; | |||
import org.sonar.process.Monitored; | |||
import org.sonar.process.ProcessEntryPoint; | |||
import org.sonar.process.Props; | |||
public class WebServer implements MonitoredProcess { | |||
public class WebServer implements Monitored { | |||
private final EmbeddedTomcat tomcat; | |||
@@ -46,12 +46,12 @@ public class WebServer implements MonitoredProcess { | |||
} | |||
@Override | |||
public void terminate() { | |||
public void stop() { | |||
tomcat.terminate(); | |||
} | |||
@Override | |||
public void awaitTermination() { | |||
public void awaitStop() { | |||
tomcat.awaitTermination(); | |||
} | |||
@@ -25,10 +25,10 @@ import org.junit.Test; | |||
import org.junit.rules.ExpectedException; | |||
import org.sonar.api.config.Settings; | |||
import org.sonar.api.database.DatabaseProperties; | |||
import org.sonar.process.NetworkUtils; | |||
import java.io.File; | |||
import java.io.IOException; | |||
import java.net.ServerSocket; | |||
import java.sql.DriverManager; | |||
import static junit.framework.Assert.fail; | |||
@@ -41,7 +41,7 @@ public class EmbeddedDatabaseTest { | |||
@Test(timeout = 10000) | |||
public void should_start_and_stop() throws IOException { | |||
int port = freeServerPort(); | |||
int port = NetworkUtils.freePort(); | |||
EmbeddedDatabase database = new EmbeddedDatabase(testSettings(port)); | |||
database.start(); | |||
@@ -59,7 +59,7 @@ public class EmbeddedDatabaseTest { | |||
@Test(timeout = 10000) | |||
public void should_support_memory_database() throws IOException { | |||
int port = freeServerPort(); | |||
int port = NetworkUtils.freePort(); | |||
EmbeddedDatabase database = new EmbeddedDatabase(testSettings(port) | |||
.setProperty(DatabaseProperties.PROP_URL, "jdbc:h2:tcp://localhost:" + port + "/mem:sonarIT;USER=sonar;PASSWORD=sonar")); | |||
@@ -94,10 +94,4 @@ public class EmbeddedDatabaseTest { | |||
.setProperty(DatabaseProperties.PROP_EMBEDDED_PORT, "" + port) | |||
.setProperty("sonar.path.data", "./target/testDB"); | |||
} | |||
static int freeServerPort() throws IOException { | |||
ServerSocket srv = new ServerSocket(0); | |||
srv.close(); | |||
return srv.getLocalPort(); | |||
} | |||
} |
@@ -29,7 +29,6 @@ import org.junit.ClassRule; | |||
import org.junit.Test; | |||
import org.junit.rules.TemporaryFolder; | |||
import org.sonar.api.config.Settings; | |||
import org.sonar.process.MonitoredProcess; | |||
import org.sonar.process.NetworkUtils; | |||
import org.sonar.process.Props; | |||
import org.sonar.search.SearchServer; | |||
@@ -72,7 +71,7 @@ public class BaseIndexTest { | |||
@AfterClass | |||
public static void teardownSearchEngine() { | |||
searchServer.terminate(); | |||
searchServer.stop(); | |||
} | |||
@Before |
@@ -139,7 +139,7 @@ public class ServerTester extends ExternalResource { | |||
*/ | |||
public void stop() { | |||
platform.doStop(); | |||
searchServer.terminate(); | |||
searchServer.stop(); | |||
FileUtils.deleteQuietly(homeDir); | |||
} | |||
@@ -69,11 +69,6 @@ sonar.jdbc.timeBetweenEvictionRunsMillis=30000 | |||
#sonar.web.javaOpts=-Xmx768m -XX:MaxPermSize=160m -XX:+HeapDumpOnOutOfMemoryError \ | |||
# -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djruby.management.enabled=false | |||
# Web server requires a JMX RMI port to be open. Default is 9003. A free port is | |||
# dynamically used is value is 0. | |||
# This JMX port must be private and must not be exposed to the Internet. | |||
#sonar.web.jmxPort=9003 | |||
# Binding IP address. For servers with more than one IP address, this property specifies which | |||
# address will be used for listening on the specified ports. | |||
# By default, ports will be used on all IP addresses associated with the server. | |||
@@ -182,10 +177,6 @@ sonar.jdbc.timeBetweenEvictionRunsMillis=30000 | |||
# This port must be private and must not be exposed to the Internet. | |||
#sonar.search.port=9001 | |||
# JMX RMI port to monitor Elasticsearch process. Default is 9002. Use 0 to get a free port. | |||
# It must be private and must not be exposed to the Internet. | |||
#sonar.search.jmxPort=9002 | |||
#-------------------------------------------------------------------------------------------------- | |||
# UPDATE CENTER |
@@ -21,12 +21,9 @@ package org.sonar.application; | |||
import org.apache.commons.io.FilenameUtils; | |||
import org.apache.commons.lang.StringUtils; | |||
import org.sonar.process.JmxUtils; | |||
import org.sonar.process.MinimumViableSystem; | |||
import org.sonar.process.ProcessLogging; | |||
import org.sonar.process.ProcessMXBean; | |||
import org.sonar.process.Props; | |||
import org.sonar.process.State; | |||
import org.sonar.process.monitor.JavaCommand; | |||
import org.sonar.process.monitor.Monitor; | |||
@@ -38,7 +35,7 @@ import java.util.Properties; | |||
/** | |||
* Entry-point of process that starts and monitors elasticsearch and web servers | |||
*/ | |||
public class App implements ProcessMXBean { | |||
public class App { | |||
private final Monitor monitor; | |||
@@ -48,7 +45,6 @@ public class App implements ProcessMXBean { | |||
App(Monitor monitor) { | |||
this.monitor = monitor; | |||
JmxUtils.registerMBean(this, "SonarQube"); | |||
} | |||
public void start(Props props) { | |||
@@ -60,10 +56,9 @@ public class App implements ProcessMXBean { | |||
List<JavaCommand> commands = new ArrayList<JavaCommand>(); | |||
File homeDir = props.nonNullValueAsFile("sonar.path.home"); | |||
File tempDir = props.nonNullValueAsFile("sonar.path.temp"); | |||
JavaCommand elasticsearch = new JavaCommand(JmxUtils.SEARCH_SERVER_NAME); | |||
JavaCommand elasticsearch = new JavaCommand("search"); | |||
elasticsearch | |||
.setWorkDir(homeDir) | |||
.setJmxPort(props.valueAsInt(DefaultSettings.SEARCH_JMX_PORT)) | |||
.addJavaOptions(props.value(DefaultSettings.SEARCH_JAVA_OPTS)) | |||
.setTempDir(tempDir.getAbsoluteFile()) | |||
.setClassName("org.sonar.search.SearchServer") | |||
@@ -74,9 +69,8 @@ public class App implements ProcessMXBean { | |||
// do not yet start SQ in cluster mode. See SONAR-5483 & SONAR-5391 | |||
if (StringUtils.isEmpty(props.value(DefaultSettings.CLUSTER_MASTER))) { | |||
JavaCommand webServer = new JavaCommand(JmxUtils.WEB_SERVER_NAME) | |||
JavaCommand webServer = new JavaCommand("web") | |||
.setWorkDir(homeDir) | |||
.setJmxPort(props.valueAsInt(DefaultSettings.WEB_JMX_PORT)) | |||
.addJavaOptions(props.nonNullValue(DefaultSettings.WEB_JAVA_OPTS)) | |||
.setTempDir(tempDir.getAbsoluteFile()) | |||
// required for logback tomcat valve | |||
@@ -94,21 +88,6 @@ public class App implements ProcessMXBean { | |||
return commands; | |||
} | |||
@Override | |||
public void terminate() { | |||
monitor.stop(); | |||
} | |||
@Override | |||
public boolean isReady() { | |||
return monitor.getState() == State.STARTED; | |||
} | |||
@Override | |||
public void ping() { | |||
} | |||
static String starPath(File homeDir, String relativePath) { | |||
File dir = new File(homeDir, relativePath); | |||
return FilenameUtils.concat(dir.getAbsolutePath(), "*"); |
@@ -35,9 +35,7 @@ class DefaultSettings { | |||
static final String CLUSTER_NAME = "sonar.cluster.name"; | |||
static final String CLUSTER_NODE_NAME = "sonar.node.name"; | |||
static final String SEARCH_PORT = "sonar.search.port"; | |||
static final String SEARCH_JMX_PORT = "sonar.search.jmxPort"; | |||
static final String SEARCH_JAVA_OPTS = "sonar.search.javaOpts"; | |||
static final String WEB_JMX_PORT = "sonar.web.jmxPort"; | |||
static final String WEB_JAVA_OPTS = "sonar.web.javaOpts"; | |||
static final String JDBC_URL = "sonar.jdbc.url"; | |||
static final String JDBC_LOGIN = "sonar.jdbc.username"; | |||
@@ -84,8 +82,6 @@ class DefaultSettings { | |||
private static Map<String, Integer> defaultPorts() { | |||
Map<String, Integer> defaults = new HashMap<String, Integer>(); | |||
defaults.put(SEARCH_PORT, 9001); | |||
defaults.put(SEARCH_JMX_PORT, 9002); | |||
defaults.put(WEB_JMX_PORT, 9003); | |||
return defaults; | |||
} | |||
} |
@@ -34,8 +34,6 @@ public class DefaultSettingsTest { | |||
DefaultSettings.init(props); | |||
assertThat(props.value("sonar.search.javaOpts")).contains("-Xmx"); | |||
assertThat(props.valueAsInt("sonar.web.jmxPort")).isEqualTo(9003); | |||
assertThat(props.valueAsInt("sonar.search.jmxPort")).isEqualTo(9002); | |||
assertThat(props.value("sonar.jdbc.username")).isEqualTo("sonar"); | |||
} | |||
@@ -52,10 +50,10 @@ public class DefaultSettingsTest { | |||
@Test | |||
public void use_random_port_if_zero() throws Exception { | |||
Properties p = new Properties(); | |||
p.setProperty("sonar.search.jmxPort", "0"); | |||
p.setProperty("sonar.search.port", "0"); | |||
Props props = new Props(p); | |||
DefaultSettings.init(props); | |||
assertThat(props.valueAsInt("sonar.web.jmxPort")).isGreaterThan(0); | |||
assertThat(props.valueAsInt("sonar.search.port")).isGreaterThan(0); | |||
} | |||
} |