diff options
author | Eric Hartmann <hartmann.eric@gmail.Com> | 2017-02-26 14:47:10 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-02-26 14:47:10 +0100 |
commit | 5b80ef622737b940407647f9431b7a7942861b05 (patch) | |
tree | 9152572464f38061e29986fa2cfe38a90e2f0d1c /sonar-application/src/main/java/org/sonar/application | |
parent | ee94866db117108ef272d33213fea15c095a7894 (diff) | |
download | sonarqube-5b80ef622737b940407647f9431b7a7942861b05.tar.gz sonarqube-5b80ef622737b940407647f9431b7a7942861b05.zip |
SONAR-8818 Add Hazelcast and its configuration (#1699)
* SONAR-8818 Introduce Hazelcast
Diffstat (limited to 'sonar-application/src/main/java/org/sonar/application')
5 files changed, 371 insertions, 4 deletions
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 01c965f822c..c1a7e61d566 100644 --- a/sonar-application/src/main/java/org/sonar/application/App.java +++ b/sonar-application/src/main/java/org/sonar/application/App.java @@ -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(); } diff --git a/sonar-application/src/main/java/org/sonar/application/AppLogging.java b/sonar-application/src/main/java/org/sonar/application/AppLogging.java index d3600dffee1..7ea96ff62bb 100644 --- a/sonar-application/src/main/java/org/sonar/application/AppLogging.java +++ b/sonar-application/src/main/java/org/sonar/application/AppLogging.java @@ -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; } diff --git a/sonar-application/src/main/java/org/sonar/application/Cluster.java b/sonar-application/src/main/java/org/sonar/application/Cluster.java new file mode 100644 index 00000000000..621af696320 --- /dev/null +++ b/sonar-application/src/main/java/org/sonar/application/Cluster.java @@ -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(); + } + } +} diff --git a/sonar-application/src/main/java/org/sonar/application/ClusterParameters.java b/sonar-application/src/main/java/org/sonar/application/ClusterParameters.java new file mode 100644 index 00000000000..c0e15e0ca1b --- /dev/null +++ b/sonar-application/src/main/java/org/sonar/application/ClusterParameters.java @@ -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; + } +} diff --git a/sonar-application/src/main/java/org/sonar/application/ClusterProperties.java b/sonar-application/src/main/java/org/sonar/application/ClusterProperties.java new file mode 100644 index 00000000000..1b86838ac5c --- /dev/null +++ b/sonar-application/src/main/java/org/sonar/application/ClusterProperties.java @@ -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)); + } + } +} |