private AtomicReference<Thread> awaitThread = new AtomicReference<>();
private volatile boolean stopAwait = false;
+ private final WebServerWatcher webServerWatcher;
private final ComputeEngine computeEngine;
@CheckForNull
private CeMainThread ceMainThread = null;
- public CeServer(ComputeEngine computeEngine) {
+ protected CeServer(WebServerWatcher webServerWatcher, ComputeEngine computeEngine) {
+ this.webServerWatcher = webServerWatcher;
this.computeEngine = computeEngine;
new MinimumViableSystem()
.checkJavaVersion()
ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(args);
Props props = entryPoint.getProps();
new ServerProcessLogging(PROCESS_NAME, LOG_LEVEL_PROPERTY).configure(props);
- CeServer server = new CeServer(new ComputeEngineImpl(props));
+ CeServer server = new CeServer(new WebServerWatcherImpl(entryPoint.getSharedDir()), new ComputeEngineImpl(props));
entryPoint.launch(server);
}
@Override
public void run() {
+ // wait for WebServer to be operational
+ boolean webServerOperational = webServerWatcher.waitForOperational();
+ if (!webServerOperational) {
+ LOG.debug("Interrupted while waiting for WebServer to be operational. Assuming it will never be. Stopping.");
+ // signal CE is done booting (obviously, since we are about to stop)
+ this.started = true;
+ // release thread (if any) in CeServer#awaitStop()
+ stopAwait();
+ return;
+ }
+
boolean startupSuccessful = attemptStartup();
this.started = true;
if (startupSuccessful) {
+ // call below is blocking
waitForStopSignal();
} else {
stopAwait();
try {
Thread.sleep(CHECK_FOR_STOP_DELAY);
} catch (InterruptedException e) {
- // Ignored, check the flag
+ // ignore the interruption itself, check the flag
}
}
attemptShutdown();
}
public void stopIt() {
+ // stop looping indefinitely
this.stop = true;
+ // interrupt current thread in case its waiting for WebServer
+ interrupt();
}
}
+
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.ce.app;
+
+import javax.annotation.Nullable;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.LoggerLevel;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ *
+ */
+public final class LogarithmicLogger implements Logger {
+ private final Logger logger;
+ private final long callRatio;
+ private long callCounter = -1;
+ private long logCounter = -1;
+
+ private LogarithmicLogger(Builder builder) {
+ this.logger = builder.logger;
+ this.callRatio = builder.callRatio;
+ }
+
+ public static Builder from(Logger logger) {
+ return new Builder(logger);
+ }
+
+ public static final class Builder {
+ private final Logger logger;
+ private long callRatio = 1;
+
+ public Builder(Logger logger) {
+ this.logger = logger;
+ }
+
+ public Builder applyingCallRatio(long callRatio) {
+ checkArgument(callRatio >= 1, "callRatio must be => 1");
+ this.callRatio = callRatio;
+ return this;
+ }
+
+ public Logger build() {
+ return new LogarithmicLogger(this);
+ }
+ }
+
+
+ private boolean shouldLog() {
+ callCounter++;
+ long ratioed = callCounter / callRatio;
+ long log = (long) Math.log(ratioed);
+ if (log > logCounter) {
+ logCounter = log;
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isTraceEnabled() {
+ return logger.isTraceEnabled();
+ }
+
+ @Override
+ public boolean isDebugEnabled() {
+ return logger.isDebugEnabled();
+ }
+
+ @Override
+ public boolean setLevel(LoggerLevel level) {
+ return logger.setLevel(level);
+ }
+
+ @Override
+ public LoggerLevel getLevel() {
+ return logger.getLevel();
+ }
+
+ @Override
+ public void trace(String msg) {
+ if (shouldLog()) {
+ logger.trace(msg);
+ }
+ }
+
+ @Override
+ public void trace(String pattern, @Nullable Object arg) {
+ if (shouldLog()) {
+ logger.trace(pattern, arg);
+ }
+ }
+
+ @Override
+ public void trace(String msg, @Nullable Object arg1, @Nullable Object arg2) {
+ if (shouldLog()) {
+ logger.trace(msg, arg1, arg2);
+ }
+ }
+
+ @Override
+ public void trace(String msg, Object... args) {
+ if (shouldLog()) {
+ logger.trace(msg, args);
+ }
+ }
+
+ @Override
+ public void debug(String msg) {
+ if (shouldLog()) {
+ logger.debug(msg);
+ }
+ }
+
+ @Override
+ public void debug(String pattern, @Nullable Object arg) {
+ if (shouldLog()) {
+ logger.debug(pattern, arg);
+ }
+ }
+
+ @Override
+ public void debug(String msg, @Nullable Object arg1, @Nullable Object arg2) {
+ if (shouldLog()) {
+ logger.debug(msg, arg1, arg2);
+ }
+ }
+
+ @Override
+ public void debug(String msg, Object... args) {
+ if (shouldLog()) {
+ logger.debug(msg, args);
+ }
+ }
+
+ @Override
+ public void info(String msg) {
+ if (shouldLog()) {
+ logger.info(msg);
+ }
+ }
+
+ @Override
+ public void info(String msg, @Nullable Object arg) {
+ if (shouldLog()) {
+ logger.info(msg, arg);
+ }
+ }
+
+ @Override
+ public void info(String msg, @Nullable Object arg1, @Nullable Object arg2) {
+ if (shouldLog()) {
+ logger.info(msg, arg1, arg2);
+ }
+ }
+
+ @Override
+ public void info(String msg, Object... args) {
+ if (shouldLog()) {
+ logger.info(msg, args);
+ }
+ }
+
+ @Override
+ public void warn(String msg) {
+ if (shouldLog()) {
+ logger.warn(msg);
+ }
+ }
+
+ @Override
+ public void warn(String msg, Throwable throwable) {
+ if (shouldLog()) {
+ logger.warn(msg, throwable);
+ }
+ }
+
+ @Override
+ public void warn(String msg, @Nullable Object arg) {
+ if (shouldLog()) {
+ logger.warn(msg, arg);
+ }
+ }
+
+ @Override
+ public void warn(String msg, @Nullable Object arg1, @Nullable Object arg2) {
+ if (shouldLog()) {
+ logger.warn(msg, arg1, arg2);
+ }
+ }
+
+ @Override
+ public void warn(String msg, Object... args) {
+ if (shouldLog()) {
+ logger.warn(msg, args);
+ }
+ }
+
+ @Override
+ public void error(String msg) {
+ if (shouldLog()) {
+ logger.error(msg);
+ }
+ }
+
+ @Override
+ public void error(String msg, @Nullable Object arg) {
+ if (shouldLog()) {
+ logger.error(msg, arg);
+ }
+ }
+
+ @Override
+ public void error(String msg, @Nullable Object arg1, @Nullable Object arg2) {
+ if (shouldLog()) {
+ logger.error(msg, arg1, arg2);
+ }
+ }
+
+ @Override
+ public void error(String msg, Object... args) {
+ if (shouldLog()) {
+ logger.error(msg, args);
+ }
+ }
+
+ @Override
+ public void error(String msg, Throwable thrown) {
+ if (shouldLog()) {
+ logger.error(msg, thrown);
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.ce.app;
+
+public interface WebServerWatcher {
+ /**
+ * This blocking call, waits for the Web Server to be operational until either the Web Server is actually
+ * operational, or the calling thread is interrupted.
+ *
+ * @return true if we detected WebServer is operational, false otherwise
+ */
+ boolean waitForOperational();
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.ce.app;
+
+import java.io.File;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.process.DefaultProcessCommands;
+
+public class WebServerWatcherImpl implements WebServerWatcher {
+ private static final Logger LOG = Loggers.get(WebServerWatcherImpl.class);
+ private static final int WEB_SERVER_PROCESS_NUMBER = 2;
+ private static final int POLL_DELAY = 200;
+ // accounting only every 5 log calls so that only one every second (because delay is 200ms) is taken into account
+ private static final int CALL_RATIO = 5;
+
+ private final File sharedDir;
+
+ public WebServerWatcherImpl(File sharedDir) {
+ this.sharedDir = sharedDir;
+ }
+
+ @Override
+ public boolean waitForOperational() {
+ try (DefaultProcessCommands processCommands = DefaultProcessCommands.secondary(sharedDir, WEB_SERVER_PROCESS_NUMBER)) {
+ if (processCommands.isOperational()) {
+ return true;
+ }
+
+ LOG.info("Waiting for Web Server to be operational...");
+ Logger logarithmicLogger = LogarithmicLogger.from(LOG).applyingCallRatio(CALL_RATIO).build();
+ while (!processCommands.isOperational()) {
+ logarithmicLogger.info("Still waiting for WebServer...");
+ try {
+ Thread.sleep(POLL_DELAY);
+ } catch (InterruptedException e) {
+ // propagate interrupted state and return that WebServer is not operational
+ Thread.interrupted();
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+}
package org.sonar.ce.app;
import com.google.common.base.Objects;
+import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
import org.junit.rules.Timeout;
import org.sonar.ce.ComputeEngine;
public class CeServerTest {
@Rule
- public Timeout timeout = Timeout.seconds(5);
-
+ public Timeout timeout = Timeout.seconds(50);
@Rule
public ExpectedException expectedException = ExpectedException.none();
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
private CeServer underTest = null;
private Thread waitingThread = null;
}
@Test
- public void constructor_does_not_start_a_new_Thread() {
+ public void constructor_does_not_start_a_new_Thread() throws IOException {
int activeCount = Thread.activeCount();
newCeServer();
}
@Test
- public void start_starts_a_new_Thread() {
+ public void start_starts_a_new_Thread() throws IOException {
int activeCount = Thread.activeCount();
newCeServer().start();
}
@Test
- public void start_throws_ISE_when_called_twice() {
+ public void start_throws_ISE_when_called_twice() throws IOException {
CeServer ceServer = newCeServer();
ceServer.start();
}
@Test
- public void isReady_throws_ISE_when_called_before_start() {
+ public void isUp_throws_ISE_when_called_before_start() throws IOException {
CeServer ceServer = newCeServer();
expectedException.expect(IllegalStateException.class);
}
@Test
- public void isReady_does_not_return_true_until_ComputeEngine_startup_returns() throws InterruptedException {
+ public void isUp_does_not_return_true_until_ComputeEngine_startup_returns() throws InterruptedException, IOException {
BlockingStartupComputeEngine computeEngine = new BlockingStartupComputeEngine(null);
CeServer ceServer = newCeServer(computeEngine);
}
@Test
- public void isReady_returns_true_when_ComputeEngine_startup_throws_any_Exception_or_Error() throws InterruptedException {
+ public void isUp_returns_true_when_ComputeEngine_startup_throws_any_Exception_or_Error() throws InterruptedException, IOException {
Throwable startupException = new Throwable("Faking failing ComputeEngine#startup()");
BlockingStartupComputeEngine computeEngine = new BlockingStartupComputeEngine(startupException);
}
@Test
- public void awaitStop_throws_ISE_if_called_before_start() {
+ public void isUp_returns_true_when_waiting_for_WebServer_failed() throws InterruptedException {
+ final CountDownLatch webServerWatcherCalled = new CountDownLatch(1);
+ CeServer ceServer = newCeServer(new WebServerWatcher() {
+ @Override
+ public boolean waitForOperational() {
+ webServerWatcherCalled.countDown();
+ return false;
+ }
+ }, DoNothingComputeEngine.INSTANCE);
+
+ ceServer.start();
+ ceServer.awaitStop();
+ assertThat(ceServer.isUp()).isTrue();
+ }
+
+ @Test
+ public void awaitStop_throws_ISE_if_called_before_start() throws IOException {
CeServer ceServer = newCeServer();
expectedException.expect(IllegalStateException.class);
}
@Test
- public void awaitStop_throws_ISE_if_called_twice() throws InterruptedException {
+ public void awaitStop_throws_ISE_if_called_twice() throws InterruptedException, IOException {
final CeServer ceServer = newCeServer();
ExceptionCatcherWaitingThread waitingThread1 = new ExceptionCatcherWaitingThread(ceServer);
ExceptionCatcherWaitingThread waitingThread2 = new ExceptionCatcherWaitingThread(ceServer);
}
@Test
- public void awaitStop_keeps_blocking_calling_thread_even_if_calling_thread_is_interrupted_but_until_stop_is_called() throws InterruptedException {
+ public void awaitStop_keeps_blocking_calling_thread_even_if_calling_thread_is_interrupted_but_until_stop_is_called() throws InterruptedException, IOException {
final CeServer ceServer = newCeServer();
Thread waitingThread = newWaitingThread(new Runnable() {
@Override
}
@Test
- public void stop_releases_thread_in_awaitStop_even_when_ComputeEngine_shutdown_fails() throws InterruptedException {
+ public void awaitStop_unblocks_when_waiting_for_WebServer_failed() throws InterruptedException {
+ final CountDownLatch webServerWatcherCalled = new CountDownLatch(1);
+ CeServer ceServer = newCeServer(new WebServerWatcher() {
+ @Override
+ public boolean waitForOperational() {
+ webServerWatcherCalled.countDown();
+ return false;
+ }
+ }, DoNothingComputeEngine.INSTANCE);
+
+ ceServer.start();
+ // if awaitStop does not unblock, the test will fail with timeout
+ ceServer.awaitStop();
+ }
+
+
+ @Test
+ public void awaitStop_unblocks_when_waiting_for_ComputeEngine_startup_fails() throws InterruptedException, IOException {
+ CeServer ceServer = newCeServer(new ComputeEngine() {
+ @Override
+ public void startup() {
+ throw new Error("Faking ComputeEngine.startup() failing");
+ }
+
+ @Override
+ public void shutdown() {
+ throw new UnsupportedOperationException("shutdown() should never be called in this context");
+ }
+ });
+
+ ceServer.start();
+ // if awaitStop does not unblock, the test will fail with timeout
+ ceServer.awaitStop();
+ }
+
+ @Test
+ public void stop_releases_thread_in_awaitStop_even_when_ComputeEngine_shutdown_fails() throws InterruptedException, IOException {
final CeServer ceServer = newCeServer(new ComputeEngine() {
@Override
public void startup() {
waitingThread.join();
}
- private CeServer newCeServer() {
- return newCeServer(new ComputeEngine() {
- @Override
- public void startup() {
- // do nothing
- }
+ private CeServer newCeServer() throws IOException {
+ return newCeServer(DoNothingComputeEngine.INSTANCE);
+ }
+ private CeServer newCeServer(ComputeEngine computeEngine) throws IOException {
+ checkState(this.underTest == null, "Only one CeServer can be created per test method");
+ this.underTest = new CeServer(new WebServerWatcher() {
@Override
- public void shutdown() {
- // do nothing
+ public boolean waitForOperational() {
+ // return instantly simulating WebServer is already operational
+ return true;
}
- });
+ }, computeEngine);
+ return underTest;
}
- private CeServer newCeServer(ComputeEngine computeEngine) {
+ private CeServer newCeServer(WebServerWatcher webServerWatcher, ComputeEngine computeEngine) {
checkState(this.underTest == null, "Only one CeServer can be created per test method");
- this.underTest = new CeServer(computeEngine);
+ this.underTest = new CeServer(webServerWatcher, computeEngine);
return underTest;
}
}
}
+ private enum DoNothingComputeEngine implements ComputeEngine {
+ INSTANCE;
+
+ @Override
+ public void startup() {
+ // do nothing
+ }
+
+ @Override
+ public void shutdown() {
+ // do nothing
+ }
+ }
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.ce.app;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.api.utils.log.LoggerLevel.DEBUG;
+import static org.sonar.api.utils.log.LoggerLevel.ERROR;
+import static org.sonar.api.utils.log.LoggerLevel.INFO;
+import static org.sonar.api.utils.log.LoggerLevel.TRACE;
+import static org.sonar.api.utils.log.LoggerLevel.WARN;
+
+public class LogarithmicLoggerTest {
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ @Test
+ public void logarithmically_logs_less_and_less_frequently_calls_to_same_Logger_method() throws InterruptedException {
+ Logger logarithmicLogger = LogarithmicLogger.from(Loggers.get(getClass())).build();
+ for (int i = 0; i < 1000; i++) {
+ logarithmicLogger.error(String.valueOf(i));
+ }
+
+ assertThat(logTester.logs(ERROR)).containsOnly(
+ "1", "3", "8", "21", "55", "149", "404"
+ );
+ assertThat(logTester.logs()).containsOnly(
+ "1", "3", "8", "21", "55", "149", "404"
+ );
+ }
+
+ @Test
+ public void logarithmically_logs_less_and_less_frequently_calls_across_log_levels() throws InterruptedException {
+ Logger logarithmicLogger = LogarithmicLogger.from(Loggers.get(getClass())).build();
+ for (int i = 0; i < 1000; i++) {
+ spawnMessageOnLevels(logarithmicLogger, i, String.valueOf(i));
+ }
+
+ assertThat(logTester.logs()).containsOnly(
+ "1", "3", "8", "21", "55", "149", "404"
+ );
+ assertThat(logTester.logs(ERROR)).containsOnly("55");
+ assertThat(logTester.logs(WARN)).containsOnly("1", "21");
+ assertThat(logTester.logs(INFO)).isEmpty();
+ assertThat(logTester.logs(DEBUG)).containsOnly("3", "8");
+ assertThat(logTester.logs(TRACE)).containsOnly("149", "404");
+ }
+
+ @Test
+ public void call_ratio_is_applied_before_logarithm() {
+ int callRatio = 10;
+ Logger logarithmicLogger = LogarithmicLogger.from(Loggers.get(getClass())).applyingCallRatio(callRatio).build();
+ for (int i = 0; i < 1000 + callRatio; i++) {
+ logarithmicLogger.error(String.valueOf(i));
+ }
+
+ assertThat(logTester.logs(ERROR)).containsOnly(
+ "10", "30", "80", "210", "550"
+ );
+ assertThat(logTester.logs()).containsOnly(
+ "10", "30", "80", "210", "550"
+ );
+ }
+
+ private static void spawnMessageOnLevels(Logger logarithmicLogger, int i, String msg) {
+ int c = i % 5;
+ switch (c) {
+ case 0:
+ logarithmicLogger.error(msg);
+ break;
+ case 1:
+ logarithmicLogger.warn(msg);
+ break;
+ case 2:
+ logarithmicLogger.info(msg);
+ break;
+ case 3:
+ logarithmicLogger.debug(msg);
+ break;
+ case 4:
+ logarithmicLogger.trace(msg);
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported value " + c);
+ }
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.ce.app;
+
+import java.io.File;
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.rules.Timeout;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.process.DefaultProcessCommands;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class WebServerWatcherImplTest {
+ private static final int WEB_SERVER_PROCESS_NUMBER = 2;
+
+ @Rule
+ public Timeout timeout = Timeout.seconds(1);
+ @Rule
+ public LogTester logTester = new LogTester();
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ private File sharedDir;
+ private WebServerWatcherImpl underTest;
+
+ @Before
+ public void setUp() throws Exception {
+ sharedDir = temporaryFolder.newFolder();
+ underTest = new WebServerWatcherImpl(sharedDir);
+ }
+
+ @Test
+ public void waitForOperational_does_not_log_anything_if_WebServer_already_operational() {
+ setWebServerOperational();
+
+ underTest.waitForOperational();
+
+ assertThat(logTester.logs()).isEmpty();
+ }
+
+ @Test
+ public void waitForOperational_blocks_until_WebServer_is_operational() throws InterruptedException {
+ final CountDownLatch startedLatch = new CountDownLatch(1);
+ final CountDownLatch doneLatch = new CountDownLatch(1);
+ Thread waitingThread = new Thread() {
+ @Override
+ public void run() {
+ startedLatch.countDown();
+ underTest.waitForOperational();
+ doneLatch.countDown();
+ }
+ };
+ waitingThread.start();
+
+ // wait for waitingThread to be running
+ assertThat(startedLatch.await(50, MILLISECONDS)).isTrue();
+
+ // assert that we can wait, in vain, more than 50ms because waitingThread is blocked
+ assertThat(doneLatch.await(50 + Math.abs(new Random().nextInt(300)), MILLISECONDS)).isFalse();
+
+ setWebServerOperational();
+
+ // wait up to 400 ms (because polling delay is 200ms) that waitingThread is done running
+ assertThat(doneLatch.await(400, MILLISECONDS)).isTrue();
+
+ assertThat(logTester.logs(LoggerLevel.INFO)).contains("Waiting for Web Server to be operational...");
+ }
+
+ private void setWebServerOperational() {
+ try (DefaultProcessCommands processCommands = DefaultProcessCommands.secondary(sharedDir, WEB_SERVER_PROCESS_NUMBER)) {
+ processCommands.setOperational();
+ }
+ }
+}
*/
package org.sonar.process;
+import java.io.File;
import org.slf4j.LoggerFactory;
public class ProcessEntryPoint implements Stoppable {
public static final String PROPERTY_SHARED_PATH = "process.sharedDir";
private final Props props;
+ private final String processKey;
+ private final int processNumber;
+ private final File sharedDir;
private final Lifecycle lifecycle = new Lifecycle();
private final ProcessCommands commands;
private final SystemExit exit;
});
ProcessEntryPoint(Props props, SystemExit exit, ProcessCommands commands) {
+ this(props, getProcessNumber(props), getSharedDir(props), exit, commands);
+ }
+
+ private ProcessEntryPoint(Props props, int processNumber, File sharedDir, SystemExit exit, ProcessCommands commands) {
this.props = props;
+ this.processKey = props.nonNullValue(PROPERTY_PROCESS_KEY);
+ this.processNumber = processNumber;
+ this.sharedDir = sharedDir;
this.exit = exit;
this.commands = commands;
this.stopWatcher = new StopWatcher(commands, this);
}
public String getKey() {
- return props.nonNullValue(PROPERTY_PROCESS_KEY);
+ return processKey;
+ }
+
+ public int getProcessNumber() {
+ return processNumber;
+ }
+
+ public File getSharedDir() {
+ return sharedDir;
}
/**
public static ProcessEntryPoint createForArguments(String[] args) {
Props props = ConfigurationUtils.loadPropsFromCommandLineArgs(args);
- ProcessCommands commands = DefaultProcessCommands.main(
- props.nonNullValueAsFile(PROPERTY_SHARED_PATH), Integer.parseInt(props.nonNullValue(PROPERTY_PROCESS_INDEX)));
- return new ProcessEntryPoint(props, new SystemExit(), commands);
+ File sharedDir = getSharedDir(props);
+ int processNumber = getProcessNumber(props);
+ ProcessCommands commands = DefaultProcessCommands.main(sharedDir, processNumber);
+ return new ProcessEntryPoint(props, processNumber, sharedDir, new SystemExit(), commands);
+ }
+
+ private static int getProcessNumber(Props props) {
+ return Integer.parseInt(props.nonNullValue(PROPERTY_PROCESS_INDEX));
+ }
+
+ private static File getSharedDir(Props props) {
+ return props.nonNullValueAsFile(PROPERTY_SHARED_PATH);
}
}
*/
package org.sonar.process;
+import java.io.IOException;
import org.apache.commons.io.FileUtils;
import org.junit.Rule;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
+import static org.sonar.process.ProcessEntryPoint.PROPERTY_PROCESS_INDEX;
+import static org.sonar.process.ProcessEntryPoint.PROPERTY_PROCESS_KEY;
+import static org.sonar.process.ProcessEntryPoint.PROPERTY_SHARED_PATH;
+import static org.sonar.process.ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT;
public class ProcessEntryPointTest {
@Test
public void test_initial_state() throws Exception {
- Props props = new Props(new Properties());
+ Props props = createProps();
ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit, mock(ProcessCommands.class));
assertThat(entryPoint.getProps()).isSameAs(props);
}
@Test
- public void fail_to_launch_multiple_times() {
- Props props = new Props(new Properties());
- props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "test");
- props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000");
+ public void fail_to_launch_multiple_times() throws IOException {
+ Props props = createProps();
ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit, mock(ProcessCommands.class));
entryPoint.launch(new NoopProcess());
@Test
public void launch_then_request_graceful_stop() throws Exception {
- Props props = new Props(new Properties());
- props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "test");
- props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000");
+ Props props = createProps();
final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit, mock(ProcessCommands.class));
final StandardProcess process = new StandardProcess();
@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_TERMINATION_TIMEOUT, "30000");
+ Props props = createProps();
final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit, mock(ProcessCommands.class));
final StandardProcess process = new StandardProcess();
}
@Test
- public void terminate_if_startup_error() {
- Props props = new Props(new Properties());
- props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "foo");
- props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000");
+ public void terminate_if_startup_error() throws IOException {
+ Props props = createProps();
final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit, mock(ProcessCommands.class));
final Monitored process = new StartupErrorProcess();
assertThat(entryPoint.getState()).isEqualTo(State.STOPPED);
}
+ private Props createProps() throws IOException {
+ Props props = new Props(new Properties());
+ props.set(PROPERTY_SHARED_PATH, temp.newFolder().getAbsolutePath());
+ props.set(PROPERTY_PROCESS_INDEX, "1");
+ props.set(PROPERTY_PROCESS_KEY, "test");
+ props.set(PROPERTY_TERMINATION_TIMEOUT, "30000");
+ return props;
+ }
+
private static class NoopProcess implements Monitored {
@Override
*/
package org.sonar.server.platform.platformlevel;
+import org.sonar.server.app.ProcessCommandWrapper;
import org.sonar.server.computation.queue.PurgeCeActivities;
import org.sonar.server.issue.filter.RegisterIssueFilters;
import org.sonar.server.platform.ServerLifecycleNotifier;
PlatformLevelStartup.super.start();
getComponentByType(IndexSynchronizer.class).execute();
getComponentByType(ServerLifecycleNotifier.class).notifyStart();
+ getComponentByType(ProcessCommandWrapper.class).notifyOperational();
}
});
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.core.platform.ComponentContainer;
+import org.sonar.process.ProcessEntryPoint;
import org.sonar.process.ProcessProperties;
import org.sonar.server.es.EsServerHolder;
import org.sonar.server.platform.BackendCleanup;
properties.setProperty(ProcessProperties.SEARCH_HOST, String.valueOf(esServerHolder.getHostName()));
properties.setProperty(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath());
properties.setProperty(ProcessProperties.PATH_DATA, new File(homeDir, "data").getAbsolutePath());
- properties.setProperty(ProcessProperties.PATH_TEMP, createTemporaryFolderIn().getAbsolutePath());
+ File temporaryFolderIn = createTemporaryFolderIn();
+ properties.setProperty(ProcessProperties.PATH_TEMP, temporaryFolderIn.getAbsolutePath());
+ properties.setProperty(ProcessEntryPoint.PROPERTY_SHARED_PATH, temporaryFolderIn.getAbsolutePath());
+ properties.setProperty(ProcessEntryPoint.PROPERTY_PROCESS_INDEX, "2");
properties.setProperty(DatabaseProperties.PROP_URL, "jdbc:h2:" + homeDir.getAbsolutePath() + "/h2");
if (updateCenterUrl != null) {
properties.setProperty(UpdateCenterClient.URL_PROPERTY, updateCenterUrl.toString());