]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8798 start and monitor ES script from main process
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Tue, 18 Jul 2017 16:13:06 +0000 (18:13 +0200)
committerDaniel Schwarz <bartfastiel@users.noreply.github.com>
Wed, 9 Aug 2017 13:09:54 +0000 (15:09 +0200)
server/sonar-process-monitor/src/main/java/org/sonar/application/SchedulerImpl.java
server/sonar-process-monitor/src/main/java/org/sonar/application/process/CommandFactory.java
server/sonar-process-monitor/src/main/java/org/sonar/application/process/CommandFactoryImpl.java
server/sonar-process-monitor/src/main/java/org/sonar/application/process/EsCommand.java [new file with mode: 0644]
server/sonar-process-monitor/src/main/java/org/sonar/application/process/EsProcessMonitor.java [new file with mode: 0644]
server/sonar-process-monitor/src/main/java/org/sonar/application/process/EsSettings.java [new file with mode: 0644]
server/sonar-process-monitor/src/main/java/org/sonar/application/process/ProcessLauncher.java
server/sonar-process-monitor/src/main/java/org/sonar/application/process/ProcessLauncherImpl.java
server/sonar-process-monitor/src/test/java/org/sonar/application/SchedulerImplTest.java
server/sonar-process-monitor/src/test/java/org/sonar/application/process/EsSettingsTest.java [new file with mode: 0644]
server/sonar-search/src/main/java/org/sonar/search/SearchServer.java

index d958deee9e59a475c1c281c46e4457b78629d10b..3c95aa7f311827a87eaa6d0676a35d587eb6d5fd 100644 (file)
@@ -31,11 +31,13 @@ import org.slf4j.LoggerFactory;
 import org.sonar.application.config.AppSettings;
 import org.sonar.application.config.ClusterSettings;
 import org.sonar.application.process.CommandFactory;
+import org.sonar.application.process.EsCommand;
 import org.sonar.application.process.JavaCommand;
 import org.sonar.application.process.ProcessLauncher;
 import org.sonar.application.process.Lifecycle;
 import org.sonar.application.process.ProcessEventListener;
 import org.sonar.application.process.ProcessLifecycleListener;
+import org.sonar.application.process.ProcessMonitor;
 import org.sonar.application.process.SQProcess;
 import org.sonar.process.ProcessId;
 
@@ -105,7 +107,7 @@ public class SchedulerImpl implements Scheduler, ProcessEventListener, ProcessLi
   private void tryToStartEs() {
     SQProcess process = processesById.get(ProcessId.ELASTICSEARCH);
     if (process != null) {
-      tryToStartProcess(process, commandFactory::createEsCommand);
+      tryToStartEsProcess(process, commandFactory::createEsCommand);
     }
   }
 
