From fa0fe58b0561cac895939487f11c22c92f18d03f Mon Sep 17 00:00:00 2001 From: Sébastien Lesaint Date: Thu, 31 Aug 2017 17:12:00 +0200 Subject: SONAR-9741 search nodes share startup health status --- .../org/sonar/application/AppStateFactory.java | 4 +- .../java/org/sonar/application/SchedulerImpl.java | 3 +- .../application/cluster/AppStateClusterImpl.java | 129 ------------- .../application/cluster/ClusterAppStateImpl.java | 129 +++++++++++++ .../health/SearchNodeHealthProvider.java | 35 +++- .../org/sonar/application/AppStateFactoryTest.java | 6 +- .../org/sonar/application/SchedulerImplTest.java | 57 ++++-- .../java/org/sonar/application/TestAppState.java | 20 +- .../org/sonar/application/TestClusterAppState.java | 36 ++++ .../cluster/AppStateClusterImplTest.java | 184 ------------------ .../cluster/ClusterAppStateImplTest.java | 184 ++++++++++++++++++ .../application/cluster/HazelcastClusterTest.java | 9 +- .../cluster/HazelcastClusterTestHelper.java | 2 +- .../health/SearchNodeHealthProviderTest.java | 212 ++++++++++++++++++++- .../sonar/server/health/HealthCheckerImplTest.java | 2 +- 15 files changed, 636 insertions(+), 376 deletions(-) delete mode 100644 server/sonar-main/src/main/java/org/sonar/application/cluster/AppStateClusterImpl.java create mode 100644 server/sonar-main/src/main/java/org/sonar/application/cluster/ClusterAppStateImpl.java create mode 100644 server/sonar-main/src/test/java/org/sonar/application/TestClusterAppState.java delete mode 100644 server/sonar-main/src/test/java/org/sonar/application/cluster/AppStateClusterImplTest.java create mode 100644 server/sonar-main/src/test/java/org/sonar/application/cluster/ClusterAppStateImplTest.java diff --git a/server/sonar-main/src/main/java/org/sonar/application/AppStateFactory.java b/server/sonar-main/src/main/java/org/sonar/application/AppStateFactory.java index 196aa2a9fa0..8f5cfbfe780 100644 --- a/server/sonar-main/src/main/java/org/sonar/application/AppStateFactory.java +++ b/server/sonar-main/src/main/java/org/sonar/application/AppStateFactory.java @@ -19,7 +19,7 @@ */ package org.sonar.application; -import org.sonar.application.cluster.AppStateClusterImpl; +import org.sonar.application.cluster.ClusterAppStateImpl; import org.sonar.application.config.AppSettings; import org.sonar.application.config.ClusterSettings; @@ -32,6 +32,6 @@ public class AppStateFactory { } public AppState create() { - return ClusterSettings.isClusterEnabled(settings) ? new AppStateClusterImpl(settings) : new AppStateImpl(); + return ClusterSettings.isClusterEnabled(settings) ? new ClusterAppStateImpl(settings) : new AppStateImpl(); } } diff --git a/server/sonar-main/src/main/java/org/sonar/application/SchedulerImpl.java b/server/sonar-main/src/main/java/org/sonar/application/SchedulerImpl.java index 621cce82b3b..3268bb249c6 100644 --- a/server/sonar-main/src/main/java/org/sonar/application/SchedulerImpl.java +++ b/server/sonar-main/src/main/java/org/sonar/application/SchedulerImpl.java @@ -28,6 +28,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.sonar.NetworkUtils; import org.sonar.api.utils.System2; import org.sonar.application.cluster.ClusterAppState; import org.sonar.application.config.AppSettings; @@ -151,7 +152,7 @@ public class SchedulerImpl implements Scheduler, ProcessEventListener, ProcessLi ClusterAppState clusterAppState = (ClusterAppState) appState; this.healthStateSharing = new HealthStateSharingImpl( clusterAppState.getHazelcastClient(), - new SearchNodeHealthProvider(settings.getProps(), System2.INSTANCE)); + new SearchNodeHealthProvider(settings.getProps(), System2.INSTANCE, clusterAppState, NetworkUtils.INSTANCE)); this.healthStateSharing.start(); } } diff --git a/server/sonar-main/src/main/java/org/sonar/application/cluster/AppStateClusterImpl.java b/server/sonar-main/src/main/java/org/sonar/application/cluster/AppStateClusterImpl.java deleted file mode 100644 index 717f41b287b..00000000000 --- a/server/sonar-main/src/main/java/org/sonar/application/cluster/AppStateClusterImpl.java +++ /dev/null @@ -1,129 +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.EnumMap; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; -import javax.annotation.Nonnull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sonar.application.AppStateListener; -import org.sonar.application.config.AppSettings; -import org.sonar.cluster.localclient.HazelcastClient; -import org.sonar.process.ProcessId; - -import static org.sonar.cluster.ClusterProperties.CLUSTER_ENABLED; -import static org.sonar.cluster.ClusterProperties.CLUSTER_LOCALENDPOINT; -import static org.sonar.cluster.ClusterProperties.CLUSTER_MEMBERUUID; - -public class AppStateClusterImpl implements ClusterAppState { - private static Logger LOGGER = LoggerFactory.getLogger(AppStateClusterImpl.class); - - private final Map localProcesses = new EnumMap<>(ProcessId.class); - private final HazelcastCluster hazelcastCluster; - - public AppStateClusterImpl(AppSettings appSettings) { - if (!appSettings.getProps().valueAsBoolean(CLUSTER_ENABLED)) { - throw new IllegalStateException("Cluster is not enabled on this instance"); - } - - ClusterProperties clusterProperties = new ClusterProperties(appSettings); - clusterProperties.validate(); - - 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()); - - String members = hazelcastCluster.getMembers().stream().collect(Collectors.joining(",")); - LOGGER.info("Joined a SonarQube cluster that contains the following hosts : [{}]", members); - } - - @Override - public void addListener(@Nonnull AppStateListener listener) { - hazelcastCluster.addListener(listener); - } - - @Override - public boolean isOperational(@Nonnull ProcessId processId, boolean local) { - if (local) { - return localProcesses.computeIfAbsent(processId, p -> false); - } - return hazelcastCluster.isOperational(processId); - } - - @Override - public void setOperational(@Nonnull ProcessId processId) { - localProcesses.put(processId, true); - hazelcastCluster.setOperational(processId); - } - - @Override - public boolean tryToLockWebLeader() { - return hazelcastCluster.tryToLockWebLeader(); - } - - @Override - public void reset() { - 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); - } - - @Override - public void registerClusterName(String clusterName) { - hazelcastCluster.registerClusterName(clusterName); - } - - @Override - public Optional getLeaderHostName() { - return hazelcastCluster.getLeaderHostName(); - } - - HazelcastCluster getHazelcastCluster() { - return hazelcastCluster; - } - - @Override - public HazelcastClient getHazelcastClient() { - return hazelcastCluster.getHazelcastClient(); - } - - /** - * Only used for testing purpose - * - * @param logger - */ - static void setLogger(Logger logger) { - AppStateClusterImpl.LOGGER = logger; - } - -} diff --git a/server/sonar-main/src/main/java/org/sonar/application/cluster/ClusterAppStateImpl.java b/server/sonar-main/src/main/java/org/sonar/application/cluster/ClusterAppStateImpl.java new file mode 100644 index 00000000000..9742941f132 --- /dev/null +++ b/server/sonar-main/src/main/java/org/sonar/application/cluster/ClusterAppStateImpl.java @@ -0,0 +1,129 @@ +/* + * 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.EnumMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.application.AppStateListener; +import org.sonar.application.config.AppSettings; +import org.sonar.cluster.localclient.HazelcastClient; +import org.sonar.process.ProcessId; + +import static org.sonar.cluster.ClusterProperties.CLUSTER_ENABLED; +import static org.sonar.cluster.ClusterProperties.CLUSTER_LOCALENDPOINT; +import static org.sonar.cluster.ClusterProperties.CLUSTER_MEMBERUUID; + +public class ClusterAppStateImpl implements ClusterAppState { + private static Logger LOGGER = LoggerFactory.getLogger(ClusterAppStateImpl.class); + + private final Map localProcesses = new EnumMap<>(ProcessId.class); + private final HazelcastCluster hazelcastCluster; + + public ClusterAppStateImpl(AppSettings appSettings) { + if (!appSettings.getProps().valueAsBoolean(CLUSTER_ENABLED)) { + throw new IllegalStateException("Cluster is not enabled on this instance"); + } + + ClusterProperties clusterProperties = new ClusterProperties(appSettings); + clusterProperties.validate(); + + 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()); + + String members = hazelcastCluster.getMembers().stream().collect(Collectors.joining(",")); + LOGGER.info("Joined a SonarQube cluster that contains the following hosts : [{}]", members); + } + + @Override + public void addListener(@Nonnull AppStateListener listener) { + hazelcastCluster.addListener(listener); + } + + @Override + public boolean isOperational(@Nonnull ProcessId processId, boolean local) { + if (local) { + return localProcesses.computeIfAbsent(processId, p -> false); + } + return hazelcastCluster.isOperational(processId); + } + + @Override + public void setOperational(@Nonnull ProcessId processId) { + localProcesses.put(processId, true); + hazelcastCluster.setOperational(processId); + } + + @Override + public boolean tryToLockWebLeader() { + return hazelcastCluster.tryToLockWebLeader(); + } + + @Override + public void reset() { + 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); + } + + @Override + public void registerClusterName(String clusterName) { + hazelcastCluster.registerClusterName(clusterName); + } + + @Override + public Optional getLeaderHostName() { + return hazelcastCluster.getLeaderHostName(); + } + + HazelcastCluster getHazelcastCluster() { + return hazelcastCluster; + } + + @Override + public HazelcastClient getHazelcastClient() { + return hazelcastCluster.getHazelcastClient(); + } + + /** + * Only used for testing purpose + * + * @param logger + */ + static void setLogger(Logger logger) { + ClusterAppStateImpl.LOGGER = logger; + } + +} diff --git a/server/sonar-main/src/main/java/org/sonar/application/health/SearchNodeHealthProvider.java b/server/sonar-main/src/main/java/org/sonar/application/health/SearchNodeHealthProvider.java index 83f424217e1..d1e184ad436 100644 --- a/server/sonar-main/src/main/java/org/sonar/application/health/SearchNodeHealthProvider.java +++ b/server/sonar-main/src/main/java/org/sonar/application/health/SearchNodeHealthProvider.java @@ -19,37 +19,54 @@ */ package org.sonar.application.health; -import java.util.Random; +import org.sonar.NetworkUtils; import org.sonar.api.utils.System2; -import org.sonar.cluster.ClusterProperties; +import org.sonar.application.cluster.ClusterAppState; import org.sonar.cluster.health.NodeDetails; import org.sonar.cluster.health.NodeHealth; import org.sonar.cluster.health.NodeHealthProvider; +import org.sonar.process.ProcessId; import org.sonar.process.Props; +import static org.sonar.cluster.ClusterProperties.CLUSTER_NODE_HOST; +import static org.sonar.cluster.ClusterProperties.CLUSTER_NODE_NAME; import static org.sonar.cluster.ClusterProperties.CLUSTER_NODE_PORT; public class SearchNodeHealthProvider implements NodeHealthProvider { private final System2 system2; + private final ClusterAppState clusterAppState; private final NodeDetails nodeDetails; - public SearchNodeHealthProvider(Props props, System2 system2) { + public SearchNodeHealthProvider(Props props, System2 system2, ClusterAppState clusterAppState, NetworkUtils networkUtils) { this.system2 = system2; + this.clusterAppState = clusterAppState; this.nodeDetails = NodeDetails.newNodeDetailsBuilder() .setType(NodeDetails.Type.SEARCH) - .setName(props.nonNullValue(ClusterProperties.CLUSTER_NAME) + new Random().nextInt(999)) - // TODO read sonar.cluster.node.host - .setHost("host hardcoded for now") + .setName(props.nonNullValue(CLUSTER_NODE_NAME)) + .setHost(getHost(props, networkUtils)) .setPort(Integer.valueOf(props.nonNullValue(CLUSTER_NODE_PORT))) - // TODO is now good enough? .setStarted(system2.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() { - return NodeHealth.newNodeHealthBuilder() - .setStatus(NodeHealth.Status.GREEN) + 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) .setDate(system2.now()) .build(); diff --git a/server/sonar-main/src/test/java/org/sonar/application/AppStateFactoryTest.java b/server/sonar-main/src/test/java/org/sonar/application/AppStateFactoryTest.java index 3998aabe983..10b9d0d6d3c 100644 --- a/server/sonar-main/src/test/java/org/sonar/application/AppStateFactoryTest.java +++ b/server/sonar-main/src/test/java/org/sonar/application/AppStateFactoryTest.java @@ -20,7 +20,7 @@ package org.sonar.application; import org.junit.Test; -import org.sonar.application.cluster.AppStateClusterImpl; +import org.sonar.application.cluster.ClusterAppStateImpl; import org.sonar.application.config.TestAppSettings; import static org.assertj.core.api.Assertions.assertThat; @@ -40,8 +40,8 @@ public class AppStateFactoryTest { settings.set(CLUSTER_NAME, "foo"); AppState appState = underTest.create(); - assertThat(appState).isInstanceOf(AppStateClusterImpl.class); - appState.close(); + assertThat(appState).isInstanceOf(ClusterAppStateImpl.class); + ((ClusterAppStateImpl) appState).close(); } @Test diff --git a/server/sonar-main/src/test/java/org/sonar/application/SchedulerImplTest.java b/server/sonar-main/src/test/java/org/sonar/application/SchedulerImplTest.java index 91bd75d1ba4..f419f4d0e17 100644 --- a/server/sonar-main/src/test/java/org/sonar/application/SchedulerImplTest.java +++ b/server/sonar-main/src/test/java/org/sonar/application/SchedulerImplTest.java @@ -24,6 +24,7 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.EnumMap; import java.util.List; +import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.junit.After; @@ -40,6 +41,7 @@ import org.mockito.Mockito; import org.sonar.application.config.TestAppSettings; import org.sonar.application.process.ProcessLauncher; import org.sonar.application.process.ProcessMonitor; +import org.sonar.cluster.localclient.HazelcastClient; import org.sonar.process.ProcessId; import org.sonar.process.command.AbstractCommand; import org.sonar.process.command.CommandFactory; @@ -47,12 +49,16 @@ import org.sonar.process.command.EsCommand; import org.sonar.process.command.JavaCommand; import static java.util.Collections.synchronizedList; +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.sonar.cluster.ClusterProperties.CLUSTER_ENABLED; +import static org.sonar.cluster.ClusterProperties.CLUSTER_NODE_HOST; +import static org.sonar.cluster.ClusterProperties.CLUSTER_NODE_NAME; +import static org.sonar.cluster.ClusterProperties.CLUSTER_NODE_PORT; import static org.sonar.cluster.ClusterProperties.CLUSTER_NODE_TYPE; import static org.sonar.process.ProcessId.COMPUTE_ENGINE; import static org.sonar.process.ProcessId.ELASTICSEARCH; @@ -77,6 +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 List orderedStops = synchronizedList(new ArrayList<>()); @Before @@ -95,7 +103,7 @@ public class SchedulerImplTest { @Test public void start_and_stop_sequence_of_ES_WEB_CE_in_order() throws Exception { - SchedulerImpl underTest = newScheduler(); + SchedulerImpl underTest = newScheduler(false); underTest.schedule(); // elasticsearch does not have preconditions to start @@ -105,7 +113,7 @@ public class SchedulerImplTest { // elasticsearch becomes operational -> web leader is starting es.operational = true; - waitForAppStateOperational(ELASTICSEARCH); + waitForAppStateOperational(appState, ELASTICSEARCH); TestProcess web = processLauncher.waitForProcess(WEB_SERVER); assertThat(web.isAlive()).isTrue(); assertThat(processLauncher.processes).hasSize(2); @@ -113,7 +121,7 @@ public class SchedulerImplTest { // web becomes operational -> CE is starting web.operational = true; - waitForAppStateOperational(WEB_SERVER); + waitForAppStateOperational(appState, WEB_SERVER); TestProcess ce = processLauncher.waitForProcess(COMPUTE_ENGINE); assertThat(ce.isAlive()).isTrue(); assertThat(processLauncher.processes).hasSize(3); @@ -148,7 +156,7 @@ public class SchedulerImplTest { @Test public void all_processes_are_stopped_if_one_process_fails_to_start() throws Exception { - SchedulerImpl underTest = newScheduler(); + SchedulerImpl underTest = newScheduler(false); processLauncher.makeStartupFail = COMPUTE_ENGINE; underTest.schedule(); @@ -237,7 +245,8 @@ public class SchedulerImplTest { public void search_node_starts_only_elasticsearch() throws Exception { settings.set(CLUSTER_ENABLED, "true"); settings.set(CLUSTER_NODE_TYPE, "search"); - SchedulerImpl underTest = newScheduler(); + addRequiredNodeProperties(); + SchedulerImpl underTest = newScheduler(true); underTest.schedule(); processLauncher.waitForProcessAlive(ProcessId.ELASTICSEARCH); @@ -248,10 +257,10 @@ public class SchedulerImplTest { @Test public void application_node_starts_only_web_and_ce() throws Exception { - appState.setOperational(ProcessId.ELASTICSEARCH); + clusterAppState.setOperational(ProcessId.ELASTICSEARCH); settings.set(CLUSTER_ENABLED, "true"); settings.set(CLUSTER_NODE_TYPE, "application"); - SchedulerImpl underTest = newScheduler(); + SchedulerImpl underTest = newScheduler(true); underTest.schedule(); TestProcess web = processLauncher.waitForProcessAlive(WEB_SERVER); @@ -265,12 +274,13 @@ public class SchedulerImplTest { @Test public void search_node_starts_even_if_web_leader_is_not_yet_operational() throws Exception { // leader takes the lock, so underTest won't get it - assertThat(appState.tryToLockWebLeader()).isTrue(); + assertThat(clusterAppState.tryToLockWebLeader()).isTrue(); - appState.setOperational(ProcessId.ELASTICSEARCH); + clusterAppState.setOperational(ProcessId.ELASTICSEARCH); settings.set(CLUSTER_ENABLED, "true"); settings.set(CLUSTER_NODE_TYPE, "search"); - SchedulerImpl underTest = newScheduler(); + addRequiredNodeProperties(); + SchedulerImpl underTest = newScheduler(true); underTest.schedule(); processLauncher.waitForProcessAlive(ProcessId.ELASTICSEARCH); @@ -282,19 +292,18 @@ public class SchedulerImplTest { @Test public void web_follower_starts_only_when_web_leader_is_operational() throws Exception { // leader takes the lock, so underTest won't get it - assertThat(appState.tryToLockWebLeader()).isTrue(); - appState.setOperational(ProcessId.ELASTICSEARCH); + assertThat(clusterAppState.tryToLockWebLeader()).isTrue(); + clusterAppState.setOperational(ProcessId.ELASTICSEARCH); settings.set(CLUSTER_ENABLED, "true"); settings.set(CLUSTER_NODE_TYPE, "application"); - SchedulerImpl underTest = newScheduler(); + SchedulerImpl underTest = newScheduler(true); underTest.schedule(); assertThat(processLauncher.processes).hasSize(0); // leader becomes operational -> follower can start - appState.setOperational(WEB_SERVER); - + clusterAppState.setOperational(WEB_SERVER); processLauncher.waitForProcessAlive(WEB_SERVER); processLauncher.waitForProcessAlive(COMPUTE_ENGINE); assertThat(processLauncher.processes).hasSize(2); @@ -306,27 +315,27 @@ public class SchedulerImplTest { public void web_server_waits_for_remote_elasticsearch_to_be_started_if_local_es_is_disabled() throws Exception { settings.set(CLUSTER_ENABLED, "true"); settings.set(CLUSTER_NODE_TYPE, "application"); - SchedulerImpl underTest = newScheduler(); + SchedulerImpl underTest = newScheduler(true); underTest.schedule(); // WEB and CE wait for ES to be up assertThat(processLauncher.processes).isEmpty(); // ES becomes operational on another node -> web leader can start - appState.setRemoteOperational(ProcessId.ELASTICSEARCH); + clusterAppState.setRemoteOperational(ProcessId.ELASTICSEARCH); processLauncher.waitForProcessAlive(WEB_SERVER); assertThat(processLauncher.processes).hasSize(1); underTest.terminate(); } - private SchedulerImpl newScheduler() { - return new SchedulerImpl(settings, appReloader, javaCommandFactory, processLauncher, appState) + private SchedulerImpl newScheduler(boolean clustered) { + return new SchedulerImpl(settings, appReloader, javaCommandFactory, processLauncher, clustered ? clusterAppState : appState) .setProcessWatcherDelayMs(1L); } private Scheduler startAll() throws InterruptedException { - SchedulerImpl scheduler = newScheduler(); + SchedulerImpl scheduler = newScheduler(false); scheduler.schedule(); processLauncher.waitForProcess(ELASTICSEARCH).operational = true; processLauncher.waitForProcess(WEB_SERVER).operational = true; @@ -334,7 +343,7 @@ public class SchedulerImplTest { return scheduler; } - private void waitForAppStateOperational(ProcessId id) throws InterruptedException { + private static void waitForAppStateOperational(AppState appState, ProcessId id) throws InterruptedException { while (true) { if (appState.isOperational(id, true)) { return; @@ -343,6 +352,12 @@ public class SchedulerImplTest { } } + private void addRequiredNodeProperties() { + settings.set(CLUSTER_NODE_NAME, randomAlphanumeric(4)); + settings.set(CLUSTER_NODE_HOST, randomAlphanumeric(4)); + settings.set(CLUSTER_NODE_PORT, String.valueOf(1 + new Random().nextInt(999))); + } + private class TestCommandFactory implements CommandFactory { @Override public EsCommand createEsCommand() { diff --git a/server/sonar-main/src/test/java/org/sonar/application/TestAppState.java b/server/sonar-main/src/test/java/org/sonar/application/TestAppState.java index 125ae867d40..27f6c4d5fb0 100644 --- a/server/sonar-main/src/test/java/org/sonar/application/TestAppState.java +++ b/server/sonar-main/src/test/java/org/sonar/application/TestAppState.java @@ -19,7 +19,6 @@ */ package org.sonar.application; -import com.google.common.base.Preconditions; import java.util.ArrayList; import java.util.EnumMap; import java.util.List; @@ -28,25 +27,14 @@ import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; import javax.annotation.Nonnull; import org.sonar.NetworkUtils; -import org.sonar.application.cluster.ClusterAppState; -import org.sonar.cluster.localclient.HazelcastClient; import org.sonar.process.ProcessId; -public class TestAppState implements ClusterAppState { +public class TestAppState implements AppState { private final Map localProcesses = new EnumMap<>(ProcessId.class); private final Map remoteProcesses = new EnumMap<>(ProcessId.class); private final List listeners = new ArrayList<>(); private final AtomicBoolean webLeaderLocked = new AtomicBoolean(false); - private final HazelcastClient hazelcastClient; - - public TestAppState() { - this(null); - } - - public TestAppState(HazelcastClient hazelcastClient) { - this.hazelcastClient = hazelcastClient; - } @Override public void addListener(@Nonnull AppStateListener listener) { @@ -99,12 +87,6 @@ public class TestAppState implements ClusterAppState { return Optional.of(NetworkUtils.INSTANCE.getHostname()); } - @Override - public HazelcastClient getHazelcastClient() { - Preconditions.checkState(hazelcastClient != null, "An HazelcastClient should be provided when testing in cluster mode"); - return hazelcastClient; - } - @Override public void close() { // nothing to do diff --git a/server/sonar-main/src/test/java/org/sonar/application/TestClusterAppState.java b/server/sonar-main/src/test/java/org/sonar/application/TestClusterAppState.java new file mode 100644 index 00000000000..94d5e664f5c --- /dev/null +++ b/server/sonar-main/src/test/java/org/sonar/application/TestClusterAppState.java @@ -0,0 +1,36 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.application; + +import org.sonar.application.cluster.ClusterAppState; +import org.sonar.cluster.localclient.HazelcastClient; + +public class TestClusterAppState extends TestAppState implements ClusterAppState { + private final HazelcastClient hazelcastClient; + + public TestClusterAppState(HazelcastClient hazelcastClient) { + this.hazelcastClient = hazelcastClient; + } + + @Override + public HazelcastClient getHazelcastClient() { + return hazelcastClient; + } +} diff --git a/server/sonar-main/src/test/java/org/sonar/application/cluster/AppStateClusterImplTest.java b/server/sonar-main/src/test/java/org/sonar/application/cluster/AppStateClusterImplTest.java deleted file mode 100644 index 7e8610c5e80..00000000000 --- a/server/sonar-main/src/test/java/org/sonar/application/cluster/AppStateClusterImplTest.java +++ /dev/null @@ -1,184 +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.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.cluster.ClusterObjectKeys.CLUSTER_NAME; -import static org.sonar.cluster.ClusterObjectKeys.SONARQUBE_VERSION; -import static org.sonar.cluster.ClusterProperties.CLUSTER_ENABLED; - -public class AppStateClusterImplTest { - - @Rule - public ExpectedException expectedException = ExpectedException.none(); - - @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 AppStateClusterImpl(settings); - } - - @Test - public void tryToLockWebLeader_returns_true_only_for_the_first_call() throws Exception { - TestAppSettings settings = newApplicationSettings(); - - try (AppStateClusterImpl underTest = new AppStateClusterImpl(settings)) { - 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); - AppStateClusterImpl.setLogger(logger); - - try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(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 (AppStateClusterImpl underTest = new AppStateClusterImpl(newApplicationSettings())) { - underTest.addListener(listener); - - underTest.setOperational(ProcessId.ELASTICSEARCH); - verify(listener, timeout(20_000)).onAppStateOperational(ProcessId.ELASTICSEARCH); - - assertThat(underTest.isOperational(ProcessId.ELASTICSEARCH, true)).isEqualTo(true); - assertThat(underTest.isOperational(ProcessId.APP, true)).isEqualTo(false); - assertThat(underTest.isOperational(ProcessId.WEB_SERVER, true)).isEqualTo(false); - assertThat(underTest.isOperational(ProcessId.COMPUTE_ENGINE, true)).isEqualTo(false); - } - } - - @Test - public void registerSonarQubeVersion_publishes_version_on_first_call() { - TestAppSettings settings = newApplicationSettings(); - - try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(settings)) { - appStateCluster.registerSonarQubeVersion("6.4.1.5"); - - HazelcastInstance hzInstance = createHazelcastClient(appStateCluster); - assertThat(hzInstance.getAtomicReference(SONARQUBE_VERSION).get()) - .isNotNull() - .isInstanceOf(String.class) - .isEqualTo("6.4.1.5"); - } - } - - @Test - public void registerClusterName_publishes_clusterName_on_first_call() { - TestAppSettings settings = newApplicationSettings(); - String clusterName = randomAlphanumeric(20); - - try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(settings)) { - appStateCluster.registerClusterName(clusterName); - - HazelcastInstance hzInstance = createHazelcastClient(appStateCluster); - assertThat(hzInstance.getAtomicReference(CLUSTER_NAME).get()) - .isNotNull() - .isInstanceOf(String.class) - .isEqualTo(clusterName); - } - } - - @Test - public void reset_throws_always_ISE() { - TestAppSettings settings = newApplicationSettings(); - - try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(settings)) { - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("state reset is not supported in cluster mode"); - appStateCluster.reset(); - } - } - - @Test - public void registerSonarQubeVersion_throws_ISE_if_initial_version_is_different() throws Exception { - // Now launch an instance that try to be part of the hzInstance cluster - TestAppSettings settings = newApplicationSettings(); - - try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(settings)) { - // Register first version - appStateCluster.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 - appStateCluster.registerSonarQubeVersion("2.0.0"); - } - } - - @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 (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(settings)) { - // Register first version - appStateCluster.registerClusterName("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"); - } - } -} diff --git a/server/sonar-main/src/test/java/org/sonar/application/cluster/ClusterAppStateImplTest.java b/server/sonar-main/src/test/java/org/sonar/application/cluster/ClusterAppStateImplTest.java new file mode 100644 index 00000000000..240db1cbfe0 --- /dev/null +++ b/server/sonar-main/src/test/java/org/sonar/application/cluster/ClusterAppStateImplTest.java @@ -0,0 +1,184 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.application.cluster; + +import com.hazelcast.core.HazelcastInstance; +import 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.cluster.ClusterObjectKeys.CLUSTER_NAME; +import static org.sonar.cluster.ClusterObjectKeys.SONARQUBE_VERSION; +import static org.sonar.cluster.ClusterProperties.CLUSTER_ENABLED; + +public class ClusterAppStateImplTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @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)) { + 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())) { + underTest.addListener(listener); + + underTest.setOperational(ProcessId.ELASTICSEARCH); + verify(listener, timeout(20_000)).onAppStateOperational(ProcessId.ELASTICSEARCH); + + assertThat(underTest.isOperational(ProcessId.ELASTICSEARCH, true)).isEqualTo(true); + assertThat(underTest.isOperational(ProcessId.APP, true)).isEqualTo(false); + assertThat(underTest.isOperational(ProcessId.WEB_SERVER, true)).isEqualTo(false); + assertThat(underTest.isOperational(ProcessId.COMPUTE_ENGINE, true)).isEqualTo(false); + } + } + + @Test + public void registerSonarQubeVersion_publishes_version_on_first_call() { + TestAppSettings settings = newApplicationSettings(); + + try (ClusterAppStateImpl appStateCluster = new ClusterAppStateImpl(settings)) { + appStateCluster.registerSonarQubeVersion("6.4.1.5"); + + HazelcastInstance hzInstance = createHazelcastClient(appStateCluster); + assertThat(hzInstance.getAtomicReference(SONARQUBE_VERSION).get()) + .isNotNull() + .isInstanceOf(String.class) + .isEqualTo("6.4.1.5"); + } + } + + @Test + public void registerClusterName_publishes_clusterName_on_first_call() { + TestAppSettings settings = newApplicationSettings(); + String clusterName = randomAlphanumeric(20); + + 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); + } + } + + @Test + public void reset_throws_always_ISE() { + TestAppSettings settings = newApplicationSettings(); + + try (ClusterAppStateImpl appStateCluster = new ClusterAppStateImpl(settings)) { + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("state reset is not supported in cluster mode"); + appStateCluster.reset(); + } + } + + @Test + public void registerSonarQubeVersion_throws_ISE_if_initial_version_is_different() throws Exception { + // Now launch an instance that try to be part of the hzInstance cluster + TestAppSettings settings = newApplicationSettings(); + + try (ClusterAppStateImpl appStateCluster = new ClusterAppStateImpl(settings)) { + // Register first version + appStateCluster.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 + appStateCluster.registerSonarQubeVersion("2.0.0"); + } + } + + @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)) { + // Register first version + appStateCluster.registerClusterName("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"); + } + } +} 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 index 702359aa2ee..11a77839f35 100644 --- 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 @@ -244,7 +244,7 @@ public class HazelcastClusterTest { settings.set(CLUSTER_NODE_HOST, InetAddress.getLoopbackAddress().getHostAddress()); AppStateListener listener = mock(AppStateListener.class); - try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(settings)) { + try (ClusterAppStateImpl appStateCluster = new ClusterAppStateImpl(settings)) { appStateCluster.addListener(listener); HazelcastInstance hzInstance = createHazelcastClient(appStateCluster.getHazelcastCluster()); @@ -277,18 +277,17 @@ public class HazelcastClusterTest { memoryAppender.start(); lc.getLogger("com.hazelcast").addAppender(memoryAppender); - try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(newApplicationSettings())) { + try (ClusterAppStateImpl appStateCluster = new ClusterAppStateImpl(newApplicationSettings())) { } assertThat(memoryAppender.events).isNotEmpty(); memoryAppender.events.stream().forEach( - e -> assertThat(e.getLoggerName()).startsWith("com.hazelcast") - ); + e -> assertThat(e.getLoggerName()).startsWith("com.hazelcast")); } @Test public void removing_the_last_application_node_must_clear_web_leader() throws InterruptedException { - try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(newSearchSettings())) { + try (ClusterAppStateImpl appStateCluster = new ClusterAppStateImpl(newSearchSettings())) { TestAppSettings appSettings = newApplicationSettings(); appSettings.set(CLUSTER_HOSTS, appStateCluster.getHazelcastCluster().getLocalEndPoint()); appSettings.set(CLUSTER_NODE_PORT, "9004"); 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 index b1a03efba0b..0345c5bf672 100644 --- 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 @@ -51,7 +51,7 @@ public class HazelcastClusterTestHelper { return hazelcastInstance; } - static HazelcastInstance createHazelcastClient(AppStateClusterImpl appStateCluster) { + static HazelcastInstance createHazelcastClient(ClusterAppStateImpl appStateCluster) { return createHazelcastClient(appStateCluster.getHazelcastCluster()); } diff --git a/server/sonar-main/src/test/java/org/sonar/application/health/SearchNodeHealthProviderTest.java b/server/sonar-main/src/test/java/org/sonar/application/health/SearchNodeHealthProviderTest.java index 0bc57d85f38..ae3084db84f 100644 --- a/server/sonar-main/src/test/java/org/sonar/application/health/SearchNodeHealthProviderTest.java +++ b/server/sonar-main/src/test/java/org/sonar/application/health/SearchNodeHealthProviderTest.java @@ -19,8 +19,218 @@ */ package org.sonar.application.health; -// TODO implement UT for SearchNodeHealthProviderTest when Daniel and Eric's branch in merged into master +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.NetworkUtils; +import org.sonar.api.utils.System2; +import org.sonar.application.cluster.ClusterAppState; +import org.sonar.cluster.ClusterProperties; +import org.sonar.cluster.health.NodeHealth; +import org.sonar.process.ProcessId; +import org.sonar.process.Props; + +import static java.lang.String.valueOf; +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.cluster.ClusterProperties.CLUSTER_NODE_HOST; +import static org.sonar.cluster.ClusterProperties.CLUSTER_NODE_NAME; +import static org.sonar.cluster.ClusterProperties.CLUSTER_NODE_PORT; + public class SearchNodeHealthProviderTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private final Random random = new Random(); + private System2 system2 = mock(System2.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, system2, 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, system2, clusterAppState, networkUtils); + } + + @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, system2, clusterAppState, networkUtils); + } + + @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, system2, clusterAppState, networkUtils); + } + + @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(system2.now()).thenReturn(1L + random.nextInt(87)); + SearchNodeHealthProvider underTest = new SearchNodeHealthProvider(new Props(properties), system2, clusterAppState, networkUtils); + + 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(system2.now()).thenReturn(1L + random.nextInt(87)); + SearchNodeHealthProvider underTest = new SearchNodeHealthProvider(new Props(properties), system2, clusterAppState, networkUtils); + + 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(system2.now()).thenReturn(1L + random.nextInt(87)); + when(networkUtils.getHostname()).thenReturn(host); + SearchNodeHealthProvider underTest = new SearchNodeHealthProvider(new Props(properties), system2, clusterAppState, networkUtils); + + 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), system2, clusterAppState, networkUtils); + + NodeHealth nodeHealth = underTest.get(); + + assertThat(nodeHealth.getDetails().getStarted()).isEqualTo(now); + + // change now + when(system2.now()).thenReturn(now); + + NodeHealth newNodeHealth = underTest.get(); + + assertThat(newNodeHealth.getDetails().getStarted()).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), system2, clusterAppState, networkUtils); + + 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), system2, clusterAppState, networkUtils); + + 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(system2.now()).thenReturn(now); + when(networkUtils.getHostname()).thenReturn(randomAlphanumeric(34)); + return now; + } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/health/HealthCheckerImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/health/HealthCheckerImplTest.java index 9f4ec3ebe7a..43e0db25c30 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/health/HealthCheckerImplTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/health/HealthCheckerImplTest.java @@ -132,7 +132,7 @@ public class HealthCheckerImplTest { @Test public void checkCluster_fails_with_ISE_in_clustering_and_HealthState_is_null() { when(webServer.isStandalone()).thenReturn(false); - HealthCheckerImpl underTest = new HealthCheckerImpl(webServer, new NodeHealthCheck[0], new ClusterHealthCheck[0], sharedHealthState); + HealthCheckerImpl underTest = new HealthCheckerImpl(webServer, new NodeHealthCheck[0], new ClusterHealthCheck[0], null); expectedException.expect(IllegalStateException.class); expectedException.expectMessage("HealthState instance can't be null when clustering is enabled"); -- cgit v1.2.3