From ae5637262461062c7e5fac17c1d6ac5792b5284d Mon Sep 17 00:00:00 2001 From: =?utf8?q?S=C3=A9bastien=20Lesaint?= Date: Fri, 18 Aug 2017 16:11:54 +0200 Subject: [PATCH] SONAR-9590 only heap and heap dump options are now in search.javaOpts --- .../process/ProcessLauncherImpl.java | 34 +-- .../sonar/application/SchedulerImplTest.java | 36 ++- .../process/ProcessLauncherImplTest.java | 17 +- .../org/sonar/process/ProcessProperties.java | 24 +- .../process/command/AbstractCommand.java | 14 +- .../process/command/CommandFactoryImpl.java | 59 +++-- .../org/sonar/process/command/EsCommand.java | 20 +- .../sonar/process/command/JavaCommand.java | 47 ++-- .../process/jmvoptions/CeJvmOptions.java | 33 +++ .../process/jmvoptions/EsJvmOptions.java | 66 +++++ .../sonar/process/jmvoptions/JvmOptions.java | 79 ++++++ .../process/jmvoptions/WebJvmOptions.java | 33 +++ .../process/jmvoptions/package-info.java | 23 ++ .../process/command/AbstractCommandTest.java | 29 ++- .../process/command/JavaCommandTest.java | 22 +- .../process/jmvoptions/CeJvmOptionsTest.java | 42 ++++ .../process/jmvoptions/EsJvmOptionsTest.java | 108 ++++++++ .../process/jmvoptions/JvmOptionsTest.java | 232 ++++++++++++++++++ .../process/jmvoptions/WebJvmOptionsTest.java | 43 ++++ .../src/main/assembly/conf/sonar.properties | 18 +- .../main/java/org/sonar/application/App.java | 2 +- .../org/sonar/test/ExceptionCauseMatcher.java | 2 +- .../org/sonarqube/tests/Category1Suite.java | 6 - .../org/sonarqube/tests/Category2Suite.java | 6 - .../org/sonarqube/tests/Category3Suite.java | 6 - .../org/sonarqube/tests/Category4Suite.java | 6 - .../org/sonarqube/tests/Category6Suite.java | 6 - 27 files changed, 804 insertions(+), 209 deletions(-) create mode 100644 server/sonar-process/src/main/java/org/sonar/process/jmvoptions/CeJvmOptions.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/jmvoptions/EsJvmOptions.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/jmvoptions/JvmOptions.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/jmvoptions/WebJvmOptions.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/jmvoptions/package-info.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/jmvoptions/CeJvmOptionsTest.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/jmvoptions/EsJvmOptionsTest.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/jmvoptions/JvmOptionsTest.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/jmvoptions/WebJvmOptionsTest.java 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 bc19b2a2a69..549e4ddfe4e 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 @@ -23,15 +23,12 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.nio.charset.Charset; -import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.function.Supplier; -import java.util.stream.Collectors; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,6 +36,7 @@ import org.sonar.process.ProcessId; import org.sonar.process.command.AbstractCommand; import org.sonar.process.command.EsCommand; import org.sonar.process.command.JavaCommand; +import org.sonar.process.jmvoptions.JvmOptions; import org.sonar.process.sharedmemoryfile.AllProcessesCommands; import org.sonar.process.sharedmemoryfile.ProcessCommands; @@ -50,11 +48,6 @@ import static org.sonar.process.ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT; public class ProcessLauncherImpl implements ProcessLauncher { private static final Logger LOG = LoggerFactory.getLogger(ProcessLauncherImpl.class); - private static final String ELASTICSEARCH_JVM_OPTIONS_HEADER = "# This file has been automatically generated by SonarQube during startup.\n" + - "# Please use the sonar.search.javaOpts in sonar.properties to specify jvm options for Elasticsearch\n" + - "\n" + - "# DO NOT EDIT THIS FILE\n" + - "\n"; private final File tempDir; private final AllProcessesCommands allProcessesCommands; @@ -106,7 +99,7 @@ public class ProcessLauncherImpl implements ProcessLauncher { try { IOUtils.copy(getClass().getResourceAsStream("elasticsearch.yml"), new FileOutputStream(new File(confDir, "elasticsearch.yml"))); - writeJvmOptions(esCommand, new File(confDir, "jvm.options")); + esCommand.getEsJvmOptions().writeToJvmOptionFile(new File(confDir, "jvm.options")); esCommand.getLog4j2Properties().store(new FileOutputStream(new File(confDir, "log4j2.properties")), "log42 properties file for ES bundled in SonarQube"); } catch (IOException e) { throw new IllegalStateException("Failed to write ES configuration files", e); @@ -150,29 +143,10 @@ public class ProcessLauncherImpl implements ProcessLauncher { return create(esCommand, commands); } - private static void writeJvmOptions(EsCommand esCommand, File jvmOptionsFile) { - String jvmOptions = esCommand.getJvmOptions() - .stream() - - // we do not expect the user to use parameters containing " -" - .map(s -> s.split(" (?=-)")) - .flatMap(Arrays::stream) - .collect(Collectors.joining("\n")); - String jvmOptionsContent = ELASTICSEARCH_JVM_OPTIONS_HEADER + jvmOptions; - try { - Files.write(jvmOptionsFile.toPath(), jvmOptionsContent.getBytes(Charset.forName("UTF-8"))); - } catch (IOException e) { - throw new IllegalStateException("Cannot write Elasticsearch jvm options file", e); - } - } - - private ProcessBuilder create(JavaCommand javaCommand) { + private ProcessBuilder create(JavaCommand javaCommand) { List commands = new ArrayList<>(); commands.add(buildJavaPath()); - commands.addAll(javaCommand.getJavaOptions()); - // TODO warning - does it work if temp dir contains a whitespace ? - // TODO move to CommandFactory ? - commands.add(format("-Djava.io.tmpdir=%s", tempDir.getAbsolutePath())); + commands.addAll(javaCommand.getJvmOptions().getAll()); commands.addAll(buildClasspath(javaCommand)); commands.add(javaCommand.getClassName()); commands.add(buildPropertiesFile(javaCommand).getAbsolutePath()); 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 231c2627450..f7336820816 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 @@ -19,6 +19,7 @@ */ package org.sonar.application; +import java.io.File; import java.io.InputStream; import java.util.ArrayList; import java.util.EnumMap; @@ -26,11 +27,13 @@ import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.junit.After; +import org.junit.Before; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.DisableOnDebug; import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; import org.junit.rules.TestRule; import org.junit.rules.Timeout; import org.mockito.Mockito; @@ -56,15 +59,17 @@ import static org.sonar.process.ProcessId.WEB_SERVER; public class SchedulerImplTest { - 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); - @Rule public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60)); @Rule public ExpectedException expectedException = ExpectedException.none(); + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private EsCommand esCommand; + private JavaCommand webLeaderCommand; + private JavaCommand webFollowerCommand; + private JavaCommand ceCommand; private AppReloader appReloader = mock(AppReloader.class); private TestAppSettings settings = new TestAppSettings(); @@ -73,6 +78,15 @@ public class SchedulerImplTest { private TestAppState appState = new TestAppState(); private List orderedStops = synchronizedList(new ArrayList<>()); + @Before + public void setUp() throws Exception { + File tempDir = temporaryFolder.newFolder(); + esCommand = new EsCommand(ELASTICSEARCH, tempDir); + webLeaderCommand = new JavaCommand(WEB_SERVER, tempDir); + webFollowerCommand = new JavaCommand(WEB_SERVER, tempDir); + ceCommand = new JavaCommand(COMPUTE_ENGINE, tempDir); + } + @After public void tearDown() throws Exception { processLauncher.close(); @@ -95,7 +109,7 @@ public class SchedulerImplTest { TestProcess web = processLauncher.waitForProcess(WEB_SERVER); assertThat(web.isAlive()).isTrue(); assertThat(processLauncher.processes).hasSize(2); - assertThat(processLauncher.commands).containsExactly(ES_COMMAND, WEB_LEADER_COMMAND); + assertThat(processLauncher.commands).containsExactly(esCommand, webLeaderCommand); // web becomes operational -> CE is starting web.operational = true; @@ -103,7 +117,7 @@ public class SchedulerImplTest { TestProcess ce = processLauncher.waitForProcess(COMPUTE_ENGINE); assertThat(ce.isAlive()).isTrue(); assertThat(processLauncher.processes).hasSize(3); - assertThat(processLauncher.commands).containsExactly(ES_COMMAND, WEB_LEADER_COMMAND, CE_COMMAND); + assertThat(processLauncher.commands).containsExactly(esCommand, webLeaderCommand, ceCommand); // all processes are up processLauncher.processes.values().forEach(p -> assertThat(p.isAlive()).isTrue()); @@ -307,20 +321,20 @@ public class SchedulerImplTest { } } - private static class TestCommandFactory implements CommandFactory { + private class TestCommandFactory implements CommandFactory { @Override public EsCommand createEsCommand() { - return ES_COMMAND; + return esCommand; } @Override public JavaCommand createWebCommand(boolean leader) { - return leader ? WEB_LEADER_COMMAND : WEB_FOLLOWER_COMMAND; + return leader ? webLeaderCommand : webFollowerCommand; } @Override public JavaCommand createCeCommand() { - return CE_COMMAND; + return ceCommand; } } diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/application/process/ProcessLauncherImplTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/application/process/ProcessLauncherImplTest.java index 044aa7b4310..a0a4bba391d 100644 --- a/server/sonar-process-monitor/src/test/java/org/sonar/application/process/ProcessLauncherImplTest.java +++ b/server/sonar-process-monitor/src/test/java/org/sonar/application/process/ProcessLauncherImplTest.java @@ -30,9 +30,10 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; +import org.sonar.process.ProcessId; import org.sonar.process.command.JavaCommand; +import org.sonar.process.jmvoptions.JvmOptions; import org.sonar.process.sharedmemoryfile.AllProcessesCommands; -import org.sonar.process.ProcessId; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.data.MapEntry.entry; @@ -54,13 +55,15 @@ public class ProcessLauncherImplTest { File tempDir = temp.newFolder(); TestProcessBuilder processBuilder = new TestProcessBuilder(); ProcessLauncher underTest = new ProcessLauncherImpl(tempDir, commands, () -> processBuilder); - JavaCommand command = new JavaCommand(ProcessId.ELASTICSEARCH); + JavaCommand command = new JavaCommand<>(ProcessId.ELASTICSEARCH, temp.newFolder()); command.addClasspath("lib/*.class"); command.addClasspath("lib/*.jar"); command.setArgument("foo", "bar"); command.setClassName("org.sonarqube.Main"); command.setEnvVariable("VAR1", "valueOfVar1"); - command.setWorkDir(temp.newFolder()); + command.setJvmOptions(new JvmOptions<>() + .add("-Dfoo=bar") + .add("-Dfoo2=bar2")); ProcessMonitor monitor = underTest.launch(command); @@ -68,7 +71,8 @@ public class ProcessLauncherImplTest { assertThat(processBuilder.started).isTrue(); assertThat(processBuilder.commands.get(0)).endsWith("java"); assertThat(processBuilder.commands).containsSequence( - "-Djava.io.tmpdir=" + tempDir.getAbsolutePath(), + "-Dfoo=bar", + "-Dfoo2=bar2", "-cp", "lib/*.class" + System.getProperty("path.separator") + "lib/*.jar", "org.sonarqube.Main"); @@ -84,9 +88,10 @@ public class ProcessLauncherImplTest { File tempDir = temp.newFolder(); TestProcessBuilder processBuilder = new TestProcessBuilder(); ProcessLauncher underTest = new ProcessLauncherImpl(tempDir, commands, () -> processBuilder); - JavaCommand command = new JavaCommand(ProcessId.ELASTICSEARCH); + JavaCommand command = new JavaCommand<>(ProcessId.ELASTICSEARCH, temp.newFolder()); command.setArgument("foo", "bar"); command.setArgument("baz", "woo"); + command.setJvmOptions(new JvmOptions<>()); underTest.launch(command); @@ -116,7 +121,7 @@ public class ProcessLauncherImplTest { expectedException.expect(IllegalStateException.class); expectedException.expectMessage("Fail to launch process [es]"); - underTest.launch(new JavaCommand(ProcessId.ELASTICSEARCH)); + underTest.launch(new JavaCommand(ProcessId.ELASTICSEARCH, temp.newFolder())); } private static class TestProcessBuilder implements ProcessLauncherImpl.ProcessBuilder { diff --git a/server/sonar-process/src/main/java/org/sonar/process/ProcessProperties.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessProperties.java index 63a8bc8fb17..d93697144a0 100644 --- a/server/sonar-process/src/main/java/org/sonar/process/ProcessProperties.java +++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessProperties.java @@ -81,10 +81,6 @@ public class ProcessProperties { */ public static final String ENABLE_STOP_COMMAND = "sonar.enableStopCommand"; - public static final String WEB_ENFORCED_JVM_ARGS = "-Djava.awt.headless=true -Dfile.encoding=UTF-8"; - - public static final String CE_ENFORCED_JVM_ARGS = "-Djava.awt.headless=true -Dfile.encoding=UTF-8"; - public static final String HTTP_PROXY_HOST = "http.proxyHost"; public static final String HTTPS_PROXY_HOST = "https.proxyHost"; public static final String HTTP_PROXY_PORT = "http.proxyPort"; @@ -121,25 +117,7 @@ public class ProcessProperties { Properties defaults = new Properties(); defaults.put(SEARCH_HOST, InetAddress.getLoopbackAddress().getHostAddress()); defaults.put(SEARCH_PORT, "9001"); - defaults.put(SEARCH_JAVA_OPTS, "-Xms512m" + - " -Xmx512m" + - " -XX:+UseConcMarkSweepGC" + - " -XX:CMSInitiatingOccupancyFraction=75" + - " -XX:+UseCMSInitiatingOccupancyOnly" + - " -XX:+AlwaysPreTouch" + - " -server" + - " -Xss1m" + - " -Djava.awt.headless=true" + - " -Dfile.encoding=UTF-8" + - " -Djna.nosys=true" + - " -Djdk.io.permissionsUseCanonicalPath=true" + - " -Dio.netty.noUnsafe=true" + - " -Dio.netty.noKeySetOptimization=true" + - " -Dio.netty.recycler.maxCapacityPerThread=0" + - " -Dlog4j.shutdownHookEnabled=false" + - " -Dlog4j2.disable.jmx=true" + - " -Dlog4j.skipJansi=true" + - " -XX:+HeapDumpOnOutOfMemoryError"); + defaults.put(SEARCH_JAVA_OPTS, "-Xms512m -Xmx512m -XX:+HeapDumpOnOutOfMemoryError"); defaults.put(SEARCH_JAVA_ADDITIONAL_OPTS, ""); defaults.put(PATH_DATA, "data"); diff --git a/server/sonar-process/src/main/java/org/sonar/process/command/AbstractCommand.java b/server/sonar-process/src/main/java/org/sonar/process/command/AbstractCommand.java index 83cfc0fb497..3a880ea6a50 100644 --- a/server/sonar-process/src/main/java/org/sonar/process/command/AbstractCommand.java +++ b/server/sonar-process/src/main/java/org/sonar/process/command/AbstractCommand.java @@ -27,16 +27,19 @@ import java.util.Properties; import javax.annotation.Nullable; import org.sonar.process.ProcessId; +import static java.util.Objects.requireNonNull; + public abstract class AbstractCommand { // unique key among the group of commands to launch private final ProcessId id; // program arguments private final Map arguments = new LinkedHashMap<>(); private final Map envVariables = new HashMap<>(System.getenv()); - private File workDir; + private final File workDir; - protected AbstractCommand(ProcessId id) { - this.id = id; + protected AbstractCommand(ProcessId id, File workDir) { + this.id = requireNonNull(id, "ProcessId can't be null"); + this.workDir = requireNonNull(workDir, "workDir can't be null"); } public ProcessId getProcessId() { @@ -47,11 +50,6 @@ public abstract class AbstractCommand { return workDir; } - public T setWorkDir(File workDir) { - this.workDir = workDir; - return castThis(); - } - @SuppressWarnings("unchecked") private T castThis() { return (T) this; diff --git a/server/sonar-process/src/main/java/org/sonar/process/command/CommandFactoryImpl.java b/server/sonar-process/src/main/java/org/sonar/process/command/CommandFactoryImpl.java index 24995b85f03..987ea7cb232 100644 --- a/server/sonar-process/src/main/java/org/sonar/process/command/CommandFactoryImpl.java +++ b/server/sonar-process/src/main/java/org/sonar/process/command/CommandFactoryImpl.java @@ -27,6 +27,10 @@ import org.sonar.process.ProcessProperties; import org.sonar.process.Props; import org.sonar.process.es.EsLogging; import org.sonar.process.es.EsSettings; +import org.sonar.process.jmvoptions.CeJvmOptions; +import org.sonar.process.jmvoptions.EsJvmOptions; +import org.sonar.process.jmvoptions.JvmOptions; +import org.sonar.process.jmvoptions.WebJvmOptions; import static org.sonar.process.ProcessProperties.HTTPS_PROXY_HOST; import static org.sonar.process.ProcessProperties.HTTPS_PROXY_PORT; @@ -48,9 +52,11 @@ public class CommandFactoryImpl implements CommandFactory { "socksProxyPort"}; private final Props props; + private final File tempDir; - public CommandFactoryImpl(Props props) { + public CommandFactoryImpl(Props props, File tempDir) { this.props = props; + this.tempDir = tempDir; } @Override @@ -65,8 +71,7 @@ public class CommandFactoryImpl implements CommandFactory { File logDir = new File(settingsMap.get("path.logs")); File confDir = new File(settingsMap.get("path.conf")); - EsCommand res = new EsCommand(ProcessId.ELASTICSEARCH) - .setWorkDir(executable.getParentFile().getParentFile()) + EsCommand res = new EsCommand(ProcessId.ELASTICSEARCH, executable.getParentFile().getParentFile()) .setExecutable(executable) .setConfDir(confDir) .setLog4j2Properties(new EsLogging().createProperties(props, logDir)) @@ -74,8 +79,9 @@ public class CommandFactoryImpl implements CommandFactory { .setClusterName(settingsMap.get("cluster.name")) .setHost(settingsMap.get("network.host")) .setPort(Integer.valueOf(settingsMap.get("transport.tcp.port"))) - .addJvmOption(props.nonNullValue(ProcessProperties.SEARCH_JAVA_OPTS)) - .addJvmOption(props.nonNullValue(ProcessProperties.SEARCH_JAVA_ADDITIONAL_OPTS)) + .setEsJvmOptions(new EsJvmOptions() + .addFromMandatoryProperty(props, ProcessProperties.SEARCH_JAVA_OPTS) + .addFromMandatoryProperty(props, ProcessProperties.SEARCH_JAVA_ADDITIONAL_OPTS)) .setEnvVariable("JAVA_HOME", System.getProperties().getProperty("java.home")); settingsMap.forEach((key, value) -> res.addEsOption("-E" + key + "=" + value)); @@ -93,10 +99,15 @@ public class CommandFactoryImpl implements CommandFactory { @Override public JavaCommand createWebCommand(boolean leader) { File homeDir = props.nonNullValueAsFile(ProcessProperties.PATH_HOME); - JavaCommand command = newJavaCommand(ProcessId.WEB_SERVER, homeDir) - .addJavaOptions(ProcessProperties.WEB_ENFORCED_JVM_ARGS) - .addJavaOptions(props.nonNullValue(ProcessProperties.WEB_JAVA_OPTS)) - .addJavaOptions(props.nonNullValue(ProcessProperties.WEB_JAVA_ADDITIONAL_OPTS)) + + WebJvmOptions jvmOptions = new WebJvmOptions(tempDir) + .addFromMandatoryProperty(props, ProcessProperties.WEB_JAVA_OPTS) + .addFromMandatoryProperty(props, ProcessProperties.WEB_JAVA_ADDITIONAL_OPTS); + addProxyJvmOptions(jvmOptions); + + JavaCommand command = new JavaCommand(ProcessId.WEB_SERVER, homeDir) + .setArguments(props.rawProperties()) + .setJvmOptions(jvmOptions) // required for logback tomcat valve .setEnvVariable(ProcessProperties.PATH_LOGS, props.nonNullValue(ProcessProperties.PATH_LOGS)) .setArgument("sonar.cluster.web.startupLeader", Boolean.toString(leader)) @@ -113,10 +124,15 @@ public class CommandFactoryImpl implements CommandFactory { @Override public JavaCommand createCeCommand() { File homeDir = props.nonNullValueAsFile(ProcessProperties.PATH_HOME); - JavaCommand command = newJavaCommand(ProcessId.COMPUTE_ENGINE, homeDir) - .addJavaOptions(ProcessProperties.CE_ENFORCED_JVM_ARGS) - .addJavaOptions(props.nonNullValue(ProcessProperties.CE_JAVA_OPTS)) - .addJavaOptions(props.nonNullValue(ProcessProperties.CE_JAVA_ADDITIONAL_OPTS)) + + CeJvmOptions jvmOptions = new CeJvmOptions(tempDir) + .addFromMandatoryProperty(props, ProcessProperties.CE_JAVA_OPTS) + .addFromMandatoryProperty(props, ProcessProperties.CE_JAVA_ADDITIONAL_OPTS); + addProxyJvmOptions(jvmOptions); + + JavaCommand command = new JavaCommand(ProcessId.COMPUTE_ENGINE, homeDir) + .setArguments(props.rawProperties()) + .setJvmOptions(jvmOptions) .setClassName("org.sonar.ce.app.CeServer") .addClasspath("./lib/common/*") .addClasspath("./lib/server/*") @@ -128,27 +144,22 @@ public class CommandFactoryImpl implements CommandFactory { return command; } - private JavaCommand newJavaCommand(ProcessId id, File homeDir) { - JavaCommand command = new JavaCommand(id) - .setWorkDir(homeDir) - .setArguments(props.rawProperties()); - + private void addProxyJvmOptions(JvmOptions jvmOptions) { for (String key : PROXY_PROPERTY_KEYS) { - getPropsValue(key).ifPresent(val -> command.addJavaOption("-D" + key + "=" + val)); + getPropsValue(key).ifPresent(val -> jvmOptions.add("-D" + key + "=" + val)); } // 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); - return command; + setSystemPropertyToDefaultIfNotSet(jvmOptions, HTTPS_PROXY_HOST, HTTP_PROXY_HOST); + setSystemPropertyToDefaultIfNotSet(jvmOptions, HTTPS_PROXY_PORT, HTTP_PROXY_PORT); } - private void setSystemPropertyToDefaultIfNotSet(JavaCommand command, + private void setSystemPropertyToDefaultIfNotSet(JvmOptions jvmOptions, String httpsProperty, String httpProperty) { Optional httpValue = getPropsValue(httpProperty); Optional httpsValue = getPropsValue(httpsProperty); if (!httpsValue.isPresent() && httpValue.isPresent()) { - command.addJavaOption("-D" + httpsProperty + "=" + httpValue.get()); + jvmOptions.add("-D" + httpsProperty + "=" + httpValue.get()); } } diff --git a/server/sonar-process/src/main/java/org/sonar/process/command/EsCommand.java b/server/sonar-process/src/main/java/org/sonar/process/command/EsCommand.java index cd6877d493d..6f1d374d770 100644 --- a/server/sonar-process/src/main/java/org/sonar/process/command/EsCommand.java +++ b/server/sonar-process/src/main/java/org/sonar/process/command/EsCommand.java @@ -24,6 +24,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Properties; import org.sonar.process.ProcessId; +import org.sonar.process.jmvoptions.EsJvmOptions; public class EsCommand extends AbstractCommand { private File executable; @@ -33,10 +34,10 @@ public class EsCommand extends AbstractCommand { private int port; private Properties log4j2Properties; private List esOptions = new ArrayList<>(); - private List jvmOptions = new ArrayList<>(); + private EsJvmOptions esJvmOptions; - public EsCommand(ProcessId id) { - super(id); + public EsCommand(ProcessId id, File workDir) { + super(id, workDir); } public File getExecutable() { @@ -104,15 +105,12 @@ public class EsCommand extends AbstractCommand { return this; } - public List getJvmOptions() { - return jvmOptions; - } - - public EsCommand addJvmOption(String s) { - if (!s.isEmpty()) { - jvmOptions.add(s); - } + public EsCommand setEsJvmOptions(EsJvmOptions esJvmOptions) { + this.esJvmOptions = esJvmOptions; return this; } + public EsJvmOptions getEsJvmOptions() { + return esJvmOptions; + } } diff --git a/server/sonar-process/src/main/java/org/sonar/process/command/JavaCommand.java b/server/sonar-process/src/main/java/org/sonar/process/command/JavaCommand.java index 253c6294fbb..7659539b41b 100644 --- a/server/sonar-process/src/main/java/org/sonar/process/command/JavaCommand.java +++ b/server/sonar-process/src/main/java/org/sonar/process/command/JavaCommand.java @@ -19,37 +19,30 @@ */ package org.sonar.process.command; +import java.io.File; import java.util.ArrayList; import java.util.List; import org.sonar.process.ProcessId; +import org.sonar.process.jmvoptions.JvmOptions; -public class JavaCommand extends AbstractCommand { +public class JavaCommand extends AbstractCommand> { // entry point private String className; - // for example -Xmx1G - private final List javaOptions = new ArrayList<>(); + private JvmOptions jvmOptions; // relative path to JAR files private final List classpath = new ArrayList<>(); - public JavaCommand(ProcessId id) { - super(id); + public JavaCommand(ProcessId id, File workDir) { + super(id, workDir); } - public List getJavaOptions() { - return javaOptions; + public JvmOptions getJvmOptions() { + return jvmOptions; } - public JavaCommand addJavaOption(String s) { - if (!s.isEmpty()) { - javaOptions.add(s); - } - return this; - } + public JavaCommand setJvmOptions(JvmOptions jvmOptions) { + this.jvmOptions = jvmOptions; - public JavaCommand addJavaOptions(String s) { - for (String opt : s.split(" ")) { - addJavaOption(opt); - } return this; } @@ -57,7 +50,7 @@ public class JavaCommand extends AbstractCommand { return className; } - public JavaCommand setClassName(String className) { + public JavaCommand setClassName(String className) { this.className = className; return this; } @@ -66,21 +59,19 @@ public class JavaCommand extends AbstractCommand { return classpath; } - public JavaCommand addClasspath(String s) { + public JavaCommand addClasspath(String s) { classpath.add(s); return this; } @Override public String toString() { - StringBuilder sb = new StringBuilder("JavaCommand{"); - sb.append("workDir=").append(getWorkDir()); - sb.append(", javaOptions=").append(javaOptions); - sb.append(", className='").append(className).append('\''); - sb.append(", classpath=").append(classpath); - sb.append(", arguments=").append(getArguments()); - sb.append(", envVariables=").append(getEnvVariables()); - sb.append('}'); - return sb.toString(); + return "JavaCommand{" + "workDir=" + getWorkDir() + + ", jvmOptions=" + jvmOptions + + ", className='" + className + '\'' + + ", classpath=" + classpath + + ", arguments=" + getArguments() + + ", envVariables=" + getEnvVariables() + + '}'; } } diff --git a/server/sonar-process/src/main/java/org/sonar/process/jmvoptions/CeJvmOptions.java b/server/sonar-process/src/main/java/org/sonar/process/jmvoptions/CeJvmOptions.java new file mode 100644 index 00000000000..4b0fc9620eb --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/jmvoptions/CeJvmOptions.java @@ -0,0 +1,33 @@ +/* + * 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.process.jmvoptions; + +import java.io.File; + +import static java.lang.String.format; + +public class CeJvmOptions extends JvmOptions { + private static final String[] MANDATORY_JVM_OPTIONS = {"-Djava.awt.headless=true", "-Dfile.encoding=UTF-8"}; + + public CeJvmOptions(File tmpDir) { + super(MANDATORY_JVM_OPTIONS); + add(format("-Djava.io.tmpdir=%s", tmpDir.getAbsolutePath())); + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/jmvoptions/EsJvmOptions.java b/server/sonar-process/src/main/java/org/sonar/process/jmvoptions/EsJvmOptions.java new file mode 100644 index 00000000000..1c823e97056 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/jmvoptions/EsJvmOptions.java @@ -0,0 +1,66 @@ +/* + * 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.process.jmvoptions; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.util.stream.Collectors; + +public class EsJvmOptions extends JvmOptions { + private static final String[] MANDATORY_OPTIONS = { + "-XX:+UseConcMarkSweepGC", + "-XX:CMSInitiatingOccupancyFraction=75", + "-XX:+UseCMSInitiatingOccupancyOnly", + "-XX:+AlwaysPreTouch", + "-server", + "-Xss1m", + "-Djava.awt.headless=true", + "-Dfile.encoding=UTF-8", + "-Djna.nosys=true", + "-Djdk.io.permissionsUseCanonicalPath=true", + "-Dio.netty.noUnsafe=true", + "-Dio.netty.noKeySetOptimization=true", + "-Dio.netty.recycler.maxCapacityPerThread=0", + "-Dlog4j.shutdownHookEnabled=false", + "-Dlog4j2.disable.jmx=true", + "-Dlog4j.skipJansi=true" + }; + private static final String ELASTICSEARCH_JVM_OPTIONS_HEADER = "# This file has been automatically generated by SonarQube during startup.\n" + + "# Please use the sonar.search.javaOpts in sonar.properties to specify jvm options for Elasticsearch\n" + + "\n" + + "# DO NOT EDIT THIS FILE\n" + + "\n"; + + public EsJvmOptions() { + super(MANDATORY_OPTIONS); + } + + public void writeToJvmOptionFile(File file) { + String jvmOptions = getAll().stream().collect(Collectors.joining("\n")); + String jvmOptionsContent = ELASTICSEARCH_JVM_OPTIONS_HEADER + jvmOptions; + try { + Files.write(file.toPath(), jvmOptionsContent.getBytes(Charset.forName("UTF-8"))); + } catch (IOException e) { + throw new IllegalStateException("Cannot write Elasticsearch jvm options file", e); + } + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/jmvoptions/JvmOptions.java b/server/sonar-process/src/main/java/org/sonar/process/jmvoptions/JvmOptions.java new file mode 100644 index 00000000000..15cc411273d --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/jmvoptions/JvmOptions.java @@ -0,0 +1,79 @@ +/* + * 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.process.jmvoptions; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import org.sonar.process.Props; + +import static java.util.Objects.requireNonNull; + +public class JvmOptions { + private static final String JVM_OPTION_NOT_NULL_ERROR_MESSAGE = "a JVM option can't be null"; + + private final LinkedHashSet options = new LinkedHashSet<>(); + + public JvmOptions(String... mandatoryJvmOptions) { + Arrays.stream(requireNonNull(mandatoryJvmOptions, JVM_OPTION_NOT_NULL_ERROR_MESSAGE)) + .forEach(this::add); + } + + public T addFromMandatoryProperty(Props props, String propertyName) { + String value = props.nonNullValue(propertyName); + if (!value.isEmpty()) { + Arrays.stream(value.split(" (?=-)")).forEach(this::add); + } + + return castThis(); + } + + /** + * Add an option. + * Argument is trimmed before being added. + * + * @throws IllegalArgumentException if argument is empty or does not start with {@code -}. + */ + public T add(String str) { + requireNonNull(str, JVM_OPTION_NOT_NULL_ERROR_MESSAGE); + String value = str.trim(); + if (value.isEmpty() || !value.startsWith("-")) { + throw new IllegalArgumentException("a JVM option can't be empty and must start with '-'"); + } + options.add(value); + + return castThis(); + } + + @SuppressWarnings("unchecked") + private T castThis() { + return (T) this; + } + + public List getAll() { + return new ArrayList<>(options); + } + + @Override + public String toString() { + return options.toString(); + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/jmvoptions/WebJvmOptions.java b/server/sonar-process/src/main/java/org/sonar/process/jmvoptions/WebJvmOptions.java new file mode 100644 index 00000000000..dda9856e613 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/jmvoptions/WebJvmOptions.java @@ -0,0 +1,33 @@ +/* + * 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.process.jmvoptions; + +import java.io.File; + +import static java.lang.String.format; + +public class WebJvmOptions extends JvmOptions { + private static final String[] MANDATORY_OPTIONS = {"-Djava.awt.headless=true", "-Dfile.encoding=UTF-8"}; + + public WebJvmOptions(File tmpDir) { + super(MANDATORY_OPTIONS); + add(format("-Djava.io.tmpdir=%s", tmpDir.getAbsolutePath())); + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/jmvoptions/package-info.java b/server/sonar-process/src/main/java/org/sonar/process/jmvoptions/package-info.java new file mode 100644 index 00000000000..f88e33e481a --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/jmvoptions/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ +@ParametersAreNonnullByDefault +package org.sonar.process.jmvoptions; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-process/src/test/java/org/sonar/process/command/AbstractCommandTest.java b/server/sonar-process/src/test/java/org/sonar/process/command/AbstractCommandTest.java index 7ddb9c54089..62f59f11a48 100644 --- a/server/sonar-process/src/test/java/org/sonar/process/command/AbstractCommandTest.java +++ b/server/sonar-process/src/test/java/org/sonar/process/command/AbstractCommandTest.java @@ -20,9 +20,11 @@ package org.sonar.process.command; import java.io.File; +import java.io.IOException; 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.ProcessId; @@ -32,10 +34,33 @@ public class AbstractCommandTest { @Rule public TemporaryFolder temp = new TemporaryFolder(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void constructor_throws_NPE_of_ProcessId_is_null() throws IOException { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("ProcessId can't be null"); + + new AbstractCommand(null, temp.newFolder()) { + + }; + } + + @Test + public void constructor_throws_NPE_of_workDir_is_null() throws IOException { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("workDir can't be null"); + + new AbstractCommand(ProcessId.WEB_SERVER, null) { + + }; + } @Test public void test_command_with_complete_information() throws Exception { - AbstractCommand command = new AbstractCommand(ProcessId.ELASTICSEARCH) { + File workDir = temp.newFolder(); + AbstractCommand command = new AbstractCommand(ProcessId.ELASTICSEARCH, workDir) { }; @@ -45,8 +70,6 @@ public class AbstractCommandTest { command.setArguments(args); command.setEnvVariable("JAVA_COMMAND_TEST", "1000"); - File workDir = temp.newFolder(); - command.setWorkDir(workDir); assertThat(command.toString()).isNotNull(); assertThat(command.getWorkDir()).isSameAs(workDir); diff --git a/server/sonar-process/src/test/java/org/sonar/process/command/JavaCommandTest.java b/server/sonar-process/src/test/java/org/sonar/process/command/JavaCommandTest.java index 96d266a7d28..dd0f71a677c 100644 --- a/server/sonar-process/src/test/java/org/sonar/process/command/JavaCommandTest.java +++ b/server/sonar-process/src/test/java/org/sonar/process/command/JavaCommandTest.java @@ -25,6 +25,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.sonar.process.ProcessId; +import org.sonar.process.jmvoptions.JvmOptions; import static org.assertj.core.api.Assertions.assertThat; @@ -35,7 +36,8 @@ public class JavaCommandTest { @Test public void test_command_with_complete_information() throws Exception { - JavaCommand command = new JavaCommand(ProcessId.ELASTICSEARCH); + File workDir = temp.newFolder(); + JavaCommand command = new JavaCommand<>(ProcessId.ELASTICSEARCH, workDir); command.setArgument("first_arg", "val1"); Properties args = new Properties(); @@ -44,15 +46,14 @@ public class JavaCommandTest { command.setClassName("org.sonar.ElasticSearch"); command.setEnvVariable("JAVA_COMMAND_TEST", "1000"); - File workDir = temp.newFolder(); - command.setWorkDir(workDir); command.addClasspath("lib/*.jar"); command.addClasspath("conf/*.xml"); - command.addJavaOption("-Xmx128m"); + JvmOptions jvmOptions = new JvmOptions() {}; + command.setJvmOptions(jvmOptions); assertThat(command.toString()).isNotNull(); assertThat(command.getClasspath()).containsOnly("lib/*.jar", "conf/*.xml"); - assertThat(command.getJavaOptions()).containsOnly("-Xmx128m"); + assertThat(command.getJvmOptions()).isSameAs(jvmOptions); assertThat(command.getWorkDir()).isSameAs(workDir); assertThat(command.getClassName()).isEqualTo("org.sonar.ElasticSearch"); @@ -61,15 +62,4 @@ public class JavaCommandTest { assertThat(command.getEnvVariables().size()).isEqualTo(System.getenv().size() + 1); } - @Test - public void addJavaOptions_adds_jvm_options() { - JavaCommand command = new JavaCommand(ProcessId.ELASTICSEARCH); - assertThat(command.getJavaOptions()).isEmpty(); - - command.addJavaOptions(""); - assertThat(command.getJavaOptions()).isEmpty(); - - command.addJavaOptions("-Xmx512m -Xms256m -Dfoo"); - assertThat(command.getJavaOptions()).containsOnly("-Xmx512m", "-Xms256m", "-Dfoo"); - } } diff --git a/server/sonar-process/src/test/java/org/sonar/process/jmvoptions/CeJvmOptionsTest.java b/server/sonar-process/src/test/java/org/sonar/process/jmvoptions/CeJvmOptionsTest.java new file mode 100644 index 00000000000..3bd8bf37d91 --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/jmvoptions/CeJvmOptionsTest.java @@ -0,0 +1,42 @@ +/* + * 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.process.jmvoptions; + +import java.io.File; +import java.io.IOException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CeJvmOptionsTest { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void constructor_sets_mandatory_JVM_options() throws IOException { + File tmpDir = temporaryFolder.newFolder(); + CeJvmOptions underTest = new CeJvmOptions(tmpDir); + + assertThat(underTest.getAll()).containsExactly( + "-Djava.awt.headless=true", "-Dfile.encoding=UTF-8", "-Djava.io.tmpdir=" + tmpDir.getAbsolutePath()); + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/jmvoptions/EsJvmOptionsTest.java b/server/sonar-process/src/test/java/org/sonar/process/jmvoptions/EsJvmOptionsTest.java new file mode 100644 index 00000000000..5f4ef878875 --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/jmvoptions/EsJvmOptionsTest.java @@ -0,0 +1,108 @@ +/* + * 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.process.jmvoptions; + +import java.io.File; +import java.io.IOException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.test.ExceptionCauseMatcher.hasType; + +public class EsJvmOptionsTest { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void constructor_sets_mandatory_JVM_options() { + EsJvmOptions underTest = new EsJvmOptions(); + + assertThat(underTest.getAll()).containsExactly( + "-XX:+UseConcMarkSweepGC", + "-XX:CMSInitiatingOccupancyFraction=75", + "-XX:+UseCMSInitiatingOccupancyOnly", + "-XX:+AlwaysPreTouch", + "-server", + "-Xss1m", + "-Djava.awt.headless=true", + "-Dfile.encoding=UTF-8", + "-Djna.nosys=true", + "-Djdk.io.permissionsUseCanonicalPath=true", + "-Dio.netty.noUnsafe=true", + "-Dio.netty.noKeySetOptimization=true", + "-Dio.netty.recycler.maxCapacityPerThread=0", + "-Dlog4j.shutdownHookEnabled=false", + "-Dlog4j2.disable.jmx=true", + "-Dlog4j.skipJansi=true"); + } + + @Test + public void writeToJvmOptionFile_writes_all_JVM_options_to_file_with_warning_header() throws IOException { + File file = temporaryFolder.newFile(); + EsJvmOptions underTest = new EsJvmOptions() + .add("-foo") + .add("-bar"); + + underTest.writeToJvmOptionFile(file); + + assertThat(file).hasContent( + "# This file has been automatically generated by SonarQube during startup.\n" + + "# Please use the sonar.search.javaOpts in sonar.properties to specify jvm options for Elasticsearch\n" + + "\n" + + "# DO NOT EDIT THIS FILE\n" + + "\n" + + "-XX:+UseConcMarkSweepGC\n" + + "-XX:CMSInitiatingOccupancyFraction=75\n" + + "-XX:+UseCMSInitiatingOccupancyOnly\n" + + "-XX:+AlwaysPreTouch\n" + + "-server\n" + + "-Xss1m\n" + + "-Djava.awt.headless=true\n" + + "-Dfile.encoding=UTF-8\n" + + "-Djna.nosys=true\n" + + "-Djdk.io.permissionsUseCanonicalPath=true\n" + + "-Dio.netty.noUnsafe=true\n" + + "-Dio.netty.noKeySetOptimization=true\n" + + "-Dio.netty.recycler.maxCapacityPerThread=0\n" + + "-Dlog4j.shutdownHookEnabled=false\n" + + "-Dlog4j2.disable.jmx=true\n" + + "-Dlog4j.skipJansi=true\n" + + "-foo\n" + + "-bar"); + + } + + @Test + public void writeToJvmOptionFile_throws_ISE_in_case_of_IOException() throws IOException { + File notAFile = temporaryFolder.newFolder(); + EsJvmOptions underTest = new EsJvmOptions(); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Cannot write Elasticsearch jvm options file"); + expectedException.expectCause(hasType(IOException.class)); + + underTest.writeToJvmOptionFile(notAFile); + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/jmvoptions/JvmOptionsTest.java b/server/sonar-process/src/test/java/org/sonar/process/jmvoptions/JvmOptionsTest.java new file mode 100644 index 00000000000..7e42d307d68 --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/jmvoptions/JvmOptionsTest.java @@ -0,0 +1,232 @@ +/* + * 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.process.jmvoptions; + +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.ArrayList; +import java.util.Properties; +import java.util.Random; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.apache.commons.lang.RandomStringUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.sonar.process.Props; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(DataProviderRunner.class) +public class JvmOptionsTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private final Random random = new Random(); + private final String randomPropertyName = RandomStringUtils.randomAlphanumeric(3); + private final Properties properties = new Properties(); + private final Props props = new Props(properties); + private final JvmOptions underTest = new JvmOptions(); + + @Test + public void constructor_without_arguments_creates_empty_JvmOptions() { + JvmOptions testJvmOptions = new JvmOptions<>(); + + assertThat(testJvmOptions.getAll()).isEmpty(); + } + + @Test + public void constructor_throws_NPE_if_argument_is_null() { + expectJvmOptionNotNullNPE(); + + new JvmOptions(null); + } + + @Test + public void constructor_throws_NPE_if_any_argument_is_null() { + ArrayList nullList = new ArrayList<>(); + nullList.add(null); + String[] arguments = Stream.of( + Stream.of("-S1"), + IntStream.range(0, random.nextInt(2)).mapToObj(i -> "-B" + i), + nullList.stream(), + IntStream.range(0, random.nextInt(2)).mapToObj(i -> "-A" + i)).flatMap(s -> s) + .toArray(String[]::new); + + expectJvmOptionNotNullNPE(); + + new JvmOptions(arguments); + } + + @Test + @UseDataProvider("variousEmptyStrings") + public void constructor_throws_IAE_if_argument_is_empty(String emptyString) { + expectJvmOptionNotEmptyAndStartByDashIAE(); + + new JvmOptions(emptyString); + } + + @Test + @UseDataProvider("variousEmptyStrings") + public void constructor_throws_IAE_if_any_argument_is_empty(String emptyString) { + String[] arguments = Stream.of( + Stream.of("-S1"), + IntStream.range(0, random.nextInt(2)).mapToObj(i -> "-B" + i), + Stream.of(emptyString), + IntStream.range(0, random.nextInt(2)).mapToObj(i -> "-A" + i)) + .flatMap(s -> s) + .toArray(String[]::new); + + expectJvmOptionNotEmptyAndStartByDashIAE(); + + new JvmOptions(arguments); + } + + @Test + public void constructor_throws_IAE_if_argument_does_not_start_with_dash() { + expectJvmOptionNotEmptyAndStartByDashIAE(); + + new JvmOptions(RandomStringUtils.randomAlphanumeric(3)); + } + + @Test + public void constructor_throws_IAE_if_any_argument_does_not_start_with_dash() { + String[] arguments = Stream.of( + Stream.of("-S1"), + IntStream.range(0, random.nextInt(2)).mapToObj(i -> "-B" + i), + Stream.of(RandomStringUtils.randomAlphanumeric(3)), + IntStream.range(0, random.nextInt(2)).mapToObj(i -> "-A" + i)) + .flatMap(s -> s) + .toArray(String[]::new); + + expectJvmOptionNotEmptyAndStartByDashIAE(); + + new JvmOptions(arguments); + } + + @Test + public void add_throws_NPE_if_argument_is_null() { + expectJvmOptionNotNullNPE(); + + underTest.add(null); + } + + @Test + @UseDataProvider("variousEmptyStrings") + public void add_throws_IAE_if_argument_is_empty(String emptyString) { + expectJvmOptionNotEmptyAndStartByDashIAE(); + + underTest.add(emptyString); + } + + @Test + public void add_throws_IAE_if_argument_does_not_start_with_dash() { + expectJvmOptionNotEmptyAndStartByDashIAE(); + + underTest.add(RandomStringUtils.randomAlphanumeric(3)); + } + + @Test + public void addFromMandatoryProperty_fails_with_IAE_if_property_does_not_exist() { + expectMissingPropertyIAE(this.randomPropertyName); + + underTest.addFromMandatoryProperty(props, this.randomPropertyName); + } + + @Test + @UseDataProvider("variousEmptyStrings") + public void addFromMandatoryProperty_fails_with_IAE_if_property_contains_an_empty_value(String emptyString) { + expectMissingPropertyIAE(this.randomPropertyName); + + underTest.addFromMandatoryProperty(props, randomPropertyName); + } + + @Test + @UseDataProvider("variousEmptyStrings") + public void addFromMandatoryProperty_adds_single_option_of_property_with_trimming(String emptyString) { + properties.put(randomPropertyName, emptyString + "-foo" + emptyString); + + underTest.addFromMandatoryProperty(props, randomPropertyName); + + assertThat(underTest.getAll()).containsOnly("-foo"); + } + + @Test + @UseDataProvider("variousEmptyStrings") + public void addFromMandatoryProperty_fails_with_IAE_if_property_does_not_start_with_dash_after_trimmed(String emptyString) { + properties.put(randomPropertyName, emptyString + "foo -bar"); + + expectJvmOptionNotEmptyAndStartByDashIAE(); + + underTest.addFromMandatoryProperty(props, randomPropertyName); + } + + @Test + @UseDataProvider("variousEmptyStrings") + public void addFromMandatoryProperty_adds_options_of_property_with_trimming(String emptyString) { + properties.put(randomPropertyName, emptyString + "-foo" + emptyString + " -bar" + emptyString + " -duck" + emptyString); + + underTest.addFromMandatoryProperty(props, randomPropertyName); + + assertThat(underTest.getAll()).containsOnly("-foo", "-bar", "-duck"); + } + + @Test + public void addFromMandatoryProperty_supports_spaces_inside_options() { + properties.put(randomPropertyName, "-foo bar -duck"); + + underTest.addFromMandatoryProperty(props, randomPropertyName); + + assertThat(underTest.getAll()).containsOnly("-foo bar", "-duck"); + } + + @Test + public void toString_prints_all_jvm_options() { + underTest.add("-foo").add("-bar"); + + assertThat(underTest.toString()).isEqualTo("[-foo, -bar]"); + } + + private void expectJvmOptionNotNullNPE() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("a JVM option can't be null"); + } + + private void expectJvmOptionNotEmptyAndStartByDashIAE() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("a JVM option can't be empty and must start with '-'"); + } + + public void expectMissingPropertyIAE(String randomPropertyName) { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Missing property: " + randomPropertyName); + } + + @DataProvider() + public static Object[][] variousEmptyStrings() { + return new Object[][] { + {""}, + {" "}, + {" "} + }; + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/jmvoptions/WebJvmOptionsTest.java b/server/sonar-process/src/test/java/org/sonar/process/jmvoptions/WebJvmOptionsTest.java new file mode 100644 index 00000000000..9387a15b808 --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/jmvoptions/WebJvmOptionsTest.java @@ -0,0 +1,43 @@ +/* + * 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.process.jmvoptions; + +import java.io.File; +import java.io.IOException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import static org.assertj.core.api.Assertions.assertThat; + +public class WebJvmOptionsTest { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void constructor_sets_mandatory_JVM_options() throws IOException { + File tmpDir = temporaryFolder.newFolder(); + WebJvmOptions underTest = new WebJvmOptions(tmpDir); + + assertThat(underTest.getAll()).containsExactly( + "-Djava.awt.headless=true", "-Dfile.encoding=UTF-8", "-Djava.io.tmpdir=" + tmpDir.getAbsolutePath()); + } + +} diff --git a/sonar-application/src/main/assembly/conf/sonar.properties b/sonar-application/src/main/assembly/conf/sonar.properties index a2e16d2cc54..94ba3a3a5ff 100644 --- a/sonar-application/src/main/assembly/conf/sonar.properties +++ b/sonar-application/src/main/assembly/conf/sonar.properties @@ -185,27 +185,11 @@ #-------------------------------------------------------------------------------------------------- # ELASTICSEARCH # Elasticsearch is used to facilitate fast and accurate information retrieval. -# It is executed in a dedicated Java process. Default heap size is 512m. +# It is executed in a dedicated Java process. Default heap size is 512Mb. # JVM options of Elasticsearch process #sonar.search.javaOpts=-Xms512m \ # -Xmx512m \ -# -XX:+UseConcMarkSweepGC \ -# -XX:CMSInitiatingOccupancyFraction=75 \ -# -XX:+UseCMSInitiatingOccupancyOnly \ -# -XX:+AlwaysPreTouch \ -# -server \ -# -Xss1m \ -# -Djava.awt.headless=true \ -# -Dfile.encoding=UTF-8 \ -# -Djna.nosys=true \ -# -Djdk.io.permissionsUseCanonicalPath=true \ -# -Dio.netty.noUnsafe=true \ -# -Dio.netty.noKeySetOptimization=true \ -# -Dio.netty.recycler.maxCapacityPerThread=0 \ -# -Dlog4j.shutdownHookEnabled=false \ -# -Dlog4j2.disable.jmx=true \ -# -Dlog4j.skipJansi=true \ # -XX:+HeapDumpOnOutOfMemoryError # Same as previous property, but allows to not repeat all other settings like -Xmx diff --git a/sonar-application/src/main/java/org/sonar/application/App.java b/sonar-application/src/main/java/org/sonar/application/App.java index b2010900e58..123467455ee 100644 --- a/sonar-application/src/main/java/org/sonar/application/App.java +++ b/sonar-application/src/main/java/org/sonar/application/App.java @@ -49,8 +49,8 @@ public class App { try (AppState appState = new AppStateFactory(settings).create()) { appState.registerSonarQubeVersion(getSonarqubeVersion()); AppReloader appReloader = new AppReloaderImpl(settingsLoader, fileSystem, appState, logging); - CommandFactory commandFactory = new CommandFactoryImpl(settings.getProps()); fileSystem.reset(); + CommandFactory commandFactory = new CommandFactoryImpl(settings.getProps(), fileSystem.getTempDir()); try (ProcessLauncher processLauncher = new ProcessLauncherImpl(fileSystem.getTempDir())) { Scheduler scheduler = new SchedulerImpl(settings, appReloader, commandFactory, processLauncher, appState); diff --git a/sonar-testing-harness/src/main/java/org/sonar/test/ExceptionCauseMatcher.java b/sonar-testing-harness/src/main/java/org/sonar/test/ExceptionCauseMatcher.java index 1c687510121..c016590617c 100644 --- a/sonar-testing-harness/src/main/java/org/sonar/test/ExceptionCauseMatcher.java +++ b/sonar-testing-harness/src/main/java/org/sonar/test/ExceptionCauseMatcher.java @@ -64,7 +64,7 @@ public class ExceptionCauseMatcher extends TypeSafeMatcher { @Override protected boolean matchesSafely(Throwable item) { - if (!item.getClass().isAssignableFrom(type)) { + if (!type.isAssignableFrom(item.getClass())) { return false; } if (expectedMessage == null) { diff --git a/tests/src/test/java/org/sonarqube/tests/Category1Suite.java b/tests/src/test/java/org/sonarqube/tests/Category1Suite.java index 524e957bbb2..d99720785b2 100644 --- a/tests/src/test/java/org/sonarqube/tests/Category1Suite.java +++ b/tests/src/test/java/org/sonarqube/tests/Category1Suite.java @@ -127,12 +127,6 @@ public class Category1Suite { .addPlugin(pluginArtifact("posttask-plugin")) - // reduce xmx and xms from 2g (default) to 1g - .setServerProperty("sonar.search.javaOpts", "-Xms512m -Xmx512m -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly" + - " -XX:+AlwaysPreTouch -server -Xss1m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djna.nosys=true" + - " -Djdk.io.permissionsUseCanonicalPath=true -Dio.netty.noUnsafe=true -Dio.netty.noKeySetOptimization=true" + - " -Dio.netty.recycler.maxCapacityPerThread=0 -Dlog4j.shutdownHookEnabled=false -Dlog4j2.disable.jmx=true -Dlog4j.skipJansi=true -XX:+HeapDumpOnOutOfMemoryError") - .addPlugin(xooPlugin()) .build(); diff --git a/tests/src/test/java/org/sonarqube/tests/Category2Suite.java b/tests/src/test/java/org/sonarqube/tests/Category2Suite.java index df2397d8642..c101253c84a 100644 --- a/tests/src/test/java/org/sonarqube/tests/Category2Suite.java +++ b/tests/src/test/java/org/sonarqube/tests/Category2Suite.java @@ -109,12 +109,6 @@ public class Category2Suite { // 1 second. Required for notification test. .setServerProperty("sonar.notifications.delay", "1") - // reduce xmx and xms from 2g (default) to 1g - .setServerProperty("sonar.search.javaOpts", "-Xms512m -Xmx512m -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly" + - " -XX:+AlwaysPreTouch -server -Xss1m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djna.nosys=true" + - " -Djdk.io.permissionsUseCanonicalPath=true -Dio.netty.noUnsafe=true -Dio.netty.noKeySetOptimization=true" + - " -Dio.netty.recycler.maxCapacityPerThread=0 -Dlog4j.shutdownHookEnabled=false -Dlog4j2.disable.jmx=true -Dlog4j.skipJansi=true -XX:+HeapDumpOnOutOfMemoryError") - .build(); } diff --git a/tests/src/test/java/org/sonarqube/tests/Category3Suite.java b/tests/src/test/java/org/sonarqube/tests/Category3Suite.java index 7407fcff030..25dd138a78e 100644 --- a/tests/src/test/java/org/sonarqube/tests/Category3Suite.java +++ b/tests/src/test/java/org/sonarqube/tests/Category3Suite.java @@ -86,11 +86,5 @@ public class Category3Suite { // used by ProjectBuilderTest .addPlugin(pluginArtifact("project-builder-plugin")) - // reduce xmx and xms from 2g (default) to 1g - .setServerProperty("sonar.search.javaOpts", "-Xms512m -Xmx512m -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly" + - " -XX:+AlwaysPreTouch -server -Xss1m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djna.nosys=true" + - " -Djdk.io.permissionsUseCanonicalPath=true -Dio.netty.noUnsafe=true -Dio.netty.noKeySetOptimization=true" + - " -Dio.netty.recycler.maxCapacityPerThread=0 -Dlog4j.shutdownHookEnabled=false -Dlog4j2.disable.jmx=true -Dlog4j.skipJansi=true -XX:+HeapDumpOnOutOfMemoryError") - .build(); } diff --git a/tests/src/test/java/org/sonarqube/tests/Category4Suite.java b/tests/src/test/java/org/sonarqube/tests/Category4Suite.java index f7ab6e9a499..6e50225b59a 100644 --- a/tests/src/test/java/org/sonarqube/tests/Category4Suite.java +++ b/tests/src/test/java/org/sonarqube/tests/Category4Suite.java @@ -125,11 +125,5 @@ public class Category4Suite { // Used by LogsTest .setServerProperty("sonar.web.accessLogs.pattern", LogsTest.ACCESS_LOGS_PATTERN) - // reduce xmx and xms from 2g (default) to 1g - .setServerProperty("sonar.search.javaOpts", "-Xms512m -Xmx512m -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly" + - " -XX:+AlwaysPreTouch -server -Xss1m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djna.nosys=true" + - " -Djdk.io.permissionsUseCanonicalPath=true -Dio.netty.noUnsafe=true -Dio.netty.noKeySetOptimization=true" + - " -Dio.netty.recycler.maxCapacityPerThread=0 -Dlog4j.shutdownHookEnabled=false -Dlog4j2.disable.jmx=true -Dlog4j.skipJansi=true -XX:+HeapDumpOnOutOfMemoryError") - .build(); } diff --git a/tests/src/test/java/org/sonarqube/tests/Category6Suite.java b/tests/src/test/java/org/sonarqube/tests/Category6Suite.java index a144c00b3bd..9d0c046703c 100644 --- a/tests/src/test/java/org/sonarqube/tests/Category6Suite.java +++ b/tests/src/test/java/org/sonarqube/tests/Category6Suite.java @@ -98,11 +98,5 @@ public class Category6Suite { .addPlugin(pluginArtifact("fake-billing-plugin")) .addPlugin(pluginArtifact("ui-extensions-plugin")) - // reduce xmx and xms from 2g (default) to 1g - .setServerProperty("sonar.search.javaOpts", "-Xms512m -Xmx512m -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly" + - " -XX:+AlwaysPreTouch -server -Xss1m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djna.nosys=true" + - " -Djdk.io.permissionsUseCanonicalPath=true -Dio.netty.noUnsafe=true -Dio.netty.noKeySetOptimization=true" + - " -Dio.netty.recycler.maxCapacityPerThread=0 -Dlog4j.shutdownHookEnabled=false -Dlog4j2.disable.jmx=true -Dlog4j.skipJansi=true -XX:+HeapDumpOnOutOfMemoryError") - .build(); } -- 2.39.5