]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9802 replace Hazelcast clients by members
authorSimon Brandhof <simon.brandhof@sonarsource.com>
Fri, 15 Sep 2017 12:05:57 +0000 (14:05 +0200)
committerSimon Brandhof <simon.brandhof@sonarsource.com>
Tue, 26 Sep 2017 21:49:37 +0000 (23:49 +0200)
Web Server and Compute Engine processes must use
plain HZ members but not clients so that they can
be involved in distributed computing (contribute to
api/system/info response)

76 files changed:
server/sonar-ce/src/main/java/org/sonar/ce/CeDistributedInformationImpl.java
server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java
server/sonar-ce/src/test/java/org/sonar/ce/CeDistributedInformationImplTest.java
server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
server/sonar-ce/src/test/java/org/sonar/ce/container/HazelcastTestHelper.java
server/sonar-main/src/main/java/org/sonar/application/AppLogging.java
server/sonar-main/src/main/java/org/sonar/application/AppReloaderImpl.java
server/sonar-main/src/main/java/org/sonar/application/AppStateFactory.java
server/sonar-main/src/main/java/org/sonar/application/SchedulerImpl.java
server/sonar-main/src/main/java/org/sonar/application/cluster/ClusterAppState.java
server/sonar-main/src/main/java/org/sonar/application/cluster/ClusterAppStateImpl.java
server/sonar-main/src/main/java/org/sonar/application/cluster/ClusterProperties.java [deleted file]
server/sonar-main/src/main/java/org/sonar/application/cluster/HazelcastCluster.java [deleted file]
server/sonar-main/src/main/java/org/sonar/application/cluster/SearchNodeHealthProvider.java [deleted file]
server/sonar-main/src/main/java/org/sonar/application/cluster/health/DelegateHealthStateRefresherExecutorService.java [new file with mode: 0644]
server/sonar-main/src/main/java/org/sonar/application/cluster/health/HealthStateSharing.java [new file with mode: 0644]
server/sonar-main/src/main/java/org/sonar/application/cluster/health/HealthStateSharingImpl.java [new file with mode: 0644]
server/sonar-main/src/main/java/org/sonar/application/cluster/health/SearchNodeHealthProvider.java [new file with mode: 0644]
server/sonar-main/src/main/java/org/sonar/application/cluster/health/package-info.java [new file with mode: 0644]
server/sonar-main/src/main/java/org/sonar/application/config/AppSettingsLoaderImpl.java
server/sonar-main/src/main/java/org/sonar/application/config/ClusterSettings.java
server/sonar-main/src/main/java/org/sonar/application/es/EsSettings.java
server/sonar-main/src/test/java/org/sonar/application/AppLoggingTest.java
server/sonar-main/src/test/java/org/sonar/application/AppReloaderImplTest.java
server/sonar-main/src/test/java/org/sonar/application/AppStateFactoryTest.java
server/sonar-main/src/test/java/org/sonar/application/SchedulerImplTest.java
server/sonar-main/src/test/java/org/sonar/application/TestClusterAppState.java
server/sonar-main/src/test/java/org/sonar/application/cluster/ClusterAppStateImplTest.java
server/sonar-main/src/test/java/org/sonar/application/cluster/ClusterPropertiesTest.java [deleted file]
server/sonar-main/src/test/java/org/sonar/application/cluster/HazelcastClusterTest.java [deleted file]
server/sonar-main/src/test/java/org/sonar/application/cluster/HazelcastClusterTestHelper.java [deleted file]
server/sonar-main/src/test/java/org/sonar/application/cluster/HazelcastTesting.java [new file with mode: 0644]
server/sonar-main/src/test/java/org/sonar/application/cluster/SearchNodeHealthProviderTest.java [deleted file]
server/sonar-main/src/test/java/org/sonar/application/cluster/health/DelegateHealthStateRefresherExecutorServiceTest.java [new file with mode: 0644]
server/sonar-main/src/test/java/org/sonar/application/cluster/health/SearchNodeHealthProviderTest.java [new file with mode: 0644]
server/sonar-main/src/test/java/org/sonar/application/config/ClusterSettingsLoopbackTest.java
server/sonar-main/src/test/java/org/sonar/application/config/ClusterSettingsTest.java
server/sonar-main/src/test/java/org/sonar/application/es/EsSettingsTest.java
server/sonar-process/pom.xml
server/sonar-process/src/main/java/org/sonar/process/NetworkUtils.java
server/sonar-process/src/main/java/org/sonar/process/NetworkUtilsImpl.java
server/sonar-process/src/main/java/org/sonar/process/ProcessProperties.java
server/sonar-process/src/main/java/org/sonar/process/cluster/ClusterObjectKeys.java [deleted file]
server/sonar-process/src/main/java/org/sonar/process/cluster/ClusterProperties.java [deleted file]
server/sonar-process/src/main/java/org/sonar/process/cluster/HazelcastClient.java [deleted file]
server/sonar-process/src/main/java/org/sonar/process/cluster/NodeType.java
server/sonar-process/src/main/java/org/sonar/process/cluster/health/DelegateHealthStateRefresherExecutorService.java [deleted file]
server/sonar-process/src/main/java/org/sonar/process/cluster/health/HealthStateSharing.java [deleted file]
server/sonar-process/src/main/java/org/sonar/process/cluster/health/HealthStateSharingImpl.java [deleted file]
server/sonar-process/src/main/java/org/sonar/process/cluster/health/NodeHealthProvider.java
server/sonar-process/src/main/java/org/sonar/process/cluster/health/SharedHealthStateImpl.java
server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMember.java [new file with mode: 0644]
server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberBuilder.java [new file with mode: 0644]
server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberImpl.java [new file with mode: 0644]
server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastObjects.java [new file with mode: 0644]
server/sonar-process/src/main/java/org/sonar/process/cluster/hz/package-info.java [new file with mode: 0644]
server/sonar-process/src/test/java/org/sonar/process/NetworkUtilsImplTest.java [new file with mode: 0644]
server/sonar-process/src/test/java/org/sonar/process/cluster/health/DelegateHealthStateRefresherExecutorServiceTest.java [deleted file]
server/sonar-process/src/test/java/org/sonar/process/cluster/hz/HazelcastMemberBuilderTest.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/cluster/StartableHazelcastMember.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/cluster/package-info.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/es/EsClientProvider.java
server/sonar-server/src/main/java/org/sonar/server/es/NewIndex.java
server/sonar-server/src/main/java/org/sonar/server/health/NodeHealthProviderImpl.java
server/sonar-server/src/main/java/org/sonar/server/hz/HazelcastLocalClient.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/hz/package-info.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/platform/WebServer.java
server/sonar-server/src/main/java/org/sonar/server/platform/WebServerImpl.java
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
server/sonar-server/src/test/java/org/sonar/server/es/EsClientProviderTest.java
server/sonar-server/src/test/java/org/sonar/server/es/NewIndexTest.java
server/sonar-server/src/test/java/org/sonar/server/health/NodeHealthModuleTest.java
server/sonar-server/src/test/java/org/sonar/server/health/NodeHealthProviderImplTest.java
server/sonar-server/src/test/java/org/sonar/server/rule/index/RuleIndexDefinitionTest.java
sonar-application/src/main/java/org/sonar/application/App.java
tests/src/test/java/org/sonarqube/tests/cluster/ClusterTest.java

index ee5b14c87549f476d2c2d076ed11214303366a86..23152f3521aabde524876dd75e69613f775aaf62 100644 (file)
@@ -25,27 +25,27 @@ import java.util.Set;
 import java.util.concurrent.locks.Lock;
 import org.picocontainer.Startable;
 import org.sonar.ce.taskprocessor.CeWorkerFactory;
-import org.sonar.process.cluster.ClusterObjectKeys;
-import org.sonar.process.cluster.HazelcastClient;
+import org.sonar.process.cluster.hz.HazelcastObjects;
+import org.sonar.process.cluster.hz.HazelcastMember;
 
 import static org.sonar.core.util.stream.MoreCollectors.toSet;