@@ -115,9 +117,9 @@ public class SchedulerImpl implements Scheduler, ProcessEventListener, ProcessLi
       return;
     }
     if (appState.isOperational(ProcessId.WEB_SERVER, false)) {
-      tryToStartProcess(process, () -> commandFactory.createWebCommand(false));
+      tryToStartJavaProcess(process, () -> commandFactory.createWebCommand(false));
     } else if (appState.tryToLockWebLeader()) {
-      tryToStartProcess(process, () -> commandFactory.createWebCommand(true));
+      tryToStartJavaProcess(process, () -> commandFactory.createWebCommand(true));
     } else {
       Optional<String> leader = appState.getLeaderHostName();
       if (leader.isPresent()) {
@@ -131,7 +133,7 @@ public class SchedulerImpl implements Scheduler, ProcessEventListener, ProcessLi
   private void tryToStartCe() {
     SQProcess process = processesById.get(ProcessId.COMPUTE_ENGINE);
     if (process != null && appState.isOperational(ProcessId.WEB_SERVER, false) && isEsClientStartable()) {
-      tryToStartProcess(process, commandFactory::createCeCommand);
+      tryToStartJavaProcess(process, commandFactory::createCeCommand);
     }
   }
 
@@ -140,12 +142,23 @@ public class SchedulerImpl implements Scheduler, ProcessEventListener, ProcessLi
     return appState.isOperational(ProcessId.ELASTICSEARCH, requireLocalEs);
   }
 
-  private void tryToStartProcess(SQProcess process, Supplier<JavaCommand> commandSupplier) {
+  private void tryToStartJavaProcess(SQProcess process, Supplier<JavaCommand> commandSupplier) {
+    tryToStart(process, () -> {
+      JavaCommand command = commandSupplier.get();
+      return processLauncher.launch(command);
+    });
+  }
+
+  private void tryToStartEsProcess(SQProcess process, Supplier<EsCommand> commandSupplier) {
+    tryToStart(process, () -> {
+      EsCommand command = commandSupplier.get();
+      return processLauncher.launch(command);
+    });
+  }
+
+  private void tryToStart(SQProcess process, Supplier<ProcessMonitor> processMonitorSupplier) {
     try {
-      process.start(() -> {
-        JavaCommand command = commandSupplier.get();
-        return processLauncher.launch(command);
-      });
+      process.start(processMonitorSupplier);
     } catch (RuntimeException e) {
       // failed to start command -> stop everything
       terminate();
index 6091fb9bd0d8621126ee85d0c561aeed126cfb2c..d893b9f7c548b91be22da81bfcb530f296a54fe1 100644 (file)
@@ -21,7 +21,7 @@ package org.sonar.application.process;
 
 public interface CommandFactory {
 
-  JavaCommand createEsCommand();
+  EsCommand createEsCommand();
 
   JavaCommand createWebCommand(boolean leader);
 
index e41304f6ed5d249cf152078fb9dca55101e33cb7..1404e4c2d0ed676a64fc0a873b9336a4a6c38991 100644 (file)
  */
 package org.sonar.application.process;
 
+import java.io.File;
+import java.util.Map;
+import java.util.Optional;
 import org.sonar.application.config.AppSettings;
 import org.sonar.process.ProcessId;
 import org.sonar.process.ProcessProperties;
 
-import java.io.File;
-import java.util.Optional;
-
-import static org.sonar.process.ProcessProperties.*;
+import static org.sonar.process.ProcessProperties.HTTPS_PROXY_HOST;
+import static org.sonar.process.ProcessProperties.HTTPS_PROXY_PORT;
+import static org.sonar.process.ProcessProperties.HTTP_PROXY_HOST;
+import static org.sonar.process.ProcessProperties.HTTP_PROXY_PORT;
 
 public class CommandFactoryImpl implements CommandFactory {
   /**
@@ -49,15 +52,43 @@ public class CommandFactoryImpl implements CommandFactory {
   }
 
   @Override
-  public JavaCommand createEsCommand() {
+  public EsCommand createEsCommand() {
     File homeDir = settings.getProps().nonNullValueAsFile(ProcessProperties.PATH_HOME);
-    return newJavaCommand(ProcessId.ELASTICSEARCH, homeDir)
-      .addJavaOptions("-Djava.awt.headless=true")
-      .addJavaOptions(settings.getProps().nonNullValue(ProcessProperties.SEARCH_JAVA_OPTS))
-      .addJavaOptions(settings.getProps().nonNullValue(ProcessProperties.SEARCH_JAVA_ADDITIONAL_OPTS))
-      .setClassName("org.sonar.search.SearchServer")
-      .addClasspath("./lib/common/*")
-      .addClasspath("./lib/search/*");
+    File executable = new File(homeDir, getExecutable());
+    if (!executable.exists()) {
+      throw new IllegalStateException("Cannot find elasticsearch binary");
+    }
+
+    Map<String, String> settingsMap = new EsSettings(settings.getProps()).build();
+
+    EsCommand res = new EsCommand(ProcessId.ELASTICSEARCH)
+      .setWorkDir(executable.getParentFile().getParentFile())
+      .setExecutable(executable)
+      .setArguments(settings.getProps().rawProperties())
+      // TODO add argument to specify log4j configuration file
+      // TODO add argument to specify yaml configuration file
+      .setUrl("http://" + settingsMap.get("http.host") + ":" + settingsMap.get("http.port"));
+
+    settingsMap.entrySet().stream()
+      .filter(entry -> !"path.home".equals(entry.getKey()))
+      .forEach(entry -> res.addEsOption("-E" + entry.getKey() + "=" + entry.getValue()));
+
+    return res;
+
+    // FIXME quid of proxy settings and sonar.search.javaOpts/javaAdditionalOpts
+    // defaults of HTTPS are the same than HTTP defaults
+    // setSystemPropertyToDefaultIfNotSet(command, HTTPS_PROXY_HOST, HTTP_PROXY_HOST);
+    // setSystemPropertyToDefaultIfNotSet(command, HTTPS_PROXY_PORT, HTTP_PROXY_PORT);
+    // command
+    // .addJavaOptions(settings.getProps().nonNullValue(ProcessProperties.SEARCH_JAVA_OPTS))
+    // .addJavaOptions(settings.getProps().nonNullValue(ProcessProperties.SEARCH_JAVA_ADDITIONAL_OPTS));
+  }
+
+  private static String getExecutable() {
+    if (System.getProperty("os.name").startsWith("Windows")) {
+      return "elasticsearch/bin/elasticsearch.bat";
+    }
+    return "elasticsearch/bin/elasticsearch";
   }
 
   @Override
diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/application/process/EsCommand.java b/server/sonar-process-monitor/src/main/java/org/sonar/application/process/EsCommand.java
new file mode 100644 (file)
index 0000000..684df33
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info 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.application.process;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import org.sonar.process.ProcessId;
+
+public class EsCommand extends AbstractCommand<EsCommand> {
+  private File executable;
+  private String url;
+  private List<String> esOptions = new ArrayList<>();
+
+  public EsCommand(ProcessId id) {
+    super(id);
+  }
+
+  public File getExecutable() {
+    return executable;
+  }
+
+  public EsCommand setExecutable(File executable) {
+    this.executable = executable;
+    return this;
+  }
+
+  public String getUrl() {
+    return url;
+  }
+
+  public EsCommand setUrl(String url) {
+    this.url = url;
+    return this;
+  }
+
+  public List<String> getEsOptions() {
+    return esOptions;
+  }
+
+  public EsCommand addEsOption(String s) {
+    if (!s.isEmpty()) {
+      esOptions.add(s);
+    }
+    return this;
+  }
+}
diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/application/process/EsProcessMonitor.java b/server/sonar-process-monitor/src/main/java/org/sonar/application/process/EsProcessMonitor.java
new file mode 100644 (file)
index 0000000..66aa95d
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info 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.application.process;
+
+import java.io.IOException;
+import java.net.ConnectException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.apache.commons.io.IOUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class EsProcessMonitor extends AbstractProcessMonitor {
+  private static final Logger LOG = LoggerFactory.getLogger(EsProcessMonitor.class);
+  private static final int WAIT_FOR_UP_DELAY_IN_MILLIS = 100;
+  private static final int WAIT_FOR_UP_TIMEOUT = 10 * 60; /* 1min */
+
+  private final AtomicBoolean nodeUp = new AtomicBoolean(false);
+  private final AtomicBoolean nodeOperational = new AtomicBoolean(false);
+  private final URL healthCheckURL;
+
+  public EsProcessMonitor(Process process, String url) throws MalformedURLException {
+    super(process);
+    this.healthCheckURL = new URL(url + "/_cluster/health?wait_for_status=yellow&timeout=30s");
+  }
+
+  @Override
+  public boolean isOperational() {
+    if (nodeOperational.get()) {
+      return true;
+    }
+
+    try {
+      boolean flag = checkOperational();
+      if (flag) {
+        nodeOperational.set(true);
+      }
+    } catch (InterruptedException e) {
+      LOG.trace("Interrupted while checking ES node is operational", e);
+      Thread.currentThread().interrupt();
+    }
+    return nodeOperational.get();
+  }
+
+  private boolean checkOperational() throws InterruptedException {
+    int i = 0;
+    Status status = checkStatus();
+    do {
+      if (status != Status.CONNECTION_REFUSED) {
+        nodeUp.set(true);
+      } else {
+        Thread.sleep(WAIT_FOR_UP_DELAY_IN_MILLIS);
+        i++;
+        status = checkStatus();
+      }
+    } while (!nodeUp.get() && i < WAIT_FOR_UP_TIMEOUT);
+    return status == Status.YELLOW || status == Status.GREEN;
+  }
+
+  private Status checkStatus() {
+    try {
+      URLConnection urlConnection = healthCheckURL.openConnection();
+      urlConnection.connect();
+      String response = IOUtils.toString(urlConnection.getInputStream());
+      if (response.contains("\"status\":\"green\"")) {
+        return Status.GREEN;
+      } else if (response.contains("\"status\":\"yellow\"")) {
+        return Status.YELLOW;
+      } else if (response.contains("\"status\":\"red\"")) {
+        return Status.RED;
+      }
+      return Status.KO;
+    } catch (ConnectException e) {
+      return Status.CONNECTION_REFUSED;
+    } catch (IOException e) {
+      LOG.error("Unexpected error occurred while checking ES node status using WebService API", e);
+      return Status.KO;
+    }
+  }
+
+  enum Status {
+    CONNECTION_REFUSED, KO, RED, YELLOW, GREEN
+  }
+
+  @Override
+  public void askForStop() {
+    process.destroy();
+  }
+
+  @Override
+  public boolean askedForRestart() {
+    // ES does not support asking for restart
+    return false;
+  }
+
+  @Override
+  public void acknowledgeAskForRestart() {
+    // nothing to do
+  }
+}
diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/application/process/EsSettings.java b/server/sonar-process-monitor/src/main/java/org/sonar/application/process/EsSettings.java
new file mode 100644 (file)
index 0000000..76b0046
--- /dev/null
@@ -0,0 +1,192 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info 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.application.process;
+
+import java.io.File;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.UUID;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.Props;
+
+import static java.lang.String.valueOf;
+
+public class EsSettings {
+
+  private static final Logger LOGGER = LoggerFactory.getLogger(EsSettings.class);
+  private static final String PROP_MARVEL_HOSTS = "sonar.search.marvelHosts";
+  private static final String CLUSTER_SEARCH_NODE_NAME = "sonar.cluster.search.nodeName";
+  private static final String STANDALONE_NODE_NAME = "sonarqube";
+
+  private final Props props;
+
+  private final boolean clusterEnabled;
+  private final String clusterName;
+  private final String nodeName;
+
+  EsSettings(Props props) {
+    this.props = props;
+
+    this.clusterName = props.nonNullValue(ProcessProperties.CLUSTER_NAME);
+    this.clusterEnabled = props.valueAsBoolean(ProcessProperties.CLUSTER_ENABLED);
+    if (this.clusterEnabled) {
+      this.nodeName = props.value(CLUSTER_SEARCH_NODE_NAME, "sonarqube-" + UUID.randomUUID().toString());
+    } else {
+      this.nodeName = STANDALONE_NODE_NAME;
+    }
+  }
+
+  Map<String, String> build() {
+    Map<String, String> builder = new HashMap<>();
+    configureFileSystem(builder);
+    configureNetwork(builder);
+    configureCluster(builder);
+    configureMarvel(builder);
+    return builder;
+  }
+
+  private void configureFileSystem(Map<String, String> builder) {
+    File homeDir = props.nonNullValueAsFile(ProcessProperties.PATH_HOME);
+    File dataDir;
+    File logDir;
+
+    // data dir
+    String dataPath = props.value(ProcessProperties.PATH_DATA);
+    if (StringUtils.isNotEmpty(dataPath)) {
+      dataDir = new File(dataPath, "es");
+    } else {
+      dataDir = new File(homeDir, "data/es");
+    }
+    builder.put("path.data", dataDir.getAbsolutePath());
+
+    String tempPath = props.value(ProcessProperties.PATH_TEMP);
+    builder.put("path.home", new File(tempPath, "es").getAbsolutePath());
+
+    // log dir
+    String logPath = props.value(ProcessProperties.PATH_LOGS);
+    if (StringUtils.isNotEmpty(logPath)) {
+      logDir = new File(logPath);
+    } else {
+      logDir = new File(homeDir, "log");
+    }
+    builder.put("path.logs", logDir.getAbsolutePath());
+  }
+
+  private void configureNetwork(Map<String, String> builder) {
+    InetAddress host = readHost();
+    int port = Integer.parseInt(props.nonNullValue(ProcessProperties.SEARCH_PORT));
+    LOGGER.info("Elasticsearch listening on {}:{}", host, port);
+
+    builder.put("transport.tcp.port", valueOf(port));
+    builder.put("transport.host", valueOf(host.getHostAddress()));
+    builder.put("network.host", valueOf(host.getHostAddress()));
+
+    // Elasticsearch sets the default value of TCP reuse address to true only on non-MSWindows machines, but why ?
+    builder.put("network.tcp.reuse_address", valueOf(true));
+
+    int httpPort = props.valueAsInt(ProcessProperties.SEARCH_HTTP_PORT, -1);
+    if (httpPort < 0) {
+      // standard configuration
+      builder.put("http.enabled", valueOf(false));
+    } else {
+      LOGGER.warn("Elasticsearch HTTP connector is enabled on port {}. MUST NOT BE USED FOR PRODUCTION", httpPort);
+      // see https://github.com/lmenezes/elasticsearch-kopf/issues/195
+      builder.put("http.cors.enabled", valueOf(true));
+      builder.put("http.cors.allow-origin", "*");
+      builder.put("http.enabled", valueOf(true));
+      builder.put("http.host", host.getHostAddress());
+      builder.put("http.port", valueOf(httpPort));
+    }
+  }
+
+  private InetAddress readHost() {
+    String hostProperty = props.nonNullValue(ProcessProperties.SEARCH_HOST);
+    try {
+      return InetAddress.getByName(hostProperty);
+    } catch (UnknownHostException e) {
+      throw new IllegalStateException("Can not resolve host [" + hostProperty + "]. Please check network settings and property " + ProcessProperties.SEARCH_HOST, e);
+    }
+  }
+
+  void configureIndexDefaults(Map<String, String> builder) {
+    configureIndexDefaultsForCluster(builder);
+    builder.put("index.number_of_shards", "1");
+    builder.put("index.refresh_interval", "30s");
+    builder.put("action.auto_create_index", String.valueOf(false));
+    builder.put("index.mapper.dynamic", String.valueOf(false));
+  }
+
+  private void configureIndexDefaultsForCluster(Map<String, String> builder) {
+    builder.put("index.number_of_replicas", String.valueOf(computeReplicationFactor()));
+  }
+
+  private int computeReplicationFactor() {
+    if (clusterEnabled) {
+      return props.valueAsInt(ProcessProperties.SEARCH_REPLICAS, 1);
+    }
+    return 0;
+  }
+
+  private void configureCluster(Map<String, String> builder) {
+    // Default value in a standalone mode, not overridable
+
+    int minimumMasterNodes = 1;
+    String initialStateTimeOut = "30s";
+
+    if (clusterEnabled) {
+      minimumMasterNodes = props.valueAsInt(ProcessProperties.SEARCH_MINIMUM_MASTER_NODES, 2);
+      initialStateTimeOut = props.value(ProcessProperties.SEARCH_INITIAL_STATE_TIMEOUT, "120s");
+
+      String hosts = props.value(ProcessProperties.CLUSTER_SEARCH_HOSTS, "");
+      LOGGER.info("Elasticsearch cluster enabled. Connect to hosts [{}]", hosts);
+      builder.put("discovery.zen.ping.unicast.hosts", hosts);
+    }
+
+    builder.put("discovery.zen.minimum_master_nodes", valueOf(minimumMasterNodes));
+    builder.put("discovery.initial_state_timeout", initialStateTimeOut);
+    builder.put("cluster.name", clusterName);
+    builder.put("cluster.routing.allocation.awareness.attributes", "rack_id");
+    builder.put("node.attr.rack_id", nodeName);
+    builder.put("node.name", nodeName);
+    builder.put("node.data", valueOf(true));
+    builder.put("node.master", valueOf(true));
+  }
+
+  private void configureMarvel(Map<String, String> builder) {
+    Set<String> marvels = new TreeSet<>();
+    marvels.addAll(Arrays.asList(StringUtils.split(props.value(PROP_MARVEL_HOSTS, ""), ",")));
+
+    // If we're collecting indexing data send them to the Marvel host(s)
+    if (!marvels.isEmpty()) {
+      String hosts = StringUtils.join(marvels, ",");
+      LOGGER.info("Elasticsearch Marvel is enabled for %s", hosts);
+      builder.put("marvel.agent.exporter.es.hosts", hosts);
+    }
+  }
+
+}
index 125cfd57627290d6c3578b4002a205b89669393f..eb25eccc9ca79c9fbb505da8f24ef2d1e9b87d47 100644 (file)
@@ -26,6 +26,13 @@ public interface ProcessLauncher extends Closeable {
   @Override
   void close();
 
+  /**
+   * Launch an ES command.
+   *
+   * @throws IllegalStateException if an error occurs
+   */
+  ProcessMonitor launch(EsCommand esCommand);
+
   /**
    * Launch a Java command.
    * 
index a952fae822ab2534f0252776fa4ffd5c1b4254ef..d297b8a02a6441b3a56f3a6d96987144f60c1c2e 100644 (file)
@@ -62,18 +62,35 @@ public class ProcessLauncherImpl implements ProcessLauncher {
     allProcessesCommands.close();
   }
 
+  @Override
+  public ProcessMonitor launch(EsCommand esCommand) {
+    Process process = null;
+    try {
+      ProcessBuilder processBuilder = create(esCommand);
+      LOG.info("Launch process[{}]: {}", esCommand.getProcessId().getKey(), String.join(" ", processBuilder.command()));
+
+      process = processBuilder.start();
+
+      return new EsProcessMonitor(process, esCommand.getUrl());
+    } catch (Exception e) {
+      // just in case
+      if (process != null) {
+        process.destroyForcibly();
+      }
+      throw new IllegalStateException(format("Fail to launch process [%s]", esCommand.getProcessId().getKey()), e);
+    }
+  }
+
   @Override
   public ProcessMonitor launch(JavaCommand javaCommand) {
     Process process = null;
-    ProcessCommands commands;
     try {
-      commands = allProcessesCommands.createAfterClean(javaCommand.getProcessId().getIpcIndex());
+      ProcessCommands commands = allProcessesCommands.createAfterClean(javaCommand.getProcessId().getIpcIndex());
 
       ProcessBuilder processBuilder = create(javaCommand);
       LOG.info("Launch process[{}]: {}", javaCommand.getProcessId().getKey(), String.join(" ", processBuilder.command()));
       process = processBuilder.start();
       return new ProcessCommandsProcessMonitor(process, commands);
-
     } catch (Exception e) {
       // just in case
       if (process != null) {
@@ -83,6 +100,14 @@ public class ProcessLauncherImpl implements ProcessLauncher {
     }
   }
 
+  private ProcessBuilder create(EsCommand esCommand) {
+    List<String> commands = new ArrayList<>();
+    commands.add(esCommand.getExecutable().getAbsolutePath());
+    commands.addAll(esCommand.getEsOptions());
+
+    return create(esCommand, commands);
+  }
+
   private ProcessBuilder create(JavaCommand javaCommand) {
     List<String> commands = new ArrayList<>();
     commands.add(buildJavaPath());
@@ -94,6 +119,10 @@ public class ProcessLauncherImpl implements ProcessLauncher {
     commands.add(javaCommand.getClassName());
     commands.add(buildPropertiesFile(javaCommand).getAbsolutePath());
 
+    return create(javaCommand, commands);
+  }
+
+  private ProcessBuilder create(AbstractCommand<?> javaCommand, List<String> commands) {
     ProcessBuilder processBuilder = processBuilderSupplier.get();
     processBuilder.command(commands);
     processBuilder.directory(javaCommand.getWorkDir());
index 9ad38ac6e59f1d5c8516e65c8d3f4526b4a984a1..a457afe3a4ec8f99d1d84346ccbb531077e8a183 100644 (file)
@@ -35,7 +35,9 @@ import org.junit.rules.TestRule;
 import org.junit.rules.Timeout;
 import org.mockito.Mockito;
 import org.sonar.application.config.TestAppSettings;
+import org.sonar.application.process.AbstractCommand;
 import org.sonar.application.process.CommandFactory;
+import org.sonar.application.process.EsCommand;
 import org.sonar.application.process.JavaCommand;
 import org.sonar.application.process.ProcessLauncher;
 import org.sonar.application.process.ProcessMonitor;
@@ -54,7 +56,7 @@ import static org.sonar.process.ProcessId.WEB_SERVER;
 
 public class SchedulerImplTest {
 
-  private static final JavaCommand ES_COMMAND = new JavaCommand(ELASTICSEARCH);
+  private static final EsCommand ES_COMMAND = new EsCommand(ELASTICSEARCH);
   private static final JavaCommand WEB_LEADER_COMMAND = new JavaCommand(WEB_SERVER);
   private static final JavaCommand WEB_FOLLOWER_COMMAND = new JavaCommand(WEB_SERVER);
   private static final JavaCommand CE_COMMAND = new JavaCommand(COMPUTE_ENGINE);
@@ -307,7 +309,7 @@ public class SchedulerImplTest {
 
   private static class TestCommandFactory implements CommandFactory {
     @Override
-    public JavaCommand createEsCommand() {
+    public EsCommand createEsCommand() {
       return ES_COMMAND;
     }
 
@@ -324,11 +326,20 @@ public class SchedulerImplTest {
 
   private class TestProcessLauncher implements ProcessLauncher {
     private final EnumMap<ProcessId, TestProcess> processes = new EnumMap<>(ProcessId.class);
-    private final List<JavaCommand> commands = synchronizedList(new ArrayList<>());
+    private final List<AbstractCommand<?>> commands = synchronizedList(new ArrayList<>());
     private ProcessId makeStartupFail = null;
 
+    @Override
+    public ProcessMonitor launch(EsCommand esCommand) {
+      return launchImpl(esCommand);
+    }
+
     @Override
     public ProcessMonitor launch(JavaCommand javaCommand) {
+      return launchImpl(javaCommand);
+    }
+
+    private ProcessMonitor launchImpl(AbstractCommand<?> javaCommand) {
       commands.add(javaCommand);
       if (makeStartupFail == javaCommand.getProcessId()) {
         throw new IllegalStateException("cannot start " + javaCommand.getProcessId());
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/process/EsSettingsTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/process/EsSettingsTest.java
new file mode 100644 (file)
index 0000000..eb0f6a9
--- /dev/null
@@ -0,0 +1,224 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info 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.application.process;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+import java.util.Properties;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.Props;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class EsSettingsTest {
+
+  @Rule
+  public TemporaryFolder temp = new TemporaryFolder();
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Test
+  public void test_default_settings() throws Exception {
+    File homeDir = temp.newFolder();
+    Props props = new Props(new Properties());
+    props.set(ProcessProperties.SEARCH_PORT, "1234");
+    props.set(ProcessProperties.SEARCH_HOST, "127.0.0.1");
+    props.set(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath());
+    props.set(ProcessProperties.CLUSTER_NAME, "sonarqube");
+
+    EsSettings esSettings = new EsSettings(props);
+
+    Map<String, String> generated = esSettings.build();
+    assertThat(generated.get("transport.tcp.port")).isEqualTo("1234");
+    assertThat(generated.get("transport.host")).isEqualTo("127.0.0.1");
+
+    // no cluster, but cluster and node names are set though
+    assertThat(generated.get("cluster.name")).isEqualTo("sonarqube");
+    assertThat(generated.get("node.name")).isEqualTo("sonarqube");
+
+    assertThat(generated.get("path.data")).isNotNull();
+    assertThat(generated.get("path.logs")).isNotNull();
+    assertThat(generated.get("path.home")).isNotNull();
+
+    // http is disabled for security reasons
+    assertThat(generated.get("http.enabled")).isEqualTo("false");
+
+    assertThat(generated.get("index.number_of_replicas")).isEqualTo("0");
+    assertThat(generated.get("discovery.zen.ping.unicast.hosts")).isNull();
+    assertThat(generated.get("discovery.zen.minimum_master_nodes")).isEqualTo("1");
+    assertThat(generated.get("discovery.initial_state_timeout")).isEqualTo("30s");
+  }
+
+  @Test
+  public void override_dirs() throws Exception {
+    File dataDir = temp.newFolder();
+    File logDir = temp.newFolder();
+    File tempDir = temp.newFolder();
+    Props props = minProps(false);
+    props.set(ProcessProperties.PATH_DATA, dataDir.getAbsolutePath());
+    props.set(ProcessProperties.PATH_LOGS, logDir.getAbsolutePath());
+    props.set(ProcessProperties.PATH_TEMP, tempDir.getAbsolutePath());
+
+    Map<String, String> settings = new EsSettings(props).build();
+
+    assertThat(settings.get("path.data")).isEqualTo(new File(dataDir, "es").getAbsolutePath());
+    assertThat(settings.get("path.logs")).isEqualTo(logDir.getAbsolutePath());
+    assertThat(settings.get("path.home")).isEqualTo(new File(tempDir, "es").getAbsolutePath());
+  }
+
+  @Test
+  public void cluster_is_enabled() throws Exception {
+    Props props = minProps(true);
+    props.set(ProcessProperties.CLUSTER_SEARCH_HOSTS, "1.2.3.4:9000,1.2.3.5:8080");
+    Map<String, String> settings = new EsSettings(props).build();
+
+    assertThat(settings.get("index.number_of_replicas")).isEqualTo("1");
+    assertThat(settings.get("discovery.zen.ping.unicast.hosts")).isEqualTo("1.2.3.4:9000,1.2.3.5:8080");
+    assertThat(settings.get("discovery.zen.minimum_master_nodes")).isEqualTo("2");
+    assertThat(settings.get("discovery.initial_state_timeout")).isEqualTo("120s");
+  }
+
+  @Test
+  public void incorrect_values_of_minimum_master_nodes() throws Exception {
+    Props props = minProps(true);
+    props.set(ProcessProperties.SEARCH_MINIMUM_MASTER_NODES, "ꝱꝲꝳପ");
+
+    EsSettings underTest = new EsSettings(props);
+
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Value of property sonar.search.minimumMasterNodes is not an integer:");
+    underTest.build();
+  }
+
+  @Test
+  public void cluster_is_enabled_with_defined_minimum_master_nodes() throws Exception {
+    Props props = minProps(true);
+    props.set(ProcessProperties.SEARCH_MINIMUM_MASTER_NODES, "5");
+    Map<String, String> settings = new EsSettings(props).build();
+
+    assertThat(settings.get("discovery.zen.minimum_master_nodes")).isEqualTo("5");
+  }
+
+  @Test
+  public void cluster_is_enabled_with_defined_initialTimeout() throws Exception {
+    Props props = minProps(true);
+    props.set(ProcessProperties.SEARCH_INITIAL_STATE_TIMEOUT, "10s");
+    Map<String, String> settings = new EsSettings(props).build();
+
+    assertThat(settings.get("discovery.initial_state_timeout")).isEqualTo("10s");
+  }
+
+  @Test
+  public void in_standalone_initialTimeout_is_not_overridable() throws Exception {
+    Props props = minProps(false);
+    props.set(ProcessProperties.SEARCH_INITIAL_STATE_TIMEOUT, "10s");
+    Map<String, String> settings = new EsSettings(props).build();
+
+    assertThat(settings.get("discovery.initial_state_timeout")).isEqualTo("30s");
+  }
+
+  @Test
+  public void in_standalone_minimumMasterNodes_is_not_overridable() throws Exception {
+    Props props = minProps(false);
+    props.set(ProcessProperties.SEARCH_MINIMUM_MASTER_NODES, "5");
+    Map<String, String> settings = new EsSettings(props).build();
+
+    assertThat(settings.get("discovery.zen.minimum_master_nodes")).isEqualTo("1");
+  }
+
+
+  @Test
+  public void in_standalone_searchReplicas_is_not_overridable() throws Exception {
+    Props props = minProps(false);
+    props.set(ProcessProperties.SEARCH_REPLICAS, "5");
+    Map<String, String> settings = new EsSettings(props).build();
+
+    assertThat(settings.get("index.number_of_replicas")).isEqualTo("0");
+  }
+
+  @Test
+  public void cluster_is_enabled_with_defined_replicas() throws Exception {
+    Props props = minProps(true);
+    props.set(ProcessProperties.SEARCH_REPLICAS, "5");
+    Map<String, String> settings = new EsSettings(props).build();
+
+    assertThat(settings.get("index.number_of_replicas")).isEqualTo("5");
+  }
+
+  @Test
+  public void incorrect_values_of_replicas() throws Exception {
+    Props props = minProps(true);
+
+    props.set(ProcessProperties.SEARCH_REPLICAS, "ꝱꝲꝳପ");
+
+    EsSettings underTest = new EsSettings(props);
+
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Value of property sonar.search.replicas is not an integer:");
+    underTest.build();
+  }
+
+  @Test
+  public void enable_marvel() throws Exception {
+    Props props = minProps(false);
+    props.set("sonar.search.marvelHosts", "127.0.0.2,127.0.0.3");
+    Map<String, String> settings = new EsSettings(props).build();
+
+    assertThat(settings.get("marvel.agent.exporter.es.hosts")).isEqualTo("127.0.0.2,127.0.0.3");
+  }
+
+  @Test
+  public void enable_http_connector() throws Exception {
+    Props props = minProps(false);
+    props.set(ProcessProperties.SEARCH_HTTP_PORT, "9010");
+    Map<String, String> settings = new EsSettings(props).build();
+
+    assertThat(settings.get("http.port")).isEqualTo("9010");
+    assertThat(settings.get("http.host")).isEqualTo("127.0.0.1");
+    assertThat(settings.get("http.enabled")).isEqualTo("true");
+  }
+
+  @Test
+  public void enable_http_connector_different_host() throws Exception {
+    Props props = minProps(false);
+    props.set(ProcessProperties.SEARCH_HTTP_PORT, "9010");
+    props.set(ProcessProperties.SEARCH_HOST, "127.0.0.2");
+    Map<String, String> settings = new EsSettings(props).build();
+
+    assertThat(settings.get("http.port")).isEqualTo("9010");
+    assertThat(settings.get("http.host")).isEqualTo("127.0.0.2");
+    assertThat(settings.get("http.enabled")).isEqualTo("true");
+  }
+
+  private Props minProps(boolean cluster) throws IOException {
+    File homeDir = temp.newFolder();
+    Props props = new Props(new Properties());
+    ProcessProperties.completeDefaults(props);
+    props.set(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath());
+    props.set(ProcessProperties.CLUSTER_ENABLED, Boolean.toString(cluster));
+    return props;
+  }
+}
index e87008f3c482d24a387bb67fada004cdbf8e7526..af6586a94922b501c83915ef25ec04ab2c3d39c0 100644 (file)
@@ -71,6 +71,9 @@ public class SearchServer implements Monitored {
       .forEach(entry -> command.add("-E" + entry.getKey() + "=" + entry.getValue()));
     url = "http://"+settingsMap.get("http.host") + ":" + settingsMap.get("http.port");
     System.out.println(command.stream().collect(Collectors.joining(" ")));
+
+
+
     ProcessBuilder builder = new ProcessBuilder(command)
       .directory(new File(path.getParent().toAbsolutePath().toString()));
     builder.redirectOutput(ProcessBuilder.Redirect.PIPE);