Browse Source

SONAR-8935 Add a log when joining a cluster

tags/6.4-RC1
Eric Hartmann 7 years ago
parent
commit
7e3819c677

+ 0
- 1
server/sonar-process-monitor/src/main/java/org/sonar/application/SchedulerImpl.java View File

@@ -161,7 +161,6 @@ public class SchedulerImpl implements Scheduler, ProcessEventListener, ProcessLi
if (process != null) {
process.stop(1, TimeUnit.MINUTES);
}

}

/**

+ 24
- 160
server/sonar-process-monitor/src/main/java/org/sonar/application/cluster/AppStateClusterImpl.java View File

@@ -20,41 +20,22 @@

package org.sonar.application.cluster;

import com.hazelcast.config.Config;
import com.hazelcast.config.JoinConfig;
import com.hazelcast.config.NetworkConfig;
import com.hazelcast.core.EntryEvent;
import com.hazelcast.core.EntryListener;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IAtomicReference;
import com.hazelcast.core.ILock;
import com.hazelcast.core.MapEvent;
import com.hazelcast.core.ReplicatedMap;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.application.AppState;
import org.sonar.application.AppStateListener;
import org.sonar.application.config.AppSettings;
import org.sonar.process.ProcessId;

public class AppStateClusterImpl implements AppState {
static final String OPERATIONAL_PROCESSES = "OPERATIONAL_PROCESSES";
static final String LEADER = "LEADER";
private static Logger LOGGER = LoggerFactory.getLogger(AppStateClusterImpl.class);

static final String SONARQUBE_VERSION = "SONARQUBE_VERSION";

private final List<AppStateListener> listeners = new ArrayList<>();
private final ReplicatedMap<ClusterProcess, Boolean> operationalProcesses;
private final Map<ProcessId, Boolean> localProcesses = new EnumMap<>(ProcessId.class);
private final String listenerUuid;

final HazelcastInstance hzInstance;
private final HazelcastCluster hazelcastCluster;

public AppStateClusterImpl(AppSettings appSettings) {
ClusterProperties clusterProperties = new ClusterProperties(appSettings);
@@ -64,56 +45,15 @@ public class AppStateClusterImpl implements AppState {
throw new IllegalStateException("Cluster is not enabled on this instance");
}

Config hzConfig = new Config();
try {
hzConfig.setInstanceName(InetAddress.getLocalHost().getHostName());
} catch (UnknownHostException e) {
// Ignore it
}

hzConfig.getGroupConfig().setName(clusterProperties.getName());

// Configure the network instance
NetworkConfig netConfig = hzConfig.getNetworkConfig();
netConfig
.setPort(clusterProperties.getPort())
.setReuseAddress(true);

if (!clusterProperties.getNetworkInterfaces().isEmpty()) {
netConfig.getInterfaces()
.setEnabled(true)
.setInterfaces(clusterProperties.getNetworkInterfaces());
}
hazelcastCluster = HazelcastCluster.create(clusterProperties);

// 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.getHosts());

// 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);

hzInstance = Hazelcast.newHazelcastInstance(hzConfig);
operationalProcesses = hzInstance.getReplicatedMap(OPERATIONAL_PROCESSES);
listenerUuid = operationalProcesses.addEntryListener(new OperationalProcessListener());
String members = hazelcastCluster.getMembers().stream().collect(Collectors.joining(","));
LOGGER.info("Joined the cluster [{}] that contains the following hosts : [{}]", hazelcastCluster.getName(), members);
}

@Override
public void addListener(@Nonnull AppStateListener listener) {
listeners.add(listener);
hazelcastCluster.addListener(listener);
}

@Override
@@ -121,39 +61,18 @@ public class AppStateClusterImpl implements AppState {
if (local) {
return localProcesses.computeIfAbsent(processId, p -> false);
}
for (Map.Entry<ClusterProcess, Boolean> entry : operationalProcesses.entrySet()) {
if (entry.getKey().getProcessId().equals(processId) && entry.getValue()) {
return true;
}
}
return false;
return hazelcastCluster.isOperational(processId);
}

@Override
public void setOperational(@Nonnull ProcessId processId) {
localProcesses.put(processId, true);
operationalProcesses.put(new ClusterProcess(getLocalUuid(), processId), Boolean.TRUE);
hazelcastCluster.setOperational(processId);
}

@Override
public boolean tryToLockWebLeader() {
IAtomicReference<String> leader = hzInstance.getAtomicReference(LEADER);
if (leader.get() == null) {
ILock lock = hzInstance.getLock(LEADER);
lock.lock();
try {
if (leader.get() == null) {
leader.set(getLocalUuid());
return true;
} else {
return false;
}
} finally {
lock.unlock();
}
} else {
return false;
}
return hazelcastCluster.tryToLockWebLeader();
}

@Override
@@ -163,80 +82,25 @@ public class AppStateClusterImpl implements AppState {

@Override
public void close() {
if (hzInstance != null) {
operationalProcesses.removeEntryListener(listenerUuid);
operationalProcesses.keySet().forEach(
clusterNodeProcess -> {
if (clusterNodeProcess.getNodeUuid().equals(getLocalUuid())) {
operationalProcesses.remove(clusterNodeProcess);
}
});
hzInstance.shutdown();
}
hazelcastCluster.close();
}

@Override
public void registerSonarQubeVersion(String sonarqubeVersion) {
IAtomicReference<String> sqVersion = hzInstance.getAtomicReference(SONARQUBE_VERSION);
if (sqVersion.get() == null) {
ILock lock = hzInstance.getLock(SONARQUBE_VERSION);
lock.lock();
try {
if (sqVersion.get() == null) {
sqVersion.set(sonarqubeVersion);
}
} finally {
lock.unlock();
}
}

String clusterVersion = sqVersion.get();
if (!sqVersion.get().equals(sonarqubeVersion)) {
hzInstance.shutdown();
throw new IllegalStateException(
String.format("The local version %s is not the same as the cluster %s", sonarqubeVersion, clusterVersion)
);
}
hazelcastCluster.registerSonarQubeVersion(sonarqubeVersion);
}

private String getLocalUuid() {
return hzInstance.getLocalEndpoint().getUuid();
HazelcastCluster getHazelcastCluster() {
return hazelcastCluster;
}

private class OperationalProcessListener implements EntryListener<ClusterProcess, Boolean> {

@Override
public void entryAdded(EntryEvent<ClusterProcess, Boolean> event) {
if (event.getValue()) {
listeners.forEach(appStateListener -> appStateListener.onAppStateOperational(event.getKey().getProcessId()));
}
}

@Override
public void entryRemoved(EntryEvent<ClusterProcess, Boolean> event) {
// Ignore it
}

@Override
public void entryUpdated(EntryEvent<ClusterProcess, Boolean> event) {
if (event.getValue()) {
listeners.forEach(appStateListener -> appStateListener.onAppStateOperational(event.getKey().getProcessId()));
}
}

@Override
public void entryEvicted(EntryEvent<ClusterProcess, Boolean> event) {
// Ignore it
}

@Override
public void mapCleared(MapEvent event) {
// Ignore it
}

@Override
public void mapEvicted(MapEvent event) {
// Ignore it
}
/**
* Only used for testing purpose
*
* @param logger
*/
static void setLogger(Logger logger) {
AppStateClusterImpl.LOGGER = logger;
}

}

