From 7b6038abea08791c5c69c0716cca2fe40688d20c Mon Sep 17 00:00:00 2001 From: =?utf8?q?S=C3=A9bastien=20Lesaint?= Date: Thu, 10 Mar 2016 14:27:41 +0100 Subject: [PATCH] SONAR-7435 CE process waiting for WebServer to be operational CE detects WebServer is operational though shared file IPC (see ProcessCommand) --- .../main/java/org/sonar/ce/app/CeServer.java | 24 +- .../org/sonar/ce/app/LogarithmicLogger.java | 250 ++++++++++++++++++ .../org/sonar/ce/app/WebServerWatcher.java | 30 +++ .../sonar/ce/app/WebServerWatcherImpl.java | 62 +++++ .../java/org/sonar/ce/app/CeServerTest.java | 116 ++++++-- .../sonar/ce/app/LogarithmicLoggerTest.java | 110 ++++++++ .../ce/app/WebServerWatcherImplTest.java | 98 +++++++ .../org/sonar/process/ProcessEntryPoint.java | 36 ++- .../sonar/process/ProcessEntryPointTest.java | 36 +-- .../platformlevel/PlatformLevelStartup.java | 2 + .../org/sonar/server/tester/ServerTester.java | 6 +- 11 files changed, 724 insertions(+), 46 deletions(-) create mode 100644 server/sonar-ce/src/main/java/org/sonar/ce/app/LogarithmicLogger.java create mode 100644 server/sonar-ce/src/main/java/org/sonar/ce/app/WebServerWatcher.java create mode 100644 server/sonar-ce/src/main/java/org/sonar/ce/app/WebServerWatcherImpl.java create mode 100644 server/sonar-ce/src/test/java/org/sonar/ce/app/LogarithmicLoggerTest.java create mode 100644 server/sonar-ce/src/test/java/org/sonar/ce/app/WebServerWatcherImplTest.java diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/app/CeServer.java b/server/sonar-ce/src/main/java/org/sonar/ce/app/CeServer.java index 89f50567509..22abe03bf12 100644 --- a/server/sonar-ce/src/main/java/org/sonar/ce/app/CeServer.java +++ b/server/sonar-ce/src/main/java/org/sonar/ce/app/CeServer.java @@ -55,11 +55,13 @@ public class CeServer implements Monitored { private AtomicReference 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() @@ -74,7 +76,7 @@ public class CeServer implements Monitored { 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); } @@ -145,9 +147,21 @@ public class CeServer implements Monitored { @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(); @@ -174,7 +188,7 @@ public class CeServer implements Monitored { try { Thread.sleep(CHECK_FOR_STOP_DELAY); } catch (InterruptedException e) { - // Ignored, check the flag + // ignore the interruption itself, check the flag } } attemptShutdown(); @@ -201,7 +215,11 @@ public class CeServer implements Monitored { } public void stopIt() { + // stop looping indefinitely this.stop = true; + // interrupt current thread in case its waiting for WebServer + interrupt(); } } + } diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/app/LogarithmicLogger.java b/server/sonar-ce/src/main/java/org/sonar/ce/app/LogarithmicLogger.java new file mode 100644 index 00000000000..812dbe74d3a --- /dev/null +++ b/server/sonar-ce/src/main/java/org/sonar/ce/app/LogarithmicLogger.java @@ -0,0 +1,250 @@ +/* + * 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); + } + } +} diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/app/WebServerWatcher.java b/server/sonar-ce/src/main/java/org/sonar/ce/app/WebServerWatcher.java new file mode 100644 index 00000000000..ff12b8cc419 --- /dev/null +++ b/server/sonar-ce/src/main/java/org/sonar/ce/app/WebServerWatcher.java @@ -0,0 +1,30 @@ +/* + * 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(); +} diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/app/WebServerWatcherImpl.java b/server/sonar-ce/src/main/java/org/sonar/ce/app/WebServerWatcherImpl.java new file mode 100644 index 00000000000..c35b7607047 --- /dev/null +++ b/server/sonar-ce/src/main/java/org/sonar/ce/app/WebServerWatcherImpl.java @@ -0,0 +1,62 @@ +/* + * 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; + } + } +} diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/app/CeServerTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/app/CeServerTest.java index 2978aba2dd6..f19ece99737 100644 --- a/server/sonar-ce/src/test/java/org/sonar/ce/app/CeServerTest.java +++ b/server/sonar-ce/src/test/java/org/sonar/ce/app/CeServerTest.java @@ -20,6 +20,7 @@ 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; @@ -27,6 +28,7 @@ import org.junit.After; 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; @@ -36,10 +38,11 @@ import static org.assertj.core.api.Assertions.assertThat; 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; @@ -57,7 +60,7 @@ public class CeServerTest { } @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(); @@ -66,7 +69,7 @@ public class CeServerTest { } @Test - public void start_starts_a_new_Thread() { + public void start_starts_a_new_Thread() throws IOException { int activeCount = Thread.activeCount(); newCeServer().start(); @@ -75,7 +78,7 @@ public class CeServerTest { } @Test - public void start_throws_ISE_when_called_twice() { + public void start_throws_ISE_when_called_twice() throws IOException { CeServer ceServer = newCeServer(); ceServer.start(); @@ -87,7 +90,7 @@ public class CeServerTest { } @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); @@ -97,7 +100,7 @@ public class CeServerTest { } @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); @@ -115,7 +118,7 @@ public class CeServerTest { } @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); @@ -135,7 +138,23 @@ public class CeServerTest { } @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); @@ -145,7 +164,7 @@ public class CeServerTest { } @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); @@ -169,7 +188,7 @@ public class CeServerTest { } @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 @@ -195,7 +214,43 @@ public class CeServerTest { } @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() { @@ -222,23 +277,25 @@ public class CeServerTest { 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; } @@ -308,4 +365,17 @@ public class CeServerTest { } } + private enum DoNothingComputeEngine implements ComputeEngine { + INSTANCE; + + @Override + public void startup() { + // do nothing + } + + @Override + public void shutdown() { + // do nothing + } + } } diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/app/LogarithmicLoggerTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/app/LogarithmicLoggerTest.java new file mode 100644 index 00000000000..b204e614181 --- /dev/null +++ b/server/sonar-ce/src/test/java/org/sonar/ce/app/LogarithmicLoggerTest.java @@ -0,0 +1,110 @@ +/* + * 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); + } + } + +} diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/app/WebServerWatcherImplTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/app/WebServerWatcherImplTest.java new file mode 100644 index 00000000000..a2b0fb7d757 --- /dev/null +++ b/server/sonar-ce/src/test/java/org/sonar/ce/app/WebServerWatcherImplTest.java @@ -0,0 +1,98 @@ +/* + * 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(); + } + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java index 84cbb22f093..10930daf679 100644 --- a/server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java +++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java @@ -19,6 +19,7 @@ */ package org.sonar.process; +import java.io.File; import org.slf4j.LoggerFactory; public class ProcessEntryPoint implements Stoppable { @@ -29,6 +30,9 @@ 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; @@ -46,7 +50,14 @@ public class ProcessEntryPoint implements Stoppable { }); 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); @@ -61,7 +72,15 @@ public class ProcessEntryPoint implements Stoppable { } public String getKey() { - return props.nonNullValue(PROPERTY_PROCESS_KEY); + return processKey; + } + + public int getProcessNumber() { + return processNumber; + } + + public File getSharedDir() { + return sharedDir; } /** @@ -138,8 +157,17 @@ public class ProcessEntryPoint implements Stoppable { 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); } } diff --git a/server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java b/server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java index 8e107af3ee1..6acf205b52e 100644 --- a/server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java +++ b/server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java @@ -19,6 +19,7 @@ */ package org.sonar.process; +import java.io.IOException; import org.apache.commons.io.FileUtils; import org.junit.Rule; import org.junit.Test; @@ -35,6 +36,10 @@ import java.util.Properties; 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 { @@ -61,7 +66,7 @@ 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); @@ -70,10 +75,8 @@ public class ProcessEntryPointTest { } @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()); @@ -87,9 +90,7 @@ public class ProcessEntryPointTest { @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(); @@ -115,9 +116,7 @@ public class ProcessEntryPointTest { @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(); @@ -146,10 +145,8 @@ public class ProcessEntryPointTest { } @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(); @@ -157,6 +154,15 @@ public class ProcessEntryPointTest { 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 diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelStartup.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelStartup.java index a7c7b04eb7e..fe68d914ed8 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelStartup.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelStartup.java @@ -19,6 +19,7 @@ */ 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; @@ -79,6 +80,7 @@ public class PlatformLevelStartup extends PlatformLevel { PlatformLevelStartup.super.start(); getComponentByType(IndexSynchronizer.class).execute(); getComponentByType(ServerLifecycleNotifier.class).notifyStart(); + getComponentByType(ProcessCommandWrapper.class).notifyOperational(); } }); diff --git a/server/sonar-server/src/test/java/org/sonar/server/tester/ServerTester.java b/server/sonar-server/src/test/java/org/sonar/server/tester/ServerTester.java index a94cd7bbb78..22130902b31 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/tester/ServerTester.java +++ b/server/sonar-server/src/test/java/org/sonar/server/tester/ServerTester.java @@ -39,6 +39,7 @@ import org.sonar.api.resources.Language; 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; @@ -107,7 +108,10 @@ public class ServerTester extends ExternalResource { 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()); -- 2.39.5