* SONAR-8818 Introduce Hazelcasttags/6.4-RC1
@@ -74,6 +74,8 @@ | |||
<protobuf.version>3.0.0-beta-2</protobuf.version> | |||
<hazelcast.version>3.8</hazelcast.version> | |||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | |||
<maven.min.version>3.2</maven.min.version> | |||
<timestamp>${maven.build.timestamp}</timestamp> | |||
@@ -660,6 +662,11 @@ | |||
<artifactId>jna</artifactId> | |||
<version>4.1.0</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>com.hazelcast</groupId> | |||
<artifactId>hazelcast</artifactId> | |||
<version>${hazelcast.version}</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.elasticsearch</groupId> | |||
<artifactId>elasticsearch</artifactId> |
@@ -31,6 +31,10 @@ | |||
<version>${project.version}</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>com.hazelcast</groupId> | |||
<artifactId>hazelcast</artifactId> | |||
</dependency> | |||
<dependency> | |||
<groupId>com.google.code.findbugs</groupId> | |||
<artifactId>jsr305</artifactId> | |||
@@ -236,8 +240,8 @@ | |||
<configuration> | |||
<rules> | |||
<requireFilesSize> | |||
<minsize>111000000</minsize> | |||
<maxsize>118000000</maxsize> | |||
<minsize>126000000</minsize> | |||
<maxsize>134000000</maxsize> | |||
<files> | |||
<file>${project.build.directory}/sonarqube-${project.version}.zip</file> | |||
</files> |
@@ -50,6 +50,7 @@ public class App implements Stoppable { | |||
private final JavaCommandFactory javaCommandFactory; | |||
private final Monitor monitor; | |||
private final Supplier<List<JavaCommand>> javaCommandSupplier; | |||
private final Cluster cluster; | |||
private App(Properties commandLineArguments) { | |||
this.commandLineArguments = commandLineArguments; | |||
@@ -59,8 +60,12 @@ public class App implements Stoppable { | |||
AppFileSystem appFileSystem = new AppFileSystem(props); | |||
appFileSystem.verifyProps(); | |||
ClusterProperties clusterProperties = new ClusterProperties(props); | |||
clusterProperties.populateProps(props); | |||
AppLogging logging = new AppLogging(); | |||
logging.configure(props); | |||
clusterProperties.validate(); | |||
this.cluster = new Cluster(clusterProperties); | |||
// used by orchestrator | |||
boolean watchForHardStop = props.valueAsBoolean(ProcessProperties.ENABLE_STOP_COMMAND, false); | |||
@@ -76,12 +81,13 @@ public class App implements Stoppable { | |||
@VisibleForTesting | |||
App(Properties commandLineArguments, Function<Properties, Props> propsSupplier, Monitor monitor, CheckFSConfigOnReload checkFsConfigOnReload, | |||
JavaCommandFactory javaCommandFactory) { | |||
JavaCommandFactory javaCommandFactory, Cluster cluster) { | |||
this.commandLineArguments = commandLineArguments; | |||
this.propsSupplier = propsSupplier; | |||
this.javaCommandFactory = javaCommandFactory; | |||
this.monitor = monitor; | |||
this.javaCommandSupplier = new ReloadableCommandSupplier(propsSupplier.apply(commandLineArguments), checkFsConfigOnReload); | |||
this.cluster = cluster; | |||
} | |||
public void start() throws InterruptedException { | |||
@@ -109,6 +115,9 @@ public class App implements Stoppable { | |||
@Override | |||
public void stopAsync() { | |||
if (cluster != null) { | |||
cluster.close(); | |||
} | |||
if (monitor != null) { | |||
monitor.stop(); | |||
} |
@@ -19,15 +19,16 @@ | |||
*/ | |||
package org.sonar.application; | |||
import ch.qos.logback.classic.Level; | |||
import ch.qos.logback.classic.Logger; | |||
import ch.qos.logback.classic.LoggerContext; | |||
import ch.qos.logback.classic.spi.ILoggingEvent; | |||
import ch.qos.logback.core.ConsoleAppender; | |||
import ch.qos.logback.core.FileAppender; | |||
import org.sonar.process.logging.LogLevelConfig; | |||
import org.sonar.process.logging.LogbackHelper; | |||
import org.sonar.process.ProcessId; | |||
import org.sonar.process.Props; | |||
import org.sonar.process.logging.LogLevelConfig; | |||
import org.sonar.process.logging.LogbackHelper; | |||
import org.sonar.process.logging.RootLoggerConfig; | |||
import static org.slf4j.Logger.ROOT_LOGGER_NAME; | |||
@@ -128,7 +129,11 @@ class AppLogging { | |||
} else { | |||
configureWithWrapperWritingToFile(ctx); | |||
} | |||
helper.apply(LogLevelConfig.newBuilder().rootLevelFor(ProcessId.APP).build(), props); | |||
helper.apply( | |||
LogLevelConfig.newBuilder() | |||
.rootLevelFor(ProcessId.APP) | |||
.immutableLevel("com.hazelcast", Level.toLevel(props.value(ClusterParameters.HAZELCAST_LOG_LEVEL.getName()))) | |||
.build(), props); | |||
return ctx; | |||
} |
@@ -0,0 +1,103 @@ | |||
/* | |||
* 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; | |||
import com.google.common.annotations.VisibleForTesting; | |||
import com.hazelcast.cluster.ClusterState; | |||
import com.hazelcast.config.Config; | |||
import com.hazelcast.config.JoinConfig; | |||
import com.hazelcast.config.NetworkConfig; | |||
import com.hazelcast.core.Hazelcast; | |||
import com.hazelcast.core.HazelcastInstance; | |||
import javax.annotation.Nonnull; | |||
/** | |||
* Manager for the cluster communication between Main Processes | |||
*/ | |||
public class Cluster implements AutoCloseable { | |||
/** | |||
* The Hazelcast instance. | |||
*/ | |||
@VisibleForTesting | |||
final HazelcastInstance hazelcastInstance; | |||
/** | |||
* Instantiates a new Cluster. | |||
* | |||
* @param clusterProperties The properties of the cluster read from configuration | |||
*/ | |||
protected Cluster(@Nonnull ClusterProperties clusterProperties) { | |||
if (clusterProperties.isEnabled()) { | |||
Config hzConfig = new Config(); | |||
// Configure the network instance | |||
NetworkConfig netConfig = hzConfig.getNetworkConfig(); | |||
netConfig.setPort(clusterProperties.getPort()) | |||
.setPortAutoIncrement(clusterProperties.isPortAutoincrement()); | |||
if (!clusterProperties.getInterfaces().isEmpty()) { | |||
netConfig.getInterfaces() | |||
.setEnabled(true) | |||
.setInterfaces(clusterProperties.getInterfaces()); | |||
} | |||
// Only allowing TCP/IP configuration | |||
JoinConfig joinConfig = netConfig.getJoin(); | |||
joinConfig.getAwsConfig().setEnabled(false); | |||
joinConfig.getMulticastConfig().setEnabled(false); | |||
joinConfig.getTcpIpConfig().setEnabled(true); | |||
joinConfig.getTcpIpConfig().setMembers(clusterProperties.getMembers()); | |||
// Tweak HazelCast configuration | |||
hzConfig | |||
// Increase the number of tries | |||
.setProperty("hazelcast.tcp.join.port.try.count", "10") | |||
// Don't bind on all interfaces | |||
.setProperty("hazelcast.socket.bind.any", "false") | |||
// Don't phone home | |||
.setProperty("hazelcast.phone.home.enabled", "false") | |||
// Use slf4j for logging | |||
.setProperty("hazelcast.logging.type", "slf4j"); | |||
// We are not using the partition group of Hazelcast, so disabling it | |||
hzConfig.getPartitionGroupConfig().setEnabled(false); | |||
hazelcastInstance = Hazelcast.newHazelcastInstance(hzConfig); | |||
} else { | |||
hazelcastInstance = null; | |||
} | |||
} | |||
/** | |||
* Is the cluster active | |||
* | |||
* @return the boolean | |||
*/ | |||
public boolean isActive() { | |||
return hazelcastInstance != null && hazelcastInstance.getCluster().getClusterState() == ClusterState.ACTIVE; | |||
} | |||
@Override | |||
public void close() { | |||
if (hazelcastInstance != null) { | |||
hazelcastInstance.shutdown(); | |||
} | |||
} | |||
} |
@@ -0,0 +1,65 @@ | |||
/* | |||
* 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; | |||
import javax.annotation.Nonnull; | |||
import org.apache.commons.lang.StringUtils; | |||
enum ClusterParameters { | |||
ENABLED("sonar.cluster.enabled", Boolean.FALSE.toString()), | |||
MEMBERS("sonar.cluster.members", ""), | |||
PORT("sonar.cluster.port", Integer.toString(9003)), | |||
PORT_AUTOINCREMENT("sonar.cluster.port_autoincrement", Boolean.FALSE.toString()), | |||
INTERFACES("sonar.cluster.interfaces", ""), | |||
NAME("sonar.cluster.name", ""), | |||
HAZELCAST_LOG_LEVEL("sonar.log.level.app.hazelcast", "WARN"); | |||
private final String name; | |||
private final String defaultValue; | |||
ClusterParameters(@Nonnull String name, @Nonnull String defaultValue) { | |||
this.name = name; | |||
this.defaultValue = defaultValue; | |||
} | |||
String getName() { | |||
return name; | |||
} | |||
String getDefaultValue() { | |||
return defaultValue; | |||
} | |||
boolean getDefaultValueAsBoolean() { | |||
return "true".equalsIgnoreCase(defaultValue); | |||
} | |||
Integer getDefaultValueAsInt() { | |||
if (StringUtils.isNotEmpty(defaultValue)) { | |||
try { | |||
return Integer.parseInt(defaultValue); | |||
} catch (NumberFormatException e) { | |||
throw new IllegalStateException("Default value of property " + name + " is not an integer: " + defaultValue, e); | |||
} | |||
} | |||
return null; | |||
} | |||
} |
@@ -0,0 +1,185 @@ | |||
/* | |||
* 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; | |||
import java.net.InetAddress; | |||
import java.net.NetworkInterface; | |||
import java.net.SocketException; | |||
import java.util.ArrayList; | |||
import java.util.Enumeration; | |||
import java.util.List; | |||
import java.util.stream.Collectors; | |||
import javax.annotation.Nonnull; | |||
import javax.annotation.Nullable; | |||
import org.apache.commons.lang.StringUtils; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
import org.sonar.process.Props; | |||
/** | |||
* Properties of the cluster configuration | |||
*/ | |||
final class ClusterProperties { | |||
private static final Logger LOGGER = LoggerFactory.getLogger(ClusterProperties.class); | |||
private final int port; | |||
private final boolean enabled; | |||
private final boolean portAutoincrement; | |||
private final List<String> members; | |||
private final List<String> interfaces; | |||
private final String name; | |||
private final String logLevel; | |||
ClusterProperties(@Nonnull Props props) { | |||
port = props.valueAsInt(ClusterParameters.PORT.getName(), ClusterParameters.PORT.getDefaultValueAsInt()); | |||
enabled = props.valueAsBoolean(ClusterParameters.ENABLED.getName(), ClusterParameters.ENABLED.getDefaultValueAsBoolean()); | |||
portAutoincrement = props.valueAsBoolean(ClusterParameters.PORT_AUTOINCREMENT.getName(), ClusterParameters.PORT_AUTOINCREMENT.getDefaultValueAsBoolean()); | |||
interfaces = extractInterfaces( | |||
props.value(ClusterParameters.INTERFACES.getName(), ClusterParameters.INTERFACES.getDefaultValue()) | |||
); | |||
name = props.value(ClusterParameters.NAME.getName(), ClusterParameters.NAME.getDefaultValue()); | |||
logLevel = props.value(ClusterParameters.HAZELCAST_LOG_LEVEL.getName(), ClusterParameters.HAZELCAST_LOG_LEVEL.getDefaultValue()); | |||
members = extractMembers( | |||
props.value(ClusterParameters.MEMBERS.getName(), ClusterParameters.MEMBERS.getDefaultValue()) | |||
); | |||
} | |||
void populateProps(@Nonnull Props props) { | |||
props.set(ClusterParameters.PORT.getName(), Integer.toString(port)); | |||
props.set(ClusterParameters.ENABLED.getName(), Boolean.toString(enabled)); | |||
props.set(ClusterParameters.PORT_AUTOINCREMENT.getName(), Boolean.toString(portAutoincrement)); | |||
props.set(ClusterParameters.INTERFACES.getName(), interfaces.stream().collect(Collectors.joining(","))); | |||
props.set(ClusterParameters.NAME.getName(), name); | |||
props.set(ClusterParameters.HAZELCAST_LOG_LEVEL.getName(), logLevel); | |||
props.set(ClusterParameters.MEMBERS.getName(), members.stream().collect(Collectors.joining(","))); | |||
} | |||
int getPort() { | |||
return port; | |||
} | |||
boolean isEnabled() { | |||
return enabled; | |||
} | |||
boolean isPortAutoincrement() { | |||
return portAutoincrement; | |||
} | |||
List<String> getMembers() { | |||
return members; | |||
} | |||
List<String> getInterfaces() { | |||
return interfaces; | |||
} | |||
String getName() { | |||
return name; | |||
} | |||
String getLogLevel() { | |||
return logLevel; | |||
} | |||
void validate() { | |||
if (!enabled) { | |||
return; | |||
} | |||
// Name is required in cluster mode | |||
checkArgument( | |||
StringUtils.isNotEmpty(name), | |||
"Cluster have been enabled but a %s has not been defined.", | |||
ClusterParameters.NAME.getName() | |||
); | |||
// Test validity of port | |||
checkArgument( | |||
port > 0 && port < 65_536, | |||
"Cluster port have been set to %d which is outside the range [1-65535].", | |||
port | |||
); | |||
// Test the interfaces parameter | |||
try { | |||
List<String> localInterfaces = findAllLocalIPs(); | |||
interfaces.forEach( | |||
inet -> checkArgument( | |||
StringUtils.isEmpty(inet) || localInterfaces.contains(inet), | |||
"Interface %s is not available on this machine.", | |||
inet | |||
) | |||
); | |||
} catch (SocketException e) { | |||
LOGGER.warn("Unable to retrieve network interfaces. Interfaces won't be checked", e); | |||
} | |||
} | |||
private static List<String> extractMembers(final String members) { | |||
List<String> result = new ArrayList<>(); | |||
for (String member : members.split(",")) { | |||
if (StringUtils.isNotEmpty(member)) { | |||
if (!member.contains(":")) { | |||
result.add( | |||
String.format("%s:%s", member, ClusterParameters.PORT.getDefaultValue()) | |||
); | |||
} else { | |||
result.add(member); | |||
} | |||
} | |||
} | |||
return result; | |||
} | |||
private static List<String> extractInterfaces(final String interfaces) { | |||
List<String> result = new ArrayList<>(); | |||
for (String iface : interfaces.split(",")) { | |||
if (StringUtils.isNotEmpty(iface)) { | |||
result.add(iface); | |||
} | |||
} | |||
return result; | |||
} | |||
private static List<String> findAllLocalIPs() throws SocketException { | |||
Enumeration<NetworkInterface> netInterfaces = NetworkInterface.getNetworkInterfaces(); | |||
List<String> localInterfaces = new ArrayList<>(); | |||
while (netInterfaces.hasMoreElements()) { | |||
NetworkInterface networkInterface = netInterfaces.nextElement(); | |||
Enumeration<InetAddress> ips = networkInterface.getInetAddresses(); | |||
while (ips.hasMoreElements()) { | |||
InetAddress ip = ips.nextElement(); | |||
localInterfaces.add(ip.getHostAddress()); | |||
} | |||
} | |||
return localInterfaces; | |||
} | |||
private static void checkArgument(boolean expression, | |||
@Nullable String messageTemplate, | |||
@Nullable Object... args) { | |||
if (!expression) { | |||
throw new IllegalArgumentException(String.format(messageTemplate, args)); | |||
} | |||
} | |||
} |
@@ -31,6 +31,7 @@ import ch.qos.logback.core.encoder.Encoder; | |||
import ch.qos.logback.core.joran.spi.ConsoleTarget; | |||
import ch.qos.logback.core.rolling.RollingFileAppender; | |||
import java.io.File; | |||
import java.io.IOException; | |||
import java.util.Iterator; | |||
import java.util.Properties; | |||
import org.junit.AfterClass; | |||
@@ -39,9 +40,10 @@ import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.ExpectedException; | |||
import org.junit.rules.TemporaryFolder; | |||
import org.sonar.process.logging.LogbackHelper; | |||
import org.slf4j.LoggerFactory; | |||
import org.sonar.process.ProcessProperties; | |||
import org.sonar.process.Props; | |||
import org.sonar.process.logging.LogbackHelper; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.slf4j.Logger.ROOT_LOGGER_NAME; | |||
@@ -230,7 +232,7 @@ public class AppLoggingTest { | |||
expectedException.expect(IllegalArgumentException.class); | |||
expectedException.expectMessage("log level ERROR in property sonar.log.level is not a supported value (allowed levels are [TRACE, DEBUG, INFO])"); | |||
underTest.configure(props); | |||
} | |||
@@ -244,6 +246,31 @@ public class AppLoggingTest { | |||
underTest.configure(props); | |||
} | |||
@Test | |||
public void no_info_log_from_hazelcast() throws IOException { | |||
props.set(ClusterParameters.ENABLED.getName(), "true"); | |||
new ClusterProperties(props).populateProps(props); | |||
underTest.configure(props); | |||
assertThat( | |||
LoggerFactory.getLogger("com.hazelcast").isInfoEnabled() | |||
).isEqualTo(false); | |||
} | |||
@Test | |||
public void configure_logging_for_hazelcast() throws IOException { | |||
props.set(ClusterParameters.ENABLED.getName(), "true"); | |||
props.set(ClusterParameters.HAZELCAST_LOG_LEVEL.getName(), "INFO"); | |||
underTest.configure(props); | |||
assertThat( | |||
LoggerFactory.getLogger("com.hazelcast").isInfoEnabled() | |||
).isEqualTo(true); | |||
assertThat( | |||
LoggerFactory.getLogger("com.hazelcast").isDebugEnabled() | |||
).isEqualTo(false); | |||
} | |||
private void emulateRunFromSonarScript() { | |||
props.set("sonar.wrapped", "true"); | |||
} |
@@ -87,7 +87,8 @@ public class AppTest { | |||
public void start_all_processes_if_cluster_mode_is_disabled() throws Exception { | |||
Props props = initDefaultProps(); | |||
Monitor monitor = mock(Monitor.class); | |||
App app = new App(props.rawProperties(), properties -> props, monitor, checkFsConfigOnReload, javaCommandFactory); | |||
Cluster cluster = mock(Cluster.class); | |||
App app = new App(props.rawProperties(), properties -> props, monitor, checkFsConfigOnReload, javaCommandFactory, cluster); | |||
app.start(); | |||
ArgumentCaptor<Supplier<List<JavaCommand>>> argument = newJavaCommandArgumentCaptor(); | |||
@@ -146,7 +147,8 @@ public class AppTest { | |||
// setup an App that emulate reloading of props returning different configuration | |||
Iterator<Props> propsIterator = Arrays.asList(initialProps, props).iterator(); | |||
Monitor monitor = mock(Monitor.class); | |||
App app = new App(initialProps.rawProperties(), properties -> propsIterator.next(), monitor, checkFsConfigOnReload, javaCommandFactory); | |||
Cluster cluster = mock(Cluster.class); | |||
App app = new App(initialProps.rawProperties(), properties -> propsIterator.next(), monitor, checkFsConfigOnReload, javaCommandFactory, cluster); | |||
// start App and capture the JavaCommand supplier it provides to the Monitor | |||
app.start(); | |||
Supplier<List<JavaCommand>> supplier = captureJavaCommandsSupplier(monitor); | |||
@@ -175,7 +177,8 @@ public class AppTest { | |||
// setup an App that emulate reloading of props returning different configuration | |||
Iterator<Props> propsIterator = Arrays.asList(initialProps, props).iterator(); | |||
Monitor monitor = mock(Monitor.class); | |||
App app = new App(initialProps.rawProperties(), properties -> propsIterator.next(), monitor, checkFsConfigOnReload, javaCommandFactory); | |||
Cluster cluster = mock(Cluster.class); | |||
App app = new App(initialProps.rawProperties(), properties -> propsIterator.next(), monitor, checkFsConfigOnReload, javaCommandFactory, cluster); | |||
// start App and capture the JavaCommand supplier it provides to the Monitor | |||
app.start(); | |||
Supplier<List<JavaCommand>> supplier = captureJavaCommandsSupplier(monitor); | |||
@@ -213,7 +216,8 @@ public class AppTest { | |||
private List<JavaCommand> start(Props props) throws Exception { | |||
Monitor monitor = mock(Monitor.class); | |||
App app = new App(props.rawProperties(), properties -> props, monitor, checkFsConfigOnReload, javaCommandFactory); | |||
Cluster cluster = mock(Cluster.class); | |||
App app = new App(props.rawProperties(), properties -> props, monitor, checkFsConfigOnReload, javaCommandFactory, cluster); | |||
app.start(); | |||
ArgumentCaptor<Supplier<List<JavaCommand>>> argument = newJavaCommandArgumentCaptor(); | |||
verify(monitor).start(argument.capture()); |
@@ -0,0 +1,181 @@ | |||
/* | |||
* 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; | |||
import java.util.Arrays; | |||
import java.util.Collections; | |||
import java.util.List; | |||
import java.util.Properties; | |||
import java.util.stream.Stream; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.ExpectedException; | |||
import org.sonar.process.Props; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.data.MapEntry.entry; | |||
public class ClusterPropertiesTest { | |||
@Rule | |||
public ExpectedException expectedException = ExpectedException.none(); | |||
@Test | |||
public void test_default_values() throws Exception { | |||
ClusterProperties props = new ClusterProperties(new Props(new Properties())); | |||
assertThat(props.getInterfaces()) | |||
.isEqualTo(Collections.emptyList()); | |||
assertThat(props.isPortAutoincrement()) | |||
.isEqualTo(false); | |||
assertThat(props.getPort()) | |||
.isEqualTo(9003); | |||
assertThat(props.isEnabled()) | |||
.isEqualTo(false); | |||
assertThat(props.getMembers()) | |||
.isEqualTo(Collections.emptyList()); | |||
assertThat(props.getName()) | |||
.isEqualTo(""); | |||
assertThat(props.getLogLevel()) | |||
.isEqualTo("WARN"); | |||
} | |||
@Test | |||
public void test_port_parameter() { | |||
Props props = new Props(new Properties()); | |||
props.set(ClusterParameters.ENABLED.getName(), "true"); | |||
props.set(ClusterParameters.NAME.getName(), "sonarqube"); | |||
Stream.of("-50", "0", "65536", "128563").forEach( | |||
port -> { | |||
props.set(ClusterParameters.PORT.getName(), port); | |||
ClusterProperties clusterProperties = new ClusterProperties(props); | |||
expectedException.expect(IllegalArgumentException.class); | |||
expectedException.expectMessage( | |||
String.format("Cluster port have been set to %s which is outside the range [1-65535].", port) | |||
); | |||
clusterProperties.validate(); | |||
} | |||
); | |||
} | |||
@Test | |||
public void test_interfaces_parameter() { | |||
Props props = new Props(new Properties()); | |||
props.set(ClusterParameters.ENABLED.getName(), "true"); | |||
props.set(ClusterParameters.NAME.getName(), "sonarqube"); | |||
props.set(ClusterParameters.INTERFACES.getName(), "8.8.8.8"); // This IP belongs to Google | |||
ClusterProperties clusterProperties = new ClusterProperties(props); | |||
expectedException.expect(IllegalArgumentException.class); | |||
expectedException.expectMessage( | |||
String.format("Interface %s is not available on this machine.", "8.8.8.8") | |||
); | |||
clusterProperties.validate(); | |||
} | |||
@Test | |||
public void test_missing_name() { | |||
Props props = new Props(new Properties()); | |||
props.set(ClusterParameters.ENABLED.getName(), "true"); | |||
props.set(ClusterParameters.NAME.getName(), ""); | |||
ClusterProperties clusterProperties = new ClusterProperties(props); | |||
expectedException.expect(IllegalArgumentException.class); | |||
expectedException.expectMessage( | |||
String.format("Cluster have been enabled but a %s has not been defined.", | |||
ClusterParameters.NAME.getName()) | |||
); | |||
clusterProperties.validate(); | |||
} | |||
@Test | |||
public void validate_does_not_fail_if_cluster_enabled_and_name_specified() { | |||
Props props = new Props(new Properties()); | |||
props.set(ClusterParameters.ENABLED.getName(), "true"); | |||
props.set(ClusterParameters.NAME.getName(), "sonarqube"); | |||
ClusterProperties clusterProperties = new ClusterProperties(props); | |||
clusterProperties.validate(); | |||
} | |||
@Test | |||
public void test_members() { | |||
Props props = new Props(new Properties()); | |||
props.set(ClusterParameters.ENABLED.getName(), "true"); | |||
assertThat( | |||
retrieveMembers(props) | |||
).isEqualTo( | |||
Collections.emptyList() | |||
); | |||
props.set(ClusterParameters.MEMBERS.getName(), "192.168.1.1"); | |||
assertThat( | |||
retrieveMembers(props) | |||
).isEqualTo( | |||
Arrays.asList("192.168.1.1:" + ClusterParameters.PORT.getDefaultValue()) | |||
); | |||
props.set(ClusterParameters.MEMBERS.getName(), "192.168.1.2:5501"); | |||
assertThat( | |||
retrieveMembers(props) | |||
).containsExactlyInAnyOrder( | |||
"192.168.1.2:5501" | |||
); | |||
props.set(ClusterParameters.MEMBERS.getName(), "192.168.1.2:5501,192.168.1.1"); | |||
assertThat( | |||
retrieveMembers(props) | |||
).containsExactlyInAnyOrder( | |||
"192.168.1.2:5501", "192.168.1.1:" + ClusterParameters.PORT.getDefaultValue() | |||
); | |||
} | |||
@Test | |||
public void test_cluster_properties() { | |||
Props props = new Props(new Properties()); | |||
props.set(ClusterParameters.ENABLED.getName(), "true"); | |||
props.set(ClusterParameters.MEMBERS.getName(), "192.168.1.1"); | |||
props.set(ClusterParameters.INTERFACES.getName(), "192.168.1.30"); | |||
props.set(ClusterParameters.PORT.getName(), "9003"); | |||
props.set(ClusterParameters.HAZELCAST_LOG_LEVEL.getName(), "INFO"); | |||
props.set(ClusterParameters.PORT_AUTOINCREMENT.getName(), "false"); | |||
props.set(ClusterParameters.NAME.getName(), "sonarqube"); | |||
ClusterProperties clusterProperties = new ClusterProperties(props); | |||
clusterProperties.populateProps(props); | |||
assertThat(props.rawProperties()).containsOnly( | |||
entry(ClusterParameters.HAZELCAST_LOG_LEVEL.getName(), "INFO"), | |||
entry(ClusterParameters.PORT.getName(), "9003"), | |||
entry(ClusterParameters.ENABLED.getName(), "true"), | |||
entry(ClusterParameters.INTERFACES.getName(), "192.168.1.30"), | |||
entry(ClusterParameters.MEMBERS.getName(), "192.168.1.1:" + ClusterParameters.PORT.getDefaultValue()), | |||
entry(ClusterParameters.NAME.getName(), "sonarqube"), | |||
entry(ClusterParameters.PORT_AUTOINCREMENT.getName(), "false") | |||
); | |||
} | |||
private List<String> retrieveMembers(Props props) { | |||
return new ClusterProperties(props).getMembers(); | |||
} | |||
} |
@@ -0,0 +1,164 @@ | |||
/* | |||
* 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; | |||
import com.hazelcast.core.HazelcastException; | |||
import java.io.IOException; | |||
import java.net.Inet4Address; | |||
import java.net.InetAddress; | |||
import java.net.InetSocketAddress; | |||
import java.net.NetworkInterface; | |||
import java.net.ServerSocket; | |||
import java.net.SocketException; | |||
import java.util.Enumeration; | |||
import java.util.Properties; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.ExpectedException; | |||
import org.sonar.process.Props; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.fail; | |||
public class ClusterTest { | |||
@Rule | |||
public ExpectedException expectedException = ExpectedException.none(); | |||
@Test | |||
public void test_cluster() throws Exception { | |||
Properties properties = new Properties(); | |||
properties.put(ClusterParameters.ENABLED.getName(), "true"); | |||
ClusterProperties clusterProperties = toClusterProperties(properties); | |||
try (Cluster cluster = new Cluster(clusterProperties)) { | |||
assertThat( | |||
cluster.isActive() | |||
).isEqualTo(true); | |||
} | |||
properties.put(ClusterParameters.ENABLED.getName(), "false"); | |||
clusterProperties = toClusterProperties(properties); | |||
try (Cluster cluster = new Cluster(clusterProperties)) { | |||
assertThat( | |||
cluster.isActive() | |||
).isEqualTo(false); | |||
} | |||
} | |||
@Test | |||
public void test_interface() throws Exception { | |||
String ipAddress = findIPv4Address().getHostAddress(); | |||
int port = findAvailablePort(); | |||
Properties properties = new Properties(); | |||
properties.put(ClusterParameters.INTERFACES.getName(), ipAddress); | |||
properties.put(ClusterParameters.PORT.getName(), Integer.toString(port)); | |||
properties.put(ClusterParameters.ENABLED.getName(), "true"); | |||
ClusterProperties clusterProperties = toClusterProperties(properties); | |||
try (Cluster cluster = new Cluster(clusterProperties)) { | |||
assertThat( | |||
cluster.hazelcastInstance.getConfig().getNetworkConfig().getInterfaces().isEnabled() | |||
).isEqualTo(true); | |||
assertThat( | |||
cluster.hazelcastInstance.getConfig().getNetworkConfig().getInterfaces().getInterfaces() | |||
).containsExactly(ipAddress); | |||
InetSocketAddress localSocket = (InetSocketAddress) cluster.hazelcastInstance.getLocalEndpoint().getSocketAddress(); | |||
assertThat( | |||
(localSocket).getPort() | |||
).isEqualTo(port); | |||
assertThat( | |||
(localSocket).getAddress().getHostAddress() | |||
).isEqualTo(ipAddress); | |||
} | |||
} | |||
@Test | |||
public void test_with_already_used_port() throws IOException { | |||
InetAddress ipAddress = findIPv4Address(); | |||
Cluster cluster = null; | |||
try (ServerSocket socket = new ServerSocket(0, 50, ipAddress)) { | |||
Properties properties = new Properties(); | |||
properties.put(ClusterParameters.INTERFACES.getName(), ipAddress.getHostAddress()); | |||
properties.put(ClusterParameters.PORT.getName(), Integer.toString(socket.getLocalPort())); | |||
properties.put(ClusterParameters.ENABLED.getName(), "true"); | |||
ClusterProperties clusterProperties = toClusterProperties(properties); | |||
expectedException.expect(HazelcastException.class); | |||
cluster = new Cluster(clusterProperties); | |||
} finally { | |||
if (cluster != null) { | |||
cluster.close(); | |||
} | |||
} | |||
} | |||
@Test | |||
public void test_adding_port_to_members() throws Exception { | |||
InetAddress ipAddress = findIPv4Address(); | |||
Properties properties = new Properties(); | |||
properties.put(ClusterParameters.ENABLED.getName(), "true"); | |||
properties.put(ClusterParameters.MEMBERS.getName(), ipAddress.getHostAddress()); | |||
ClusterProperties clusterProperties = toClusterProperties(properties); | |||
try (Cluster cluster = new Cluster(clusterProperties)) { | |||
assertThat( | |||
cluster.hazelcastInstance.getConfig().getNetworkConfig().getJoin().getTcpIpConfig().getMembers() | |||
).containsExactly(ipAddress.getHostAddress() + ":9003"); | |||
} | |||
} | |||
private ClusterProperties toClusterProperties(Properties properties) { | |||
return new ClusterProperties(new Props(properties)); | |||
} | |||
private InetAddress findIPv4Address() throws SocketException { | |||
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces(); | |||
while (interfaces.hasMoreElements()) { | |||
NetworkInterface networkInterface = interfaces.nextElement(); | |||
if (!networkInterface.isVirtual() | |||
&& !networkInterface.isLoopback() | |||
&& !networkInterface.getName().startsWith("docker")) { | |||
Enumeration<InetAddress> ips = networkInterface.getInetAddresses(); | |||
while (ips.hasMoreElements()) { | |||
InetAddress ip = ips.nextElement(); | |||
if (ip instanceof Inet4Address) { | |||
return ip; | |||
} | |||
} | |||
} | |||
} | |||
fail("Missing IPv4 address"); | |||
return null; | |||
} | |||
private int findAvailablePort() throws IOException { | |||
try (ServerSocket ignored = new ServerSocket(0)) { | |||
return ignored.getLocalPort(); | |||
} | |||
} | |||
} |