+ 1
- 0
server/sonar-process-monitor/src/main/java/org/sonar/application/cluster/ClusterProperties.java View File

@@ -82,6 +82,7 @@ public final class ClusterProperties {
if (!enabled) {
return;
}

// Test validity of port
checkArgument(
port > 0 && port < 65_536,

+ 234
- 0
server/sonar-process-monitor/src/main/java/org/sonar/application/cluster/HazelcastCluster.java View File

@@ -0,0 +1,234 @@
/*
* 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.cluster;

import com.hazelcast.config.Config;
import com.hazelcast.config.JoinConfig;
import com.hazelcast.config.NetworkConfig;
import com.hazelcast.core.EntryEvent;
import com.hazelcast.core.EntryListener;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IAtomicReference;
import com.hazelcast.core.ILock;
import com.hazelcast.core.MapEvent;
import com.hazelcast.core.ReplicatedMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.sonar.application.AppStateListener;
import org.sonar.process.ProcessId;

import static java.util.stream.Collectors.toList;
import static org.sonar.process.NetworkUtils.getHostName;

public class HazelcastCluster implements AutoCloseable {
static final String OPERATIONAL_PROCESSES = "OPERATIONAL_PROCESSES";
static final String LEADER = "LEADER";
static final String HOSTNAME = "HOSTNAME";
static final String SONARQUBE_VERSION = "SONARQUBE_VERSION";

private final List<AppStateListener> listeners = new ArrayList<>();
private final ReplicatedMap<ClusterProcess, Boolean> operationalProcesses;
private final String operationalProcessListenerUUID;

final HazelcastInstance hzInstance;

private HazelcastCluster(Config hzConfig) {
// Create the Hazelcast instance
hzInstance = Hazelcast.newHazelcastInstance(hzConfig);

// Get or create the replicated map
operationalProcesses = hzInstance.getReplicatedMap(OPERATIONAL_PROCESSES);
operationalProcessListenerUUID = operationalProcesses.addEntryListener(new OperationalProcessListener());
}

String getLocalUuid() {
return hzInstance.getLocalEndpoint().getUuid();
}

String getName() {
return hzInstance.getConfig().getGroupConfig().getName();
}

List<String> getMembers() {
return hzInstance.getCluster().getMembers().stream()
.filter(m -> !m.localMember())
.map(m -> m.getStringAttribute(HOSTNAME))
.collect(toList());
}

void addListener(AppStateListener listener) {
listeners.add(listener);
}

boolean isOperational(ProcessId processId) {
for (Map.Entry<ClusterProcess, Boolean> entry : operationalProcesses.entrySet()) {
if (entry.getKey().getProcessId().equals(processId) && entry.getValue()) {
return true;
}
}
return false;
}

void setOperational(ProcessId processId) {
operationalProcesses.put(new ClusterProcess(getLocalUuid(), processId), Boolean.TRUE);
}

boolean tryToLockWebLeader() {
IAtomicReference<String> leader = hzInstance.getAtomicReference(LEADER);
if (leader.get() == null) {
ILock lock = hzInstance.getLock(LEADER);
lock.lock();
try {
if (leader.get() == null) {
leader.set(getLocalUuid());
return true;
} else {
return false;
}
} finally {
lock.unlock();
}
} else {
return false;
}
}

public void registerSonarQubeVersion(String sonarqubeVersion) {
IAtomicReference<String> sqVersion = hzInstance.getAtomicReference(SONARQUBE_VERSION);
if (sqVersion.get() == null) {
ILock lock = hzInstance.getLock(SONARQUBE_VERSION);
lock.lock();
try {
if (sqVersion.get() == null) {
sqVersion.set(sonarqubeVersion);
}
} finally {
lock.unlock();
}
}

String clusterVersion = sqVersion.get();
if (!sqVersion.get().equals(sonarqubeVersion)) {
throw new IllegalStateException(
String.format("The local version %s is not the same as the cluster %s", sonarqubeVersion, clusterVersion)
);
}
}

@Override
public void close() {
if (hzInstance != null) {
// Removing listeners
operationalProcesses.removeEntryListener(operationalProcessListenerUUID);

// Removing the operationalProcess from the replicated map
operationalProcesses.keySet().forEach(
clusterNodeProcess -> {
if (clusterNodeProcess.getNodeUuid().equals(getLocalUuid())) {
operationalProcesses.remove(clusterNodeProcess);
}
});

// Shutdown Hazelcast properly
hzInstance.shutdown();
}
}

public static HazelcastCluster create(ClusterProperties clusterProperties) {
Config hzConfig = new Config();
hzConfig.getGroupConfig().setName(clusterProperties.getName());

// Configure the network instance
NetworkConfig netConfig = hzConfig.getNetworkConfig();
netConfig
.setPort(clusterProperties.getPort())
.setReuseAddress(true);

if (!clusterProperties.getNetworkInterfaces().isEmpty()) {
netConfig.getInterfaces()
.setEnabled(true)
.setInterfaces(clusterProperties.getNetworkInterfaces());
}

// 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.getHosts());

// 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");

// Trying to resolve the hostname
hzConfig.getMemberAttributeConfig().setStringAttribute(HOSTNAME, getHostName());

// We are not using the partition group of Hazelcast, so disabling it
hzConfig.getPartitionGroupConfig().setEnabled(false);
return new HazelcastCluster(hzConfig);
}

private class OperationalProcessListener implements EntryListener<ClusterProcess, Boolean> {
@Override
public void entryAdded(EntryEvent<ClusterProcess, Boolean> event) {
if (event.getValue()) {
listeners.forEach(appStateListener -> appStateListener.onAppStateOperational(event.getKey().getProcessId()));
}
}

@Override
public void entryRemoved(EntryEvent<ClusterProcess, Boolean> event) {
// Ignore it
}

@Override
public void entryUpdated(EntryEvent<ClusterProcess, Boolean> event) {
if (event.getValue()) {
listeners.forEach(appStateListener -> appStateListener.onAppStateOperational(event.getKey().getProcessId()));
}
}

@Override
public void entryEvicted(EntryEvent<ClusterProcess, Boolean> event) {
// Ignore it
}

@Override
public void mapCleared(MapEvent event) {
// Ignore it
}

@Override
public void mapEvicted(MapEvent event) {
// Ignore it
}
}
}

+ 35
- 47
server/sonar-process-monitor/src/test/java/org/sonar/application/cluster/AppStateClusterImplTest.java View File

@@ -20,28 +20,28 @@
package org.sonar.application.cluster;

import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.ReplicatedMap;
import java.net.InetAddress;
import java.util.UUID;
import java.io.IOException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.DisableOnDebug;
import org.junit.rules.ExpectedException;
import org.junit.rules.TestRule;
import org.junit.rules.Timeout;
import org.slf4j.Logger;
import org.sonar.application.AppStateListener;
import org.sonar.application.config.TestAppSettings;
import org.sonar.process.ProcessId;
import org.sonar.process.ProcessProperties;

import static org.assertj.core.api.Java6Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.sonar.application.cluster.AppStateClusterImpl.OPERATIONAL_PROCESSES;
import static org.sonar.application.cluster.AppStateClusterImpl.SONARQUBE_VERSION;
import static org.sonar.application.cluster.HazelcastCluster.SONARQUBE_VERSION;
import static org.sonar.application.cluster.HazelcastTestHelper.createHazelcastClient;
import static org.sonar.application.cluster.HazelcastTestHelper.newClusterSettings;

public class AppStateClusterImplTest {

@@ -72,6 +72,23 @@ public class AppStateClusterImplTest {
}
}

@Test
public void log_when_sonarqube_is_joining_a_cluster () throws IOException, InterruptedException, IllegalAccessException, NoSuchFieldException {
// Now launch an instance that try to be part of the hzInstance cluster
TestAppSettings settings = newClusterSettings();

Logger logger = mock(Logger.class);
AppStateClusterImpl.setLogger(logger);

try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(settings)) {
verify(logger).info(
eq("Joined the cluster [{}] that contains the following hosts : [{}]"),
eq("sonarqube"),
anyString()
);
}
}

@Test
public void test_listeners() throws InterruptedException {
AppStateListener listener = mock(AppStateListener.class);
@@ -89,57 +106,35 @@ public class AppStateClusterImplTest {
}

@Test
public void simulate_network_cluster() throws InterruptedException {
public void registerSonarQubeVersion_publishes_version_on_first_call() {
TestAppSettings settings = newClusterSettings();
settings.set(ProcessProperties.CLUSTER_NETWORK_INTERFACES, InetAddress.getLoopbackAddress().getHostAddress());
AppStateListener listener = mock(AppStateListener.class);

try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(settings)) {
appStateCluster.addListener(listener);
appStateCluster.registerSonarQubeVersion("6.4.1.5");

HazelcastInstance hzInstance = createHazelcastClient(appStateCluster);
String uuid = UUID.randomUUID().toString();
ReplicatedMap<ClusterProcess, Boolean> replicatedMap = hzInstance.getReplicatedMap(OPERATIONAL_PROCESSES);
// process is not up yet --> no events are sent to listeners
replicatedMap.put(
new ClusterProcess(uuid, ProcessId.ELASTICSEARCH),
Boolean.FALSE);

// process is up yet --> notify listeners
replicatedMap.replace(
new ClusterProcess(uuid, ProcessId.ELASTICSEARCH),
Boolean.TRUE);

// should be called only once
verify(listener, timeout(20_000)).onAppStateOperational(ProcessId.ELASTICSEARCH);
verifyNoMoreInteractions(listener);

hzInstance.shutdown();
assertThat(hzInstance.getAtomicReference(SONARQUBE_VERSION).get())
.isNotNull()
.isInstanceOf(String.class)
.isEqualTo("6.4.1.5");
}
}

@Test
public void registerSonarQubeVersion_publishes_version_on_first_call() {
public void reset_throws_always_ISE() {
TestAppSettings settings = newClusterSettings();

try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(settings)) {
appStateCluster.registerSonarQubeVersion("6.4.1.5");

HazelcastInstance hzInstance = createHazelcastClient(appStateCluster);
assertThat(hzInstance.getAtomicReference(SONARQUBE_VERSION).get())
.isNotNull()
.isInstanceOf(String.class)
.isEqualTo("6.4.1.5");
expectedException.expect(IllegalStateException.class);
expectedException.expectMessage("state reset is not supported in cluster mode");
appStateCluster.reset();
}
}

@Test
public void registerSonarQubeVersion_throws_ISE_if_initial_version_is_different() throws Exception {
// Now launch an instance that try to be part of the hzInstance cluster
TestAppSettings settings = new TestAppSettings();
settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
settings.set(ProcessProperties.CLUSTER_NAME, "sonarqube");

TestAppSettings settings = newClusterSettings();

try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(settings)) {
// Register first version
@@ -152,11 +147,4 @@ public class AppStateClusterImplTest {
appStateCluster.registerSonarQubeVersion("2.0.0");
}
}

private static TestAppSettings newClusterSettings() {
TestAppSettings settings = new TestAppSettings();
settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
settings.set(ProcessProperties.CLUSTER_NAME, "sonarqube");
return settings;
}
}

+ 200
- 0
server/sonar-process-monitor/src/test/java/org/sonar/application/cluster/HazelcastClusterTest.java View File

@@ -0,0 +1,200 @@
/*
* 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.cluster;

import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.ReplicatedMap;
import java.net.InetAddress;
import java.util.AbstractMap;
import java.util.UUID;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.DisableOnDebug;
import org.junit.rules.ExpectedException;
import org.junit.rules.TestRule;
import org.junit.rules.Timeout;
import org.sonar.application.AppStateListener;
import org.sonar.application.config.TestAppSettings;
import org.sonar.process.ProcessId;
import org.sonar.process.ProcessProperties;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.sonar.application.cluster.HazelcastCluster.LEADER;
import static org.sonar.application.cluster.HazelcastCluster.OPERATIONAL_PROCESSES;
import static org.sonar.application.cluster.HazelcastCluster.SONARQUBE_VERSION;
import static org.sonar.application.cluster.HazelcastTestHelper.createHazelcastClient;
import static org.sonar.application.cluster.HazelcastTestHelper.newClusterSettings;
import static org.sonar.process.ProcessProperties.CLUSTER_NAME;

public class HazelcastClusterTest {
@Rule
public TestRule safeGuard = new DisableOnDebug(Timeout.seconds(10));

@Rule
public ExpectedException expectedException = ExpectedException.none();

@Test
public void test_two_tryToLockWebLeader_must_return_true() {
ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
assertThat(hzCluster.tryToLockWebLeader()).isEqualTo(true);
assertThat(hzCluster.tryToLockWebLeader()).isEqualTo(false);
}
}

@Test
public void when_another_process_locked_webleader_tryToLockWebLeader_must_return_false() {
ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
HazelcastInstance hzInstance = createHazelcastClient(hzCluster);
hzInstance.getAtomicReference(LEADER).set("aaaa");
assertThat(hzCluster.tryToLockWebLeader()).isEqualTo(false);
}
}

@Test
public void members_must_be_empty_when_there_is_no_other_node() {
ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
assertThat(hzCluster.getMembers()).isEmpty();
}
}

@Test
public void set_operational_is_writing_to_cluster() {
ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {

hzCluster.setOperational(ProcessId.ELASTICSEARCH);

assertThat(hzCluster.isOperational(ProcessId.ELASTICSEARCH)).isTrue();
assertThat(hzCluster.isOperational(ProcessId.WEB_SERVER)).isFalse();
assertThat(hzCluster.isOperational(ProcessId.COMPUTE_ENGINE)).isFalse();

// Connect via Hazelcast client to test values
HazelcastInstance hzInstance = createHazelcastClient(hzCluster);
ReplicatedMap<ClusterProcess, Boolean> operationalProcesses = hzInstance.getReplicatedMap(OPERATIONAL_PROCESSES);
assertThat(operationalProcesses)
.containsExactly(new AbstractMap.SimpleEntry<>(new ClusterProcess(hzCluster.getLocalUuid(), ProcessId.ELASTICSEARCH), Boolean.TRUE));
}
}

@Test
public void cluster_name_comes_from_configuration() {
TestAppSettings testAppSettings = newClusterSettings();
testAppSettings.set(CLUSTER_NAME, "a_cluster_");
ClusterProperties clusterProperties = new ClusterProperties(testAppSettings);
try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
assertThat(hzCluster.getName()).isEqualTo("a_cluster_");
}
}

@Test
public void localUUID_must_not_be_empty() {
ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
assertThat(hzCluster.getLocalUuid()).isNotEmpty();
}
}

@Test
public void when_a_process_is_set_operational_listener_must_be_triggered() {
ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
AppStateListener listener = mock(AppStateListener.class);
hzCluster.addListener(listener);

// ElasticSearch is not operational
assertThat(hzCluster.isOperational(ProcessId.ELASTICSEARCH)).isFalse();

// Simulate a node that set ElasticSearch operational
HazelcastInstance hzInstance = createHazelcastClient(hzCluster);
ReplicatedMap<ClusterProcess, Boolean> operationalProcesses = hzInstance.getReplicatedMap(OPERATIONAL_PROCESSES);
operationalProcesses.put(new ClusterProcess(UUID.randomUUID().toString(), ProcessId.ELASTICSEARCH), Boolean.TRUE);
verify(listener, timeout(20_000)).onAppStateOperational(ProcessId.ELASTICSEARCH);
verifyNoMoreInteractions(listener);

// ElasticSearch is operational
assertThat(hzCluster.isOperational(ProcessId.ELASTICSEARCH)).isTrue();
}
}


@Test
public void registerSonarQubeVersion_publishes_version_on_first_call() {
ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
hzCluster.registerSonarQubeVersion("1.0.0.0");

HazelcastInstance hzInstance = createHazelcastClient(hzCluster);
assertThat(hzInstance.getAtomicReference(SONARQUBE_VERSION).get()).isEqualTo("1.0.0.0");
}
}


@Test
public void registerSonarQubeVersion_throws_ISE_if_initial_version_is_different() throws Exception {
ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
// Register first version
hzCluster.registerSonarQubeVersion("1.0.0");

expectedException.expect(IllegalStateException.class);
expectedException.expectMessage("The local version 2.0.0 is not the same as the cluster 1.0.0");

// Registering a second different version must trigger an exception
hzCluster.registerSonarQubeVersion("2.0.0");
}
}

@Test
public void simulate_network_cluster() throws InterruptedException {
TestAppSettings settings = newClusterSettings();
settings.set(ProcessProperties.CLUSTER_NETWORK_INTERFACES, InetAddress.getLoopbackAddress().getHostAddress());
AppStateListener listener = mock(AppStateListener.class);

try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(settings)) {
appStateCluster.addListener(listener);

HazelcastInstance hzInstance = createHazelcastClient(appStateCluster.getHazelcastCluster());
String uuid = UUID.randomUUID().toString();
ReplicatedMap<ClusterProcess, Boolean> replicatedMap = hzInstance.getReplicatedMap(OPERATIONAL_PROCESSES);
// process is not up yet --> no events are sent to listeners
replicatedMap.put(
new ClusterProcess(uuid, ProcessId.ELASTICSEARCH),
Boolean.FALSE);

// process is up yet --> notify listeners
replicatedMap.replace(
new ClusterProcess(uuid, ProcessId.ELASTICSEARCH),
Boolean.TRUE);

// should be called only once
verify(listener, timeout(20_000)).onAppStateOperational(ProcessId.ELASTICSEARCH);
verifyNoMoreInteractions(listener);

hzInstance.shutdown();
}
}
}

+ 18
- 3
server/sonar-process-monitor/src/test/java/org/sonar/application/cluster/HazelcastTestHelper.java View File

@@ -24,19 +24,34 @@ import com.hazelcast.client.HazelcastClient;
import com.hazelcast.client.config.ClientConfig;
import com.hazelcast.core.HazelcastInstance;
import java.net.InetSocketAddress;
import org.sonar.application.config.TestAppSettings;
import org.sonar.process.ProcessProperties;

public class HazelcastTestHelper {

static HazelcastInstance createHazelcastClient(AppStateClusterImpl appStateCluster) {
static HazelcastInstance createHazelcastClient(HazelcastCluster hzCluster) {
ClientConfig clientConfig = new ClientConfig();
InetSocketAddress socketAddress = (InetSocketAddress) appStateCluster.hzInstance.getLocalEndpoint().getSocketAddress();
InetSocketAddress socketAddress = (InetSocketAddress) hzCluster.hzInstance.getLocalEndpoint().getSocketAddress();

clientConfig.getNetworkConfig().getAddresses().add(
String.format("%s:%d",
socketAddress.getHostString(),
socketAddress.getPort()
));
clientConfig.getGroupConfig().setName(appStateCluster.hzInstance.getConfig().getGroupConfig().getName());
clientConfig.getGroupConfig().setName(hzCluster.getName());
return HazelcastClient.newHazelcastClient(clientConfig);
}

static HazelcastInstance createHazelcastClient(AppStateClusterImpl appStateCluster) {
return createHazelcastClient(appStateCluster.getHazelcastCluster());
}


static TestAppSettings newClusterSettings() {
TestAppSettings settings = new TestAppSettings();
settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
settings.set(ProcessProperties.CLUSTER_NAME, "sonarqube");
return settings;
}

}

+ 48
- 0
server/sonar-process/src/main/java/org/sonar/process/NetworkUtils.java View File

@@ -20,11 +20,21 @@
package org.sonar.process;

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.net.UnknownHostException;
import java.util.stream.Collectors;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ArrayUtils;

import static java.lang.String.format;
import static java.util.Collections.list;
import static org.apache.commons.lang.StringUtils.isBlank;

public final class NetworkUtils {

private static final RandomPortFinder RANDOM_PORT_FINDER = new RandomPortFinder();
@@ -37,6 +47,44 @@ public final class NetworkUtils {
return RANDOM_PORT_FINDER.getNextAvailablePort();
}

/**
* Identifying the localhost machine
* It will try to retrieve the hostname and the IPv4 addresses
*
* @return "hostname (ipv4_1, ipv4_2...)"
*/
public static String getHostName() {
String hostname;
String ips;
try {
hostname = InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
hostname = "unresolved hostname";
}

try {
ips = list(NetworkInterface.getNetworkInterfaces()).stream()
.flatMap(netif ->
list(netif.getInetAddresses()).stream()
.filter(inetAddress ->
// Removing IPv6 for the time being
inetAddress instanceof Inet4Address &&
// Removing loopback addresses, useless for identifying a server
!inetAddress.isLoopbackAddress() &&
// Removing interfaces without IPs
!isBlank(inetAddress.getHostAddress())
)
.map(InetAddress::getHostAddress)
)
.filter(p -> !isBlank(p))
.collect(Collectors.joining(","));
} catch (SocketException e) {
ips = "unresolved IPs";
}

return format("%s (%s)", hostname, ips);
}

static class RandomPortFinder {
private static final int MAX_TRY = 10;
// Firefox blocks some reserved ports : http://www-archive.mozilla.org/projects/netlib/PortBanning.html

+ 5
- 0
server/sonar-process/src/test/java/org/sonar/process/NetworkUtilsTest.java View File

@@ -75,4 +75,9 @@ public class NetworkUtilsTest {

randomPortFinder.getNextAvailablePort();
}

@Test
public void getHostName_must_return_a_value() {
assertThat(NetworkUtils.getHostName()).containsPattern(".* \\(.*\\)");
}
}

+ 0
- 2
server/sonar-server/src/main/java/org/sonar/server/es/EsClientProvider.java View File

@@ -35,8 +35,6 @@ import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.process.ProcessProperties;

import static org.apache.commons.lang.StringUtils.isBlank;

@ComputeEngineSide
@ServerSide
public class EsClientProvider extends ProviderAdapter {

Loading…
Cancel
Save