-import static org.sonar.process.cluster.ClusterObjectKeys.WORKER_UUIDS;
+import static org.sonar.process.cluster.hz.HazelcastObjects.WORKER_UUIDS;
 
 /**
  * Provide the set of worker's UUID in a clustered SonarQube instance
  */
 public class CeDistributedInformationImpl implements CeDistributedInformation, Startable {
-  private final HazelcastClient hazelcastClient;
+  private final HazelcastMember hazelcastMember;
   private final CeWorkerFactory ceCeWorkerFactory;
 
-  public CeDistributedInformationImpl(HazelcastClient hazelcastClient, CeWorkerFactory ceCeWorkerFactory) {
-    this.hazelcastClient = hazelcastClient;
+  public CeDistributedInformationImpl(HazelcastMember hazelcastMember, CeWorkerFactory ceCeWorkerFactory) {
+    this.hazelcastMember = hazelcastMember;
     this.ceCeWorkerFactory = ceCeWorkerFactory;
   }
 
   @Override
   public Set<String> getWorkerUUIDs() {
-    Set<String> connectedWorkerUUIDs = hazelcastClient.getMemberUuids();
+    Set<String> connectedWorkerUUIDs = hazelcastMember.getMemberUuids();
 
     return getClusteredWorkerUUIDs().entrySet().stream()
       .filter(e -> connectedWorkerUUIDs.contains(e.getKey()))
@@ -56,12 +56,12 @@ public class CeDistributedInformationImpl implements CeDistributedInformation, S
 
   @Override
   public void broadcastWorkerUUIDs() {
-    getClusteredWorkerUUIDs().put(hazelcastClient.getUUID(), ceCeWorkerFactory.getWorkerUUIDs());
+    getClusteredWorkerUUIDs().put(hazelcastMember.getUuid(), ceCeWorkerFactory.getWorkerUUIDs());
   }
 
   @Override
   public Lock acquireCleanJobLock() {
-    return hazelcastClient.getLock(ClusterObjectKeys.CE_CLEANING_JOB_LOCK);
+    return hazelcastMember.getLock(HazelcastObjects.CE_CLEANING_JOB_LOCK);
   }
 
   @Override
@@ -72,10 +72,10 @@ public class CeDistributedInformationImpl implements CeDistributedInformation, S
   @Override
   public void stop() {
     // Removing the worker UUIDs
-    getClusteredWorkerUUIDs().remove(hazelcastClient.getUUID());
+    getClusteredWorkerUUIDs().remove(hazelcastMember.getUuid());
   }
 
   private Map<String, Set<String>> getClusteredWorkerUUIDs() {
-    return hazelcastClient.getReplicatedMap(WORKER_UUIDS);
+    return hazelcastMember.getReplicatedMap(WORKER_UUIDS);
   }
 }
index 16529cd55e0f4d269b293514b6bf3ac8b094a20c..b443c7f689482817f3816b30942e600fa0dc880f 100644 (file)
@@ -73,16 +73,17 @@ import org.sonar.db.DatabaseChecker;
 import org.sonar.db.DbClient;
 import org.sonar.db.DefaultDatabase;
 import org.sonar.db.purge.PurgeProfiler;
+import org.sonar.process.NetworkUtils;
+import org.sonar.process.ProcessProperties;
 import org.sonar.process.Props;
-import org.sonar.process.cluster.ClusterProperties;
 import org.sonar.process.logging.LogbackHelper;
+import org.sonar.server.cluster.StartableHazelcastMember;
 import org.sonar.server.component.ComponentFinder;
 import org.sonar.server.component.index.ComponentIndexer;
 import org.sonar.server.computation.task.projectanalysis.ProjectAnalysisTaskModule;
 import org.sonar.server.debt.DebtModelPluginRepository;
 import org.sonar.server.debt.DebtRulesXMLImporter;
 import org.sonar.server.event.NewAlerts;
-import org.sonar.server.hz.HazelcastLocalClient;
 import org.sonar.server.issue.IssueFieldsSetter;
 import org.sonar.server.issue.index.IssueIndex;
 import org.sonar.server.issue.index.IssueIndexer;
@@ -234,6 +235,7 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer {
       SonarRuntimeImpl.forSonarQube(ApiVersion.load(System2.INSTANCE), SonarQubeSide.COMPUTE_ENGINE),
       CeProcessLogging.class,
       UuidFactoryImpl.INSTANCE,
+      NetworkUtils.INSTANCE,
       WebServerImpl.class,
       LogbackHelper.class,
       DefaultDatabase.class,
@@ -417,9 +419,9 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer {
       // cleaning
       CeCleaningModule.class);
 
-    if (props.valueAsBoolean(ClusterProperties.CLUSTER_ENABLED)) {
+    if (props.valueAsBoolean(ProcessProperties.CLUSTER_ENABLED)) {
       container.add(
-        HazelcastLocalClient.class,
+        StartableHazelcastMember.class,
         CeDistributedInformationImpl.class);
     } else {
       container.add(StandaloneCeDistributedInformation.class);
index 638b01d43ead30d8511be8ecb0086e610c4788c0..8792800ce5c62b3f272564a0a7f782312595653e 100644 (file)
@@ -27,13 +27,13 @@ import java.util.Map;
 import java.util.Set;
 import org.junit.Test;
 import org.sonar.ce.taskprocessor.CeWorkerFactory;
-import org.sonar.server.hz.HazelcastLocalClient;
+import org.sonar.server.cluster.StartableHazelcastMember;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.data.MapEntry.entry;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
-import static org.sonar.process.cluster.ClusterObjectKeys.WORKER_UUIDS;
+import static org.sonar.process.cluster.hz.HazelcastObjects.WORKER_UUIDS;
 
 public class CeDistributedInformationImplTest {
   private String clientUUID1 = "1";
@@ -45,11 +45,11 @@ public class CeDistributedInformationImplTest {
     clientUUID3, ImmutableSet.of("4", "5", "6")
   );
 
-  private HazelcastLocalClient hzClientWrapper = mock(HazelcastLocalClient.class);
+  private StartableHazelcastMember hzClientWrapper = mock(StartableHazelcastMember.class);
 
   @Test
   public void getWorkerUUIDs_returns_union_of_workers_uuids_of_local_and_cluster_worker_uuids() {
-    when(hzClientWrapper.getUUID()).thenReturn(clientUUID1);
+    when(hzClientWrapper.getUuid()).thenReturn(clientUUID1);
     when(hzClientWrapper.getMemberUuids()).thenReturn(ImmutableSet.of(clientUUID1, clientUUID2, clientUUID3));
     when(hzClientWrapper.getReplicatedMap(WORKER_UUIDS)).thenReturn(workerMap);
 
@@ -59,7 +59,7 @@ public class CeDistributedInformationImplTest {
 
   @Test
   public void getWorkerUUIDs_must_filter_absent_client() {
-    when(hzClientWrapper.getUUID()).thenReturn(clientUUID1);
+    when(hzClientWrapper.getUuid()).thenReturn(clientUUID1);
     when(hzClientWrapper.getMemberUuids()).thenReturn(ImmutableSet.of(clientUUID1, clientUUID2));
     when(hzClientWrapper.getReplicatedMap(WORKER_UUIDS)).thenReturn(workerMap);
 
@@ -74,7 +74,7 @@ public class CeDistributedInformationImplTest {
     connectedClients.add(clientUUID1);
     connectedClients.add(clientUUID2);
 
-    when(hzClientWrapper.getUUID()).thenReturn(clientUUID1);
+    when(hzClientWrapper.getUuid()).thenReturn(clientUUID1);
     when(hzClientWrapper.getMemberUuids()).thenReturn(connectedClients);
     when(hzClientWrapper.getReplicatedMap(WORKER_UUIDS)).thenReturn(modifiableWorkerMap);
 
@@ -101,7 +101,7 @@ public class CeDistributedInformationImplTest {
     Map modifiableWorkerMap = new HashMap();
     modifiableWorkerMap.putAll(workerMap);
 
-    when(hzClientWrapper.getUUID()).thenReturn(clientUUID1);
+    when(hzClientWrapper.getUuid()).thenReturn(clientUUID1);
     when(hzClientWrapper.getMemberUuids()).thenReturn(connectedClients);
     when(hzClientWrapper.getReplicatedMap(WORKER_UUIDS)).thenReturn(modifiableWorkerMap);
 
index 727166bb963d22d29069067aa82e060096707b00..640b8e76c4349bdfcaaf2887e6250144ddda64e1 100644 (file)
  */
 package org.sonar.ce.container;
 
-import com.hazelcast.core.HazelcastInstance;
 import java.io.File;
 import java.io.IOException;
 import java.net.InetAddress;
 import java.util.Date;
+import java.util.Optional;
 import java.util.Properties;
 import java.util.stream.Collectors;
 import org.apache.commons.dbcp.BasicDataSource;
+import org.hamcrest.CoreMatchers;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -45,19 +46,22 @@ import org.sonar.process.NetworkUtils;
 import org.sonar.process.ProcessId;
 import org.sonar.process.ProcessProperties;
 import org.sonar.process.Props;
-import org.sonar.server.hz.HazelcastLocalClient;
+import org.sonar.server.cluster.StartableHazelcastMember;
 
 import static java.lang.String.valueOf;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assume.assumeThat;
 import static org.mockito.Mockito.mock;
 import static org.sonar.process.ProcessEntryPoint.PROPERTY_PROCESS_INDEX;
+import static org.sonar.process.ProcessEntryPoint.PROPERTY_PROCESS_KEY;
 import static org.sonar.process.ProcessEntryPoint.PROPERTY_SHARED_PATH;
+import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_HOST;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_PORT;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_TYPE;
 import static org.sonar.process.ProcessProperties.PATH_DATA;
 import static org.sonar.process.ProcessProperties.PATH_HOME;
 import static org.sonar.process.ProcessProperties.PATH_TEMP;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_ENABLED;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_LOCALENDPOINT;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_TYPE;
 
 public class ComputeEngineContainerImplTest {
   private static final int CONTAINER_ITSELF = 1;
@@ -83,13 +87,16 @@ public class ComputeEngineContainerImplTest {
 
   @Test
   public void real_start_with_cluster() throws IOException {
-    int port = NetworkUtils.INSTANCE.getNextAvailablePort(InetAddress.getLoopbackAddress());
-    HazelcastInstance hzInstance = HazelcastTestHelper.createHazelcastCluster(NetworkUtils.INSTANCE.getHostname(), port);
+    Optional<InetAddress> localhost = NetworkUtils.INSTANCE.getLocalNonLoopbackIpv4Address();
+    // test is ignored if offline
+    assumeThat(localhost.isPresent(), CoreMatchers.is(true));
 
     Properties properties = getProperties();
-    properties.setProperty(CLUSTER_NODE_TYPE, "application");
+    properties.setProperty(PROPERTY_PROCESS_KEY, ProcessId.COMPUTE_ENGINE.getKey());
     properties.setProperty(CLUSTER_ENABLED, "true");
-    properties.setProperty(CLUSTER_LOCALENDPOINT, String.format("%s:%d", hzInstance.getCluster().getLocalMember().getAddress().getHost(), port));
+    properties.setProperty(CLUSTER_NODE_TYPE, "application");
+    properties.setProperty(CLUSTER_NODE_HOST, localhost.get().getHostAddress());
+    properties.setProperty(CLUSTER_NODE_PORT, "" + NetworkUtils.INSTANCE.getNextAvailablePort(localhost.get()));
 
     // required persisted properties
     insertProperty(CoreProperties.SERVER_ID, "a_startup_id");
@@ -102,7 +109,7 @@ public class ComputeEngineContainerImplTest {
     assertThat(
       picoContainer.getComponentAdapters().stream()
         .map(ComponentAdapter::getComponentImplementation)
-        .collect(Collectors.toList())).contains((Class) HazelcastLocalClient.class,
+        .collect(Collectors.toList())).contains((Class) StartableHazelcastMember.class,
           (Class) CeDistributedInformationImpl.class);
     underTest.stop();
   }
@@ -143,7 +150,7 @@ public class ComputeEngineContainerImplTest {
     );
     assertThat(picoContainer.getParent().getParent().getParent().getComponentAdapters()).hasSize(
       COMPONENTS_IN_LEVEL_1_AT_CONSTRUCTION
-        + 25 // level 1
+        + 26 // level 1
         + 49 // content of DaoModule
         + 3 // content of EsSearchModule
         + 64 // content of CorePropertyDefinitions
@@ -152,7 +159,7 @@ public class ComputeEngineContainerImplTest {
     assertThat(
       picoContainer.getComponentAdapters().stream()
         .map(ComponentAdapter::getComponentImplementation)
-        .collect(Collectors.toList())).doesNotContain((Class) HazelcastLocalClient.class,
+        .collect(Collectors.toList())).doesNotContain((Class) StartableHazelcastMember.class,
           (Class) CeDistributedInformationImpl.class).contains(
             (Class) StandaloneCeDistributedInformation.class);
     assertThat(picoContainer.getParent().getParent().getParent().getParent()).isNull();
index f59da2d47e7be4936370ba2054ccbf5e7ab6d864..07cb6f55d9377a9569ef2dbe8f1cc7b9e4ba4c47 100644 (file)
@@ -23,12 +23,9 @@ package org.sonar.ce.container;
 import com.hazelcast.config.Config;
 import com.hazelcast.config.JoinConfig;
 import com.hazelcast.config.NetworkConfig;
-import com.hazelcast.core.Client;
-import com.hazelcast.core.ClientListener;
 import com.hazelcast.core.Hazelcast;
 import com.hazelcast.core.HazelcastInstance;
 import java.net.InetAddress;
-import org.sonar.process.cluster.ClusterObjectKeys;
 
 /**
  * TODO move outside main sources
@@ -75,25 +72,6 @@ public class HazelcastTestHelper {
     // We are not using the partition group of Hazelcast, so disabling it
     hzConfig.getPartitionGroupConfig().setEnabled(false);
     HazelcastInstance hzInstance = Hazelcast.newHazelcastInstance(hzConfig);
-    hzInstance.getClientService().addClientListener(new ConnectedClientListener(hzInstance));
     return hzInstance;
   }
-
-  private static class ConnectedClientListener implements ClientListener {
-    private final HazelcastInstance hzInstance;
-
-    private ConnectedClientListener(HazelcastInstance hzInstance) {
-      this.hzInstance = hzInstance;
-    }
-
-    @Override
-    public void clientConnected(Client client) {
-      hzInstance.getSet(ClusterObjectKeys.LOCAL_MEMBER_UUIDS).add(client.getUuid());
-    }
-
-    @Override
-    public void clientDisconnected(Client client) {
-      hzInstance.getSet(ClusterObjectKeys.LOCAL_MEMBER_UUIDS).remove(client.getUuid());
-    }
-  }
 }
index 5636ff0a8316451bce605e2c2f29bce77dc3acca..a292075750e20adb28e4bbf5bbce96e4e64bfe99 100644 (file)
@@ -34,7 +34,6 @@ import org.sonar.process.logging.RootLoggerConfig;
 
 import static org.slf4j.Logger.ROOT_LOGGER_NAME;
 import static org.sonar.application.process.StreamGobbler.LOGGER_GOBBLER;
-import static org.sonar.process.cluster.ClusterProperties.HAZELCAST_LOG_LEVEL;
 import static org.sonar.process.logging.RootLoggerConfig.newRootLoggerConfigBuilder;
 
 /**
@@ -140,8 +139,7 @@ public class AppLogging {
       LogLevelConfig.newBuilder(helper.getRootLoggerName())
         .rootLevelFor(ProcessId.APP)
         .immutableLevel("com.hazelcast",
-          Level.toLevel(
-            appSettings.getProps().nonNullValue(HAZELCAST_LOG_LEVEL)))
+          Level.toLevel("WARN"))
         .build(),
       appSettings.getProps());
 
index 59a40ab9a30c4bf8b0572152dff3ac84df686169..3463c1effd6e87d2efb6160c62cd174b7646437c 100644 (file)
@@ -28,7 +28,7 @@ import org.sonar.process.MessageException;
 import org.sonar.process.Props;
 
 import static java.lang.String.format;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
 import static org.sonar.process.ProcessProperties.PATH_DATA;
 import static org.sonar.process.ProcessProperties.PATH_LOGS;
 import static org.sonar.process.ProcessProperties.PATH_TEMP;
index 8f5cfbfe78083ef884b3d3ce8b810ed94f6b894d..a2b9cbd7d700358f792498d918f56bab11115d86 100644 (file)
@@ -22,6 +22,14 @@ package org.sonar.application;
 import org.sonar.application.cluster.ClusterAppStateImpl;
 import org.sonar.application.config.AppSettings;
 import org.sonar.application.config.ClusterSettings;
+import org.sonar.process.ProcessId;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.Props;
+import org.sonar.process.cluster.NodeType;
+import org.sonar.process.cluster.hz.HazelcastMember;
+import org.sonar.process.cluster.hz.HazelcastMemberBuilder;
+
+import static java.util.Arrays.asList;
 
 public class AppStateFactory {
 
@@ -32,6 +40,22 @@ public class AppStateFactory {
   }
 
   public AppState create() {
-    return ClusterSettings.isClusterEnabled(settings) ? new ClusterAppStateImpl(settings) : new AppStateImpl();
+    if (ClusterSettings.isClusterEnabled(settings)) {
+      HazelcastMember hzMember = createHzMember(settings.getProps());
+      return new ClusterAppStateImpl(hzMember);
+    }
+    return new AppStateImpl();
+  }
+
+  private static HazelcastMember createHzMember(Props props) {
+    HazelcastMemberBuilder builder = new HazelcastMemberBuilder()
+      .setClusterName("sonarqube")
+      .setNetworkInterface(props.nonNullValue(ProcessProperties.CLUSTER_NODE_HOST))
+      .setMembers(asList(props.nonNullValue(ProcessProperties.CLUSTER_HOSTS).split(",")))
+      .setNodeType(NodeType.parse(props.nonNullValue(ProcessProperties.CLUSTER_NODE_TYPE)))
+      .setNodeName(props.nonNullValue(ProcessProperties.CLUSTER_NODE_NAME))
+      .setPort(Integer.parseInt(props.nonNullValue(ProcessProperties.CLUSTER_NODE_PORT)))
+      .setProcessId(ProcessId.APP);
+    return builder.build();
   }
 }
index 6aa1d8951a1d8a40dd06c1efe993013c765afd34..4862cfa89dea607a53c374178901151c8cd62f1f 100644 (file)
@@ -29,7 +29,7 @@ import java.util.function.Supplier;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.sonar.application.cluster.ClusterAppState;
-import org.sonar.application.cluster.SearchNodeHealthProvider;
+import org.sonar.application.cluster.health.SearchNodeHealthProvider;
 import org.sonar.application.command.CommandFactory;
 import org.sonar.application.command.EsCommand;
 import org.sonar.application.command.JavaCommand;
@@ -43,8 +43,8 @@ import org.sonar.application.process.ProcessMonitor;
 import org.sonar.application.process.SQProcess;
 import org.sonar.process.NetworkUtils;
 import org.sonar.process.ProcessId;
-import org.sonar.process.cluster.health.HealthStateSharing;
-import org.sonar.process.cluster.health.HealthStateSharingImpl;
+import org.sonar.application.cluster.health.HealthStateSharing;
+import org.sonar.application.cluster.health.HealthStateSharingImpl;
 
 public class SchedulerImpl implements Scheduler, ProcessEventListener, ProcessLifecycleListener, AppStateListener {
 
@@ -150,7 +150,7 @@ public class SchedulerImpl implements Scheduler, ProcessEventListener, ProcessLi
       && ClusterSettings.isLocalElasticsearchEnabled(settings)) {
       ClusterAppState clusterAppState = (ClusterAppState) appState;
       this.healthStateSharing = new HealthStateSharingImpl(
-        clusterAppState.getHazelcastClient(),
+        clusterAppState.getHazelcastMember(),
         new SearchNodeHealthProvider(settings.getProps(), clusterAppState, NetworkUtils.INSTANCE));
       this.healthStateSharing.start();
     }
index b7ac1511610b5c2f03b353a3da595f7d3b5c8b97..54338ee12831f55f5c57dae326da84406c1b14c6 100644 (file)
@@ -20,8 +20,8 @@
 package org.sonar.application.cluster;
 
 import org.sonar.application.AppState;
-import org.sonar.process.cluster.HazelcastClient;
+import org.sonar.process.cluster.hz.HazelcastMember;
 
 public interface ClusterAppState extends AppState {
-  HazelcastClient getHazelcastClient();
+  HazelcastMember getHazelcastMember();
 }
index 3363538f349af3eab0016603c706637547aefc01..80940e96b38949ca5af6767f5c9d511fa8da6cee 100644 (file)
 
 package org.sonar.application.cluster;
 
+import com.hazelcast.core.EntryEvent;
+import com.hazelcast.core.EntryListener;
+import com.hazelcast.core.HazelcastInstanceNotActiveException;
+import com.hazelcast.core.IAtomicReference;
+import com.hazelcast.core.MapEvent;
+import com.hazelcast.core.Member;
+import com.hazelcast.core.MemberAttributeEvent;
+import com.hazelcast.core.MembershipEvent;
+import com.hazelcast.core.MembershipListener;
+import com.hazelcast.core.ReplicatedMap;
+import java.util.ArrayList;
 import java.util.EnumMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
-import java.util.stream.Collectors;
-import javax.annotation.Nonnull;
+import java.util.concurrent.locks.Lock;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.sonar.application.AppStateListener;
-import org.sonar.application.config.AppSettings;
+import org.sonar.process.MessageException;
 import org.sonar.process.ProcessId;
-import org.sonar.process.cluster.HazelcastClient;
+import org.sonar.process.cluster.NodeType;
+import org.sonar.process.cluster.hz.HazelcastMember;
 
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_ENABLED;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_LOCALENDPOINT;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_MEMBERUUID;
+import static java.lang.String.format;
+import static org.sonar.process.cluster.hz.HazelcastObjects.CLUSTER_NAME;
+import static org.sonar.process.cluster.hz.HazelcastObjects.LEADER;
+import static org.sonar.process.cluster.hz.HazelcastObjects.OPERATIONAL_PROCESSES;
+import static org.sonar.process.cluster.hz.HazelcastObjects.SONARQUBE_VERSION;
 
 public class ClusterAppStateImpl implements ClusterAppState {
-  private static Logger LOGGER = LoggerFactory.getLogger(ClusterAppStateImpl.class);
 
-  private final Map<ProcessId, Boolean> localProcesses = new EnumMap<>(ProcessId.class);
-  private final HazelcastCluster hazelcastCluster;
+  private static final Logger LOGGER = LoggerFactory.getLogger(ClusterAppStateImpl.class);
 
-  public ClusterAppStateImpl(AppSettings appSettings) {
-    if (!appSettings.getProps().valueAsBoolean(CLUSTER_ENABLED)) {
-      throw new IllegalStateException("Cluster is not enabled on this instance");
-    }
+  private final HazelcastMember hzMember;
+  private final List<AppStateListener> listeners = new ArrayList<>();
+  private final Map<ProcessId, Boolean> operationalLocalProcesses = new EnumMap<>(ProcessId.class);
+  private final ReplicatedMap<ClusterProcess, Boolean> operationalProcesses;
+  private final String operationalProcessListenerUUID;
+  private final String nodeDisconnectedListenerUUID;
 
-    ClusterProperties clusterProperties = new ClusterProperties(appSettings);
-    clusterProperties.validate();
+  public ClusterAppStateImpl(HazelcastMember hzMember) {
+    this.hzMember = hzMember;
 
-    hazelcastCluster = HazelcastCluster.create(clusterProperties);
-    // Add the local endpoint to be used by processes
-    appSettings.getProps().set(CLUSTER_LOCALENDPOINT, hazelcastCluster.getLocalEndPoint());
-    appSettings.getProps().set(CLUSTER_MEMBERUUID, hazelcastCluster.getLocalUUID());
+    // Get or create the replicated map
+    operationalProcesses = (ReplicatedMap) hzMember.getReplicatedMap(OPERATIONAL_PROCESSES);
+    operationalProcessListenerUUID = operationalProcesses.addEntryListener(new OperationalProcessListener());
+    nodeDisconnectedListenerUUID = hzMember.getCluster().addMembershipListener(new NodeDisconnectedListener());
+  }
 
-    String members = hazelcastCluster.getMembers().stream().collect(Collectors.joining(","));
-    LOGGER.info("Joined a SonarQube cluster that contains the following hosts : [{}]", members);
+  @Override
+  public HazelcastMember getHazelcastMember() {
+    return hzMember;
   }
 
   @Override
-  public void addListener(@Nonnull AppStateListener listener) {
-    hazelcastCluster.addListener(listener);
+  public void addListener(AppStateListener listener) {
+    listeners.add(listener);
   }
 
   @Override
-  public boolean isOperational(@Nonnull ProcessId processId, boolean local) {
+  public boolean isOperational(ProcessId processId, boolean local) {
     if (local) {
-      return localProcesses.computeIfAbsent(processId, p -> false);
+      return operationalLocalProcesses.computeIfAbsent(processId, p -> false);
     }
-    return hazelcastCluster.isOperational(processId);
+    for (Map.Entry<ClusterProcess, Boolean> entry : operationalProcesses.entrySet()) {
+      if (entry.getKey().getProcessId().equals(processId) && entry.getValue()) {
+        return true;
+      }
+    }
+    return false;
   }
 
   @Override
-  public void setOperational(@Nonnull ProcessId processId) {
-    localProcesses.put(processId, true);
-    hazelcastCluster.setOperational(processId);
+  public void setOperational(ProcessId processId) {
+    operationalLocalProcesses.put(processId, true);
+    operationalProcesses.put(new ClusterProcess(hzMember.getUuid(), processId), Boolean.TRUE);
   }
 
   @Override
   public boolean tryToLockWebLeader() {
-    return hazelcastCluster.tryToLockWebLeader();
+    IAtomicReference<String> leader = hzMember.getAtomicReference(LEADER);
+    if (leader.get() == null) {
+      Lock lock = hzMember.getLock(LEADER);
+      lock.lock();
+      try {
+        if (leader.get() == null) {
+          leader.set(hzMember.getUuid());
+          return true;
+        }
+        return false;
+      } finally {
+        lock.unlock();
+      }
+    } else {
+      return false;
+    }
   }
 
   @Override
@@ -88,42 +124,161 @@ public class ClusterAppStateImpl implements ClusterAppState {
     throw new IllegalStateException("state reset is not supported in cluster mode");
   }
 
-  @Override
-  public void close() {
-    hazelcastCluster.close();
-  }
-
   @Override
   public void registerSonarQubeVersion(String sonarqubeVersion) {
-    hazelcastCluster.registerSonarQubeVersion(sonarqubeVersion);
+    IAtomicReference<String> sqVersion = hzMember.getAtomicReference(SONARQUBE_VERSION);
+    if (sqVersion.get() == null) {
+      Lock lock = hzMember.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(
+        format("The local version %s is not the same as the cluster %s", sonarqubeVersion, clusterVersion));
+    }
   }
 
   @Override
   public void registerClusterName(String clusterName) {
-    hazelcastCluster.registerClusterName(clusterName);
+    IAtomicReference<String> property = hzMember.getAtomicReference(CLUSTER_NAME);
+    if (property.get() == null) {
+      Lock lock = hzMember.getLock(CLUSTER_NAME);
+      lock.lock();
+      try {
+        if (property.get() == null) {
+          property.set(clusterName);
+        }
+      } finally {
+        lock.unlock();
+      }
+    }
+
+    String clusterValue = property.get();
+    if (!property.get().equals(clusterName)) {
+      throw new MessageException(
+        format("This node has a cluster name [%s], which does not match [%s] from the cluster", clusterName, clusterValue));
+    }
   }
 
   @Override
   public Optional<String> getLeaderHostName() {
-    return hazelcastCluster.getLeaderHostName();
-  }
-
-  HazelcastCluster getHazelcastCluster() {
-    return hazelcastCluster;
+    String leaderId = (String) hzMember.getAtomicReference(LEADER).get();
+    if (leaderId != null) {
+      Optional<Member> leader = hzMember.getCluster().getMembers().stream().filter(m -> m.getUuid().equals(leaderId)).findFirst();
+      if (leader.isPresent()) {
+        return Optional.of(
+          format("%s (%s)", leader.get().getStringAttribute(HazelcastMember.Attribute.HOSTNAME), leader.get().getStringAttribute(HazelcastMember.Attribute.IP_ADDRESSES)));
+      }
+    }
+    return Optional.empty();
   }
 
   @Override
-  public HazelcastClient getHazelcastClient() {
-    return hazelcastCluster.getHazelcastClient();
+  public void close() {
+    if (hzMember != null) {
+      try {
+        // Removing listeners
+        operationalProcesses.removeEntryListener(operationalProcessListenerUUID);
+        hzMember.getCluster().removeMembershipListener(nodeDisconnectedListenerUUID);
+
+        // Removing the operationalProcess from the replicated map
+        operationalProcesses.keySet().forEach(
+          clusterNodeProcess -> {
+            if (clusterNodeProcess.getNodeUuid().equals(hzMember.getUuid())) {
+              operationalProcesses.remove(clusterNodeProcess);
+            }
+          });
+
+        // Shutdown Hazelcast properly
+        hzMember.close();
+      } catch (HazelcastInstanceNotActiveException e) {
+        // hazelcastCluster may be already closed by the shutdown hook
+        LOGGER.debug("Unable to close Hazelcast cluster", e);
+      }
+    }
   }
 
-  /**
-   * Only used for testing purpose
-   *
-   * @param logger
-   */
-  static void setLogger(Logger logger) {
-    ClusterAppStateImpl.LOGGER = logger;
+  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
+    }
   }
 
+  private class NodeDisconnectedListener implements MembershipListener {
+    @Override
+    public void memberAdded(MembershipEvent membershipEvent) {
+      // Nothing to do
+    }
+
+    @Override
+    public void memberRemoved(MembershipEvent membershipEvent) {
+      removeOperationalProcess(membershipEvent.getMember().getUuid());
+      if (membershipEvent.getMembers().stream()
+        .noneMatch(this::isAppNode)) {
+        purgeSharedMemoryForAppNodes();
+      }
+    }
+
+    @Override
+    public void memberAttributeChanged(MemberAttributeEvent memberAttributeEvent) {
+      // Nothing to do
+    }
+
+    private boolean isAppNode(Member member) {
+      return NodeType.APPLICATION.getValue().equals(member.getStringAttribute(HazelcastMember.Attribute.NODE_TYPE));
+    }
+
+    private void removeOperationalProcess(String uuid) {
+      for (ClusterProcess clusterProcess : operationalProcesses.keySet()) {
+        if (clusterProcess.getNodeUuid().equals(uuid)) {
+          LOGGER.debug("Set node process off for [{}:{}] : ", clusterProcess.getNodeUuid(), clusterProcess.getProcessId());
+          hzMember.getReplicatedMap(OPERATIONAL_PROCESSES).put(clusterProcess, Boolean.FALSE);
+        }
+      }
+    }
+
+    private void purgeSharedMemoryForAppNodes() {
+      LOGGER.info("No more application nodes, clearing cluster information about application nodes.");
+      hzMember.getAtomicReference(LEADER).clear();
+      hzMember.getAtomicReference(SONARQUBE_VERSION).clear();
+    }
+  }
 }
diff --git a/server/sonar-main/src/main/java/org/sonar/application/cluster/ClusterProperties.java b/server/sonar-main/src/main/java/org/sonar/application/cluster/ClusterProperties.java
deleted file mode 100644 (file)
index 1845f30..0000000
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * 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 java.net.InetAddress;
-import java.net.NetworkInterface;
-import java.net.SocketException;
-import java.util.ArrayList;
-import java.util.Enumeration;
-import java.util.List;
-import javax.annotation.Nullable;
-import org.apache.commons.lang.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.sonar.application.config.AppSettings;
-import org.sonar.process.cluster.NodeType;
-
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_HOSTS;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_HOST;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_NAME;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_PORT;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_TYPE;
-
-/**
- * Properties of the cluster configuration
- */
-final class ClusterProperties {
-  private static final String DEFAULT_PORT = "9003";
-  private static final Logger LOGGER = LoggerFactory.getLogger(ClusterProperties.class);
-  static final String HAZELCAST_CLUSTER_NAME = "sonarqube";
-
-  private final int port;
-  private final List<String> hosts;
-  private final List<String> networkInterfaces;
-  private final NodeType nodeType;
-  private final String nodeName;
-
-  ClusterProperties(AppSettings appSettings) {
-    port = appSettings.getProps().valueAsInt(CLUSTER_NODE_PORT);
-    networkInterfaces = extractNetworkInterfaces(appSettings.getProps().nonNullValue(CLUSTER_NODE_HOST));
-    hosts = extractHosts(appSettings.getProps().nonNullValue(CLUSTER_HOSTS));
-    nodeType = NodeType.parse(appSettings.getProps().nonNullValue(CLUSTER_NODE_TYPE));
-    nodeName = appSettings.getProps().nonNullValue(CLUSTER_NODE_NAME);
-  }
-
-  int getPort() {
-    return port;
-  }
-
-  public NodeType getNodeType() {
-    return nodeType;
-  }
-
-  List<String> getHosts() {
-    return hosts;
-  }
-
-  List<String> getNetworkInterfaces() {
-    return networkInterfaces;
-  }
-
-  public String getNodeName() {
-    return nodeName;
-  }
-
-  void validate() {
-    // 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 networkInterfaces parameter
-    try {
-      List<String> localInterfaces = findAllLocalIPs();
-
-      networkInterfaces.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 networkInterfaces. Interfaces won't be checked", e);
-    }
-  }
-
-  private static List<String> extractHosts(final String hosts) {
-    List<String> result = new ArrayList<>();
-    for (String host : hosts.split(",")) {
-      if (StringUtils.isNotEmpty(host)) {
-        if (!host.contains(":")) {
-          result.add(
-            String.format("%s:%s", host, DEFAULT_PORT));
-        } else {
-          result.add(host);
-        }
-      }
-    }
-    return result;
-  }
-
-  private static List<String> extractNetworkInterfaces(final String networkInterfaces) {
-    List<String> result = new ArrayList<>();
-    for (String iface : networkInterfaces.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));
-    }
-  }
-}
diff --git a/server/sonar-main/src/main/java/org/sonar/application/cluster/HazelcastCluster.java b/server/sonar-main/src/main/java/org/sonar/application/cluster/HazelcastCluster.java
deleted file mode 100644 (file)
index 38f0eb6..0000000
+++ /dev/null
@@ -1,427 +0,0 @@
-/*
- * 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.google.common.collect.ImmutableSet;
-import com.hazelcast.config.Config;
-import com.hazelcast.config.JoinConfig;
-import com.hazelcast.config.NetworkConfig;
-import com.hazelcast.core.Client;
-import com.hazelcast.core.ClientListener;
-import com.hazelcast.core.EntryEvent;
-import com.hazelcast.core.EntryListener;
-import com.hazelcast.core.Hazelcast;
-import com.hazelcast.core.HazelcastInstance;
-import com.hazelcast.core.HazelcastInstanceNotActiveException;
-import com.hazelcast.core.IAtomicReference;
-import com.hazelcast.core.ILock;
-import com.hazelcast.core.MapEvent;
-import com.hazelcast.core.Member;
-import com.hazelcast.core.MemberAttributeEvent;
-import com.hazelcast.core.MembershipEvent;
-import com.hazelcast.core.MembershipListener;
-import com.hazelcast.core.ReplicatedMap;
-import com.hazelcast.nio.Address;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.locks.Lock;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.sonar.application.AppStateListener;
-import org.sonar.process.MessageException;
-import org.sonar.process.NetworkUtils;
-import org.sonar.process.ProcessId;
-import org.sonar.process.cluster.NodeType;
-import org.sonar.process.cluster.ClusterObjectKeys;
-import org.sonar.process.cluster.HazelcastClient;
-
-import static java.lang.String.format;
-import static java.util.stream.Collectors.toList;
-import static org.sonar.application.cluster.ClusterProperties.HAZELCAST_CLUSTER_NAME;
-import static org.sonar.process.cluster.ClusterObjectKeys.CLUSTER_NAME;
-import static org.sonar.process.cluster.ClusterObjectKeys.HOSTNAME;
-import static org.sonar.process.cluster.ClusterObjectKeys.IP_ADDRESSES;
-import static org.sonar.process.cluster.ClusterObjectKeys.LEADER;
-import static org.sonar.process.cluster.ClusterObjectKeys.LOCAL_MEMBER_UUIDS;
-import static org.sonar.process.cluster.ClusterObjectKeys.NODE_NAME;
-import static org.sonar.process.cluster.ClusterObjectKeys.NODE_TYPE;
-import static org.sonar.process.cluster.ClusterObjectKeys.OPERATIONAL_PROCESSES;
-import static org.sonar.process.cluster.ClusterObjectKeys.SONARQUBE_VERSION;
-
-public class HazelcastCluster implements AutoCloseable {
-  private static final Logger LOGGER = LoggerFactory.getLogger(HazelcastCluster.class);
-
-  private final List<AppStateListener> listeners = new ArrayList<>();
-  private final ReplicatedMap<ClusterProcess, Boolean> operationalProcesses;
-  private final String operationalProcessListenerUUID;
-  private final String clientListenerUUID;
-  private final String nodeDisconnectedListenerUUID;
-
-  protected 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());
-    clientListenerUUID = hzInstance.getClientService().addClientListener(new ConnectedClientListener());
-    nodeDisconnectedListenerUUID = hzInstance.getCluster().addMembershipListener(new NodeDisconnectedListener());
-  }
-
-  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 -> format("%s (%s)", m.getStringAttribute(HOSTNAME), m.getStringAttribute(IP_ADDRESSES)))
-      .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(
-        format("The local version %s is not the same as the cluster %s", sonarqubeVersion, clusterVersion));
-    }
-  }
-
-  public String getSonarQubeVersion() {
-    IAtomicReference<String> sqVersion = hzInstance.getAtomicReference(SONARQUBE_VERSION);
-    return sqVersion.get();
-  }
-
-  public void registerClusterName(String nodeValue) {
-    IAtomicReference<String> property = hzInstance.getAtomicReference(CLUSTER_NAME);
-    if (property.get() == null) {
-      ILock lock = hzInstance.getLock(CLUSTER_NAME);
-      lock.lock();
-      try {
-        if (property.get() == null) {
-          property.set(nodeValue);
-        }
-      } finally {
-        lock.unlock();
-      }
-    }
-
-    String clusterValue = property.get();
-    if (!property.get().equals(nodeValue)) {
-      throw new MessageException(
-        format("This node has a cluster name [%s], which does not match [%s] from the cluster", nodeValue, clusterValue));
-    }
-  }
-
-  @Override
-  public void close() {
-    if (hzInstance != null) {
-      try {
-        // Removing listeners
-        operationalProcesses.removeEntryListener(operationalProcessListenerUUID);
-        hzInstance.getClientService().removeClientListener(clientListenerUUID);
-        hzInstance.getCluster().removeMembershipListener(nodeDisconnectedListenerUUID);
-
-        // Removing the operationalProcess from the replicated map
-        operationalProcesses.keySet().forEach(
-          clusterNodeProcess -> {
-            if (clusterNodeProcess.getNodeUuid().equals(getLocalUUID())) {
-              operationalProcesses.remove(clusterNodeProcess);
-            }
-          });
-
-        // Shutdown Hazelcast properly
-        hzInstance.shutdown();
-      } catch (HazelcastInstanceNotActiveException e) {
-        // hazelcastCluster may be already closed by the shutdown hook
-        LOGGER.debug("Unable to close Hazelcast cluster", e);
-      }
-    }
-  }
-
-  public static HazelcastCluster create(ClusterProperties clusterProperties) {
-    Config hzConfig = new Config();
-    hzConfig.getGroupConfig().setName(HAZELCAST_CLUSTER_NAME);
-
-    // 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(NODE_NAME, clusterProperties.getNodeName());
-    hzConfig.getMemberAttributeConfig()
-      .setStringAttribute(HOSTNAME, NetworkUtils.INSTANCE.getHostname());
-    hzConfig.getMemberAttributeConfig()
-      .setStringAttribute(IP_ADDRESSES, NetworkUtils.INSTANCE.getIPAddresses());
-    hzConfig.getMemberAttributeConfig()
-      .setStringAttribute(NODE_TYPE, clusterProperties.getNodeType().getValue());
-
-    // We are not using the partition group of Hazelcast, so disabling it
-    hzConfig.getPartitionGroupConfig().setEnabled(false);
-    return new HazelcastCluster(hzConfig);
-  }
-
-  Optional<String> getLeaderHostName() {
-    String leaderId = (String) hzInstance.getAtomicReference(LEADER).get();
-    if (leaderId != null) {
-      Optional<Member> leader = hzInstance.getCluster().getMembers().stream().filter(m -> m.getUuid().equals(leaderId)).findFirst();
-      if (leader.isPresent()) {
-        return Optional.of(
-          format("%s (%s)", leader.get().getStringAttribute(HOSTNAME), leader.get().getStringAttribute(IP_ADDRESSES)));
-      }
-    }
-    return Optional.empty();
-  }
-
-  String getLocalEndPoint() {
-    Address localAddress = hzInstance.getCluster().getLocalMember().getAddress();
-    return format("%s:%d", localAddress.getHost(), localAddress.getPort());
-  }
-
-  public HazelcastClient getHazelcastClient() {
-    return new HazelcastInstanceClient(hzInstance);
-  }
-
-  private static class HazelcastInstanceClient implements HazelcastClient {
-    private final HazelcastInstance hzInstance;
-
-    private HazelcastInstanceClient(HazelcastInstance hzInstance) {
-      this.hzInstance = hzInstance;
-    }
-
-    @Override
-    public <E> Set<E> getSet(String s) {
-      return hzInstance.getSet(s);
-    }
-
-    @Override
-    public <E> List<E> getList(String s) {
-      return hzInstance.getList(s);
-    }
-
-    @Override
-    public <K, V> Map<K, V> getMap(String s) {
-      return hzInstance.getMap(s);
-    }
-
-    @Override
-    public <K, V> Map<K, V> getReplicatedMap(String s) {
-      return hzInstance.getReplicatedMap(s);
-    }
-
-    @Override
-    public String getUUID() {
-      return hzInstance.getLocalEndpoint().getUuid();
-    }
-
-    @Override
-    public Set<String> getMemberUuids() {
-      ImmutableSet.Builder<String> builder = ImmutableSet.builder();
-      builder.addAll(hzInstance.getSet(ClusterObjectKeys.LOCAL_MEMBER_UUIDS));
-      hzInstance.getCluster().getMembers().stream().map(Member::getUuid).forEach(builder::add);
-      return builder.build();
-    }
-
-    @Override
-    public Lock getLock(String s) {
-      return hzInstance.getLock(s);
-    }
-
-    @Override
-    public long getClusterTime() {
-      return hzInstance.getCluster().getClusterTime();
-    }
-  }
-
-  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
-    }
-  }
-
-  private class ConnectedClientListener implements ClientListener {
-    @Override
-    public void clientConnected(Client client) {
-      hzInstance.getSet(LOCAL_MEMBER_UUIDS).add(client.getUuid());
-    }
-
-    @Override
-    public void clientDisconnected(Client client) {
-      hzInstance.getSet(LOCAL_MEMBER_UUIDS).remove(client.getUuid());
-    }
-  }
-
-  private class NodeDisconnectedListener implements MembershipListener {
-    @Override
-    public void memberAdded(MembershipEvent membershipEvent) {
-      // Nothing to do
-    }
-
-    @Override
-    public void memberRemoved(MembershipEvent membershipEvent) {
-      removeOperationalProcess(membershipEvent.getMember().getUuid());
-      if (membershipEvent.getMembers().stream()
-        .noneMatch(this::isAppNode)) {
-        purgeSharedMemoryForAppNodes();
-      }
-    }
-
-    @Override
-    public void memberAttributeChanged(MemberAttributeEvent memberAttributeEvent) {
-      // Nothing to do
-    }
-
-    private boolean isAppNode(Member member) {
-      return NodeType.APPLICATION.getValue().equals(member.getStringAttribute(NODE_TYPE));
-    }
-
-    private void removeOperationalProcess(String uuid) {
-      for (ClusterProcess clusterProcess : operationalProcesses.keySet()) {
-        if (clusterProcess.getNodeUuid().equals(uuid)) {
-          LOGGER.debug("Set node process off for [{}:{}] : ", clusterProcess.getNodeUuid(), clusterProcess.getProcessId());
-          hzInstance.getReplicatedMap(OPERATIONAL_PROCESSES).put(clusterProcess, Boolean.FALSE);
-        }
-      }
-    }
-
-    private void purgeSharedMemoryForAppNodes() {
-      LOGGER.info("No more application nodes, clearing cluster information about application nodes.");
-      hzInstance.getAtomicReference(LEADER).clear();
-      hzInstance.getAtomicReference(SONARQUBE_VERSION).clear();
-    }
-  }
-}
diff --git a/server/sonar-main/src/main/java/org/sonar/application/cluster/SearchNodeHealthProvider.java b/server/sonar-main/src/main/java/org/sonar/application/cluster/SearchNodeHealthProvider.java
deleted file mode 100644 (file)
index ff2461f..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * 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 org.sonar.process.NetworkUtils;
-import org.sonar.process.ProcessId;
-import org.sonar.process.Props;
-import org.sonar.process.cluster.health.NodeDetails;
-import org.sonar.process.cluster.health.NodeHealth;
-import org.sonar.process.cluster.health.NodeHealthProvider;
-
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_HOST;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_NAME;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_PORT;
-
-public class SearchNodeHealthProvider implements NodeHealthProvider {
-
-  private final ClusterAppState clusterAppState;
-  private final NodeDetails nodeDetails;
-
-  public SearchNodeHealthProvider(Props props, ClusterAppState clusterAppState, NetworkUtils networkUtils) {
-    this(props, clusterAppState, networkUtils, new Clock());
-  }
-
-  SearchNodeHealthProvider(Props props, ClusterAppState clusterAppState, NetworkUtils networkUtils, Clock clock) {
-    this.clusterAppState = clusterAppState;
-    this.nodeDetails = NodeDetails.newNodeDetailsBuilder()
-      .setType(NodeDetails.Type.SEARCH)
-      .setName(props.nonNullValue(CLUSTER_NODE_NAME))
-      .setHost(getHost(props, networkUtils))
-      .setPort(Integer.valueOf(props.nonNullValue(CLUSTER_NODE_PORT)))
-      .setStartedAt(clock.now())
-      .build();
-  }
-
-  private static String getHost(Props props, NetworkUtils networkUtils) {
-    String host = props.value(CLUSTER_NODE_HOST);
-    if (host != null && !host.isEmpty()) {
-      return host;
-    }
-    return networkUtils.getHostname();
-  }
-
-  @Override
-  public NodeHealth get() {
-    NodeHealth.Builder builder = NodeHealth.newNodeHealthBuilder();
-    if (clusterAppState.isOperational(ProcessId.ELASTICSEARCH, true)) {
-      builder.setStatus(NodeHealth.Status.GREEN);
-    } else {
-      builder.setStatus(NodeHealth.Status.RED)
-        .addCause("Elasticsearch is not operational");
-    }
-    return builder
-      .setDetails(nodeDetails)
-      .build();
-  }
-
-  static class Clock {
-    long now() {
-      return System.currentTimeMillis();
-    }
-  }
-}
diff --git a/server/sonar-main/src/main/java/org/sonar/application/cluster/health/DelegateHealthStateRefresherExecutorService.java b/server/sonar-main/src/main/java/org/sonar/application/cluster/health/DelegateHealthStateRefresherExecutorService.java
new file mode 100644 (file)
index 0000000..e1e4b2b
--- /dev/null
@@ -0,0 +1,124 @@
+/*
+ * 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.health;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import org.sonar.process.cluster.health.HealthStateRefresherExecutorService;
+
+class DelegateHealthStateRefresherExecutorService implements HealthStateRefresherExecutorService {
+  private final ScheduledExecutorService delegate;
+
+  DelegateHealthStateRefresherExecutorService(ScheduledExecutorService delegate) {
+    this.delegate = delegate;
+  }
+
+  @Override
+  public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
+    return delegate.schedule(command, delay, unit);
+  }
+
+  @Override
+  public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
+    return delegate.schedule(callable, delay, unit);
+  }
+
+  @Override
+  public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
+    return delegate.scheduleAtFixedRate(command, initialDelay, period, unit);
+  }
+
+  @Override
+  public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) {
+    return delegate.scheduleWithFixedDelay(command, initialDelay, delay, unit);
+  }
+
+  @Override
+  public void shutdown() {
+    delegate.shutdown();
+  }
+
+  @Override
+  public List<Runnable> shutdownNow() {
+    return delegate.shutdownNow();
+  }
+
+  @Override
+  public boolean isShutdown() {
+    return delegate.isShutdown();
+  }
+
+  @Override
+  public boolean isTerminated() {
+    return delegate.isTerminated();
+  }
+
+  @Override
+  public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
+    return delegate.awaitTermination(timeout, unit);
+  }
+
+  @Override
+  public <T> Future<T> submit(Callable<T> task) {
+    return delegate.submit(task);
+  }
+
+  @Override
+  public <T> Future<T> submit(Runnable task, T result) {
+    return delegate.submit(task, result);
+  }
+
+  @Override
+  public Future<?> submit(Runnable task) {
+    return delegate.submit(task);
+  }
+
+  @Override
+  public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException {
+    return delegate.invokeAll(tasks);
+  }
+
+  @Override
+  public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException {
+    return delegate.invokeAll(tasks, timeout, unit);
+  }
+
+  @Override
+  public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException {
+    return delegate.invokeAny(tasks);
+  }
+
+  @Override
+  public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+    return delegate.invokeAny(tasks, timeout, unit);
+  }
+
+  @Override
+  public void execute(Runnable command) {
+    delegate.execute(command);
+  }
+}
diff --git a/server/sonar-main/src/main/java/org/sonar/application/cluster/health/HealthStateSharing.java b/server/sonar-main/src/main/java/org/sonar/application/cluster/health/HealthStateSharing.java
new file mode 100644 (file)
index 0000000..f749ca5
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * 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.health;
+
+public interface HealthStateSharing {
+  void start();
+
+  void stop();
+}
diff --git a/server/sonar-main/src/main/java/org/sonar/application/cluster/health/HealthStateSharingImpl.java b/server/sonar-main/src/main/java/org/sonar/application/cluster/health/HealthStateSharingImpl.java
new file mode 100644 (file)
index 0000000..1d28495
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * 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.health;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.process.cluster.health.HealthStateRefresher;
+import org.sonar.process.cluster.health.HealthStateRefresherExecutorService;
+import org.sonar.process.cluster.health.NodeHealthProvider;
+import org.sonar.process.cluster.health.SharedHealthStateImpl;
+import org.sonar.process.cluster.hz.HazelcastMember;
+
+import static java.lang.String.format;
+
+public class HealthStateSharingImpl implements HealthStateSharing {
+  private static final Logger LOG = LoggerFactory.getLogger(HealthStateSharingImpl.class);
+
+  private final HazelcastMember hzMember;
+  private final NodeHealthProvider nodeHealthProvider;
+  private HealthStateRefresherExecutorService executorService;
+  private HealthStateRefresher healthStateRefresher;
+
+  public HealthStateSharingImpl(HazelcastMember hzMember, NodeHealthProvider nodeHealthProvider) {
+    this.hzMember = hzMember;
+    this.nodeHealthProvider = nodeHealthProvider;
+  }
+
+  @Override
+  public void start() {
+    executorService = new DelegateHealthStateRefresherExecutorService(
+      Executors.newSingleThreadScheduledExecutor(
+        new ThreadFactoryBuilder()
+          .setDaemon(false)
+          .setNameFormat("health_state_refresh-%d")
+          .build()));
+    healthStateRefresher = new HealthStateRefresher(executorService, nodeHealthProvider, new SharedHealthStateImpl(hzMember));
+    healthStateRefresher.start();
+  }
+
+  @Override
+  public void stop() {
+    healthStateRefresher.stop();
+    stopExecutorService(executorService);
+  }
+
+  private static void stopExecutorService(ScheduledExecutorService executorService) {
+    // Disable new tasks from being submitted
+    executorService.shutdown();
+    try {
+      // Wait a while for existing tasks to terminate
+      if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
+        // Cancel currently executing tasks
+        executorService.shutdownNow();
+        // Wait a while for tasks to respond to being canceled
+        if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
+          LOG.warn("Pool {} did not terminate", HealthStateSharingImpl.class.getSimpleName());
+        }
+      }
+    } catch (InterruptedException ie) {
+      LOG.warn(format("Termination of pool %s failed", HealthStateSharingImpl.class.getSimpleName()), ie);
+      // (Re-)Cancel if current thread also interrupted
+      executorService.shutdownNow();
+      Thread.currentThread().interrupt();
+    }
+  }
+
+}
diff --git a/server/sonar-main/src/main/java/org/sonar/application/cluster/health/SearchNodeHealthProvider.java b/server/sonar-main/src/main/java/org/sonar/application/cluster/health/SearchNodeHealthProvider.java
new file mode 100644 (file)
index 0000000..79e4274
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * 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.health;
+
+import org.sonar.application.cluster.ClusterAppState;
+import org.sonar.process.NetworkUtils;
+import org.sonar.process.ProcessId;
+import org.sonar.process.Props;
+import org.sonar.process.cluster.health.NodeDetails;
+import org.sonar.process.cluster.health.NodeHealth;
+import org.sonar.process.cluster.health.NodeHealthProvider;
+
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_HOST;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_NAME;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_PORT;
+
+public class SearchNodeHealthProvider implements NodeHealthProvider {
+
+  private final ClusterAppState clusterAppState;
+  private final NodeDetails nodeDetails;
+
+  public SearchNodeHealthProvider(Props props, ClusterAppState clusterAppState, NetworkUtils networkUtils) {
+    this(props, clusterAppState, networkUtils, new Clock());
+  }
+
+  SearchNodeHealthProvider(Props props, ClusterAppState clusterAppState, NetworkUtils networkUtils, Clock clock) {
+    this.clusterAppState = clusterAppState;
+    this.nodeDetails = NodeDetails.newNodeDetailsBuilder()
+      .setType(NodeDetails.Type.SEARCH)
+      .setName(props.nonNullValue(CLUSTER_NODE_NAME))
+      .setHost(getHost(props, networkUtils))
+      .setPort(Integer.valueOf(props.nonNullValue(CLUSTER_NODE_PORT)))
+      .setStartedAt(clock.now())
+      .build();
+  }
+
+  private static String getHost(Props props, NetworkUtils networkUtils) {
+    String host = props.value(CLUSTER_NODE_HOST);
+    if (host != null && !host.isEmpty()) {
+      return host;
+    }
+    return networkUtils.getHostname();
+  }
+
+  @Override
+  public NodeHealth get() {
+    NodeHealth.Builder builder = NodeHealth.newNodeHealthBuilder();
+    if (clusterAppState.isOperational(ProcessId.ELASTICSEARCH, true)) {
+      builder.setStatus(NodeHealth.Status.GREEN);
+    } else {
+      builder.setStatus(NodeHealth.Status.RED)
+        .addCause("Elasticsearch is not operational");
+    }
+    return builder
+      .setDetails(nodeDetails)
+      .build();
+  }
+
+  static class Clock {
+    long now() {
+      return System.currentTimeMillis();
+    }
+  }
+}
diff --git a/server/sonar-main/src/main/java/org/sonar/application/cluster/health/package-info.java b/server/sonar-main/src/main/java/org/sonar/application/cluster/health/package-info.java
new file mode 100644 (file)
index 0000000..21c6fef
--- /dev/null
@@ -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.application.cluster.health;
+
+import javax.annotation.ParametersAreNonnullByDefault;
index e9c5ebc89ecd0d41ec57eeb928a85d182fb34736..801e61410ace9d0b256997d9933f66a4fa50598a 100644 (file)
@@ -30,6 +30,7 @@ import java.util.Properties;
 import java.util.function.Consumer;
 import org.slf4j.LoggerFactory;
 import org.sonar.process.ConfigurationUtils;
+import org.sonar.process.NetworkUtils;
 import org.sonar.process.ProcessProperties;
 import org.sonar.process.Props;
 
@@ -42,7 +43,8 @@ public class AppSettingsLoaderImpl implements AppSettingsLoader {
   private final Consumer<Props>[] consumers;
 
   public AppSettingsLoaderImpl(String[] cliArguments) {
-    this(cliArguments, detectHomeDir(), new FileSystemSettings(), new JdbcSettings(), new ClusterSettings());
+    this(cliArguments, detectHomeDir(),
+      new FileSystemSettings(), new JdbcSettings(), new ClusterSettings(NetworkUtils.INSTANCE));
   }
 
   AppSettingsLoaderImpl(String[] cliArguments, File homeDir, Consumer<Props>... consumers) {
index de30b62bc3b9b6bfc66a1ff0d58b492976f5c618..e3cf57f2526c2e5d5e3915fa8969d680fb8d2598 100644 (file)
  */
 package org.sonar.application.config;
 
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.net.HostAndPort;
-import com.google.common.net.InetAddresses;
 import java.net.InetAddress;
-import java.net.NetworkInterface;
 import java.net.SocketException;
 import java.net.UnknownHostException;
 import java.util.Arrays;
@@ -31,142 +27,116 @@ import java.util.List;
 import java.util.function.Consumer;
 import org.apache.commons.lang.StringUtils;
 import org.sonar.process.MessageException;
-import org.sonar.process.cluster.NodeType;
+import org.sonar.process.NetworkUtils;
 import org.sonar.process.ProcessId;
 import org.sonar.process.Props;
+import org.sonar.process.cluster.NodeType;
 
-import static com.google.common.net.InetAddresses.forString;
 import static java.lang.String.format;
 import static java.util.Arrays.asList;
 import static java.util.Arrays.stream;
 import static java.util.Collections.singletonList;
 import static java.util.stream.Collectors.joining;
 import static org.apache.commons.lang.StringUtils.isBlank;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_ENABLED;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_HOSTS;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_HOST;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_TYPE;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_SEARCH_HOSTS;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_WEB_LEADER;
+import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_HOSTS;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_HOST;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_TYPE;
+import static org.sonar.process.ProcessProperties.CLUSTER_SEARCH_HOSTS;
+import static org.sonar.process.ProcessProperties.CLUSTER_WEB_STARTUP_LEADER;
 import static org.sonar.process.ProcessProperties.JDBC_URL;
 import static org.sonar.process.ProcessProperties.SEARCH_HOST;
 
 public class ClusterSettings implements Consumer<Props> {
 
+  private final NetworkUtils network;
+
+  public ClusterSettings(NetworkUtils network) {
+    this.network = network;
+  }
+
   @Override
   public void accept(Props props) {
     if (isClusterEnabled(props)) {
-      checkProperties(props);
+      checkClusterProperties(props);
     }
   }
 
-  private static void checkProperties(Props props) {
-    // Cluster web leader is not allowed
-    if (props.value(CLUSTER_WEB_LEADER) != null) {
-      throw new MessageException(format("Property [%s] is forbidden", CLUSTER_WEB_LEADER));
-    }
-
-    // Mandatory properties
-    ensureMandatoryProperty(props, CLUSTER_NODE_TYPE);
-    String nodeTypeValue = props.nonNullValue(CLUSTER_NODE_TYPE);
-    if (!NodeType.isValid(nodeTypeValue)) {
-      throw new MessageException(format("Invalid value for property [%s]: [%s], only [%s] are allowed", CLUSTER_NODE_TYPE, nodeTypeValue,
-        Arrays.stream(NodeType.values()).map(NodeType::getValue).collect(joining(", "))));
+  private void checkClusterProperties(Props props) {
+    // for internal use
+    if (props.value(CLUSTER_WEB_STARTUP_LEADER) != null) {
+      throw new MessageException(format("Property [%s] is forbidden", CLUSTER_WEB_STARTUP_LEADER));
     }
-    ensureMandatoryProperty(props, CLUSTER_HOSTS);
-    ensureMandatoryProperty(props, CLUSTER_SEARCH_HOSTS);
 
-    NodeType nodeType = NodeType.parse(nodeTypeValue);
+    NodeType nodeType = toNodeType(props);
     switch (nodeType) {
       case APPLICATION:
         ensureNotH2(props);
-        ensureMandatoryProperty(props, "sonar.auth.jwtBase64Hs256Secret");
+        requireValue(props, "sonar.auth.jwtBase64Hs256Secret");
         break;
       case SEARCH:
-        ensureMandatoryProperty(props, SEARCH_HOST);
-        ensureNotLoopback(props, SEARCH_HOST);
+        requireValue(props, SEARCH_HOST);
+        ensureLocalButNotLoopbackAddress(props, SEARCH_HOST);
         break;
       default:
-        throw new IllegalArgumentException("Unsupported node type: " + nodeType);
+        throw new UnsupportedOperationException("Unknown value: " + nodeType);
     }
-
-    // Loopback interfaces are forbidden for the ports accessed
-    // by other nodes of cluster
-    ensureNotLoopback(props, CLUSTER_HOSTS);
-    ensureNotLoopback(props, CLUSTER_NODE_HOST);
-    ensureNotLoopback(props, CLUSTER_SEARCH_HOSTS);
-
-    ensureLocalAddress(props, SEARCH_HOST);
-    ensureLocalAddress(props, CLUSTER_NODE_HOST);
+    ensureNotLoopbackAddresses(props, CLUSTER_HOSTS);
+    requireValue(props, CLUSTER_NODE_HOST);
+    ensureLocalButNotLoopbackAddress(props, CLUSTER_NODE_HOST);
+    ensureNotLoopbackAddresses(props, CLUSTER_SEARCH_HOSTS);
   }
 
-  private static void ensureNotH2(Props props) {
-    String jdbcUrl = props.value(JDBC_URL);
-    if (isBlank(jdbcUrl) || jdbcUrl.startsWith("jdbc:h2:")) {
-      throw new MessageException("Embedded database is not supported in cluster mode");
+  private static NodeType toNodeType(Props props) {
+    String nodeTypeValue = requireValue(props, CLUSTER_NODE_TYPE);
+    if (!NodeType.isValid(nodeTypeValue)) {
+      throw new MessageException(format("Invalid value for property %s: [%s], only [%s] are allowed", CLUSTER_NODE_TYPE, nodeTypeValue,
+        Arrays.stream(NodeType.values()).map(NodeType::getValue).collect(joining(", "))));
     }
+    return NodeType.parse(nodeTypeValue);
   }
 
-  private static void ensureMandatoryProperty(Props props, String key) {
-    if (isBlank(props.value(key))) {
-      throw new MessageException(format("Property [%s] is mandatory", key));
+  private static String requireValue(Props props, String key) {
+    String value = props.value(key);
+    if (isBlank(value)) {
+      throw new MessageException(format("Property %s is mandatory", key));
     }
+    return value;
   }
 
-  @VisibleForTesting
-  private static void ensureNotLoopback(Props props, String key) {
-    String ipList = props.value(key);
-    if (ipList == null) {
-      return;
+  private static void ensureNotH2(Props props) {
+    String jdbcUrl = props.value(JDBC_URL);
+    if (isBlank(jdbcUrl) || jdbcUrl.startsWith("jdbc:h2:")) {
+      throw new MessageException("Embedded database is not supported in cluster mode");
     }
-
-    stream(ipList.split(","))
-      .filter(StringUtils::isNotBlank)
-      .map(StringUtils::trim)
-      .forEach(ip -> {
-        InetAddress inetAddress = convertToInetAddress(ip, key);
-        if (inetAddress.isLoopbackAddress()) {
-          throw new MessageException(format("The interface address [%s] of [%s] must not be a loopback address", ip, key));
-        }
-      });
   }
 
-  private static void ensureLocalAddress(Props props, String key) {
-    String ipList = props.value(key);
-
-    if (ipList == null) {
-      return;
-    }
-
-    stream(ipList.split(","))
+  private void ensureNotLoopbackAddresses(Props props, String propertyKey) {
+    stream(requireValue(props, propertyKey).split(","))
       .filter(StringUtils::isNotBlank)
       .map(StringUtils::trim)
+      .map(s -> StringUtils.substringBefore(s, ":"))
       .forEach(ip -> {
-        InetAddress inetAddress = convertToInetAddress(ip, key);
         try {
-          if (NetworkInterface.getByInetAddress(inetAddress) == null) {
-            throw new MessageException(format("The interface address [%s] of [%s] is not a local address", ip, key));
+          if (network.isLoopbackInetAddress(network.toInetAddress(ip))) {
+            throw new MessageException(format("Property %s must not be a loopback address: %s", propertyKey, ip));
           }
-        } catch (SocketException e) {
-          throw new MessageException(format("The interface address [%s] of [%s] is not a local address", ip, key));
+        } catch (UnknownHostException e) {
+          throw new MessageException(format("Property %s must not a valid address: %s [%s]", propertyKey, ip, e.getMessage()));
         }
       });
   }
 
-  private static InetAddress convertToInetAddress(String text, String key) {
-    InetAddress inetAddress;
-    HostAndPort hostAndPort = HostAndPort.fromString(text);
-    if (!InetAddresses.isInetAddress(hostAndPort.getHostText())) {
-      try {
-        inetAddress = InetAddress.getByName(hostAndPort.getHostText());
-      } catch (UnknownHostException e) {
-        throw new MessageException(format("The interface address [%s] of [%s] cannot be resolved : %s", text, key, e.getMessage()));
+  private void ensureLocalButNotLoopbackAddress(Props props, String propertyKey) {
+    String propertyValue = props.nonNullValue(propertyKey).trim();
+    try {
+      InetAddress address = network.toInetAddress(propertyValue);
+      if (!network.isLocalInetAddress(address) || network.isLoopbackInetAddress(address)) {
+        throw new MessageException(format("Property %s must be a local non-loopback address: %s", propertyKey, propertyValue));
       }
-    } else {
-      inetAddress = forString(hostAndPort.getHostText());
+    } catch (UnknownHostException | SocketException e) {
+      throw new MessageException(format("Property %s must be a local non-loopback address: %s [%s]", propertyKey, propertyValue, e.getMessage()));
     }
-
-    return inetAddress;
   }
 
   public static boolean isClusterEnabled(AppSettings settings) {
@@ -181,7 +151,7 @@ public class ClusterSettings implements Consumer<Props> {
     if (!isClusterEnabled(settings)) {
       return asList(ProcessId.ELASTICSEARCH, ProcessId.WEB_SERVER, ProcessId.COMPUTE_ENGINE);
     }
-    NodeType nodeType = NodeType.parse(settings.getValue(CLUSTER_NODE_TYPE).orElse(null));
+    NodeType nodeType = NodeType.parse(settings.getValue(CLUSTER_NODE_TYPE).orElse(""));
     switch (nodeType) {
       case APPLICATION:
         return asList(ProcessId.WEB_SERVER, ProcessId.COMPUTE_ENGINE);
@@ -195,7 +165,7 @@ public class ClusterSettings implements Consumer<Props> {
   public static boolean isLocalElasticsearchEnabled(AppSettings settings) {
     // elasticsearch is enabled on "search" nodes, but disabled on "application" nodes
     if (isClusterEnabled(settings.getProps())) {
-      return NodeType.parse(settings.getValue(CLUSTER_NODE_TYPE).orElse(null)) == NodeType.SEARCH;
+      return NodeType.parse(settings.getValue(CLUSTER_NODE_TYPE).orElse("")) == NodeType.SEARCH;
     }
 
     // elasticsearch is enabled in standalone mode
index 1cd3187ad47c0e67174b95417aa9914fece25800..e8dc7e3b6eb7308bb85cbbc3d5cc09334021ccc8 100644 (file)
@@ -31,10 +31,10 @@ import org.sonar.process.Props;
 import org.sonar.process.System2;
 
 import static java.lang.String.valueOf;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_ENABLED;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NAME;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_NAME;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_SEARCH_HOSTS;
+import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_NAME;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_NAME;
+import static org.sonar.process.ProcessProperties.CLUSTER_SEARCH_HOSTS;
 
 public class EsSettings {
 
index f67911a340a163c005c2a3335bc610fb77ba0503..51e553032ce00afbd03fda6fb8a455927e3e0796 100644 (file)
@@ -48,8 +48,7 @@ import org.sonar.process.logging.LogbackHelper;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.slf4j.Logger.ROOT_LOGGER_NAME;
 import static org.sonar.application.process.StreamGobbler.LOGGER_GOBBLER;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_ENABLED;
-import static org.sonar.process.cluster.ClusterProperties.HAZELCAST_LOG_LEVEL;
+import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
 
 public class AppLoggingTest {
 
@@ -257,18 +256,6 @@ public class AppLoggingTest {
       LoggerFactory.getLogger("com.hazelcast").isInfoEnabled()).isEqualTo(false);
   }
 
-  @Test
-  public void configure_logging_for_hazelcast() throws IOException {
-    settings.getProps().set(CLUSTER_ENABLED, "true");
-    settings.getProps().set(HAZELCAST_LOG_LEVEL, "INFO");
-    underTest.configure();
-
-    assertThat(
-      LoggerFactory.getLogger("com.hazelcast").isInfoEnabled()).isEqualTo(true);
-    assertThat(
-      LoggerFactory.getLogger("com.hazelcast").isDebugEnabled()).isEqualTo(false);
-  }
-
   private void emulateRunFromSonarScript() {
     settings.getProps().set("sonar.wrapped", "true");
   }
index 0cc9db5b2799d290aedf5e27ed052d6fa4527d03..e031457ffe000eeaf4c4099948984e559a2b4cc9 100644 (file)
@@ -35,7 +35,7 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
 
 public class AppReloaderImplTest {
 
index c4765df820ec76fea3cb706e4fc17e1e96f6e948..8919ed84c75dce6d112244329db1d8329af55991 100644 (file)
  */
 package org.sonar.application;
 
+import java.net.InetAddress;
+import java.util.Optional;
+import org.hamcrest.CoreMatchers;
 import org.junit.Test;
 import org.sonar.application.cluster.ClusterAppStateImpl;
 import org.sonar.application.config.TestAppSettings;
+import org.sonar.process.NetworkUtils;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_ENABLED;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NAME;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_TYPE;
+import static org.junit.Assume.assumeThat;
+import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_HOSTS;
+import static org.sonar.process.ProcessProperties.CLUSTER_NAME;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_HOST;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_TYPE;
 
 public class AppStateFactoryTest {
 
@@ -35,13 +42,18 @@ public class AppStateFactoryTest {
 
   @Test
   public void create_cluster_implementation_if_cluster_is_enabled() {
+    Optional<InetAddress> ip = NetworkUtils.INSTANCE.getLocalNonLoopbackIpv4Address();
+    assumeThat(ip.isPresent(), CoreMatchers.is(true));
+
     settings.set(CLUSTER_ENABLED, "true");
     settings.set(CLUSTER_NODE_TYPE, "application");
+    settings.set(CLUSTER_NODE_HOST, ip.get().getHostAddress());
+    settings.set(CLUSTER_HOSTS, ip.get().getHostAddress());
     settings.set(CLUSTER_NAME, "foo");
 
     AppState appState = underTest.create();
     assertThat(appState).isInstanceOf(ClusterAppStateImpl.class);
-    ((ClusterAppStateImpl) appState).close();
+    appState.close();
   }
 
   @Test
index a2fd049c44a81c231ba156448fb218f7b8317158..a3125ad6823ba1c6173e203cb78e0c628f29d335 100644 (file)
@@ -46,7 +46,7 @@ import org.sonar.application.config.TestAppSettings;
 import org.sonar.application.process.ProcessLauncher;
 import org.sonar.application.process.ProcessMonitor;
 import org.sonar.process.ProcessId;
-import org.sonar.process.cluster.HazelcastClient;
+import org.sonar.process.cluster.hz.HazelcastMember;
 
 import static java.util.Collections.synchronizedList;
 import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
@@ -58,11 +58,11 @@ import static org.mockito.Mockito.verify;
 import static org.sonar.process.ProcessId.COMPUTE_ENGINE;
 import static org.sonar.process.ProcessId.ELASTICSEARCH;
 import static org.sonar.process.ProcessId.WEB_SERVER;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_ENABLED;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_HOST;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_NAME;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_PORT;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_TYPE;
+import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_HOST;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_NAME;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_PORT;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_TYPE;
 
 public class SchedulerImplTest {
 
@@ -83,8 +83,8 @@ public class SchedulerImplTest {
   private TestCommandFactory javaCommandFactory = new TestCommandFactory();
   private TestProcessLauncher processLauncher = new TestProcessLauncher();
   private TestAppState appState = new TestAppState();
-  private HazelcastClient hazelcastClient = mock(HazelcastClient.class);
-  private TestClusterAppState clusterAppState = new TestClusterAppState(hazelcastClient);
+  private HazelcastMember hazelcastMember = mock(HazelcastMember.class);
+  private TestClusterAppState clusterAppState = new TestClusterAppState(hazelcastMember);
   private List<ProcessId> orderedStops = synchronizedList(new ArrayList<>());
 
   @Before
index 322a42d3c066e31ebf6ceefb3f2282c063b0e699..25fa3e714eda719954d1cb090014a7246ad491e8 100644 (file)
 package org.sonar.application;
 
 import org.sonar.application.cluster.ClusterAppState;
-import org.sonar.process.cluster.HazelcastClient;
+import org.sonar.process.cluster.hz.HazelcastMember;
 
 public class TestClusterAppState extends TestAppState implements ClusterAppState {
-  private final HazelcastClient hazelcastClient;
+  private final HazelcastMember hazelcastMember;
 
-  public TestClusterAppState(HazelcastClient hazelcastClient) {
-    this.hazelcastClient = hazelcastClient;
+  public TestClusterAppState(HazelcastMember hazelcastMember) {
+    this.hazelcastMember = hazelcastMember;
   }
 
   @Override
-  public HazelcastClient getHazelcastClient() {
-    return hazelcastClient;
+  public HazelcastMember getHazelcastMember() {
+    return hazelcastMember;
   }
 }
index 3ad627e9aab347e8a556f516c72155146b60636f..72e117924a2bbb442a8ecd9a7d3ff40172ff8a92 100644 (file)
  */
 package org.sonar.application.cluster;
 
-import com.hazelcast.core.HazelcastInstance;
-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.MessageException;
 import org.sonar.process.ProcessId;
 
-import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
 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.sonar.application.cluster.HazelcastClusterTestHelper.createHazelcastClient;
-import static org.sonar.application.cluster.HazelcastClusterTestHelper.newApplicationSettings;
-import static org.sonar.process.cluster.ClusterObjectKeys.CLUSTER_NAME;
-import static org.sonar.process.cluster.ClusterObjectKeys.SONARQUBE_VERSION;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_ENABLED;
+import static org.sonar.application.cluster.HazelcastTesting.newHzMember;
+import static org.sonar.process.cluster.hz.HazelcastObjects.CLUSTER_NAME;
+import static org.sonar.process.cluster.hz.HazelcastObjects.SONARQUBE_VERSION;
 
 public class ClusterAppStateImplTest {
 
@@ -54,46 +45,18 @@ public class ClusterAppStateImplTest {
   @Rule
   public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60));
 
-  @Test
-  public void instantiation_throws_ISE_if_cluster_mode_is_disabled() throws Exception {
-    TestAppSettings settings = new TestAppSettings();
-    settings.set(CLUSTER_ENABLED, "false");
-
-    expectedException.expect(IllegalStateException.class);
-    expectedException.expectMessage("Cluster is not enabled on this instance");
-
-    new ClusterAppStateImpl(settings);
-  }
-
   @Test
   public void tryToLockWebLeader_returns_true_only_for_the_first_call() throws Exception {
-    TestAppSettings settings = newApplicationSettings();
-
-    try (ClusterAppStateImpl underTest = new ClusterAppStateImpl(settings)) {
+    try (ClusterAppStateImpl underTest = new ClusterAppStateImpl(newHzMember())) {
       assertThat(underTest.tryToLockWebLeader()).isEqualTo(true);
       assertThat(underTest.tryToLockWebLeader()).isEqualTo(false);
     }
   }
 
-  @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 = newApplicationSettings();
-
-    Logger logger = mock(Logger.class);
-    ClusterAppStateImpl.setLogger(logger);
-
-    try (ClusterAppStateImpl appStateCluster = new ClusterAppStateImpl(settings)) {
-      verify(logger).info(
-        eq("Joined a SonarQube cluster that contains the following hosts : [{}]"),
-        anyString());
-    }
-  }
-
   @Test
   public void test_listeners() throws InterruptedException {
     AppStateListener listener = mock(AppStateListener.class);
-    try (ClusterAppStateImpl underTest = new ClusterAppStateImpl(newApplicationSettings())) {
+    try (ClusterAppStateImpl underTest = new ClusterAppStateImpl(newHzMember())) {
       underTest.addListener(listener);
 
       underTest.setOperational(ProcessId.ELASTICSEARCH);
@@ -108,77 +71,61 @@ public class ClusterAppStateImplTest {
 
   @Test
   public void registerSonarQubeVersion_publishes_version_on_first_call() {
-    TestAppSettings settings = newApplicationSettings();
 
-    try (ClusterAppStateImpl appStateCluster = new ClusterAppStateImpl(settings)) {
-      appStateCluster.registerSonarQubeVersion("6.4.1.5");
+    try (ClusterAppStateImpl underTest = new ClusterAppStateImpl(newHzMember())) {
+      underTest.registerSonarQubeVersion("6.4.1.5");
 
-      HazelcastInstance hzInstance = createHazelcastClient(appStateCluster);
-      assertThat(hzInstance.getAtomicReference(SONARQUBE_VERSION).get())
-        .isNotNull()
-        .isInstanceOf(String.class)
+      assertThat(underTest.getHazelcastMember().getAtomicReference(SONARQUBE_VERSION).get())
         .isEqualTo("6.4.1.5");
     }
   }
 
   @Test
   public void registerClusterName_publishes_clusterName_on_first_call() {
-    TestAppSettings settings = newApplicationSettings();
-    String clusterName = randomAlphanumeric(20);
+    try (ClusterAppStateImpl underTest = new ClusterAppStateImpl(newHzMember())) {
+      underTest.registerClusterName("foo");
 
-    try (ClusterAppStateImpl appStateCluster = new ClusterAppStateImpl(settings)) {
-      appStateCluster.registerClusterName(clusterName);
-
-      HazelcastInstance hzInstance = createHazelcastClient(appStateCluster);
-      assertThat(hzInstance.getAtomicReference(CLUSTER_NAME).get())
-        .isNotNull()
-        .isInstanceOf(String.class)
-        .isEqualTo(clusterName);
+      assertThat(underTest.getHazelcastMember().getAtomicReference(CLUSTER_NAME).get())
+        .isEqualTo("foo");
     }
   }
 
   @Test
-  public void reset_throws_always_ISE() {
-    TestAppSettings settings = newApplicationSettings();
-
-    try (ClusterAppStateImpl appStateCluster = new ClusterAppStateImpl(settings)) {
+  public void reset_always_throws_ISE() {
+    try (ClusterAppStateImpl underTest = new ClusterAppStateImpl(newHzMember())) {
       expectedException.expect(IllegalStateException.class);
       expectedException.expectMessage("state reset is not supported in cluster mode");
-      appStateCluster.reset();
+
+      underTest.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 = newApplicationSettings();
-
-    try (ClusterAppStateImpl appStateCluster = new ClusterAppStateImpl(settings)) {
+    try (ClusterAppStateImpl underTest = new ClusterAppStateImpl(newHzMember())) {
       // Register first version
-      appStateCluster.registerSonarQubeVersion("1.0.0");
+      underTest.getHazelcastMember().getAtomicReference(SONARQUBE_VERSION).set("6.6.0.1111");
 
       expectedException.expect(IllegalStateException.class);
-      expectedException.expectMessage("The local version 2.0.0 is not the same as the cluster 1.0.0");
+      expectedException.expectMessage("The local version 6.7.0.9999 is not the same as the cluster 6.6.0.1111");
 
       // Registering a second different version must trigger an exception
-      appStateCluster.registerSonarQubeVersion("2.0.0");
+      underTest.registerSonarQubeVersion("6.7.0.9999");
     }
   }
 
   @Test
   public void registerClusterName_throws_MessageException_if_clusterName_is_different() throws Exception {
-    // Now launch an instance that try to be part of the hzInstance cluster
-    TestAppSettings settings = newApplicationSettings();
-
-    try (ClusterAppStateImpl appStateCluster = new ClusterAppStateImpl(settings)) {
+    try (ClusterAppStateImpl underTest = new ClusterAppStateImpl(newHzMember())) {
       // Register first version
-      appStateCluster.registerClusterName("goodClusterName");
+      underTest.getHazelcastMember().getAtomicReference(CLUSTER_NAME).set("goodClusterName");
 
       expectedException.expect(MessageException.class);
       expectedException.expectMessage("This node has a cluster name [badClusterName], which does not match [goodClusterName] from the cluster");
 
       // Registering a second different cluster name must trigger an exception
-      appStateCluster.registerClusterName("badClusterName");
+      underTest.registerClusterName("badClusterName");
     }
   }
 }
diff --git a/server/sonar-main/src/test/java/org/sonar/application/cluster/ClusterPropertiesTest.java b/server/sonar-main/src/test/java/org/sonar/application/cluster/ClusterPropertiesTest.java
deleted file mode 100644 (file)
index 71c28d4..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * 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 java.util.Arrays;
-import java.util.Collections;
-import java.util.stream.Stream;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.sonar.application.config.AppSettings;
-import org.sonar.application.config.TestAppSettings;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_ENABLED;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_HOSTS;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NAME;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_HOST;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_PORT;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_TYPE;
-
-public class ClusterPropertiesTest {
-  @Rule
-  public ExpectedException expectedException = ExpectedException.none();
-
-  private AppSettings appSettings = new TestAppSettings();
-
-  @Test
-  public void test_default_values() throws Exception {
-    appSettings.getProps().set(CLUSTER_ENABLED, "true");
-    appSettings.getProps().set(CLUSTER_NODE_TYPE, "application");
-    ClusterProperties props = new ClusterProperties(appSettings);
-
-    assertThat(props.getNetworkInterfaces())
-      .isEqualTo(Collections.emptyList());
-    assertThat(props.getPort())
-      .isEqualTo(9003);
-    assertThat(props.getHosts())
-      .isEqualTo(Collections.emptyList());
-  }
-
-  @Test
-  public void test_port_parameter() {
-    appSettings.getProps().set(CLUSTER_ENABLED, "true");
-    appSettings.getProps().set(CLUSTER_NAME, "sonarqube");
-    appSettings.getProps().set(CLUSTER_NODE_TYPE, "application");
-
-    Stream.of("-50", "0", "65536", "128563").forEach(
-      port -> {
-        appSettings.getProps().set(CLUSTER_NODE_PORT, port);
-
-        ClusterProperties clusterProperties = new ClusterProperties(appSettings);
-        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() {
-    appSettings.getProps().set(CLUSTER_ENABLED, "true");
-    appSettings.getProps().set(CLUSTER_NAME, "sonarqube");
-    appSettings.getProps().set(CLUSTER_NODE_HOST, "8.8.8.8"); // This IP belongs to Google
-    appSettings.getProps().set(CLUSTER_NODE_TYPE, "application");
-
-    ClusterProperties clusterProperties = new ClusterProperties(appSettings);
-    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 validate_does_not_fail_if_cluster_enabled_and_name_specified() {
-    appSettings.getProps().set(CLUSTER_ENABLED, "true");
-    appSettings.getProps().set(CLUSTER_NAME, "sonarqube");
-    appSettings.getProps().set(CLUSTER_NODE_TYPE, "application");
-
-    ClusterProperties clusterProperties = new ClusterProperties(appSettings);
-    clusterProperties.validate();
-  }
-
-  @Test
-  public void test_members() {
-    appSettings.getProps().set(CLUSTER_ENABLED, "true");
-    appSettings.getProps().set(CLUSTER_NAME, "sonarqube");
-    appSettings.getProps().set(CLUSTER_NODE_TYPE, "application");
-
-    assertThat(
-      new ClusterProperties(appSettings).getHosts()).isEqualTo(
-        Collections.emptyList());
-
-    appSettings.getProps().set(CLUSTER_HOSTS, "192.168.1.1");
-    assertThat(
-      new ClusterProperties(appSettings).getHosts()).isEqualTo(
-        Arrays.asList("192.168.1.1:9003"));
-
-    appSettings.getProps().set(CLUSTER_HOSTS, "192.168.1.2:5501");
-    assertThat(
-      new ClusterProperties(appSettings).getHosts()).containsExactlyInAnyOrder(
-        "192.168.1.2:5501");
-
-    appSettings.getProps().set(CLUSTER_HOSTS, "192.168.1.2:5501,192.168.1.1");
-    assertThat(
-      new ClusterProperties(appSettings).getHosts()).containsExactlyInAnyOrder(
-        "192.168.1.2:5501", "192.168.1.1:9003");
-  }
-}
diff --git a/server/sonar-main/src/test/java/org/sonar/application/cluster/HazelcastClusterTest.java b/server/sonar-main/src/test/java/org/sonar/application/cluster/HazelcastClusterTest.java
deleted file mode 100644 (file)
index 812e5cc..0000000
+++ /dev/null
@@ -1,353 +0,0 @@
-/*
- * 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 ch.qos.logback.classic.LoggerContext;
-import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.core.AppenderBase;
-import com.hazelcast.core.HazelcastInstance;
-import com.hazelcast.core.ItemEvent;
-import com.hazelcast.core.ItemListener;
-import com.hazelcast.core.ReplicatedMap;
-import java.net.InetAddress;
-import java.util.AbstractMap;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.UUID;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import org.junit.After;
-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.LoggerFactory;
-import org.sonar.process.NetworkUtils;
-import org.sonar.application.AppStateListener;
-import org.sonar.application.config.TestAppSettings;
-import org.sonar.process.cluster.ClusterObjectKeys;
-import org.sonar.process.ProcessId;
-
-import static java.lang.String.format;
-import static junit.framework.TestCase.fail;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.within;
-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.HazelcastClusterTestHelper.closeAllHazelcastClients;
-import static org.sonar.application.cluster.HazelcastClusterTestHelper.createHazelcastClient;
-import static org.sonar.application.cluster.HazelcastClusterTestHelper.newApplicationSettings;
-import static org.sonar.application.cluster.HazelcastClusterTestHelper.newSearchSettings;
-import static org.sonar.process.cluster.ClusterObjectKeys.LEADER;
-import static org.sonar.process.cluster.ClusterObjectKeys.OPERATIONAL_PROCESSES;
-import static org.sonar.process.cluster.ClusterObjectKeys.SONARQUBE_VERSION;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_HOSTS;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NAME;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_HOST;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_PORT;
-
-public class HazelcastClusterTest {
-  @Rule
-  public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60));
-
-  @Rule
-  public ExpectedException expectedException = ExpectedException.none();
-
-  @After
-  public void closeHazelcastClients() {
-    closeAllHazelcastClients();
-  }
-
-  @Test
-  public void test_two_tryToLockWebLeader_must_return_true() {
-    ClusterProperties clusterProperties = new ClusterProperties(newApplicationSettings());
-    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(newApplicationSettings());
-    try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
-      HazelcastInstance hzInstance = createHazelcastClient(hzCluster);
-      hzInstance.getAtomicReference(LEADER).set("aaaa");
-      assertThat(hzCluster.tryToLockWebLeader()).isEqualTo(false);
-    }
-  }
-
-  @Test
-  public void when_no_leader_getLeaderHostName_must_return_NO_LEADER() {
-    ClusterProperties clusterProperties = new ClusterProperties(newApplicationSettings());
-    try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
-      assertThat(hzCluster.getLeaderHostName()).isEmpty();
-    }
-  }
-
-  @Test
-  public void when_no_leader_getLeaderHostName_must_return_the_hostname() {
-    ClusterProperties clusterProperties = new ClusterProperties(newApplicationSettings());
-    try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
-      assertThat(hzCluster.tryToLockWebLeader()).isTrue();
-      assertThat(hzCluster.getLeaderHostName().get()).isEqualTo(
-        format("%s (%s)", NetworkUtils.INSTANCE.getHostname(), NetworkUtils.INSTANCE.getIPAddresses()));
-    }
-  }
-
-  @Test
-  public void members_must_be_empty_when_there_is_no_other_node() {
-    ClusterProperties clusterProperties = new ClusterProperties(newApplicationSettings());
-    try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
-      assertThat(hzCluster.getMembers()).isEmpty();
-    }
-  }
-
-  @Test
-  public void set_operational_is_writing_to_cluster() {
-    ClusterProperties clusterProperties = new ClusterProperties(newApplicationSettings());
-    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 hazelcast_cluster_name_is_hardcoded_and_not_affected_by_settings() {
-    TestAppSettings testAppSettings = newApplicationSettings();
-    testAppSettings.set(CLUSTER_NAME, "a_cluster_");
-    ClusterProperties clusterProperties = new ClusterProperties(testAppSettings);
-    try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
-      assertThat(hzCluster.getName()).isEqualTo("sonarqube");
-    }
-  }
-
-  @Test
-  public void cluster_must_keep_a_list_of_clients() throws InterruptedException {
-    TestAppSettings testAppSettings = newApplicationSettings();
-    testAppSettings.set(CLUSTER_NAME, "a_cluster_");
-    ClusterProperties clusterProperties = new ClusterProperties(testAppSettings);
-    try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
-      assertThat(hzCluster.hzInstance.getSet(ClusterObjectKeys.LOCAL_MEMBER_UUIDS)).isEmpty();
-      HazelcastInstance hzClient = createHazelcastClient(hzCluster);
-      assertThat(hzCluster.hzInstance.getSet(ClusterObjectKeys.LOCAL_MEMBER_UUIDS)).containsExactly(hzClient.getLocalEndpoint().getUuid());
-
-      CountDownLatch latch = new CountDownLatch(1);
-      hzCluster.hzInstance.getSet(ClusterObjectKeys.LOCAL_MEMBER_UUIDS).addItemListener(new ItemListener<Object>() {
-        @Override
-        public void itemAdded(ItemEvent<Object> item) {
-        }
-
-        @Override
-        public void itemRemoved(ItemEvent<Object> item) {
-          latch.countDown();
-        }
-      }, false);
-
-      hzClient.shutdown();
-      if (latch.await(5, TimeUnit.SECONDS)) {
-        assertThat(hzCluster.hzInstance.getSet(ClusterObjectKeys.LOCAL_MEMBER_UUIDS).size()).isEqualTo(0);
-      } else {
-        fail("The client UUID have not been removed from the Set within 5 seconds' time lapse");
-      }
-    }
-  }
-
-  @Test
-  public void localUUID_must_not_be_empty() {
-    ClusterProperties clusterProperties = new ClusterProperties(newApplicationSettings());
-    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(newApplicationSettings());
-    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(newApplicationSettings());
-    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(newApplicationSettings());
-    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 = newApplicationSettings();
-    settings.set(CLUSTER_NODE_HOST, InetAddress.getLoopbackAddress().getHostAddress());
-    AppStateListener listener = mock(AppStateListener.class);
-
-    try (ClusterAppStateImpl appStateCluster = new ClusterAppStateImpl(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();
-    }
-  }
-
-  @Test
-  public void hazelcast_must_log_through_sl4fj() {
-    MemoryAppender<ILoggingEvent> memoryAppender = new MemoryAppender<>();
-    LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
-    lc.reset();
-    memoryAppender.setContext(lc);
-    memoryAppender.start();
-    lc.getLogger("com.hazelcast").addAppender(memoryAppender);
-
-    try (ClusterAppStateImpl appStateCluster = new ClusterAppStateImpl(newApplicationSettings())) {
-    }
-
-    assertThat(memoryAppender.events).isNotEmpty();
-    memoryAppender.events.stream().forEach(
-      e -> assertThat(e.getLoggerName()).startsWith("com.hazelcast"));
-  }
-
-  @Test
-  public void getClusterTime_returns_time_of_cluster() {
-    ClusterProperties clusterProperties = new ClusterProperties(newApplicationSettings());
-    try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
-      assertThat(hzCluster.getHazelcastClient().getClusterTime())
-        .isCloseTo(hzCluster.hzInstance.getCluster().getClusterTime(), within(1000L));
-    }
-  }
-
-  @Test
-  public void removing_the_last_application_node_must_clear_web_leader() throws InterruptedException {
-    try (ClusterAppStateImpl appStateCluster = new ClusterAppStateImpl(newSearchSettings())) {
-      TestAppSettings appSettings = newApplicationSettings();
-      appSettings.set(CLUSTER_HOSTS, appStateCluster.getHazelcastCluster().getLocalEndPoint());
-      appSettings.set(CLUSTER_NODE_PORT, "9004");
-      ClusterProperties clusterProperties = new ClusterProperties(appSettings);
-      // Simulate a connection from an application node
-      HazelcastCluster appNode = HazelcastCluster.create(clusterProperties);
-      appNode.tryToLockWebLeader();
-      appNode.setOperational(ProcessId.WEB_SERVER);
-      appNode.setOperational(ProcessId.COMPUTE_ENGINE);
-      appNode.registerSonarQubeVersion("6.6.0.22999");
-
-      assertThat(appStateCluster.getLeaderHostName()).isPresent();
-      assertThat(appStateCluster.isOperational(ProcessId.WEB_SERVER, false)).isTrue();
-      assertThat(appStateCluster.isOperational(ProcessId.COMPUTE_ENGINE, false)).isTrue();
-      assertThat(appStateCluster.getHazelcastCluster().getSonarQubeVersion()).isEqualTo("6.6.0.22999");
-
-      // Shutdown the node
-      appNode.close();
-
-      // Propagation of all information take some time, let's wait 5s maximum
-      int counter = 10;
-      while (appStateCluster.getHazelcastCluster().getSonarQubeVersion() != null && counter > 0) {
-        Thread.sleep(500);
-        counter--;
-      }
-
-      assertThat(appStateCluster.getLeaderHostName()).isNotPresent();
-      assertThat(appStateCluster.isOperational(ProcessId.WEB_SERVER, false)).isFalse();
-      assertThat(appStateCluster.isOperational(ProcessId.COMPUTE_ENGINE, false)).isFalse();
-      assertThat(appStateCluster.getHazelcastCluster().getSonarQubeVersion()).isNull();
-    }
-
-  }
-
-  @Test
-  public void configuration_tweaks_of_hazelcast_must_be_present() {
-    try (HazelcastCluster hzCluster = HazelcastCluster.create(new ClusterProperties(newApplicationSettings()))) {
-      assertThat(hzCluster.hzInstance.getConfig().getProperty("hazelcast.tcp.join.port.try.count")).isEqualTo("10");
-      assertThat(hzCluster.hzInstance.getConfig().getProperty("hazelcast.phone.home.enabled")).isEqualTo("false");
-      assertThat(hzCluster.hzInstance.getConfig().getProperty("hazelcast.logging.type")).isEqualTo("slf4j");
-      assertThat(hzCluster.hzInstance.getConfig().getProperty("hazelcast.socket.bind.any")).isEqualTo("false");
-    }
-  }
-
-  private class MemoryAppender<E> extends AppenderBase<E> {
-    private final List<E> events = new ArrayList();
-
-    @Override
-    protected void append(E eventObject) {
-      events.add(eventObject);
-    }
-  }
-}
diff --git a/server/sonar-main/src/test/java/org/sonar/application/cluster/HazelcastClusterTestHelper.java b/server/sonar-main/src/test/java/org/sonar/application/cluster/HazelcastClusterTestHelper.java
deleted file mode 100644 (file)
index c698073..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * 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.client.HazelcastClient;
-import com.hazelcast.client.config.ClientConfig;
-import com.hazelcast.core.HazelcastInstance;
-import java.net.InetSocketAddress;
-import java.util.ArrayList;
-import java.util.List;
-import org.sonar.application.config.TestAppSettings;
-
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_ENABLED;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NAME;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_TYPE;
-
-public class HazelcastClusterTestHelper {
-
-  // Be careful this test won't work if parallel tests is used
-  private static final List<HazelcastInstance> HAZELCAST_INSTANCES = new ArrayList<>();
-
-  static HazelcastInstance createHazelcastClient(HazelcastCluster hzCluster) {
-    ClientConfig clientConfig = new ClientConfig();
-    InetSocketAddress socketAddress = (InetSocketAddress) hzCluster.hzInstance.getLocalEndpoint().getSocketAddress();
-
-    clientConfig.getNetworkConfig().getAddresses().add(
-      String.format("%s:%d",
-        socketAddress.getHostString(),
-        socketAddress.getPort()));
-    clientConfig.getGroupConfig().setName(hzCluster.getName());
-    HazelcastInstance hazelcastInstance = HazelcastClient.newHazelcastClient(clientConfig);
-    HAZELCAST_INSTANCES.add(hazelcastInstance);
-    return hazelcastInstance;
-  }
-
-  static HazelcastInstance createHazelcastClient(ClusterAppStateImpl appStateCluster) {
-    return createHazelcastClient(appStateCluster.getHazelcastCluster());
-  }
-
-  static void closeAllHazelcastClients() {
-    HAZELCAST_INSTANCES.stream().forEach(
-      hz -> {
-        try {
-          hz.shutdown();
-        } catch (Exception ex) {
-          // Ignore it
-        }
-      });
-  }
-
-  static TestAppSettings newApplicationSettings() {
-    TestAppSettings settings = new TestAppSettings();
-    settings.set(CLUSTER_ENABLED, "true");
-    settings.set(CLUSTER_NAME, "sonarqube");
-    settings.set(CLUSTER_NODE_TYPE, "application");
-    return settings;
-  }
-
-  static TestAppSettings newSearchSettings() {
-    TestAppSettings settings = new TestAppSettings();
-    settings.set(CLUSTER_ENABLED, "true");
-    settings.set(CLUSTER_NAME, "sonarqube");
-    settings.set(CLUSTER_NODE_TYPE, "search");
-    return settings;
-  }
-}
diff --git a/server/sonar-main/src/test/java/org/sonar/application/cluster/HazelcastTesting.java b/server/sonar-main/src/test/java/org/sonar/application/cluster/HazelcastTesting.java
new file mode 100644 (file)
index 0000000..cf8fea2
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * 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 java.net.InetAddress;
+import org.sonar.process.NetworkUtils;
+import org.sonar.process.ProcessId;
+import org.sonar.process.cluster.NodeType;
+import org.sonar.process.cluster.hz.HazelcastMember;
+import org.sonar.process.cluster.hz.HazelcastMemberBuilder;
+
+public class HazelcastTesting {
+
+  private HazelcastTesting() {
+    // do not instantiate
+  }
+
+  public static HazelcastMember newHzMember() {
+    // use loopback for support of offline builds
+    InetAddress loopback = InetAddress.getLoopbackAddress();
+
+    return new HazelcastMemberBuilder()
+      .setNodeType(NodeType.APPLICATION)
+      .setProcessId(ProcessId.COMPUTE_ENGINE)
+      .setClusterName("foo")
+      .setNodeName("bar")
+      .setPort(NetworkUtils.INSTANCE.getNextAvailablePort(loopback))
+      .setNetworkInterface(loopback.getHostAddress())
+      .build();
+  }
+}
diff --git a/server/sonar-main/src/test/java/org/sonar/application/cluster/SearchNodeHealthProviderTest.java b/server/sonar-main/src/test/java/org/sonar/application/cluster/SearchNodeHealthProviderTest.java
deleted file mode 100644 (file)
index 6b67636..0000000
+++ /dev/null
@@ -1,234 +0,0 @@
-/*
- * 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 java.util.Properties;
-import java.util.Random;
-import javax.annotation.Nullable;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.sonar.process.NetworkUtils;
-import org.sonar.process.ProcessId;
-import org.sonar.process.Props;
-import org.sonar.process.cluster.ClusterProperties;
-import org.sonar.process.cluster.health.NodeHealth;
-
-import static java.lang.String.valueOf;
-import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
-import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_HOST;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_NAME;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_PORT;
-
-public class SearchNodeHealthProviderTest {
-  @Rule
-  public ExpectedException expectedException = ExpectedException.none();
-
-  private final Random random = new Random();
-  private SearchNodeHealthProvider.Clock clock = mock(SearchNodeHealthProvider.Clock.class);
-  private NetworkUtils networkUtils = mock(NetworkUtils.class);
-  private ClusterAppState clusterAppState = mock(ClusterAppState.class);
-
-  @Test
-  public void constructor_throws_IAE_if_property_node_name_is_not_set() {
-    Props props = new Props(new Properties());
-
-    expectedException.expect(IllegalArgumentException.class);
-    expectedException.expectMessage("Missing property: sonar.cluster.node.name");
-
-    new SearchNodeHealthProvider(props, clusterAppState, networkUtils);
-  }
-
-  @Test
-  public void constructor_throws_NPE_if_NetworkUtils_getHostname_returns_null_and_property_is_not_set() {
-    Properties properties = new Properties();
-    properties.put(ClusterProperties.CLUSTER_NODE_NAME, randomAlphanumeric(3));
-    Props props = new Props(properties);
-
-    expectedException.expect(NullPointerException.class);
-
-    new SearchNodeHealthProvider(props, clusterAppState, networkUtils, clock);
-  }
-
-  @Test
-  public void constructor_throws_IAE_if_property_node_port_is_not_set() {
-    Properties properties = new Properties();
-    properties.put(ClusterProperties.CLUSTER_NODE_NAME, randomAlphanumeric(3));
-    when(networkUtils.getHostname()).thenReturn(randomAlphanumeric(34));
-    Props props = new Props(properties);
-
-    expectedException.expect(IllegalArgumentException.class);
-    expectedException.expectMessage("Missing property: sonar.cluster.node.port");
-
-    new SearchNodeHealthProvider(props, clusterAppState, networkUtils, clock);
-  }
-
-  @Test
-  public void constructor_throws_FormatException_if_property_node_port_is_not_an_integer() {
-    String port = randomAlphanumeric(3);
-    Properties properties = new Properties();
-    properties.put(ClusterProperties.CLUSTER_NODE_NAME, randomAlphanumeric(3));
-    properties.put(ClusterProperties.CLUSTER_NODE_PORT, port);
-    when(networkUtils.getHostname()).thenReturn(randomAlphanumeric(34));
-    Props props = new Props(properties);
-
-    expectedException.expect(NumberFormatException.class);
-    expectedException.expectMessage("For input string: \"" + port + "\"");
-
-    new SearchNodeHealthProvider(props, clusterAppState, networkUtils, clock);
-  }
-
-  @Test
-  public void get_returns_name_and_port_from_properties_at_constructor_time() {
-    String name = randomAlphanumeric(3);
-    int port = 1 + random.nextInt(4);
-    Properties properties = new Properties();
-    properties.setProperty(CLUSTER_NODE_NAME, name);
-    properties.setProperty(CLUSTER_NODE_PORT, valueOf(port));
-    when(networkUtils.getHostname()).thenReturn(randomAlphanumeric(34));
-    when(clock.now()).thenReturn(1L + random.nextInt(87));
-    SearchNodeHealthProvider underTest = new SearchNodeHealthProvider(new Props(properties), clusterAppState, networkUtils, clock);
-
-    NodeHealth nodeHealth = underTest.get();
-
-    assertThat(nodeHealth.getDetails().getName()).isEqualTo(name);
-    assertThat(nodeHealth.getDetails().getPort()).isEqualTo(port);
-
-    // change values in properties
-    properties.setProperty(CLUSTER_NODE_NAME, randomAlphanumeric(6));
-    properties.setProperty(CLUSTER_NODE_PORT, valueOf(1 + random.nextInt(99)));
-
-    NodeHealth newNodeHealth = underTest.get();
-
-    assertThat(newNodeHealth.getDetails().getName()).isEqualTo(name);
-    assertThat(newNodeHealth.getDetails().getPort()).isEqualTo(port);
-  }
-
-  @Test
-  public void get_returns_host_from_property_if_set_at_constructor_time() {
-    String host = randomAlphanumeric(55);
-    Properties properties = new Properties();
-    properties.setProperty(CLUSTER_NODE_NAME, randomAlphanumeric(3));
-    properties.setProperty(CLUSTER_NODE_PORT, valueOf(1 + random.nextInt(4)));
-    properties.setProperty(CLUSTER_NODE_HOST, host);
-    when(clock.now()).thenReturn(1L + random.nextInt(87));
-    SearchNodeHealthProvider underTest = new SearchNodeHealthProvider(new Props(properties), clusterAppState, networkUtils, clock);
-
-    NodeHealth nodeHealth = underTest.get();
-
-    assertThat(nodeHealth.getDetails().getHost()).isEqualTo(host);
-
-    // change now
-    properties.setProperty(CLUSTER_NODE_HOST, randomAlphanumeric(96));
-
-    NodeHealth newNodeHealth = underTest.get();
-
-    assertThat(newNodeHealth.getDetails().getHost()).isEqualTo(host);
-  }
-
-  @Test
-  public void get_returns_host_from_NetworkUtils_getHostname_if_property_is_not_set_at_constructor_time() {
-    getReturnsHostFromNetworkUtils(null);
-  }
-
-  @Test
-  public void get_returns_host_from_NetworkUtils_getHostname_if_property_is_empty_at_constructor_time() {
-    getReturnsHostFromNetworkUtils(random.nextBoolean() ? "" : "   ");
-  }
-
-  private void getReturnsHostFromNetworkUtils(@Nullable String hostPropertyValue) {
-    String host = randomAlphanumeric(34);
-    Properties properties = new Properties();
-    properties.setProperty(CLUSTER_NODE_NAME, randomAlphanumeric(3));
-    properties.setProperty(CLUSTER_NODE_PORT, valueOf(1 + random.nextInt(4)));
-    if (hostPropertyValue != null) {
-      properties.setProperty(CLUSTER_NODE_HOST, hostPropertyValue);
-    }
-    when(clock.now()).thenReturn(1L + random.nextInt(87));
-    when(networkUtils.getHostname()).thenReturn(host);
-    SearchNodeHealthProvider underTest = new SearchNodeHealthProvider(new Props(properties), clusterAppState, networkUtils, clock);
-
-    NodeHealth nodeHealth = underTest.get();
-
-    assertThat(nodeHealth.getDetails().getHost()).isEqualTo(host);
-
-    // change now
-    when(networkUtils.getHostname()).thenReturn(randomAlphanumeric(96));
-
-    NodeHealth newNodeHealth = underTest.get();
-
-    assertThat(newNodeHealth.getDetails().getHost()).isEqualTo(host);
-  }
-
-  @Test
-  public void get_returns_started_from_System2_now_at_constructor_time() {
-    Properties properties = new Properties();
-    long now = setRequiredPropertiesAndMocks(properties);
-    SearchNodeHealthProvider underTest = new SearchNodeHealthProvider(new Props(properties), clusterAppState, networkUtils, clock);
-
-    NodeHealth nodeHealth = underTest.get();
-
-    assertThat(nodeHealth.getDetails().getStartedAt()).isEqualTo(now);
-
-    // change now
-    when(clock.now()).thenReturn(now);
-
-    NodeHealth newNodeHealth = underTest.get();
-
-    assertThat(newNodeHealth.getDetails().getStartedAt()).isEqualTo(now);
-  }
-
-  @Test
-  public void get_returns_status_GREEN_if_elasticsearch_process_is_operational_in_ClusterAppState() {
-    Properties properties = new Properties();
-    setRequiredPropertiesAndMocks(properties);
-    when(clusterAppState.isOperational(ProcessId.ELASTICSEARCH, true)).thenReturn(true);
-    SearchNodeHealthProvider underTest = new SearchNodeHealthProvider(new Props(properties), clusterAppState, networkUtils, clock);
-
-    NodeHealth nodeHealth = underTest.get();
-
-    assertThat(nodeHealth.getStatus()).isEqualTo(NodeHealth.Status.GREEN);
-  }
-
-  @Test
-  public void get_returns_status_RED_with_cause_if_elasticsearch_process_is_not_operational_in_ClusterAppState() {
-    Properties properties = new Properties();
-    setRequiredPropertiesAndMocks(properties);
-    when(clusterAppState.isOperational(ProcessId.ELASTICSEARCH, true)).thenReturn(false);
-    SearchNodeHealthProvider underTest = new SearchNodeHealthProvider(new Props(properties), clusterAppState, networkUtils, clock);
-
-    NodeHealth nodeHealth = underTest.get();
-
-    assertThat(nodeHealth.getStatus()).isEqualTo(NodeHealth.Status.RED);
-    assertThat(nodeHealth.getCauses()).containsOnly("Elasticsearch is not operational");
-  }
-
-  private long setRequiredPropertiesAndMocks(Properties properties) {
-    properties.setProperty(CLUSTER_NODE_NAME, randomAlphanumeric(3));
-    properties.setProperty(CLUSTER_NODE_PORT, valueOf(1 + random.nextInt(4)));
-    long now = 1L + random.nextInt(87);
-    when(clock.now()).thenReturn(now);
-    when(networkUtils.getHostname()).thenReturn(randomAlphanumeric(34));
-    return now;
-  }
-}
diff --git a/server/sonar-main/src/test/java/org/sonar/application/cluster/health/DelegateHealthStateRefresherExecutorServiceTest.java b/server/sonar-main/src/test/java/org/sonar/application/cluster/health/DelegateHealthStateRefresherExecutorServiceTest.java
new file mode 100644 (file)
index 0000000..d422a56
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+ * 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.health;
+
+import java.util.Collection;
+import java.util.Random;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.junit.Test;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class DelegateHealthStateRefresherExecutorServiceTest {
+  private Random random = new Random();
+  private Runnable runnable = mock(Runnable.class);
+  private Callable callable = mock(Callable.class);
+  private Collection<Callable<Object>> callables = IntStream.range(0, random.nextInt(5))
+    .mapToObj(i -> (Callable<Object>) mock(Callable.class))
+    .collect(Collectors.toList());
+  private int initialDelay = random.nextInt(333);
+  private int delay = random.nextInt(333);
+  private int period = random.nextInt(333);
+  private int timeout = random.nextInt(333);
+  private Object result = new Object();
+  private ScheduledExecutorService executorService = mock(ScheduledExecutorService.class);
+  private DelegateHealthStateRefresherExecutorService underTest = new DelegateHealthStateRefresherExecutorService(executorService);
+
+  @Test
+  public void schedule() {
+    underTest.schedule(runnable, delay, SECONDS);
+
+    verify(executorService).schedule(runnable, delay, SECONDS);
+  }
+
+  @Test
+  public void schedule1() {
+    underTest.schedule(callable, delay, SECONDS);
+
+    verify(executorService).schedule(callable, delay, SECONDS);
+  }
+
+  @Test
+  public void scheduleAtFixedRate() {
+    underTest.scheduleAtFixedRate(runnable, initialDelay, period, SECONDS);
+    verify(executorService).scheduleAtFixedRate(runnable, initialDelay, period, SECONDS);
+  }
+
+  @Test
+  public void scheduleWithFixeddelay() {
+    underTest.scheduleWithFixedDelay(runnable, initialDelay, delay, TimeUnit.SECONDS);
+    verify(executorService).scheduleWithFixedDelay(runnable, initialDelay, delay, TimeUnit.SECONDS);
+  }
+
+  @Test
+  public void shutdown() {
+    underTest.shutdown();
+    verify(executorService).shutdown();
+  }
+
+  @Test
+  public void shutdownNow() {
+    underTest.shutdownNow();
+    verify(executorService).shutdownNow();
+  }
+
+  @Test
+  public void isShutdown() {
+    underTest.isShutdown();
+    verify(executorService).isShutdown();
+  }
+
+  @Test
+  public void isTerminated() {
+    underTest.isTerminated();
+    verify(executorService).isTerminated();
+  }
+
+  @Test
+  public void awaitTermination() throws InterruptedException {
+    underTest.awaitTermination(timeout, TimeUnit.SECONDS);
+
+    verify(executorService).awaitTermination(timeout, TimeUnit.SECONDS);
+  }
+
+  @Test
+  public void submit() {
+    underTest.submit(callable);
+
+    verify(executorService).submit(callable);
+  }
+
+  @Test
+  public void submit1() {
+    underTest.submit(runnable, result);
+
+    verify(executorService).submit(runnable, result);
+  }
+
+  @Test
+  public void submit2() {
+    underTest.submit(runnable);
+    verify(executorService).submit(runnable);
+  }
+
+  @Test
+  public void invokeAll() throws InterruptedException {
+    underTest.invokeAll(callables);
+    verify(executorService).invokeAll(callables);
+  }
+
+  @Test
+  public void invokeAll1() throws InterruptedException {
+    underTest.invokeAll(callables, timeout, SECONDS);
+    verify(executorService).invokeAll(callables, timeout, SECONDS);
+  }
+
+  @Test
+  public void invokeAny() throws InterruptedException, ExecutionException {
+    underTest.invokeAny(callables);
+    verify(executorService).invokeAny(callables);
+  }
+
+  @Test
+  public void invokeAny2() throws InterruptedException, ExecutionException, TimeoutException {
+    underTest.invokeAny(callables, timeout, SECONDS);
+    verify(executorService).invokeAny(callables, timeout, SECONDS);
+  }
+
+}
diff --git a/server/sonar-main/src/test/java/org/sonar/application/cluster/health/SearchNodeHealthProviderTest.java b/server/sonar-main/src/test/java/org/sonar/application/cluster/health/SearchNodeHealthProviderTest.java
new file mode 100644 (file)
index 0000000..62ee674
--- /dev/null
@@ -0,0 +1,235 @@
+/*
+ * 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.health;
+
+import java.util.Properties;
+import java.util.Random;
+import javax.annotation.Nullable;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.application.cluster.ClusterAppState;
+import org.sonar.process.NetworkUtils;
+import org.sonar.process.ProcessId;
+import org.sonar.process.Props;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.cluster.health.NodeHealth;
+
+import static java.lang.String.valueOf;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_HOST;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_NAME;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_PORT;
+
+public class SearchNodeHealthProviderTest {
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private final Random random = new Random();
+  private SearchNodeHealthProvider.Clock clock = mock(SearchNodeHealthProvider.Clock.class);
+  private NetworkUtils networkUtils = mock(NetworkUtils.class);
+  private ClusterAppState clusterAppState = mock(ClusterAppState.class);
+
+  @Test
+  public void constructor_throws_IAE_if_property_node_name_is_not_set() {
+    Props props = new Props(new Properties());
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Missing property: sonar.cluster.node.name");
+
+    new SearchNodeHealthProvider(props, clusterAppState, networkUtils);
+  }
+
+  @Test
+  public void constructor_throws_NPE_if_NetworkUtils_getHostname_returns_null_and_property_is_not_set() {
+    Properties properties = new Properties();
+    properties.put(ProcessProperties.CLUSTER_NODE_NAME, randomAlphanumeric(3));
+    Props props = new Props(properties);
+
+    expectedException.expect(NullPointerException.class);
+
+    new SearchNodeHealthProvider(props, clusterAppState, networkUtils, clock);
+  }
+
+  @Test
+  public void constructor_throws_IAE_if_property_node_port_is_not_set() {
+    Properties properties = new Properties();
+    properties.put(ProcessProperties.CLUSTER_NODE_NAME, randomAlphanumeric(3));
+    when(networkUtils.getHostname()).thenReturn(randomAlphanumeric(34));
+    Props props = new Props(properties);
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Missing property: sonar.cluster.node.port");
+
+    new SearchNodeHealthProvider(props, clusterAppState, networkUtils, clock);
+  }
+
+  @Test
+  public void constructor_throws_FormatException_if_property_node_port_is_not_an_integer() {
+    String port = randomAlphanumeric(3);
+    Properties properties = new Properties();
+    properties.put(ProcessProperties.CLUSTER_NODE_NAME, randomAlphanumeric(3));
+    properties.put(ProcessProperties.CLUSTER_NODE_PORT, port);
+    when(networkUtils.getHostname()).thenReturn(randomAlphanumeric(34));
+    Props props = new Props(properties);
+
+    expectedException.expect(NumberFormatException.class);
+    expectedException.expectMessage("For input string: \"" + port + "\"");
+
+    new SearchNodeHealthProvider(props, clusterAppState, networkUtils, clock);
+  }
+
+  @Test
+  public void get_returns_name_and_port_from_properties_at_constructor_time() {
+    String name = randomAlphanumeric(3);
+    int port = 1 + random.nextInt(4);
+    Properties properties = new Properties();
+    properties.setProperty(CLUSTER_NODE_NAME, name);
+    properties.setProperty(CLUSTER_NODE_PORT, valueOf(port));
+    when(networkUtils.getHostname()).thenReturn(randomAlphanumeric(34));
+    when(clock.now()).thenReturn(1L + random.nextInt(87));
+    SearchNodeHealthProvider underTest = new SearchNodeHealthProvider(new Props(properties), clusterAppState, networkUtils, clock);
+
+    NodeHealth nodeHealth = underTest.get();
+
+    assertThat(nodeHealth.getDetails().getName()).isEqualTo(name);
+    assertThat(nodeHealth.getDetails().getPort()).isEqualTo(port);
+
+    // change values in properties
+    properties.setProperty(CLUSTER_NODE_NAME, randomAlphanumeric(6));
+    properties.setProperty(CLUSTER_NODE_PORT, valueOf(1 + random.nextInt(99)));
+
+    NodeHealth newNodeHealth = underTest.get();
+
+    assertThat(newNodeHealth.getDetails().getName()).isEqualTo(name);
+    assertThat(newNodeHealth.getDetails().getPort()).isEqualTo(port);
+  }
+
+  @Test
+  public void get_returns_host_from_property_if_set_at_constructor_time() {
+    String host = randomAlphanumeric(55);
+    Properties properties = new Properties();
+    properties.setProperty(CLUSTER_NODE_NAME, randomAlphanumeric(3));
+    properties.setProperty(CLUSTER_NODE_PORT, valueOf(1 + random.nextInt(4)));
+    properties.setProperty(CLUSTER_NODE_HOST, host);
+    when(clock.now()).thenReturn(1L + random.nextInt(87));
+    SearchNodeHealthProvider underTest = new SearchNodeHealthProvider(new Props(properties), clusterAppState, networkUtils, clock);
+
+    NodeHealth nodeHealth = underTest.get();
+
+    assertThat(nodeHealth.getDetails().getHost()).isEqualTo(host);
+
+    // change now
+    properties.setProperty(CLUSTER_NODE_HOST, randomAlphanumeric(96));
+
+    NodeHealth newNodeHealth = underTest.get();
+
+    assertThat(newNodeHealth.getDetails().getHost()).isEqualTo(host);
+  }
+
+  @Test
+  public void get_returns_host_from_NetworkUtils_getHostname_if_property_is_not_set_at_constructor_time() {
+    getReturnsHostFromNetworkUtils(null);
+  }
+
+  @Test
+  public void get_returns_host_from_NetworkUtils_getHostname_if_property_is_empty_at_constructor_time() {
+    getReturnsHostFromNetworkUtils(random.nextBoolean() ? "" : "   ");
+  }
+
+  private void getReturnsHostFromNetworkUtils(@Nullable String hostPropertyValue) {
+    String host = randomAlphanumeric(34);
+    Properties properties = new Properties();
+    properties.setProperty(CLUSTER_NODE_NAME, randomAlphanumeric(3));
+    properties.setProperty(CLUSTER_NODE_PORT, valueOf(1 + random.nextInt(4)));
+    if (hostPropertyValue != null) {
+      properties.setProperty(CLUSTER_NODE_HOST, hostPropertyValue);
+    }
+    when(clock.now()).thenReturn(1L + random.nextInt(87));
+    when(networkUtils.getHostname()).thenReturn(host);
+    SearchNodeHealthProvider underTest = new SearchNodeHealthProvider(new Props(properties), clusterAppState, networkUtils, clock);
+
+    NodeHealth nodeHealth = underTest.get();
+
+    assertThat(nodeHealth.getDetails().getHost()).isEqualTo(host);
+
+    // change now
+    when(networkUtils.getHostname()).thenReturn(randomAlphanumeric(96));
+
+    NodeHealth newNodeHealth = underTest.get();
+
+    assertThat(newNodeHealth.getDetails().getHost()).isEqualTo(host);
+  }
+
+  @Test
+  public void get_returns_started_from_System2_now_at_constructor_time() {
+    Properties properties = new Properties();
+    long now = setRequiredPropertiesAndMocks(properties);
+    SearchNodeHealthProvider underTest = new SearchNodeHealthProvider(new Props(properties), clusterAppState, networkUtils, clock);
+
+    NodeHealth nodeHealth = underTest.get();
+
+    assertThat(nodeHealth.getDetails().getStartedAt()).isEqualTo(now);
+
+    // change now
+    when(clock.now()).thenReturn(now);
+
+    NodeHealth newNodeHealth = underTest.get();
+
+    assertThat(newNodeHealth.getDetails().getStartedAt()).isEqualTo(now);
+  }
+
+  @Test
+  public void get_returns_status_GREEN_if_elasticsearch_process_is_operational_in_ClusterAppState() {
+    Properties properties = new Properties();
+    setRequiredPropertiesAndMocks(properties);
+    when(clusterAppState.isOperational(ProcessId.ELASTICSEARCH, true)).thenReturn(true);
+    SearchNodeHealthProvider underTest = new SearchNodeHealthProvider(new Props(properties), clusterAppState, networkUtils, clock);
+
+    NodeHealth nodeHealth = underTest.get();
+
+    assertThat(nodeHealth.getStatus()).isEqualTo(NodeHealth.Status.GREEN);
+  }
+
+  @Test
+  public void get_returns_status_RED_with_cause_if_elasticsearch_process_is_not_operational_in_ClusterAppState() {
+    Properties properties = new Properties();
+    setRequiredPropertiesAndMocks(properties);
+    when(clusterAppState.isOperational(ProcessId.ELASTICSEARCH, true)).thenReturn(false);
+    SearchNodeHealthProvider underTest = new SearchNodeHealthProvider(new Props(properties), clusterAppState, networkUtils, clock);
+
+    NodeHealth nodeHealth = underTest.get();
+
+    assertThat(nodeHealth.getStatus()).isEqualTo(NodeHealth.Status.RED);
+    assertThat(nodeHealth.getCauses()).containsOnly("Elasticsearch is not operational");
+  }
+
+  private long setRequiredPropertiesAndMocks(Properties properties) {
+    properties.setProperty(CLUSTER_NODE_NAME, randomAlphanumeric(3));
+    properties.setProperty(CLUSTER_NODE_PORT, valueOf(1 + random.nextInt(4)));
+    long now = 1L + random.nextInt(87);
+    when(clock.now()).thenReturn(now);
+    when(networkUtils.getHostname()).thenReturn(randomAlphanumeric(34));
+    return now;
+  }
+}
index 43c359f826255d64b0559cbe5e1d1ec227e53a33..245b935d5a83b7f12739d3a286d2c4635640183d 100644 (file)
 
 package org.sonar.application.config;
 
-import java.net.NetworkInterface;
+import java.net.InetAddress;
 import java.net.SocketException;
-import java.util.Collections;
-import java.util.Enumeration;
+import java.util.Optional;
+import org.hamcrest.CoreMatchers;
 import org.junit.Before;
 import org.junit.Rule;
-import org.junit.experimental.theories.DataPoints;
-import org.junit.experimental.theories.FromDataPoints;
-import org.junit.experimental.theories.Theories;
-import org.junit.experimental.theories.Theory;
+import org.junit.Test;
 import org.junit.rules.ExpectedException;
-import org.junit.runner.RunWith;
 import org.sonar.process.MessageException;
-
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_ENABLED;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_HOSTS;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_HOST;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_TYPE;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_SEARCH_HOSTS;
+import org.sonar.process.NetworkUtils;
+import org.sonar.process.ProcessProperties;
+
+import static org.junit.Assume.assumeThat;
+import static org.mockito.Mockito.spy;
+import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_HOSTS;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_HOST;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_TYPE;
+import static org.sonar.process.ProcessProperties.CLUSTER_SEARCH_HOSTS;
 import static org.sonar.process.ProcessProperties.JDBC_URL;
 import static org.sonar.process.ProcessProperties.SEARCH_HOST;
 
-@RunWith(Theories.class)
 public class ClusterSettingsLoopbackTest {
 
-  private TestAppSettings settings;
-  private static final String LOOPBACK_FORBIDDEN = " must not be a loopback address";
-  private static final String NOT_LOCAL_ADDRESS = " is not a local address";
-  private static final String NOT_RESOLVABLE = " cannot be resolved";
-
-  @DataPoints("parameter")
-  public static final ValueAndResult[] VALID_SINGLE_IP = {
-    // Valid IPs
-    new ValueAndResult("1.2.3.4", NOT_LOCAL_ADDRESS),
-    new ValueAndResult("1.2.3.4:9001", NOT_LOCAL_ADDRESS),
-    new ValueAndResult("2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb", NOT_LOCAL_ADDRESS),
-    new ValueAndResult("[2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb]:9001", NOT_LOCAL_ADDRESS),
-
-    // Valid Name
-    new ValueAndResult("www.sonarqube.org", NOT_LOCAL_ADDRESS),
-    new ValueAndResult("www.google.fr", NOT_LOCAL_ADDRESS),
-    new ValueAndResult("www.google.com, www.sonarsource.com, wwww.sonarqube.org", NOT_LOCAL_ADDRESS),
-
-    new ValueAndResult("...", NOT_RESOLVABLE),
-    new ValueAndResult("භඦආ\uD801\uDC8C\uD801\uDC8B", NOT_RESOLVABLE),
-
-    // Valide IPs List
-    new ValueAndResult("1.2.3.4,2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb", NOT_LOCAL_ADDRESS),
-    new ValueAndResult("1.2.3.4:9001,[2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb]:9001", NOT_LOCAL_ADDRESS),
-    new ValueAndResult("2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb,1.2.3.4:9001", NOT_LOCAL_ADDRESS),
-    new ValueAndResult("[2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb]:9001,2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccc", NOT_LOCAL_ADDRESS),
-
-    // Loopback IPs
-    new ValueAndResult("localhost", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("127.0.0.1", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("127.1.1.1", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("127.243.136.241", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("::1", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("0:0:0:0:0:0:0:1", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("localhost:9001", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("127.0.0.1:9001", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("127.1.1.1:9001", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("127.243.136.241:9001", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("[::1]:9001", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("[0:0:0:0:0:0:0:1]:9001", LOOPBACK_FORBIDDEN),
-
-    // Loopback IPs list
-    new ValueAndResult("127.0.0.1,192.168.11.25", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("192.168.11.25,127.1.1.1", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb,0:0:0:0:0:0:0:1", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("0:0:0:0:0:0:0:1,2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("2a01:e34:ef1f:dbb0:c3f6:a978:c5c0:9ccb,::1", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("::1,2a01:e34:ef1f:dbb0:c3f6:a978:c5c0:9ccb", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("::1,2a01:e34:ef1f:dbb0:c3f6:a978:c5c0:9ccb,2a01:e34:ef1f:dbb0:b3f6:a978:c5c0:9ccb", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("localhost:9001", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("127.0.0.1:9001", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("127.1.1.1:9001", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("127.243.136.241:9001", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("[::1]:9001", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("[0:0:0:0:0:0:0:1]:9001", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("127.0.0.1,192.168.11.25:9001", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("192.168.11.25:9001,127.1.1.1:9001", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb,[0:0:0:0:0:0:0:1]:9001", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("[0:0:0:0:0:0:0:1]:9001,[2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb]:9001", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("[2a01:e34:ef1f:dbb0:c3f6:a978:c5c0:9ccb]:9001,[::1]:9001", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("[::1]:9001,[2a01:e34:ef1f:dbb0:c3f6:a978:c5c0:9ccb]:9001", LOOPBACK_FORBIDDEN),
-    new ValueAndResult("[::1]:9001,[2a01:e34:ef1f:dbb0:c3f6:a978:c5c0:9ccb]:9001,[2a01:e34:ef1f:dbb0:b3f6:a978:c5c0:9ccb]:9001", LOOPBACK_FORBIDDEN)
-  };
-
-  @DataPoints("key")
-  public static final Key[] KEYS = {
-    new Key(SEARCH_HOST, false, false),
-    new Key(CLUSTER_NODE_HOST, true, false),
-    new Key(CLUSTER_SEARCH_HOSTS, true, true),
-    new Key(CLUSTER_HOSTS, true, true)
-  };
-
-  @DataPoints("unresolvable_hosts")
-  public static final String[] UNRESOLVABLE_HOSTS = {
-  };
-
   @Rule
   public ExpectedException expectedException = ExpectedException.none();
 
+  private InetAddress loopback = InetAddress.getLoopbackAddress();
+  private InetAddress nonLoopbackLocal;
+  private NetworkUtils network = spy(NetworkUtils.INSTANCE);
+
   @Before
-  public void resetSettings() {
-    settings = getClusterSettings();
-  }
+  public void setUp() throws Exception {
+    Optional<InetAddress> opt = network.getLocalNonLoopbackIpv4Address();
+    assumeThat(opt.isPresent(), CoreMatchers.is(true));
 
-  @Theory
-  public void accept_throws_MessageException(
-    @FromDataPoints("key") Key propertyKey,
-    @FromDataPoints("parameter") ValueAndResult valueAndResult) {
-    // Skip the test if the value is a list and if the key is not accepting a list
-    if (settings == null) {
-      System.out.println("No network found, skipping the test");
-      return;
-    }
-    if ((valueAndResult.isList() && propertyKey.acceptList) || !valueAndResult.isList()) {
-      settings.set(propertyKey.getKey(), valueAndResult.getValue());
-
-      // If the key accept non local IPs there won't be any exception
-      if (!propertyKey.acceptNonLocal || valueAndResult.getMessage() != NOT_LOCAL_ADDRESS) {
-        expectedException.expect(MessageException.class);
-        expectedException.expectMessage(valueAndResult.getMessage());
-      }
-
-      new ClusterSettings().accept(settings.getProps());
-    }
+    nonLoopbackLocal = opt.get();
   }
 
-  private static TestAppSettings getClusterSettings() {
-    String localAddress = null;
-    try {
-      Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
-      for (NetworkInterface networkInterface : Collections.list(nets)) {
-        if (!networkInterface.isLoopback() && networkInterface.isUp()) {
-          localAddress = networkInterface.getInetAddresses().nextElement().getHostAddress();
-        }
-      }
-      if (localAddress == null) {
-        return null;
-      }
-
-    } catch (SocketException e) {
-      return null;
-    }
-
-    TestAppSettings testAppSettings = new TestAppSettings()
-      .set(CLUSTER_ENABLED, "true")
-      .set(CLUSTER_NODE_TYPE, "search")
-      .set(CLUSTER_SEARCH_HOSTS, "192.168.233.1, 192.168.233.2,192.168.233.3")
-      .set(CLUSTER_HOSTS, "192.168.233.1, 192.168.233.2,192.168.233.3")
-      .set(SEARCH_HOST, localAddress)
-      .set(JDBC_URL, "jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useConfigs=maxPerformance");
-    return testAppSettings;
+  @Test
+  public void ClusterSettings_throws_MessageException_if_host_of_search_node_is_loopback() throws Exception {
+    verifySearchFailureIfLoopback(ProcessProperties.CLUSTER_NODE_HOST);
+    verifySearchFailureIfLoopback(ProcessProperties.CLUSTER_SEARCH_HOSTS);
+    verifySearchFailureIfLoopback(ProcessProperties.CLUSTER_HOSTS);
+    verifySearchFailureIfLoopback(ProcessProperties.SEARCH_HOST);
   }
 
-  private static class Key {
-    private final String key;
-    private final boolean acceptList;
-    private final boolean acceptNonLocal;
-
-    private Key(String key, boolean acceptList, boolean acceptNonLocal) {
-      this.key = key;
-      this.acceptList = acceptList;
-      this.acceptNonLocal = acceptNonLocal;
-    }
-
-    public String getKey() {
-      return key;
-    }
+  @Test
+  public void ClusterSettings_throws_MessageException_if_host_of_app_node_is_loopback() throws Exception {
+    verifyAppFailureIfLoopback(ProcessProperties.CLUSTER_NODE_HOST);
+    verifyAppFailureIfLoopback(ProcessProperties.CLUSTER_SEARCH_HOSTS);
+    verifyAppFailureIfLoopback(ProcessProperties.CLUSTER_HOSTS);
+  }
 
-    public boolean acceptList() {
-      return acceptList;
-    }
+  private void verifySearchFailureIfLoopback(String propertyKey) throws Exception {
+    TestAppSettings settings = newSettingsForSearchNode();
+    verifyFailure(propertyKey, settings);
+  }
 
-    public boolean acceptNonLocal() {
-      return acceptNonLocal;
-    }
+  private void verifyAppFailureIfLoopback(String propertyKey) throws Exception {
+    TestAppSettings settings = newSettingsForAppNode();
+    verifyFailure(propertyKey, settings);
   }
 
-  private static class ValueAndResult {
-    private final String value;
-    private final String message;
+  private void verifyFailure(String propertyKey, TestAppSettings settings) {
+    settings.set(propertyKey, loopback.getHostAddress());
 
-    private ValueAndResult(String value, String message) {
-      this.value = value;
-      this.message = message;
-    }
+    expectedException.expect(MessageException.class);
+    expectedException.expectMessage("Property " + propertyKey + " must be a local non-loopback address: " + loopback.getHostAddress());
 
-    public String getValue() {
-      return value;
-    }
+    new ClusterSettings(network).accept(settings.getProps());
+  }
 
-    public String getMessage() {
-      return message;
-    }
+  private TestAppSettings newSettingsForAppNode() throws SocketException {
+    return new TestAppSettings()
+      .set(CLUSTER_ENABLED, "true")
+      .set(CLUSTER_NODE_TYPE, "application")
+      .set(CLUSTER_NODE_HOST, nonLoopbackLocal.getHostAddress())
+      .set(CLUSTER_HOSTS, nonLoopbackLocal.getHostAddress())
+      .set(CLUSTER_SEARCH_HOSTS, nonLoopbackLocal.getHostAddress())
+      .set("sonar.auth.jwtBase64Hs256Secret", "abcde")
+      .set(JDBC_URL, "jdbc:mysql://localhost:3306/sonar");
+  }
 
-    public boolean isList() {
-      return value != null && value.contains(",");
-    }
+  private TestAppSettings newSettingsForSearchNode() throws SocketException {
+    return new TestAppSettings()
+      .set(CLUSTER_ENABLED, "true")
+      .set(CLUSTER_NODE_TYPE, "search")
+      .set(CLUSTER_NODE_HOST, nonLoopbackLocal.getHostAddress())
+      .set(CLUSTER_HOSTS, nonLoopbackLocal.getHostAddress())
+      .set(CLUSTER_SEARCH_HOSTS, nonLoopbackLocal.getHostAddress())
+      .set(SEARCH_HOST, nonLoopbackLocal.getHostAddress());
   }
 }
index e52273b07611924d14983d15501fe53573e14e32..f3f528da701b68978758b2eebcd153bdf0ed5c33 100644 (file)
  */
 package org.sonar.application.config;
 
-import java.net.Inet4Address;
 import java.net.InetAddress;
-import java.net.NetworkInterface;
 import java.net.SocketException;
-import java.util.Collections;
-import java.util.Enumeration;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.sonar.process.MessageException;
+import org.sonar.process.NetworkUtils;
 
 import static java.lang.String.format;
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_ENABLED;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_HOSTS;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_TYPE;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_SEARCH_HOSTS;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
 import static org.sonar.process.ProcessId.COMPUTE_ENGINE;
 import static org.sonar.process.ProcessId.ELASTICSEARCH;
 import static org.sonar.process.ProcessId.WEB_SERVER;
+import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_HOSTS;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_HOST;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_TYPE;
+import static org.sonar.process.ProcessProperties.CLUSTER_SEARCH_HOSTS;
 import static org.sonar.process.ProcessProperties.JDBC_URL;
 import static org.sonar.process.ProcessProperties.SEARCH_HOST;
 
@@ -47,8 +48,17 @@ public class ClusterSettingsTest {
   @Rule
   public ExpectedException expectedException = ExpectedException.none();
 
+  private InetAddress nonLoopbackLocal = InetAddress.getLoopbackAddress();
+  private NetworkUtils network = spy(NetworkUtils.INSTANCE);
+
+  @Before
+  public void setUp() throws Exception {
+    when(network.isLocalInetAddress(nonLoopbackLocal)).thenReturn(true);
+    when(network.isLoopbackInetAddress(nonLoopbackLocal)).thenReturn(false);
+  }
+
   @Test
-  public void test_isClusterEnabled() {
+  public void test_isClusterEnabled() throws Exception {
     TestAppSettings settings = newSettingsForAppNode().set(CLUSTER_ENABLED, "true");
     assertThat(ClusterSettings.isClusterEnabled(settings)).isTrue();
 
@@ -68,19 +78,7 @@ public class ClusterSettingsTest {
   }
 
   @Test
-  public void getEnabledProcesses_fails_if_no_node_type_is_set_for_a_cluster_node() {
-    TestAppSettings settings = new TestAppSettings();
-    settings.set(CLUSTER_ENABLED, "true");
-    settings.set(CLUSTER_NODE_TYPE, "foo");
-
-    expectedException.expect(IllegalArgumentException.class);
-    expectedException.expectMessage("Invalid value for [sonar.cluster.node.type]: [foo]");
-
-    ClusterSettings.getEnabledProcesses(settings);
-  }
-
-  @Test
-  public void getEnabledProcesses_returns_configured_processes_in_cluster_mode() {
+  public void getEnabledProcesses_returns_configured_processes_in_cluster_mode() throws Exception {
     TestAppSettings settings = newSettingsForAppNode();
     assertThat(ClusterSettings.getEnabledProcesses(settings)).containsOnly(COMPUTE_ENGINE, WEB_SERVER);
 
@@ -94,9 +92,9 @@ public class ClusterSettingsTest {
     settings.set(CLUSTER_ENABLED, "true");
 
     expectedException.expect(MessageException.class);
-    expectedException.expectMessage("Property [sonar.cluster.node.type] is mandatory");
+    expectedException.expectMessage("Property sonar.cluster.node.type is mandatory");
 
-    new ClusterSettings().accept(settings.getProps());
+    new ClusterSettings(network).accept(settings.getProps());
   }
 
   @Test
@@ -106,32 +104,20 @@ public class ClusterSettingsTest {
     settings.set(CLUSTER_NODE_TYPE, "bla");
 
     expectedException.expect(MessageException.class);
-    expectedException.expectMessage("Invalid value for property [sonar.cluster.node.type]: [bla], only [application, search] are allowed");
+    expectedException.expectMessage("Invalid value for property sonar.cluster.node.type: [bla], only [application, search] are allowed");
 
-    new ClusterSettings().accept(settings.getProps());
+    new ClusterSettings(network).accept(settings.getProps());
   }
 
   @Test
-  public void accept_throws_MessageException_if_internal_property_for_web_leader_is_configured() {
+  public void accept_throws_MessageException_if_internal_property_for_startup_leader_is_configured() throws Exception {
     TestAppSettings settings = newSettingsForAppNode();
     settings.set("sonar.cluster.web.startupLeader", "true");
 
     expectedException.expect(MessageException.class);
     expectedException.expectMessage("Property [sonar.cluster.web.startupLeader] is forbidden");
 
-    new ClusterSettings().accept(settings.getProps());
-  }
-
-  @Test
-  public void accept_throws_MessageException_if_search_enabled_with_loopback() {
-    TestAppSettings settings = newSettingsForSearchNode();
-    settings.set(CLUSTER_SEARCH_HOSTS, "192.168.1.1,192.168.1.2");
-    settings.set(SEARCH_HOST, "::1");
-
-    expectedException.expect(MessageException.class);
-    expectedException.expectMessage("The interface address [::1] of [sonar.search.host] must not be a loopback address");
-
-    new ClusterSettings().accept(settings.getProps());
+    new ClusterSettings(network).accept(settings.getProps());
   }
 
   @Test
@@ -141,38 +127,38 @@ public class ClusterSettingsTest {
     // this property is supposed to fail if cluster is enabled
     settings.set("sonar.cluster.web.startupLeader", "true");
 
-    new ClusterSettings().accept(settings.getProps());
+    new ClusterSettings(network).accept(settings.getProps());
   }
 
   @Test
-  public void accept_throws_MessageException_if_h2_on_application_node() {
+  public void accept_throws_MessageException_if_h2_on_application_node() throws Exception {
     TestAppSettings settings = newSettingsForAppNode();
     settings.set("sonar.jdbc.url", "jdbc:h2:mem");
 
     expectedException.expect(MessageException.class);
     expectedException.expectMessage("Embedded database is not supported in cluster mode");
 
-    new ClusterSettings().accept(settings.getProps());
+    new ClusterSettings(network).accept(settings.getProps());
   }
 
   @Test
-  public void accept_does_not_verify_h2_on_search_node() {
+  public void accept_does_not_verify_h2_on_search_node() throws Exception {
     TestAppSettings settings = newSettingsForSearchNode();
     settings.set("sonar.jdbc.url", "jdbc:h2:mem");
 
     // do not fail
-    new ClusterSettings().accept(settings.getProps());
+    new ClusterSettings(network).accept(settings.getProps());
   }
 
   @Test
-  public void accept_throws_MessageException_on_application_node_if_default_jdbc_url() {
+  public void accept_throws_MessageException_on_application_node_if_default_jdbc_url() throws Exception {
     TestAppSettings settings = newSettingsForAppNode();
     settings.clearProperty(JDBC_URL);
 
     expectedException.expect(MessageException.class);
     expectedException.expectMessage("Embedded database is not supported in cluster mode");
 
-    new ClusterSettings().accept(settings.getProps());
+    new ClusterSettings(network).accept(settings.getProps());
   }
 
   @Test
@@ -182,63 +168,56 @@ public class ClusterSettingsTest {
   }
 
   @Test
-  public void isLocalElasticsearchEnabled_returns_true_on_search_node() {
+  public void isLocalElasticsearchEnabled_returns_true_on_search_node() throws Exception {
     TestAppSettings settings = newSettingsForSearchNode();
 
     assertThat(ClusterSettings.isLocalElasticsearchEnabled(settings)).isTrue();
   }
 
   @Test
-  public void isLocalElasticsearchEnabled_returns_true_for_a_application_node() {
+  public void isLocalElasticsearchEnabled_returns_true_for_a_application_node() throws Exception {
     TestAppSettings settings = newSettingsForAppNode();
 
     assertThat(ClusterSettings.isLocalElasticsearchEnabled(settings)).isFalse();
   }
 
   @Test
-  public void accept_throws_MessageException_if_searchHost_is_missing() {
+  public void accept_throws_MessageException_if_searchHost_is_missing() throws Exception {
     TestAppSettings settings = newSettingsForSearchNode();
     settings.clearProperty(SEARCH_HOST);
     assertThatPropertyIsMandatory(settings, SEARCH_HOST);
   }
 
   @Test
-  public void accept_throws_MessageException_if_searchHost_is_blank() {
+  public void accept_throws_MessageException_if_searchHost_is_empty() throws Exception {
     TestAppSettings settings = newSettingsForSearchNode();
-    settings.set(SEARCH_HOST, " ");
+    settings.set(SEARCH_HOST, "");
     assertThatPropertyIsMandatory(settings, SEARCH_HOST);
   }
 
   @Test
-  public void accept_throws_MessageException_if_clusterHosts_is_missing() {
+  public void accept_throws_MessageException_if_clusterHosts_is_missing() throws Exception {
     TestAppSettings settings = newSettingsForSearchNode();
     settings.clearProperty(CLUSTER_HOSTS);
     assertThatPropertyIsMandatory(settings, CLUSTER_HOSTS);
   }
 
   @Test
-  public void accept_throws_MessageException_if_clusterHosts_is_blank() {
-    TestAppSettings settings = newSettingsForSearchNode();
-    settings.set(CLUSTER_HOSTS, " ");
-    assertThatPropertyIsMandatory(settings, CLUSTER_HOSTS);
-  }
-
-  @Test
-  public void accept_throws_MessageException_if_clusterSearchHosts_is_missing() {
+  public void accept_throws_MessageException_if_clusterSearchHosts_is_missing() throws Exception {
     TestAppSettings settings = newSettingsForSearchNode();
     settings.clearProperty(CLUSTER_SEARCH_HOSTS);
     assertThatPropertyIsMandatory(settings, CLUSTER_SEARCH_HOSTS);
   }
 
   @Test
-  public void accept_throws_MessageException_if_clusterSearchHosts_is_blank() {
+  public void accept_throws_MessageException_if_clusterSearchHosts_is_empty() throws Exception {
     TestAppSettings settings = newSettingsForSearchNode();
-    settings.set(CLUSTER_SEARCH_HOSTS, " ");
+    settings.set(CLUSTER_SEARCH_HOSTS, "");
     assertThatPropertyIsMandatory(settings, CLUSTER_SEARCH_HOSTS);
   }
 
   @Test
-  public void accept_throws_MessageException_if_jwt_token_is_not_set_on_application_nodes() {
+  public void accept_throws_MessageException_if_jwt_token_is_not_set_on_application_nodes() throws Exception {
     TestAppSettings settings = newSettingsForAppNode();
     settings.clearProperty("sonar.auth.jwtBase64Hs256Secret");
     assertThatPropertyIsMandatory(settings, "sonar.auth.jwtBase64Hs256Secret");
@@ -246,47 +225,29 @@ public class ClusterSettingsTest {
 
   private void assertThatPropertyIsMandatory(TestAppSettings settings, String key) {
     expectedException.expect(MessageException.class);
-    expectedException.expectMessage(format("Property [%s] is mandatory", key));
+    expectedException.expectMessage(format("Property %s is mandatory", key));
 
-    new ClusterSettings().accept(settings.getProps());
+    new ClusterSettings(network).accept(settings.getProps());
   }
 
-  private static TestAppSettings newSettingsForAppNode() {
+  private TestAppSettings newSettingsForAppNode() throws SocketException {
     return new TestAppSettings()
       .set(CLUSTER_ENABLED, "true")
       .set(CLUSTER_NODE_TYPE, "application")
-      .set(CLUSTER_SEARCH_HOSTS, "localhost")
-      .set(CLUSTER_HOSTS, "192.168.233.1, 192.168.233.2,192.168.233.3")
+      .set(CLUSTER_NODE_HOST, nonLoopbackLocal.getHostAddress())
+      .set(CLUSTER_HOSTS, nonLoopbackLocal.getHostAddress())
+      .set(CLUSTER_SEARCH_HOSTS, nonLoopbackLocal.getHostAddress())
       .set("sonar.auth.jwtBase64Hs256Secret", "abcde")
       .set(JDBC_URL, "jdbc:mysql://localhost:3306/sonar");
   }
 
-  private static TestAppSettings newSettingsForSearchNode() {
+  private TestAppSettings newSettingsForSearchNode() throws SocketException {
     return new TestAppSettings()
       .set(CLUSTER_ENABLED, "true")
       .set(CLUSTER_NODE_TYPE, "search")
-      .set(CLUSTER_SEARCH_HOSTS, "192.168.233.1")
-      .set(CLUSTER_HOSTS, "192.168.233.1, 192.168.233.2,192.168.233.3")
-      .set(SEARCH_HOST, getNonLoopbackIpv4Address().getHostName());
-  }
-
-  private static InetAddress getNonLoopbackIpv4Address() {
-    try {
-      Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
-      for (NetworkInterface networkInterface : Collections.list(nets)) {
-        if (!networkInterface.isLoopback() && networkInterface.isUp()) {
-          Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();
-          while (inetAddresses.hasMoreElements()) {
-            InetAddress inetAddress = inetAddresses.nextElement();
-            if (inetAddress instanceof Inet4Address) {
-              return inetAddress;
-            }
-          }
-        }
-      }
-    } catch (SocketException se) {
-      throw new RuntimeException("Cannot find a non loopback card required for tests", se);
-    }
-    throw new RuntimeException("Cannot find a non loopback card required for tests");
+      .set(CLUSTER_NODE_HOST, nonLoopbackLocal.getHostAddress())
+      .set(CLUSTER_HOSTS, nonLoopbackLocal.getHostAddress())
+      .set(CLUSTER_SEARCH_HOSTS, nonLoopbackLocal.getHostAddress())
+      .set(SEARCH_HOST, nonLoopbackLocal.getHostAddress());
   }
 }
index 9e823170c78ade945623265f1c3a0a5e4b4486da..b6aad669aecfba7663b0b0b0ed7f3d0c4c720ea2 100644 (file)
@@ -31,7 +31,6 @@ import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.junit.rules.TemporaryFolder;
 import org.sonar.application.logging.ListAppender;
-import org.sonar.process.cluster.ClusterProperties;
 import org.sonar.process.ProcessProperties;
 import org.sonar.process.Props;
 import org.sonar.process.System2;
@@ -40,8 +39,8 @@ import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NAME;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_SEARCH_HOSTS;
+import static org.sonar.process.ProcessProperties.CLUSTER_NAME;
+import static org.sonar.process.ProcessProperties.CLUSTER_SEARCH_HOSTS;
 
 public class EsSettingsTest {
 
@@ -150,9 +149,9 @@ public class EsSettingsTest {
     props.set(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath());
     props.set(ProcessProperties.PATH_TEMP, temp.newFolder().getAbsolutePath());
     props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath());
-    props.set(ClusterProperties.CLUSTER_NAME, "sonarqube-1");
-    props.set(ClusterProperties.CLUSTER_ENABLED, "true");
-    props.set(ClusterProperties.CLUSTER_NODE_NAME, "node-1");
+    props.set(ProcessProperties.CLUSTER_NAME, "sonarqube-1");
+    props.set(ProcessProperties.CLUSTER_ENABLED, "true");
+    props.set(ProcessProperties.CLUSTER_NODE_NAME, "node-1");
 
     EsSettings esSettings = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE);
 
@@ -165,8 +164,8 @@ public class EsSettingsTest {
   public void test_node_name_default_for_cluster_mode() throws Exception {
     File homeDir = temp.newFolder();
     Props props = new Props(new Properties());
-    props.set(ClusterProperties.CLUSTER_NAME, "sonarqube");
-    props.set(ClusterProperties.CLUSTER_ENABLED, "true");
+    props.set(ProcessProperties.CLUSTER_NAME, "sonarqube");
+    props.set(ProcessProperties.CLUSTER_ENABLED, "true");
     props.set(ProcessProperties.SEARCH_PORT, "1234");
     props.set(ProcessProperties.SEARCH_HOST, "127.0.0.1");
     props.set(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath());
@@ -181,8 +180,8 @@ public class EsSettingsTest {
   public void test_node_name_default_for_standalone_mode() throws Exception {
     File homeDir = temp.newFolder();
     Props props = new Props(new Properties());
-    props.set(ClusterProperties.CLUSTER_NAME, "sonarqube");
-    props.set(ClusterProperties.CLUSTER_ENABLED, "false");
+    props.set(ProcessProperties.CLUSTER_NAME, "sonarqube");
+    props.set(ProcessProperties.CLUSTER_ENABLED, "false");
     props.set(ProcessProperties.SEARCH_PORT, "1234");
     props.set(ProcessProperties.SEARCH_HOST, "127.0.0.1");
     props.set(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath());
@@ -296,7 +295,7 @@ public class EsSettingsTest {
     Props props = new Props(new Properties());
     ProcessProperties.completeDefaults(props);
     props.set(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath());
-    props.set(ClusterProperties.CLUSTER_ENABLED, Boolean.toString(cluster));
+    props.set(ProcessProperties.CLUSTER_ENABLED, Boolean.toString(cluster));
     return props;
   }
 }
index 232f828648ec712872974eb56ebc3dbb369d5978..58adf12de382700eda8d7322ff270254e742bc41 100644 (file)
   </properties>
 
   <dependencies>
+    <dependency>
+      <groupId>com.hazelcast</groupId>
+      <artifactId>hazelcast</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-api</artifactId>
index 950266c05e69667a225615939b012efbee720753..7d4fc4d51f33e40599bba8606da5f67d5835a5ee 100644 (file)
  */
 package org.sonar.process;
 
+import java.net.Inet4Address;
 import java.net.InetAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.Optional;
+import java.util.function.Predicate;
 
 public interface NetworkUtils {
   NetworkUtils INSTANCE = new NetworkUtilsImpl();
@@ -40,4 +45,32 @@ public interface NetworkUtils {
    * @return "ipv4_1, ipv4_2"
    */
   String getIPAddresses();
+
+  /**
+   * Converts a text representation of an IP address or host name to
+   * a {@link InetAddress}.
+   * If text value references an IPv4 or IPv6 address, then DNS is
+   * not used.
+   */
+  InetAddress toInetAddress(String hostOrAddress) throws UnknownHostException;
+
+  boolean isLocalInetAddress(InetAddress address) throws SocketException;
+
+  boolean isLoopbackInetAddress(InetAddress address);
+
+  /**
+   * Returns the machine {@link InetAddress} that matches the specified
+   * predicate. If multiple addresses match then a single one
+   * is picked in a non deterministic way.
+   */
+  Optional<InetAddress> getLocalInetAddress(Predicate<InetAddress> predicate);
+
+  /**
+   * Returns a local {@link InetAddress} that is IPv4 and not
+   * loopback. If multiple addresses match then a single one
+   * is picked in a non deterministic way.
+   */
+  default Optional<InetAddress> getLocalNonLoopbackIpv4Address() {
+    return getLocalInetAddress(a -> !a.isLoopbackAddress() && a instanceof Inet4Address);
+  }
 }
index 9dde01f7bd9ea1ba437b48413f66bf5c30678836..9da928a7c9dc2ddae0d8fd53a72ed205ea7ac565 100644 (file)
@@ -20,6 +20,7 @@
 package org.sonar.process;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.net.InetAddresses;
 import java.io.IOException;
 import java.net.Inet4Address;
 import java.net.InetAddress;
@@ -27,17 +28,20 @@ import java.net.NetworkInterface;
 import java.net.ServerSocket;
 import java.net.SocketException;
 import java.net.UnknownHostException;
+import java.util.Collections;
 import java.util.HashSet;
+import java.util.Optional;
 import java.util.Set;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 import static java.util.Collections.list;
 import static org.apache.commons.lang.StringUtils.isBlank;
 
-public final class NetworkUtilsImpl implements NetworkUtils {
+public class NetworkUtilsImpl implements NetworkUtils {
 
-  private static final Set<Integer> ALREADY_ALLOCATED = new HashSet<>();
-  private static final int MAX_TRIES = 50;
+  private static final Set<Integer> PORTS_ALREADY_ALLOCATED = new HashSet<>();
+  private static final int PORT_MAX_TRIES = 50;
 
   NetworkUtilsImpl() {
     // prevent instantiation
@@ -55,10 +59,10 @@ public final class NetworkUtilsImpl implements NetworkUtils {
    */
   @VisibleForTesting
   static int getNextAvailablePort(InetAddress address, PortAllocator portAllocator) {
-    for (int i = 0; i < MAX_TRIES; i++) {
+    for (int i = 0; i < PORT_MAX_TRIES; i++) {
       int port = portAllocator.getAvailable(address);
       if (isValidPort(port)) {
-        ALREADY_ALLOCATED.add(port);
+        PORTS_ALREADY_ALLOCATED.add(port);
         return port;
       }
     }
@@ -66,7 +70,7 @@ public final class NetworkUtilsImpl implements NetworkUtils {
   }
 
   private static boolean isValidPort(int port) {
-    return port > 1023 && !ALREADY_ALLOCATED.contains(port);
+    return port > 1023 && !PORTS_ALREADY_ALLOCATED.contains(port);
   }
 
   static class PortAllocator {
@@ -118,4 +122,35 @@ public final class NetworkUtilsImpl implements NetworkUtils {
 
     return ips;
   }
+
+  @Override
+  public InetAddress toInetAddress(String hostOrAddress) throws UnknownHostException {
+    if (InetAddresses.isInetAddress(hostOrAddress)) {
+      return InetAddresses.forString(hostOrAddress);
+    }
+    return InetAddress.getByName(hostOrAddress);
+  }
+
+  @Override
+  public boolean isLocalInetAddress(InetAddress address) throws SocketException {
+    return NetworkInterface.getByInetAddress(address) != null ;
+  }
+
+  @Override
+  public boolean isLoopbackInetAddress(InetAddress address) {
+    return address.isLoopbackAddress();
+  }
+
+  @Override
+  public Optional<InetAddress> getLocalInetAddress(Predicate<InetAddress> predicate) {
+    try {
+      return Collections.list(NetworkInterface.getNetworkInterfaces()).stream()
+        .flatMap(ni -> Collections.list(ni.getInetAddresses()).stream())
+        .filter(a -> a.getHostAddress() != null)
+        .filter(predicate)
+        .findFirst();
+    } catch (SocketException e) {
+      throw new IllegalStateException("Can not retrieve network interfaces", e);
+    }
+  }
 }
index 23dd460b3d6ee24d68038f7afb57aa076fb6b3a0..5d90670108ab86ad03fd0263ceac25bc1933c3ed 100644 (file)
@@ -23,8 +23,7 @@ import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.util.Map;
 import java.util.Properties;
-
-import static org.sonar.process.cluster.ClusterProperties.putClusterDefaults;
+import java.util.UUID;
 
 /**
  * Constants shared by search, web server and app processes.
@@ -76,6 +75,17 @@ public class ProcessProperties {
   public static final String HTTP_PROXY_USER = "http.proxyUser";
   public static final String HTTP_PROXY_PASSWORD = "http.proxyPassword";
 
+  public static final String CLUSTER_ENABLED = "sonar.cluster.enabled";
+  public static final String CLUSTER_NODE_TYPE = "sonar.cluster.node.type";
+  public static final String CLUSTER_SEARCH_HOSTS = "sonar.cluster.search.hosts";
+  public static final String CLUSTER_HOSTS = "sonar.cluster.hosts";
+  public static final String CLUSTER_NODE_PORT = "sonar.cluster.node.port";
+  public static final int CLUSTER_NODE_PORT_DEFAULT_VALUE = 9003;
+  public static final String CLUSTER_NODE_HOST = "sonar.cluster.node.host";
+  public static final String CLUSTER_NODE_NAME = "sonar.cluster.node.name";
+  public static final String CLUSTER_NAME = "sonar.cluster.name";
+  public static final String CLUSTER_WEB_STARTUP_LEADER = "sonar.cluster.web.startupLeader";
+
   private ProcessProperties() {
     // only static stuff
   }
@@ -89,18 +99,6 @@ public class ProcessProperties {
     fixPortIfZero(props, SEARCH_HOST, SEARCH_PORT);
   }
 
-  private static void fixPortIfZero(Props props, String addressPropertyKey, String portPropertyKey) {
-    String port = props.value(portPropertyKey);
-    if ("0".equals(port)) {
-      String address = props.nonNullValue(addressPropertyKey);
-      try {
-        props.set(portPropertyKey, String.valueOf(NetworkUtils.INSTANCE.getNextAvailablePort(InetAddress.getByName(address))));
-      } catch (UnknownHostException e) {
-        throw new IllegalStateException("Cannot resolve address [" + address + "] set by property [" + addressPropertyKey + "]", e);
-      }
-    }
-  }
-
   public static Properties defaults() {
     Properties defaults = new Properties();
     defaults.put(SEARCH_HOST, InetAddress.getLoopbackAddress().getHostAddress());
@@ -124,8 +122,23 @@ public class ProcessProperties {
     defaults.put(JDBC_MIN_EVICTABLE_IDLE_TIME_MILLIS, "600000");
     defaults.put(JDBC_TIME_BETWEEN_EVICTION_RUNS_MILLIS, "30000");
 
-    putClusterDefaults(defaults);
+    defaults.put(CLUSTER_ENABLED, "false");
+    defaults.put(CLUSTER_NAME, "sonarqube");
+    defaults.put(CLUSTER_NODE_PORT, Integer.toString(CLUSTER_NODE_PORT_DEFAULT_VALUE));
+    defaults.put(CLUSTER_NODE_NAME, "sonarqube-" + UUID.randomUUID().toString());
 
     return defaults;
   }
+
+  private static void fixPortIfZero(Props props, String addressPropertyKey, String portPropertyKey) {
+    String port = props.value(portPropertyKey);
+    if ("0".equals(port)) {
+      String address = props.nonNullValue(addressPropertyKey);
+      try {
+        props.set(portPropertyKey, String.valueOf(NetworkUtils.INSTANCE.getNextAvailablePort(InetAddress.getByName(address))));
+      } catch (UnknownHostException e) {
+        throw new IllegalStateException("Cannot resolve address [" + address + "] set by property [" + addressPropertyKey + "]", e);
+      }
+    }
+  }
 }
diff --git a/server/sonar-process/src/main/java/org/sonar/process/cluster/ClusterObjectKeys.java b/server/sonar-process/src/main/java/org/sonar/process/cluster/ClusterObjectKeys.java
deleted file mode 100644 (file)
index 0f9104e..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * 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.cluster;
-
-/**
- * This class holds all object keys accessible via Hazelcast
- */
-public final class ClusterObjectKeys {
-
-  private ClusterObjectKeys() {
-    // Holder for clustered objects
-  }
-
-  /**
-   * The key of replicated map that hold all operational processes
-   */
-  public static final String OPERATIONAL_PROCESSES = "OPERATIONAL_PROCESSES";
-  /**
-   * The key of atomic reference holding the leader UUID
-   */
-  public static final String LEADER = "LEADER";
-  /**
-   * The key of the hostname attribute of a member
-   */
-  public static final String HOSTNAME = "HOSTNAME";
-  /**
-   * The key of the ips list attribute of a member
-   */
-  public static final String IP_ADDRESSES = "IP_ADDRESSES";
-  /**
-   * The key of the node name attribute of a member
-   */
-  public static final String NODE_NAME = "NODE_NAME";
-  /**
-   * The role of the sonar-application inside the SonarQube cluster
-   * {@link NodeType}
-   */
-  public static final String NODE_TYPE = "NODE_TYPE";
-  /**
-   * The key of atomic reference holding the SonarQube version of the cluster
-   */
-  public static final String SONARQUBE_VERSION = "SONARQUBE_VERSION";
-  /**
-   * The key of atomic reference holding the name of the cluster (used for precondition checks)
-   */
-  public static final String CLUSTER_NAME = "CLUSTER_NAME";
-  /**
-   * The key of the Set holding the UUIDs of clients
-   */
-  public static final String LOCAL_MEMBER_UUIDS = "LOCAL_MEMBER_UUIDS";
-  /**
-   * The key of replicated map holding the CeWorker UUIDs
-   */
-  public static final String WORKER_UUIDS = "WORKER_UUIDS";
-  /**
-   * The key of the lock for executing CE_CLEANING_JOB
-   * {@link CeCleaningSchedulerImpl}
-   */
-  public static final String CE_CLEANING_JOB_LOCK = "CE_CLEANING_JOB_LOCK";
-  /**
-   * THe key of the replicated map holding the health state information of all SQ nodes.
-   */
-  public static final String SQ_HEALTH_STATE = "sq_health_state";
-}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/cluster/ClusterProperties.java b/server/sonar-process/src/main/java/org/sonar/process/cluster/ClusterProperties.java
deleted file mode 100644 (file)
index 36d3fdb..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * 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.cluster;
-
-import java.util.Properties;
-import java.util.UUID;
-
-import static java.lang.String.valueOf;
-
-public final class ClusterProperties {
-  public static final String CLUSTER_ENABLED = "sonar.cluster.enabled";
-  public static final String CLUSTER_NODE_TYPE = "sonar.cluster.node.type";
-  public static final String CLUSTER_SEARCH_HOSTS = "sonar.cluster.search.hosts";
-  public static final String CLUSTER_HOSTS = "sonar.cluster.hosts";
-  public static final String CLUSTER_NODE_PORT = "sonar.cluster.node.port";
-  public static final String CLUSTER_NODE_HOST = "sonar.cluster.node.host";
-  public static final String CLUSTER_NODE_NAME = "sonar.cluster.node.name";
-  public static final String CLUSTER_NAME = "sonar.cluster.name";
-  public static final String HAZELCAST_LOG_LEVEL = "sonar.log.level.app.hazelcast";
-  public static final String CLUSTER_WEB_LEADER = "sonar.cluster.web.startupLeader";
-  // Internal property used by sonar-application to share the local endpoint of Hazelcast
-  public static final String CLUSTER_LOCALENDPOINT = "sonar.cluster.hazelcast.localEndPoint";
-  // Internal property used by sonar-application to share the local UUID of the Hazelcast member
-  public static final String CLUSTER_MEMBERUUID = "sonar.cluster.hazelcast.memberUUID";
-
-  private ClusterProperties() {
-    // prevents instantiation
-  }
-
-  public static void putClusterDefaults(Properties properties) {
-    properties.put(CLUSTER_ENABLED, valueOf(false));
-    properties.put(CLUSTER_NAME, "sonarqube");
-    properties.put(CLUSTER_NODE_HOST, "");
-    properties.put(CLUSTER_HOSTS, "");
-    properties.put(CLUSTER_NODE_PORT, "9003");
-    properties.put(CLUSTER_NODE_NAME, "sonarqube-" + UUID.randomUUID().toString());
-    properties.put(HAZELCAST_LOG_LEVEL, "WARN");
-  }
-}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/cluster/HazelcastClient.java b/server/sonar-process/src/main/java/org/sonar/process/cluster/HazelcastClient.java
deleted file mode 100644 (file)
index d71f08e..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * 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.cluster;
-
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.locks.Lock;
-
-/**
- * The interface Hazelcast client wrapper.
- */
-public interface HazelcastClient {
-  /**
-   * Gets the set shared by the cluster and identified by name
-   */
-  <E> Set<E> getSet(String name);
-
-  /**
-   * Gets the list shared by the cluster and identified by name
-   */
-  <E> List<E> getList(String name);
-
-  /**
-   * Gets the map shared by the cluster and identified by name
-   */
-  <K, V> Map<K, V> getMap(String name);
-
-  /**
-   * Gets the replicated map shared by the cluster and identified by name
-   */
-  <K,V> Map<K,V> getReplicatedMap(String name);
-
-  /**
-   * The UUID of the Hazelcast client.
-   *
-   * <p>The uuid of the member of the current client is a member, otherwise the UUID of the client if the
-   * member is a local client of one of the members.</p>
-   */
-  String getUUID();
-
-  /**
-   * The UUIDs of all the members (both members and local clients of these members) currently connected to the
-   * Hazelcast cluster.
-   */
-  Set<String> getMemberUuids();
-
-  /**
-   * Gets lock among the cluster, identified by name
-   */
-  Lock getLock(String name);
-
-  /**
-   * Retrieves the cluster time which is (alsmost) identical on all members of the cluster.
-   */
-  long getClusterTime();
-}
index 5b1f8a363e70d8640bb24fa0c05bb7495bf9136f..5dca5aed9d450ac035fad40588c0101598de30d4 100644 (file)
@@ -20,7 +20,6 @@
 package org.sonar.process.cluster;
 
 import static java.util.Arrays.stream;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_TYPE;
 
 public enum NodeType {
   APPLICATION("application"), SEARCH("search");
@@ -39,7 +38,7 @@ public enum NodeType {
     return stream(values())
       .filter(t -> nodeType.equals(t.value))
       .findFirst()
-      .orElseThrow(() -> new IllegalArgumentException("Invalid value for [" + CLUSTER_NODE_TYPE + "]: [" + nodeType + "]"));
+      .orElseThrow(() -> new IllegalArgumentException("Invalid value: " + nodeType));
   }
 
   public static boolean isValid(String nodeType) {
diff --git a/server/sonar-process/src/main/java/org/sonar/process/cluster/health/DelegateHealthStateRefresherExecutorService.java b/server/sonar-process/src/main/java/org/sonar/process/cluster/health/DelegateHealthStateRefresherExecutorService.java
deleted file mode 100644 (file)
index c88e3fe..0000000
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * 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.cluster.health;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-class DelegateHealthStateRefresherExecutorService implements HealthStateRefresherExecutorService {
-  private final ScheduledExecutorService delegate;
-
-  DelegateHealthStateRefresherExecutorService(ScheduledExecutorService delegate) {
-    this.delegate = delegate;
-  }
-
-  @Override
-  public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
-    return delegate.schedule(command, delay, unit);
-  }
-
-  @Override
-  public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
-    return delegate.schedule(callable, delay, unit);
-  }
-
-  @Override
-  public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
-    return delegate.scheduleAtFixedRate(command, initialDelay, period, unit);
-  }
-
-  @Override
-  public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) {
-    return delegate.scheduleWithFixedDelay(command, initialDelay, delay, unit);
-  }
-
-  @Override
-  public void shutdown() {
-    delegate.shutdown();
-  }
-
-  @Override
-  public List<Runnable> shutdownNow() {
-    return delegate.shutdownNow();
-  }
-
-  @Override
-  public boolean isShutdown() {
-    return delegate.isShutdown();
-  }
-
-  @Override
-  public boolean isTerminated() {
-    return delegate.isTerminated();
-  }
-
-  @Override
-  public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
-    return delegate.awaitTermination(timeout, unit);
-  }
-
-  @Override
-  public <T> Future<T> submit(Callable<T> task) {
-    return delegate.submit(task);
-  }
-
-  @Override
-  public <T> Future<T> submit(Runnable task, T result) {
-    return delegate.submit(task, result);
-  }
-
-  @Override
-  public Future<?> submit(Runnable task) {
-    return delegate.submit(task);
-  }
-
-  @Override
-  public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException {
-    return delegate.invokeAll(tasks);
-  }
-
-  @Override
-  public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException {
-    return delegate.invokeAll(tasks, timeout, unit);
-  }
-
-  @Override
-  public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException {
-    return delegate.invokeAny(tasks);
-  }
-
-  @Override
-  public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
-    return delegate.invokeAny(tasks, timeout, unit);
-  }
-
-  @Override
-  public void execute(Runnable command) {
-    delegate.execute(command);
-  }
-}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/cluster/health/HealthStateSharing.java b/server/sonar-process/src/main/java/org/sonar/process/cluster/health/HealthStateSharing.java
deleted file mode 100644 (file)
index ddc96ad..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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.cluster.health;
-
-public interface HealthStateSharing {
-  void start();
-
-  void stop();
-}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/cluster/health/HealthStateSharingImpl.java b/server/sonar-process/src/main/java/org/sonar/process/cluster/health/HealthStateSharingImpl.java
deleted file mode 100644 (file)
index a5945c6..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * 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.cluster.health;
-
-import com.google.common.util.concurrent.ThreadFactoryBuilder;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.sonar.process.cluster.HazelcastClient;
-
-import static java.lang.String.format;
-
-public class HealthStateSharingImpl implements HealthStateSharing {
-  private static final Logger LOG = LoggerFactory.getLogger(HealthStateSharingImpl.class);
-
-  private final HazelcastClient hazelcastClient;
-  private final NodeHealthProvider nodeHealthProvider;
-  private HealthStateRefresherExecutorService executorService;
-  private HealthStateRefresher healthStateRefresher;
-
-  public HealthStateSharingImpl(HazelcastClient hazelcastClient, NodeHealthProvider nodeHealthProvider) {
-    this.hazelcastClient = hazelcastClient;
-    this.nodeHealthProvider = nodeHealthProvider;
-  }
-
-  @Override
-  public void start() {
-    executorService = new DelegateHealthStateRefresherExecutorService(
-      Executors.newSingleThreadScheduledExecutor(
-        new ThreadFactoryBuilder()
-          .setDaemon(false)
-          .setNameFormat("health_state_refresh-%d")
-          .build()));
-    healthStateRefresher = new HealthStateRefresher(executorService, nodeHealthProvider, new SharedHealthStateImpl(hazelcastClient));
-    healthStateRefresher.start();
-  }
-
-  @Override
-  public void stop() {
-    healthStateRefresher.stop();
-    stopExecutorService(executorService);
-  }
-
-  private static void stopExecutorService(ScheduledExecutorService executorService) {
-    // Disable new tasks from being submitted
-    executorService.shutdown();
-    try {
-      // Wait a while for existing tasks to terminate
-      if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
-        // Cancel currently executing tasks
-        executorService.shutdownNow();
-        // Wait a while for tasks to respond to being canceled
-        if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
-          LOG.warn("Pool {} did not terminate", HealthStateSharingImpl.class.getSimpleName());
-        }
-      }
-    } catch (InterruptedException ie) {
-      LOG.warn(format("Termination of pool %s failed", HealthStateSharingImpl.class.getSimpleName()), ie);
-      // (Re-)Cancel if current thread also interrupted
-      executorService.shutdownNow();
-      Thread.currentThread().interrupt();
-    }
-  }
-
-}
index 979a915ca6e8898c08a1484c1432b9555be99aaf..c60d1902b2b4d7af8da161f7cdc5a341072d7878 100644 (file)
@@ -21,7 +21,7 @@ package org.sonar.process.cluster.health;
 
 public interface NodeHealthProvider {
   /**
-   * Returns the {@link NodeHealth} for the current SonarQube instance.
+   * Returns the {@link NodeHealth} for the current SonarQube node.
    *
    * <p>Implementation must support being called very frequently and from concurrent threads</p>
    */
index 243b2d63eaf0e182fffec873e028ef58ae681e9d..632f35821b2fe8ffca7acca06e7a623b4e15b999 100644 (file)
@@ -27,8 +27,8 @@ import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.sonar.process.cluster.ClusterObjectKeys;
-import org.sonar.process.cluster.HazelcastClient;
+import org.sonar.process.cluster.hz.HazelcastMember;
+import org.sonar.process.cluster.hz.HazelcastObjects;
 
 import static java.util.Objects.requireNonNull;
 
@@ -36,10 +36,10 @@ public class SharedHealthStateImpl implements SharedHealthState {
   private static final Logger LOG = LoggerFactory.getLogger(SharedHealthStateImpl.class);
   private static final int TIMEOUT_30_SECONDS = 30 * 1000;
 
-  private final HazelcastClient hazelcastClient;
+  private final HazelcastMember hzMember;
 
-  public SharedHealthStateImpl(HazelcastClient hazelcastClient) {
-    this.hazelcastClient = hazelcastClient;
+  public SharedHealthStateImpl(HazelcastMember hzMember) {
+    this.hzMember = hzMember;
   }
 
   @Override
@@ -50,13 +50,13 @@ public class SharedHealthStateImpl implements SharedHealthState {
     if (LOG.isTraceEnabled()) {
       LOG.trace("Reading {} and adding {}", new HashMap<>(sqHealthState), nodeHealth);
     }
-    sqHealthState.put(hazelcastClient.getUUID(), new TimestampedNodeHealth(nodeHealth, hazelcastClient.getClusterTime()));
+    sqHealthState.put(hzMember.getUuid(), new TimestampedNodeHealth(nodeHealth, hzMember.getClusterTime()));
   }
 
   @Override
   public void clearMine() {
     Map<String, TimestampedNodeHealth> sqHealthState = readReplicatedMap();
-    String clientUUID = hazelcastClient.getUUID();
+    String clientUUID = hzMember.getUuid();
     if (LOG.isTraceEnabled()) {
       LOG.trace("Reading {} and clearing for {}", new HashMap<>(sqHealthState), clientUUID);
     }
@@ -65,10 +65,10 @@ public class SharedHealthStateImpl implements SharedHealthState {
 
   @Override
   public Set<NodeHealth> readAll() {
-    long clusterTime = hazelcastClient.getClusterTime();
+    long clusterTime = hzMember.getClusterTime();
     long timeout = clusterTime - TIMEOUT_30_SECONDS;
     Map<String, TimestampedNodeHealth> sqHealthState = readReplicatedMap();
-    Set<String> hzMemberUUIDs = hazelcastClient.getMemberUuids();
+    Set<String> hzMemberUUIDs = hzMember.getMemberUuids();
     Set<NodeHealth> existingNodeHealths = sqHealthState.entrySet().stream()
       .filter(outOfDate(timeout))
       .filter(ofNonExistentMember(hzMemberUUIDs))
@@ -101,7 +101,7 @@ public class SharedHealthStateImpl implements SharedHealthState {
   }
 
   private Map<String, TimestampedNodeHealth> readReplicatedMap() {
-    return hazelcastClient.getReplicatedMap(ClusterObjectKeys.SQ_HEALTH_STATE);
+    return hzMember.getReplicatedMap(HazelcastObjects.SQ_HEALTH_STATE);
   }
 
 }
diff --git a/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMember.java b/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMember.java
new file mode 100644 (file)
index 0000000..7521fc9
--- /dev/null
@@ -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.process.cluster.hz;
+
+import com.hazelcast.core.Cluster;
+import com.hazelcast.core.IAtomicReference;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.locks.Lock;
+import org.sonar.process.ProcessId;
+import org.sonar.process.cluster.NodeType;
+
+public interface HazelcastMember extends AutoCloseable {
+
+  interface Attribute {
+    /**
+     * The key of the hostname attribute of a member
+     */
+    String HOSTNAME = "HOSTNAME";
+    /**
+     * The key of the ips list attribute of a member
+     */
+    String IP_ADDRESSES = "IP_ADDRESSES";
+    /**
+     * The key of the node name attribute of a member
+     */
+    String NODE_NAME = "NODE_NAME";
+    /**
+     * The role of the sonar-application inside the SonarQube cluster
+     * {@link NodeType}
+     */
+    String NODE_TYPE = "NODE_TYPE";
+    /**
+     * Key of process as defined by {@link ProcessId#getKey()}
+     */
+    String PROCESS_KEY = "PROCESS_KEY";
+  }
+
+  <E> IAtomicReference<E> getAtomicReference(String name);
+
+  /**
+   * Gets the set shared by the cluster and identified by name
+   */
+  <E> Set<E> getSet(String name);
+
+  /**
+   * Gets the list shared by the cluster and identified by name
+   */
+  <E> List<E> getList(String name);
+
+  /**
+   * Gets the map shared by the cluster and identified by name
+   */
+  <K, V> Map<K, V> getMap(String name);
+
+  /**
+   * Gets the replicated map shared by the cluster and identified by name.
+   * Result can be casted to {@link com.hazelcast.core.ReplicatedMap} if needed to
+   * benefit from listeners.
+   */
+  <K, V> Map<K, V> getReplicatedMap(String name);
+
+  String getUuid();
+
+  /**
+   * The UUIDs of all the members (both members and local clients of these members) currently connected to the
+   * Hazelcast cluster.
+   */
+  Set<String> getMemberUuids();
+
+  /**
+   * Gets lock among the cluster, identified by name
+   */
+  Lock getLock(String name);
+
+  /**
+   * Retrieves the cluster time which is (almost) identical on all members of the cluster.
+   */
+  long getClusterTime();
+
+  Cluster getCluster();
+
+  @Override
+  void close();
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberBuilder.java b/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberBuilder.java
new file mode 100644 (file)
index 0000000..806b0d7
--- /dev/null
@@ -0,0 +1,142 @@
+/*
+ * 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.cluster.hz;
+
+import com.hazelcast.config.Config;
+import com.hazelcast.config.JoinConfig;
+import com.hazelcast.config.MemberAttributeConfig;
+import com.hazelcast.config.NetworkConfig;
+import com.hazelcast.core.Hazelcast;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.annotation.CheckForNull;
+import org.sonar.process.NetworkUtils;
+import org.sonar.process.ProcessId;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.cluster.NodeType;
+import org.sonar.process.cluster.hz.HazelcastMember.Attribute;
+
+import static java.lang.String.format;
+import static java.util.Collections.singletonList;
+import static java.util.Objects.requireNonNull;
+
+public class HazelcastMemberBuilder {
+
+  private String clusterName;
+  private String nodeName;
+  private int port;
+  private NodeType nodeType;
+  private ProcessId processId;
+  private String networkInterface;
+  private List<String> members = new ArrayList<>();
+
+  public HazelcastMemberBuilder setClusterName(String s) {
+    this.clusterName = s;
+    return this;
+  }
+
+  public HazelcastMemberBuilder setNodeName(String s) {
+    this.nodeName = s;
+    return this;
+  }
+
+  public HazelcastMemberBuilder setNodeType(NodeType t) {
+    this.nodeType = t;
+    return this;
+  }
+
+  public HazelcastMemberBuilder setProcessId(ProcessId p) {
+    this.processId = p;
+    return this;
+  }
+
+  public HazelcastMemberBuilder setPort(int i) {
+    this.port = i;
+    return this;
+  }
+
+  public HazelcastMemberBuilder setNetworkInterface(String s) {
+    this.networkInterface = s;
+    return this;
+  }
+
+  @CheckForNull
+  List<String> getMembers() {
+    return members;
+  }
+
+  /**
+   * Adds references to cluster members. If port is missing, then default
+   * port is automatically added.
+   */
+  public HazelcastMemberBuilder setMembers(Collection<String> c) {
+    this.members = c.stream()
+      .map(host -> host.contains(":") ? host : format("%s:%d", host, ProcessProperties.CLUSTER_NODE_PORT_DEFAULT_VALUE))
+      .collect(Collectors.toList());
+    return this;
+  }
+
+  public HazelcastMember build() {
+    Config config = new Config();
+    config.getGroupConfig().setName(requireNonNull(clusterName, "Cluster name is missing"));
+
+    // Configure network
+    NetworkConfig netConfig = config.getNetworkConfig();
+    netConfig
+      .setPort(port)
+      .setPortAutoIncrement(false)
+      .setReuseAddress(true);
+    netConfig.getInterfaces()
+      .setEnabled(true)
+      .setInterfaces(singletonList(requireNonNull(networkInterface, "Network interface is missing")));
+
+    // Only allowing TCP/IP configuration
+    JoinConfig joinConfig = netConfig.getJoin();
+    joinConfig.getAwsConfig().setEnabled(false);
+    joinConfig.getMulticastConfig().setEnabled(false);
+    joinConfig.getTcpIpConfig().setEnabled(true);
+    joinConfig.getTcpIpConfig().setMembers(requireNonNull(members, "Members are missing"));
+    // We are not using the partition group of Hazelcast, so disabling it
+    config.getPartitionGroupConfig().setEnabled(false);
+
+    // Tweak HazelCast configuration
+    config
+      // 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");
+
+    MemberAttributeConfig attributes = config.getMemberAttributeConfig();
+    attributes.setStringAttribute(Attribute.HOSTNAME, NetworkUtils.INSTANCE.getHostname());
+    attributes.setStringAttribute(Attribute.IP_ADDRESSES, NetworkUtils.INSTANCE.getIPAddresses());
+    attributes.setStringAttribute(Attribute.NODE_NAME, requireNonNull(nodeName, "Node name is missing"));
+    attributes.setStringAttribute(Attribute.NODE_TYPE, requireNonNull(nodeType, "Node type is missing").getValue());
+    attributes.setStringAttribute(Attribute.PROCESS_KEY, requireNonNull(processId, "Process key is missing").getKey());
+
+    return new HazelcastMemberImpl(Hazelcast.newHazelcastInstance(config));
+  }
+
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberImpl.java b/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastMemberImpl.java
new file mode 100644 (file)
index 0000000..606c589
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ * 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.cluster.hz;
+
+import com.hazelcast.core.Cluster;
+import com.hazelcast.core.HazelcastInstance;
+import com.hazelcast.core.HazelcastInstanceNotActiveException;
+import com.hazelcast.core.IAtomicReference;
+import com.hazelcast.core.Member;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.locks.Lock;
+import java.util.stream.Collectors;
+import org.slf4j.LoggerFactory;
+
+class HazelcastMemberImpl implements HazelcastMember {
+
+  private final HazelcastInstance hzInstance;
+
+  HazelcastMemberImpl(HazelcastInstance hzInstance) {
+    this.hzInstance = hzInstance;
+  }
+
+  @Override
+  public <E> IAtomicReference<E> getAtomicReference(String name) {
+    return hzInstance.getAtomicReference(name);
+  }
+
+  @Override
+  public <E> Set<E> getSet(String s) {
+    return hzInstance.getSet(s);
+  }
+
+  @Override
+  public <E> List<E> getList(String s) {
+    return hzInstance.getList(s);
+  }
+
+  @Override
+  public <K, V> Map<K, V> getMap(String s) {
+    return hzInstance.getMap(s);
+  }
+
+  @Override
+  public <K, V> Map<K, V> getReplicatedMap(String s) {
+    return hzInstance.getReplicatedMap(s);
+  }
+
+  @Override
+  public String getUuid() {
+    return hzInstance.getLocalEndpoint().getUuid();
+  }
+
+  @Override
+  public Set<String> getMemberUuids() {
+    return hzInstance.getCluster().getMembers().stream().map(Member::getUuid).collect(Collectors.toSet());
+  }
+
+  @Override
+  public Lock getLock(String s) {
+    return hzInstance.getLock(s);
+  }
+
+  @Override
+  public long getClusterTime() {
+    return hzInstance.getCluster().getClusterTime();
+  }
+
+  @Override
+  public Cluster getCluster() {
+    return hzInstance.getCluster();
+  }
+
+  @Override
+  public void close() {
+    try {
+      hzInstance.shutdown();
+    } catch (HazelcastInstanceNotActiveException e) {
+      LoggerFactory.getLogger(getClass()).debug("Unable to shutdown Hazelcast member", e);
+    }
+  }
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastObjects.java b/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/HazelcastObjects.java
new file mode 100644 (file)
index 0000000..97e5952
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.process.cluster.hz;
+
+import org.sonar.process.ProcessId;
+import org.sonar.process.cluster.NodeType;
+
+/**
+ * This class holds all object keys accessible via Hazelcast
+ */
+public final class HazelcastObjects {
+
+  private HazelcastObjects() {
+    // Holder for clustered objects
+  }
+
+  /**
+   * The key of replicated map that hold all operational processes
+   */
+  public static final String OPERATIONAL_PROCESSES = "OPERATIONAL_PROCESSES";
+  /**
+   * The key of atomic reference holding the leader UUID
+   */
+  public static final String LEADER = "LEADER";
+  /**
+   * The key of atomic reference holding the SonarQube version of the cluster
+   */
+  public static final String SONARQUBE_VERSION = "SONARQUBE_VERSION";
+  /**
+   * The key of atomic reference holding the name of the cluster (used for precondition checks)
+   */
+  public static final String CLUSTER_NAME = "CLUSTER_NAME";
+  /**
+   * The key of replicated map holding the CeWorker UUIDs
+   */
+  public static final String WORKER_UUIDS = "WORKER_UUIDS";
+  /**
+   * The key of the lock for executing CE_CLEANING_JOB
+   * {@link CeCleaningSchedulerImpl}
+   */
+  public static final String CE_CLEANING_JOB_LOCK = "CE_CLEANING_JOB_LOCK";
+  /**
+   * THe key of the replicated map holding the health state information of all SQ nodes.
+   */
+  public static final String SQ_HEALTH_STATE = "sq_health_state";
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/package-info.java b/server/sonar-process/src/main/java/org/sonar/process/cluster/hz/package-info.java
new file mode 100644 (file)
index 0000000..7c6a7ee
--- /dev/null
@@ -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.cluster.hz;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-process/src/test/java/org/sonar/process/NetworkUtilsImplTest.java b/server/sonar-process/src/test/java/org/sonar/process/NetworkUtilsImplTest.java
new file mode 100644 (file)
index 0000000..2e5d8c9
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ * 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;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+import org.hamcrest.CoreMatchers;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assume.assumeThat;
+
+public class NetworkUtilsImplTest {
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private NetworkUtilsImpl underTest = new NetworkUtilsImpl();
+
+  @Test
+  public void getNextAvailablePort_returns_a_port() throws Exception {
+    int port = underTest.getNextAvailablePort(InetAddress.getLocalHost());
+    assertThat(port)
+      .isGreaterThan(1_023)
+      .isLessThanOrEqualTo(65_535);
+  }
+
+  @Test
+  public void getNextAvailablePort_does_not_return_twice_the_same_port() throws Exception {
+    Set<Integer> ports = new HashSet<>(Arrays.asList(
+      underTest.getNextAvailablePort(InetAddress.getLocalHost()),
+      underTest.getNextAvailablePort(InetAddress.getLocalHost()),
+      underTest.getNextAvailablePort(InetAddress.getLocalHost())));
+    assertThat(ports).hasSize(3);
+  }
+
+  @Test
+  public void getLocalNonLoopbackIpv4Address_returns_a_valid_local_and_non_loopback_ipv4() {
+    Optional<InetAddress> address = underTest.getLocalNonLoopbackIpv4Address();
+
+    // address is empty on offline builds
+    assumeThat(address.isPresent(), CoreMatchers.is(true));
+
+    assertThat(address.get()).isInstanceOf(Inet4Address.class);
+    assertThat(address.get().isLoopbackAddress()).isFalse();
+  }
+
+  @Test
+  public void getLocalInetAddress_filters_local_addresses() {
+    InetAddress address = underTest.getLocalInetAddress(InetAddress::isLoopbackAddress).get();
+    assertThat(address.isLoopbackAddress()).isTrue();
+  }
+
+  @Test
+  public void getLocalInetAddress_returns_empty_if_no_local_addresses_match() {
+    Optional<InetAddress> address = underTest.getLocalInetAddress(a -> false);
+    assertThat(address).isEmpty();
+  }
+
+  @Test
+  public void toInetAddress_supports_host_names() throws Exception {
+    assertThat(underTest.toInetAddress("localhost")).isNotNull();
+    // do not test values that require DNS calls. Build must support offline mode.
+  }
+
+  @Test
+  public void toInetAddress_supports_ipv4() throws Exception {
+    assertThat(underTest.toInetAddress("1.2.3.4")).isNotNull();
+  }
+
+  @Test
+  public void toInetAddress_supports_ipv6() throws Exception {
+    assertThat(underTest.toInetAddress("2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb")).isNotNull();
+    assertThat(underTest.toInetAddress("[2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb]")).isNotNull();
+  }
+
+}
diff --git a/server/sonar-process/src/test/java/org/sonar/process/cluster/health/DelegateHealthStateRefresherExecutorServiceTest.java b/server/sonar-process/src/test/java/org/sonar/process/cluster/health/DelegateHealthStateRefresherExecutorServiceTest.java
deleted file mode 100644 (file)
index 2acbf6b..0000000
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * 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.cluster.health;
-
-import java.util.Collection;
-import java.util.Random;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-import org.junit.Test;
-
-import static java.util.concurrent.TimeUnit.SECONDS;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-
-public class DelegateHealthStateRefresherExecutorServiceTest {
-  private Random random = new Random();
-  private Runnable runnable = mock(Runnable.class);
-  private Callable callable = mock(Callable.class);
-  private Collection<Callable<Object>> callables = IntStream.range(0, random.nextInt(5))
-    .mapToObj(i -> (Callable<Object>) mock(Callable.class))
-    .collect(Collectors.toList());
-  private int initialDelay = random.nextInt(333);
-  private int delay = random.nextInt(333);
-  private int period = random.nextInt(333);
-  private int timeout = random.nextInt(333);
-  private Object result = new Object();
-  private ScheduledExecutorService executorService = mock(ScheduledExecutorService.class);
-  private DelegateHealthStateRefresherExecutorService underTest = new DelegateHealthStateRefresherExecutorService(executorService);
-
-  @Test
-  public void schedule() {
-    underTest.schedule(runnable, delay, SECONDS);
-
-    verify(executorService).schedule(runnable, delay, SECONDS);
-  }
-
-  @Test
-  public void schedule1() {
-    underTest.schedule(callable, delay, SECONDS);
-
-    verify(executorService).schedule(callable, delay, SECONDS);
-  }
-
-  @Test
-  public void scheduleAtFixedRate() {
-    underTest.scheduleAtFixedRate(runnable, initialDelay, period, SECONDS);
-    verify(executorService).scheduleAtFixedRate(runnable, initialDelay, period, SECONDS);
-  }
-
-  @Test
-  public void scheduleWithFixeddelay() {
-    underTest.scheduleWithFixedDelay(runnable, initialDelay, delay, TimeUnit.SECONDS);
-    verify(executorService).scheduleWithFixedDelay(runnable, initialDelay, delay, TimeUnit.SECONDS);
-  }
-
-  @Test
-  public void shutdown() {
-    underTest.shutdown();
-    verify(executorService).shutdown();
-  }
-
-  @Test
-  public void shutdownNow() {
-    underTest.shutdownNow();
-    verify(executorService).shutdownNow();
-  }
-
-  @Test
-  public void isShutdown() {
-    underTest.isShutdown();
-    verify(executorService).isShutdown();
-  }
-
-  @Test
-  public void isTerminated() {
-    underTest.isTerminated();
-    verify(executorService).isTerminated();
-  }
-
-  @Test
-  public void awaitTermination() throws InterruptedException {
-    underTest.awaitTermination(timeout, TimeUnit.SECONDS);
-
-    verify(executorService).awaitTermination(timeout, TimeUnit.SECONDS);
-  }
-
-  @Test
-  public void submit() {
-    underTest.submit(callable);
-
-    verify(executorService).submit(callable);
-  }
-
-  @Test
-  public void submit1() {
-    underTest.submit(runnable, result);
-
-    verify(executorService).submit(runnable, result);
-  }
-
-  @Test
-  public void submit2() {
-    underTest.submit(runnable);
-    verify(executorService).submit(runnable);
-  }
-
-  @Test
-  public void invokeAll() throws InterruptedException {
-    underTest.invokeAll(callables);
-    verify(executorService).invokeAll(callables);
-  }
-
-  @Test
-  public void invokeAll1() throws InterruptedException {
-    underTest.invokeAll(callables, timeout, SECONDS);
-    verify(executorService).invokeAll(callables, timeout, SECONDS);
-  }
-
-  @Test
-  public void invokeAny() throws InterruptedException, ExecutionException {
-    underTest.invokeAny(callables);
-    verify(executorService).invokeAny(callables);
-  }
-
-  @Test
-  public void invokeAny2() throws InterruptedException, ExecutionException, TimeoutException {
-    underTest.invokeAny(callables, timeout, SECONDS);
-    verify(executorService).invokeAny(callables, timeout, SECONDS);
-  }
-
-}
diff --git a/server/sonar-process/src/test/java/org/sonar/process/cluster/hz/HazelcastMemberBuilderTest.java b/server/sonar-process/src/test/java/org/sonar/process/cluster/hz/HazelcastMemberBuilderTest.java
new file mode 100644 (file)
index 0000000..9316ad6
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * 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.cluster.hz;
+
+import java.net.InetAddress;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.DisableOnDebug;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+import org.sonar.process.NetworkUtils;
+import org.sonar.process.ProcessId;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.cluster.NodeType;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class HazelcastMemberBuilderTest {
+
+  @Rule
+  public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60));
+
+  // use loopback for support of offline builds
+  private InetAddress loopback = InetAddress.getLoopbackAddress();
+  private HazelcastMemberBuilder underTest = new HazelcastMemberBuilder();
+
+  @Test
+  public void build_member() {
+    HazelcastMember member = underTest
+      .setNodeType(NodeType.APPLICATION)
+      .setProcessId(ProcessId.COMPUTE_ENGINE)
+      .setClusterName("foo")
+      .setNodeName("bar")
+      .setPort(NetworkUtils.INSTANCE.getNextAvailablePort(loopback))
+      .setNetworkInterface(loopback.getHostAddress())
+      .build();
+
+    assertThat(member.getUuid()).isNotEmpty();
+    assertThat(member.getClusterTime()).isGreaterThan(0);
+    assertThat(member.getCluster().getMembers()).hasSize(1);
+    assertThat(member.getMemberUuids()).containsOnlyOnce(member.getUuid());
+
+    assertThat(member.getAtomicReference("baz")).isNotNull();
+    assertThat(member.getLock("baz")).isNotNull();
+    assertThat(member.getReplicatedMap("baz")).isNotNull();
+    assertThat(member.getList("baz")).isNotNull();
+    assertThat(member.getMap("baz")).isNotNull();
+    assertThat(member.getSet("baz")).isNotNull();
+
+    member.close();
+  }
+
+  @Test
+  public void default_port_is_added_when_missing() {
+    underTest.setMembers(asList("foo", "bar:9100", "1.2.3.4"));
+
+    assertThat(underTest.getMembers()).containsExactly(
+      "foo:" + ProcessProperties.CLUSTER_NODE_PORT_DEFAULT_VALUE,
+      "bar:9100",
+      "1.2.3.4:" + ProcessProperties.CLUSTER_NODE_PORT_DEFAULT_VALUE);
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/cluster/StartableHazelcastMember.java b/server/sonar-server/src/main/java/org/sonar/server/cluster/StartableHazelcastMember.java
new file mode 100644 (file)
index 0000000..f9347fd
--- /dev/null
@@ -0,0 +1,149 @@
+/*
+ * 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.server.cluster;
+
+import com.hazelcast.core.Cluster;
+import com.hazelcast.core.IAtomicReference;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.locks.Lock;
+import org.sonar.api.Startable;
+import org.sonar.api.config.Configuration;
+import org.sonar.process.NetworkUtils;
+import org.sonar.process.ProcessId;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.cluster.NodeType;
+import org.sonar.process.cluster.hz.HazelcastMember;
+import org.sonar.process.cluster.hz.HazelcastMemberBuilder;
+
+import static java.lang.String.format;
+import static java.util.Arrays.asList;
+import static java.util.Objects.requireNonNull;
+import static org.sonar.process.ProcessEntryPoint.PROPERTY_PROCESS_KEY;
+import static org.sonar.process.ProcessProperties.CLUSTER_HOSTS;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_HOST;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_TYPE;
+
+/**
+ * Implementation of {@link HazelcastMember} as used by Compute Engine and
+ * Web Server processes. It is configured by {@link Configuration}
+ * and its lifecycle is managed by picocontainer.
+ */
+public class StartableHazelcastMember implements HazelcastMember, Startable {
+
+  private final Configuration config;
+  private final NetworkUtils networkUtils;
+  private HazelcastMember member = null;
+
+  public StartableHazelcastMember(Configuration config, NetworkUtils networkUtils) {
+    this.config = config;
+    this.networkUtils = networkUtils;
+  }
+
+  @Override
+  public <E> IAtomicReference<E> getAtomicReference(String name) {
+    return nonNullMember().getAtomicReference(name);
+  }
+
+  @Override
+  public <E> Set<E> getSet(String name) {
+    return nonNullMember().getSet(name);
+  }
+
+  @Override
+  public <E> List<E> getList(String name) {
+    return nonNullMember().getList(name);
+  }
+
+  @Override
+  public <K, V> Map<K, V> getMap(String name) {
+    return nonNullMember().getMap(name);
+  }
+
+  @Override
+  public <K, V> Map<K, V> getReplicatedMap(String name) {
+    return nonNullMember().getReplicatedMap(name);
+  }
+
+  @Override
+  public String getUuid() {
+    return nonNullMember().getUuid();
+  }
+
+  @Override
+  public Set<String> getMemberUuids() {
+    return nonNullMember().getMemberUuids();
+  }
+
+  @Override
+  public Lock getLock(String name) {
+    return nonNullMember().getLock(name);
+  }
+
+  @Override
+  public long getClusterTime() {
+    return nonNullMember().getClusterTime();
+  }
+
+  @Override
+  public Cluster getCluster() {
+    return nonNullMember().getCluster();
+  }
+
+  private HazelcastMember nonNullMember() {
+    return requireNonNull(member, "Hazelcast member not started");
+  }
+
+  @Override
+  public void close() {
+    if (member != null) {
+      member.close();
+      member = null;
+    }
+  }
+
+  @Override
+  public void start() {
+    String networkAddress = config.get(CLUSTER_NODE_HOST).orElseThrow(() -> new IllegalStateException("Missing node host"));
+    int freePort;
+    try {
+      freePort = networkUtils.getNextAvailablePort(InetAddress.getByName(networkAddress));
+    } catch (UnknownHostException e) {
+      throw new IllegalStateException(format("Can not resolve address %s", networkAddress), e);
+    }
+    this.member = new HazelcastMemberBuilder()
+      .setClusterName(config.get(ProcessProperties.CLUSTER_NAME).orElseThrow(() -> new IllegalStateException("Missing cluster name")))
+      .setNodeName(config.get(ProcessProperties.CLUSTER_NODE_NAME).orElseThrow(() -> new IllegalStateException("Missing node name")))
+      .setNodeType(NodeType.parse(config.get(CLUSTER_NODE_TYPE).orElseThrow(() -> new IllegalStateException("Missing node type"))))
+      .setPort(freePort)
+      .setProcessId(ProcessId.fromKey(config.get(PROPERTY_PROCESS_KEY).orElseThrow(() -> new IllegalStateException("Missing process key"))))
+      .setMembers(asList(config.getStringArray(CLUSTER_HOSTS)))
+      .setNetworkInterface(networkAddress)
+      .build();
+  }
+
+  @Override
+  public void stop() {
+    close();
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/cluster/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/cluster/package-info.java
new file mode 100644 (file)
index 0000000..28ed7dc
--- /dev/null
@@ -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.server.cluster;
+
+import javax.annotation.ParametersAreNonnullByDefault;
index 2dbd02ebebabae6cee56ad5c9f24be91762ad47c..68ee4d08ec2be0f2391a117c172328323e054174 100644 (file)
@@ -46,10 +46,10 @@ import org.sonar.process.cluster.NodeType;
 import org.sonar.process.ProcessProperties;
 
 import static java.util.Collections.unmodifiableList;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_ENABLED;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NAME;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_TYPE;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_SEARCH_HOSTS;
+import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_NAME;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_TYPE;
+import static org.sonar.process.ProcessProperties.CLUSTER_SEARCH_HOSTS;
 import static org.sonar.process.cluster.NodeType.SEARCH;
 
 @ComputeEngineSide
index 7b925cc3578178849afcffcbf5deb0dfbfe9fda9..263fa0d9b4667c0f090cc59aa983e8dfe107e38a 100644 (file)
@@ -39,7 +39,7 @@ import static com.google.common.base.Preconditions.checkArgument;
 import static java.lang.String.format;
 import static java.lang.String.valueOf;
 import static java.util.Objects.requireNonNull;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
 import static org.sonar.server.es.DefaultIndexSettings.ANALYZER;
 import static org.sonar.server.es.DefaultIndexSettings.FIELDDATA_ENABLED;
 import static org.sonar.server.es.DefaultIndexSettings.FIELD_FIELDDATA;
index fda3e243fdd362a869ee9c9fd47aab8db807fd11..ee9c0433ddecdf8516935d4da8ed5259e2347c4b 100644 (file)
@@ -28,9 +28,9 @@ import org.sonar.process.cluster.health.NodeHealth;
 import org.sonar.process.cluster.health.NodeHealthProvider;
 
 import static java.lang.String.format;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_HOST;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_NAME;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_PORT;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_HOST;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_NAME;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_PORT;
 import static org.sonar.process.cluster.health.NodeDetails.newNodeDetailsBuilder;
 import static org.sonar.process.cluster.health.NodeHealth.newNodeHealthBuilder;
 
diff --git a/server/sonar-server/src/main/java/org/sonar/server/hz/HazelcastLocalClient.java b/server/sonar-server/src/main/java/org/sonar/server/hz/HazelcastLocalClient.java
deleted file mode 100644 (file)
index e393f95..0000000
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * 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.server.hz;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Preconditions;
-import com.google.common.collect.ImmutableSet;
-import com.hazelcast.client.config.ClientConfig;
-import com.hazelcast.core.HazelcastInstance;
-import com.hazelcast.core.Member;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.locks.Lock;
-import org.picocontainer.Startable;
-import org.sonar.api.config.Configuration;
-import org.sonar.process.cluster.ClusterObjectKeys;
-import org.sonar.process.cluster.HazelcastClient;
-
-import static org.apache.commons.lang.StringUtils.isNotEmpty;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_ENABLED;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_LOCALENDPOINT;
-
-/**
- * This class will connect as a Hazelcast client to the local instance of Hazelcluster
- */
-public class HazelcastLocalClient implements Startable, HazelcastClient {
-
-  private static final String HAZELCAST_CLUSTER_NAME = "sonarqube";
-  private final ClientConfig hzConfig;
-
-  @VisibleForTesting
-  HazelcastInstance hzInstance;
-
-  public HazelcastLocalClient(Configuration config) {
-    boolean clusterEnabled = config.getBoolean(CLUSTER_ENABLED).orElse(false);
-    String clusterLocalEndPoint = config.get(CLUSTER_LOCALENDPOINT).orElse(null);
-
-    Preconditions.checkState(clusterEnabled, "Cluster is not enabled");
-    Preconditions.checkState(isNotEmpty(clusterLocalEndPoint), "LocalEndPoint have not been set");
-
-    hzConfig = new ClientConfig();
-    hzConfig.getGroupConfig().setName(HAZELCAST_CLUSTER_NAME);
-    hzConfig.getNetworkConfig().addAddress(clusterLocalEndPoint);
-
-    // Tweak HazelCast configuration
-    hzConfig
-      // Increase the number of tries
-      .setProperty("hazelcast.tcp.join.port.try.count", "10")
-      // Don't phone home
-      .setProperty("hazelcast.phone.home.enabled", "false")
-      // Use slf4j for logging
-      .setProperty("hazelcast.logging.type", "slf4j");
-  }
-
-  @Override
-  public <E> Set<E> getSet(String name) {
-    return hzInstance.getSet(name);
-  }
-
-  @Override
-  public <E> List<E> getList(String name) {
-    return hzInstance.getList(name);
-  }
-
-  @Override
-  public <K, V> Map<K, V> getMap(String name) {
-    return hzInstance.getMap(name);
-  }
-
-  @Override
-  public <K, V> Map<K, V> getReplicatedMap(String name) {
-    return hzInstance.getReplicatedMap(name);
-  }
-
-  @Override
-  public String getUUID() {
-    return hzInstance.getLocalEndpoint().getUuid();
-  }
-
-  @Override
-  public Set<String> getMemberUuids() {
-    ImmutableSet.Builder<String> builder = ImmutableSet.builder();
-    builder.addAll(hzInstance.getSet(ClusterObjectKeys.LOCAL_MEMBER_UUIDS));
-    hzInstance.getCluster().getMembers().stream().map(Member::getUuid).forEach(builder::add);
-    return builder.build();
-  }
-
-  @Override
-  public Lock getLock(String name) {
-    return hzInstance.getLock(name);
-  }
-
-  @Override
-  public long getClusterTime() {
-    return hzInstance.getCluster().getClusterTime();
-  }
-
-  @Override
-  public void start() {
-    this.hzInstance = com.hazelcast.client.HazelcastClient.newHazelcastClient(hzConfig);
-  }
-
-  @Override
-  public void stop() {
-    // Shutdown Hazelcast properly
-    hzInstance.shutdown();
-  }
-}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/hz/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/hz/package-info.java
deleted file mode 100644 (file)
index 41fc35d..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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.server.hz;
-
-import javax.annotation.ParametersAreNonnullByDefault;
index 5e38c93de156c9cb0734d1e4ab873ab54e173e3d..367a73c16140748cdb25831d6498294d4f80df16 100644 (file)
  */
 package org.sonar.server.platform;
 
-import org.sonar.process.cluster.ClusterProperties;
-
 public interface WebServer {
 
   /**
-   * WebServer is standalone when property {@link ClusterProperties#CLUSTER_ENABLED} is {@code false} or
+   * WebServer is standalone when property {@link org.sonar.process.ProcessProperties#CLUSTER_ENABLED} is {@code false} or
    * undefined.
    */
   boolean isStandalone();
index 4a70046aed156b1b77ed59082c7483367211e1f1..980db415ac17246fb2c340974d6545bc7c6a69f8 100644 (file)
@@ -22,8 +22,8 @@ package org.sonar.server.platform;
 import org.sonar.api.config.Configuration;
 import org.sonar.api.utils.log.Loggers;
 
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_ENABLED;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_WEB_LEADER;
+import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_WEB_STARTUP_LEADER;
 
 public class WebServerImpl implements WebServer {
 
@@ -33,7 +33,7 @@ public class WebServerImpl implements WebServer {
   public WebServerImpl(Configuration config) {
     this.clusterEnabled = config.getBoolean(CLUSTER_ENABLED).orElse(false);
     if (this.clusterEnabled) {
-      this.startupLeader = config.getBoolean(CLUSTER_WEB_LEADER).orElse(false);
+      this.startupLeader = config.getBoolean(CLUSTER_WEB_STARTUP_LEADER).orElse(false);
       Loggers.get(WebServerImpl.class).info("Cluster enabled (startup {})", startupLeader ? "leader" : "follower");
     } else {
       this.startupLeader = true;
index a2c9619381147864b6439eb857ef0cf0b4f83945..46d62bdf8127c26be946d041a6228ee1b5e1dc84 100644 (file)
@@ -37,6 +37,7 @@ import org.sonar.server.authentication.AuthenticationModule;
 import org.sonar.server.batch.BatchWsModule;
 import org.sonar.server.branch.BranchFeatureProxyImpl;
 import org.sonar.server.ce.ws.CeWsModule;
+import org.sonar.server.cluster.StartableHazelcastMember;
 import org.sonar.server.component.ComponentCleanerService;
 import org.sonar.server.component.ComponentFinder;
 import org.sonar.server.component.ComponentService;
@@ -62,7 +63,6 @@ import org.sonar.server.es.metadata.MetadataIndexDefinition;
 import org.sonar.server.event.NewAlerts;
 import org.sonar.server.favorite.FavoriteModule;
 import org.sonar.server.health.NodeHealthModule;
-import org.sonar.server.hz.HazelcastLocalClient;
 import org.sonar.server.issue.AddTagsAction;
 import org.sonar.server.issue.AssignAction;
 import org.sonar.server.issue.CommentAction;
@@ -247,7 +247,7 @@ public class PlatformLevel4 extends PlatformLevel {
       EsDbCompatibilityImpl.class);
 
     addIfCluster(
-      HazelcastLocalClient.class,
+      StartableHazelcastMember.class,
       NodeHealthModule.class);
 
     add(
index b2a06d1fb9d02eadd71e40da5cca289244baeee4..978ccbc8f3166d489c8a1d8f45c6740b131cab48 100644 (file)
@@ -34,10 +34,10 @@ import org.sonar.process.ProcessProperties;
 
 import static java.lang.String.format;
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_ENABLED;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NAME;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_TYPE;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_SEARCH_HOSTS;
+import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_NAME;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_TYPE;
+import static org.sonar.process.ProcessProperties.CLUSTER_SEARCH_HOSTS;
 
 public class EsClientProviderTest {
 
index 2ecd674b7c4a7cf1b7811d42a8a6b391309c6651..794d13c28b731cb01bf0e3e0e9b34175fd0c0cf6 100644 (file)
@@ -32,7 +32,7 @@ import org.sonar.process.ProcessProperties;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.data.MapEntry.entry;
 import static org.junit.Assert.fail;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
 import static org.sonar.server.es.NewIndex.SettingsConfiguration.newBuilder;
 
 public class NewIndexTest {
index b3bce64ab8d3c9f8ee8ecd064d779db2422c9897..3f11fd744ec4afee12d9d640224a32da85f10368 100644 (file)
@@ -31,8 +31,8 @@ import org.sonar.api.platform.Server;
 import org.sonar.api.utils.System2;
 import org.sonar.core.platform.ComponentContainer;
 import org.sonar.process.NetworkUtils;
-import org.sonar.process.cluster.HazelcastClient;
 import org.sonar.process.cluster.health.SharedHealthStateImpl;
+import org.sonar.process.cluster.hz.HazelcastMember;
 
 import static java.lang.String.valueOf;
 import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
@@ -61,7 +61,7 @@ public class NodeHealthModuleTest {
       mapSettings.asConfig(),
       server,
       networkUtils,
-      mock(HazelcastClient.class));
+      mock(HazelcastMember.class));
     // HealthAction dependencies
     container.add(mock(HealthChecker.class));
 
index 42f67bd88e637f72082bed77e4198e76677c86d5..20fee2a8a9066ded372cef843e5da20747a48183 100644 (file)
@@ -37,9 +37,9 @@ import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_HOST;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_NAME;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NODE_PORT;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_HOST;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_NAME;
+import static org.sonar.process.ProcessProperties.CLUSTER_NODE_PORT;
 
 public class NodeHealthProviderImplTest {
   @Rule
index 79563e92a2524ca1df2773cd69c26f9146067f11..e31915bafa876ad4cff485517fd9eea8fd5449a1 100644 (file)
@@ -32,7 +32,7 @@ import org.sonar.server.es.NewIndex;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.elasticsearch.index.query.QueryBuilders.matchQuery;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
 import static org.sonar.server.es.DefaultIndexSettingsElement.ENGLISH_HTML_ANALYZER;
 import static org.sonar.server.rule.index.RuleIndexDefinition.FIELD_RULE_HTML_DESCRIPTION;
 import static org.sonar.server.rule.index.RuleIndexDefinition.FIELD_RULE_KEY;
index 7666e949d2cafde00edc3f48b2cf033e80464f90..c1eaec0a8a433f1b802d367a186e5a10bdf4abbd 100644 (file)
@@ -33,7 +33,7 @@ import org.sonar.application.command.CommandFactory;
 import org.sonar.application.command.CommandFactoryImpl;
 
 import static org.sonar.application.config.SonarQubeVersionHelper.getSonarqubeVersion;
-import static org.sonar.process.cluster.ClusterProperties.CLUSTER_NAME;
+import static org.sonar.process.ProcessProperties.CLUSTER_NAME;
 
 public class App {
 
@@ -50,7 +50,7 @@ public class App {
 
     try (AppState appState = new AppStateFactory(settings).create()) {
       appState.registerSonarQubeVersion(getSonarqubeVersion());
-      appState.registerClusterName(settings.getProps().value(CLUSTER_NAME, "sonarqube"));
+      appState.registerClusterName(settings.getProps().nonNullValue(CLUSTER_NAME));
       AppReloader appReloader = new AppReloaderImpl(settingsLoader, fileSystem, appState, logging);
       fileSystem.reset();
       CommandFactory commandFactory = new CommandFactoryImpl(settings.getProps(), fileSystem.getTempDir(), System2.INSTANCE);
index 1626f3525b827a72d7b677ce394fc994d730eedc..5bf9ac21c7bc48845e3ce5c26881369bf85ebe9d 100644 (file)
@@ -38,6 +38,7 @@ import org.junit.rules.TemporaryFolder;
 import org.junit.rules.TestRule;
 import org.junit.rules.Timeout;
 import org.sonarqube.ws.WsSystem;
+import org.sonarqube.ws.client.GetRequest;
 
 import static com.google.common.base.Preconditions.checkState;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -68,6 +69,24 @@ public class ClusterTest {
     db.stop();
   }
 
+  /**
+   * TODO WIP
+   */
+  @Test
+  public void wip() throws Exception {
+    try (Cluster cluster = newCluster(3, 2)) {
+      cluster.getNodes().forEach(Node::start);
+
+      Node app = cluster.getAppNode(0);
+      app.waitForHealthGreen();
+
+      System.out.println("-----------------------------------------------------------------------");
+      String json = app.wsClient().wsConnector().call(new GetRequest("api/system/cluster_info")).content();
+      System.out.println(json);
+      System.out.println("-----------------------------------------------------------------------");
+    }
+  }
+
   @Test
   public void test_high_availability_topology() throws Exception {
     try (Cluster cluster = newCluster(3, 2)) {