]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7435 CE process waiting for WebServer to be operational
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Thu, 10 Mar 2016 13:27:41 +0000 (14:27 +0100)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Mon, 21 Mar 2016 15:44:04 +0000 (16:44 +0100)
CE detects WebServer is operational though shared file IPC (see ProcessCommand)

server/sonar-ce/src/main/java/org/sonar/ce/app/CeServer.java
server/sonar-ce/src/main/java/org/sonar/ce/app/LogarithmicLogger.java [new file with mode: 0644]
server/sonar-ce/src/main/java/org/sonar/ce/app/WebServerWatcher.java [new file with mode: 0644]
server/sonar-ce/src/main/java/org/sonar/ce/app/WebServerWatcherImpl.java [new file with mode: 0644]
server/sonar-ce/src/test/java/org/sonar/ce/app/CeServerTest.java
server/sonar-ce/src/test/java/org/sonar/ce/app/LogarithmicLoggerTest.java [new file with mode: 0644]
server/sonar-ce/src/test/java/org/sonar/ce/app/WebServerWatcherImplTest.java [new file with mode: 0644]
server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java
server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelStartup.java
server/sonar-server/src/test/java/org/sonar/server/tester/ServerTester.java

index 89f5056750936f3125c3b80f96a18f251bc26860..22abe03bf1298ca32c4fc450ba5e8be5d6cb5fdd 100644 (file)
@@ -55,11 +55,13 @@ public class CeServer implements Monitored {
   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()
@@ -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 (file)
index 0000000..812dbe7
--- /dev/null
@@ -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 (file)
index 0000000..ff12b8c
--- /dev/null
@@ -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 (file)
index 0000000..c35b760
--- /dev/null
@@ -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;
+    }
+  }
+}
index 2978aba2dd661621a70301c25e21d363d832962d..f19ece9973707fa91137b50be76a71ea7fea8424 100644 (file)
@@ -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 (file)
index 0000000..b204e61
--- /dev/null
@@ -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 (file)
index 0000000..a2b0fb7
--- /dev/null
@@ -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();
+    }
+  }
+}
index 84cbb22f0937cfc646ceee61aa726059df007c5f..10930daf679afcd5270843977cf85adf1f38dffc 100644 (file)
@@ -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);
   }
 }
index 8e107af3ee1a29ff6bdbd2abfbecaf179cd63d92..6acf205b52e4301555499f8eb70e5ed9fb319e53 100644 (file)
@@ -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
index a7c7b04eb7e71542554611f1346774ba3380116d..fe68d914ed85376babc9df4ecbd7603cabe21e98 100644 (file)
@@ -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();
       }
     });
 
index a94cd7bbb78507abb94198fd90e756a1ca6dd9c0..22130902b317b0d153863428b7f512f70cb778c6 100644 (file)
@@ -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());