From: Sébastien Lesaint Date: Tue, 18 Jul 2017 16:13:06 +0000 (+0200) Subject: SONAR-8798 start and monitor ES script from main process X-Git-Tag: 6.6-RC1~699 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=070e44fc4a7a7ec0f008aa2cbff48075a247e40c;p=sonarqube.git SONAR-8798 start and monitor ES script from main process --- diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/application/SchedulerImpl.java b/server/sonar-process-monitor/src/main/java/org/sonar/application/SchedulerImpl.java index d958deee9e5..3c95aa7f311 100644 --- a/server/sonar-process-monitor/src/main/java/org/sonar/application/SchedulerImpl.java +++ b/server/sonar-process-monitor/src/main/java/org/sonar/application/SchedulerImpl.java @@ -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 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 commandSupplier) { + private void tryToStartJavaProcess(SQProcess process, Supplier commandSupplier) { + tryToStart(process, () -> { + JavaCommand command = commandSupplier.get(); + return processLauncher.launch(command); + }); + } + + private void tryToStartEsProcess(SQProcess process, Supplier commandSupplier) { + tryToStart(process, () -> { + EsCommand command = commandSupplier.get(); + return processLauncher.launch(command); + }); + } + + private void tryToStart(SQProcess process, Supplier 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(); diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/application/process/CommandFactory.java b/server/sonar-process-monitor/src/main/java/org/sonar/application/process/CommandFactory.java index 6091fb9bd0d..d893b9f7c54 100644 --- a/server/sonar-process-monitor/src/main/java/org/sonar/application/process/CommandFactory.java +++ b/server/sonar-process-monitor/src/main/java/org/sonar/application/process/CommandFactory.java @@ -21,7 +21,7 @@ package org.sonar.application.process; public interface CommandFactory { - JavaCommand createEsCommand(); + EsCommand createEsCommand(); JavaCommand createWebCommand(boolean leader); diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/application/process/CommandFactoryImpl.java b/server/sonar-process-monitor/src/main/java/org/sonar/application/process/CommandFactoryImpl.java index e41304f6ed5..1404e4c2d0e 100644 --- a/server/sonar-process-monitor/src/main/java/org/sonar/application/process/CommandFactoryImpl.java +++ b/server/sonar-process-monitor/src/main/java/org/sonar/application/process/CommandFactoryImpl.java @@ -19,14 +19,17 @@ */ 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 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 index 00000000000..684df33df11 --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/application/process/EsCommand.java @@ -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 { + private File executable; + private String url; + private List 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 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 index 00000000000..66aa95d91a7 --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/application/process/EsProcessMonitor.java @@ -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 index 00000000000..76b0046e2c3 --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/application/process/EsSettings.java @@ -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 build() { + Map builder = new HashMap<>(); + configureFileSystem(builder); + configureNetwork(builder); + configureCluster(builder); + configureMarvel(builder); + return builder; + } + + private void configureFileSystem(Map 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 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 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 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 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 builder) { + Set 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); + } + } + +} diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/application/process/ProcessLauncher.java b/server/sonar-process-monitor/src/main/java/org/sonar/application/process/ProcessLauncher.java index 125cfd57627..eb25eccc9ca 100644 --- a/server/sonar-process-monitor/src/main/java/org/sonar/application/process/ProcessLauncher.java +++ b/server/sonar-process-monitor/src/main/java/org/sonar/application/process/ProcessLauncher.java @@ -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. * diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/application/process/ProcessLauncherImpl.java b/server/sonar-process-monitor/src/main/java/org/sonar/application/process/ProcessLauncherImpl.java index a952fae822a..d297b8a02a6 100644 --- a/server/sonar-process-monitor/src/main/java/org/sonar/application/process/ProcessLauncherImpl.java +++ b/server/sonar-process-monitor/src/main/java/org/sonar/application/process/ProcessLauncherImpl.java @@ -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 commands = new ArrayList<>(); + commands.add(esCommand.getExecutable().getAbsolutePath()); + commands.addAll(esCommand.getEsOptions()); + + return create(esCommand, commands); + } + private ProcessBuilder create(JavaCommand javaCommand) { List 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 commands) { ProcessBuilder processBuilder = processBuilderSupplier.get(); processBuilder.command(commands); processBuilder.directory(javaCommand.getWorkDir()); diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/SchedulerImplTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/SchedulerImplTest.java index 9ad38ac6e59..a457afe3a4e 100644 --- a/server/sonar-process-monitor/src/test/java/org/sonar/application/SchedulerImplTest.java +++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/SchedulerImplTest.java @@ -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 processes = new EnumMap<>(ProcessId.class); - private final List commands = synchronizedList(new ArrayList<>()); + private final List> 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 index 00000000000..eb0f6a90e22 --- /dev/null +++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/process/EsSettingsTest.java @@ -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 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 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 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 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 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 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 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 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 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 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 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 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; + } +} diff --git a/server/sonar-search/src/main/java/org/sonar/search/SearchServer.java b/server/sonar-search/src/main/java/org/sonar/search/SearchServer.java index e87008f3c48..af6586a9492 100644 --- a/server/sonar-search/src/main/java/org/sonar/search/SearchServer.java +++ b/server/sonar-search/src/main/java/org/sonar/search/SearchServer.java @@ -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);