diff options
author | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2017-09-04 11:35:20 +0200 |
---|---|---|
committer | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2017-09-13 15:50:51 +0200 |
commit | 55de8d527c66d2a7d87014f89415346722c78644 (patch) | |
tree | 85ab1e4c64903ea0d58e714bbbb812f52c01a680 /server | |
parent | 17dc4c8315d78a346df6e2b93313d6bc72f14992 (diff) | |
download | sonarqube-55de8d527c66d2a7d87014f89415346722c78644.tar.gz sonarqube-55de8d527c66d2a7d87014f89415346722c78644.zip |
SONAR-9741 add HealthChecker#checkCluster with check of ES status
Diffstat (limited to 'server')
10 files changed, 456 insertions, 62 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/health/ClusterHealthCheck.java b/server/sonar-server/src/main/java/org/sonar/server/health/ClusterHealthCheck.java new file mode 100644 index 00000000000..ddb2644d370 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/health/ClusterHealthCheck.java @@ -0,0 +1,27 @@ +/* + * 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.health; + +import java.util.Set; +import org.sonar.cluster.health.NodeHealth; + +public interface ClusterHealthCheck { + Health check(Set<NodeHealth> nodeHealths); +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/health/EsStatusCheck.java b/server/sonar-server/src/main/java/org/sonar/server/health/EsStatusCheck.java new file mode 100644 index 00000000000..1110a9e362e --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/health/EsStatusCheck.java @@ -0,0 +1,55 @@ +/* + * 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.health; + +import org.elasticsearch.cluster.health.ClusterHealthStatus; +import org.sonar.server.es.EsClient; + +import static org.sonar.server.health.Health.newHealthCheckBuilder; + +public abstract class EsStatusCheck { + private static final Health YELLOW_HEALTH = newHealthCheckBuilder() + .setStatus(Health.Status.YELLOW) + .addCause("Elasticsearch status is YELLOW") + .build(); + private static final Health RED_HEALTH = newHealthCheckBuilder() + .setStatus(Health.Status.RED) + .addCause("Elasticsearch status is RED") + .build(); + protected final EsClient esClient; + + EsStatusCheck(EsClient esClient) { + this.esClient = esClient; + } + + Health checkEsStatus() { + ClusterHealthStatus esStatus = esClient.prepareClusterStats().get().getStatus(); + switch (esStatus) { + case GREEN: + return Health.GREEN; + case YELLOW: + return YELLOW_HEALTH; + case RED: + return RED_HEALTH; + default: + throw new IllegalArgumentException("Unsupported Elasticsearch status " + esStatus); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/health/EsStatusClusterCheck.java b/server/sonar-server/src/main/java/org/sonar/server/health/EsStatusClusterCheck.java new file mode 100644 index 00000000000..0c21eecb282 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/health/EsStatusClusterCheck.java @@ -0,0 +1,37 @@ +/* + * 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.health; + +import java.util.Set; +import org.sonar.cluster.health.NodeHealth; +import org.sonar.server.es.EsClient; + +public class EsStatusClusterCheck extends EsStatusCheck implements ClusterHealthCheck { + + public EsStatusClusterCheck(EsClient esClient) { + super(esClient); + } + + @Override + public Health check(Set<NodeHealth> nodeHealths) { + return checkEsStatus(); + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/health/EsStatusNodeCheck.java b/server/sonar-server/src/main/java/org/sonar/server/health/EsStatusNodeCheck.java index 34d190de136..8c4cd5f5357 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/health/EsStatusNodeCheck.java +++ b/server/sonar-server/src/main/java/org/sonar/server/health/EsStatusNodeCheck.java @@ -19,42 +19,19 @@ */ package org.sonar.server.health; -import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.sonar.server.es.EsClient; -import static org.sonar.server.health.Health.newHealthCheckBuilder; - /** * Checks the ElasticSearch cluster status. */ -public class EsStatusNodeCheck implements NodeHealthCheck { - private static final Health YELLOW_HEALTH = newHealthCheckBuilder() - .setStatus(Health.Status.YELLOW) - .addCause("Elasticsearch status is YELLOW") - .build(); - private static final Health RED_HEALTH = newHealthCheckBuilder() - .setStatus(Health.Status.RED) - .addCause("Elasticsearch status is RED") - .build(); - - private final EsClient esClient; +public class EsStatusNodeCheck extends EsStatusCheck implements NodeHealthCheck { public EsStatusNodeCheck(EsClient esClient) { - this.esClient = esClient; + super(esClient); } @Override public Health check() { - ClusterHealthStatus esStatus = esClient.prepareClusterStats().get().getStatus(); - switch (esStatus) { - case GREEN: - return Health.GREEN; - case YELLOW: - return YELLOW_HEALTH; - case RED: - return RED_HEALTH; - default: - throw new IllegalArgumentException("Unsupported Elasticsearch status " + esStatus); - } + return super.checkEsStatus(); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/health/HealthChecker.java b/server/sonar-server/src/main/java/org/sonar/server/health/HealthChecker.java index 2dccef66743..f455e97d52b 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/health/HealthChecker.java +++ b/server/sonar-server/src/main/java/org/sonar/server/health/HealthChecker.java @@ -25,4 +25,11 @@ public interface HealthChecker { * of a cluster. */ Health checkNode(); + + /** + * Perform a check of the health of the SonarQube cluster. + * + * @throws IllegalStateException if clustering is not enabled. + */ + Health checkCluster(); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/health/HealthCheckerImpl.java b/server/sonar-server/src/main/java/org/sonar/server/health/HealthCheckerImpl.java index 0904003062e..630e308c06f 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/health/HealthCheckerImpl.java +++ b/server/sonar-server/src/main/java/org/sonar/server/health/HealthCheckerImpl.java @@ -19,27 +19,64 @@ */ package org.sonar.server.health; -import java.util.Arrays; import java.util.List; +import java.util.Set; import java.util.function.BinaryOperator; import java.util.stream.Stream; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.sonar.cluster.health.NodeHealth; +import org.sonar.cluster.health.SharedHealthState; +import org.sonar.server.platform.WebServer; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.ImmutableList.copyOf; import static org.sonar.server.health.Health.newHealthCheckBuilder; /** - * Implementation of {@link HealthChecker} that executes implementations of {@link NodeHealthCheck} in the container - * and aggregates their results. + * Implementation of {@link HealthChecker} based on {@link NodeHealthCheck} and {@link ClusterHealthCheck} instances + * available in the container. */ public class HealthCheckerImpl implements HealthChecker { + private final WebServer webServer; private final List<NodeHealthCheck> nodeHealthChecks; + private final List<ClusterHealthCheck> clusterHealthChecks; + @CheckForNull + private final SharedHealthState sharedHealthState; - public HealthCheckerImpl(NodeHealthCheck... nodeHealthChecks) { - this.nodeHealthChecks = Arrays.asList(nodeHealthChecks); + /** + * Constructor used by Pico in standalone mode and in safe mode. + */ + public HealthCheckerImpl(WebServer webServer, NodeHealthCheck[] nodeHealthChecks) { + this(webServer, nodeHealthChecks, new ClusterHealthCheck[0], null); + } + + /** + * Constructor used by Pico in cluster mode. + */ + public HealthCheckerImpl(WebServer webServer, NodeHealthCheck[] nodeHealthChecks, ClusterHealthCheck[] clusterHealthChecks, + @Nullable SharedHealthState sharedHealthState) { + this.webServer = webServer; + this.nodeHealthChecks = copyOf(nodeHealthChecks); + this.clusterHealthChecks = copyOf(clusterHealthChecks); + this.sharedHealthState = sharedHealthState; } @Override public Health checkNode() { - return nodeHealthChecks.stream().map(NodeHealthCheck::check) + return nodeHealthChecks.stream() + .map(NodeHealthCheck::check) + .reduce(Health.GREEN, HealthReducer.INSTANCE); + } + + @Override + public Health checkCluster() { + checkState(!webServer.isStandalone(), "Clustering is not enabled"); + checkState(sharedHealthState != null, "HealthState instance can't be null when clustering is enabled"); + + Set<NodeHealth> nodeHealths = sharedHealthState.readAll(); + return clusterHealthChecks.stream() + .map(clusterHealthCheck -> clusterHealthCheck.check(nodeHealths)) .reduce(Health.GREEN, HealthReducer.INSTANCE); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthActionModule.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthActionModule.java index 871a9314ee6..fe7f38efd12 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthActionModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthActionModule.java @@ -22,6 +22,7 @@ package org.sonar.server.platform.ws; import org.sonar.core.platform.Module; import org.sonar.server.health.CeStatusNodeCheck; import org.sonar.server.health.DbConnectionNodeCheck; +import org.sonar.server.health.EsStatusClusterCheck; import org.sonar.server.health.EsStatusNodeCheck; import org.sonar.server.health.HealthCheckerImpl; import org.sonar.server.health.WebServerStatusNodeCheck; @@ -36,6 +37,9 @@ public class HealthActionModule extends Module { EsStatusNodeCheck.class, CeStatusNodeCheck.class); + // ClusterHealthCheck implementations + add(EsStatusClusterCheck.class); + add(HealthCheckerImpl.class, HealthAction.class); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/health/EsStatusClusterCheckTest.java b/server/sonar-server/src/test/java/org/sonar/server/health/EsStatusClusterCheckTest.java new file mode 100644 index 00000000000..e91143f5016 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/health/EsStatusClusterCheckTest.java @@ -0,0 +1,63 @@ +/* + * 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.health; + +import java.util.Random; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.cluster.health.NodeDetails; +import org.sonar.cluster.health.NodeHealth; +import org.sonar.server.es.EsTester; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; + +public class EsStatusClusterCheckTest { + + @Rule + public EsTester esTester = new EsTester(); + + private EsStatusClusterCheck underTest = new EsStatusClusterCheck(esTester.client()); + + @Test + public void check_ignores_NodeHealth_arg_and_returns_GREEN_without_cause_if_ES_cluster_status_is_GREEN() { + Random random = new Random(); + Set<NodeHealth> nodeHealths = IntStream.range(0, random.nextInt(20)) + .mapToObj(i -> NodeHealth.newNodeHealthBuilder() + .setStatus(NodeHealth.Status.values()[random.nextInt(NodeHealth.Status.values().length)]) + .setDetails(NodeDetails.newNodeDetailsBuilder() + .setType(random.nextBoolean() ? NodeDetails.Type.APPLICATION : NodeDetails.Type.SEARCH) + .setName(randomAlphanumeric(23)) + .setHost(randomAlphanumeric(23)) + .setPort(1 + random.nextInt(96)) + .setStarted(1 + random.nextInt(966)) + .build()) + .setDate(1 + random.nextInt(23)) + .build()) + .collect(Collectors.toSet()); + Health health = underTest.check(nodeHealths); + + assertThat(health).isEqualTo(Health.GREEN); + } + +} 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 42c1b93673d..9f4ec3ebe7a 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 @@ -24,96 +24,250 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Random; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; -import org.apache.commons.lang.RandomStringUtils; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.cluster.health.NodeDetails; +import org.sonar.cluster.health.NodeHealth; +import org.sonar.cluster.health.SharedHealthState; +import org.sonar.server.platform.WebServer; +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.sonar.cluster.health.NodeDetails.newNodeDetailsBuilder; +import static org.sonar.cluster.health.NodeHealth.newNodeHealthBuilder; +import static org.sonar.server.health.Health.newHealthCheckBuilder; import static org.sonar.server.health.Health.Status.GREEN; import static org.sonar.server.health.Health.Status.RED; import static org.sonar.server.health.Health.Status.YELLOW; public class HealthCheckerImplTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + private final WebServer webServer = mock(WebServer.class); + private final SharedHealthState sharedHealthState = mock(SharedHealthState.class); private final Random random = new Random(); @Test - public void check_returns_green_status_without_any_cause_when_there_is_no_HealthCheck() { - HealthCheckerImpl underTest = new HealthCheckerImpl(); + public void check_returns_green_status_without_any_cause_when_there_is_no_NodeHealthCheck() { + HealthCheckerImpl underTest = new HealthCheckerImpl(webServer, new NodeHealthCheck[0]); assertThat(underTest.checkNode()).isEqualTo(Health.GREEN); } @Test - public void checkNode_returns_GREEN_status_if_only_GREEN_statuses_returned_by_HealthChecks() { + public void checkNode_returns_GREEN_status_if_only_GREEN_statuses_returned_by_NodeHealthCheck() { List<Health.Status> statuses = IntStream.range(1, 1 + random.nextInt(20)).mapToObj(i -> GREEN).collect(Collectors.toList()); - HealthCheckerImpl underTest = newHealthCheckerImpl(statuses.stream()); + HealthCheckerImpl underTest = newNodeHealthCheckerImpl(statuses.stream()); assertThat(underTest.checkNode().getStatus()) - .describedAs("%s should have been computed from %s statuses", GREEN, statuses) - .isEqualTo(GREEN); + .describedAs("%s should have been computed from %s statuses", GREEN, statuses) + .isEqualTo(GREEN); } @Test - public void checkNode_returns_YELLOW_status_if_only_GREEN_and_at_least_one_YELLOW_statuses_returned_by_HealthChecks() { + public void checkNode_returns_YELLOW_status_if_only_GREEN_and_at_least_one_YELLOW_statuses_returned_by_NodeHealthCheck() { List<Health.Status> statuses = new ArrayList<>(); Stream.concat( - IntStream.range(0, 1 + random.nextInt(20)).mapToObj(i -> YELLOW), // at least 1 YELLOW - IntStream.range(0, random.nextInt(20)).mapToObj(i -> GREEN)).forEach(statuses::add); // between 0 and 19 GREEN + IntStream.range(0, 1 + random.nextInt(20)).mapToObj(i -> YELLOW), // at least 1 YELLOW + IntStream.range(0, random.nextInt(20)).mapToObj(i -> GREEN)).forEach(statuses::add); // between 0 and 19 GREEN Collections.shuffle(statuses); - HealthCheckerImpl underTest = newHealthCheckerImpl(statuses.stream()); + HealthCheckerImpl underTest = newNodeHealthCheckerImpl(statuses.stream()); assertThat(underTest.checkNode().getStatus()) - .describedAs("%s should have been computed from %s statuses", YELLOW, statuses) - .isEqualTo(YELLOW); + .describedAs("%s should have been computed from %s statuses", YELLOW, statuses) + .isEqualTo(YELLOW); } @Test - public void checkNode_returns_RED_status_if_at_least_one_RED_status_returned_by_HealthChecks() { + public void checkNode_returns_RED_status_if_at_least_one_RED_status_returned_by_NodeHealthCheck() { List<Health.Status> statuses = new ArrayList<>(); Stream.of( - IntStream.range(0, 1 + random.nextInt(20)).mapToObj(i -> RED), // at least 1 RED - IntStream.range(0, random.nextInt(20)).mapToObj(i -> YELLOW), // between 0 and 19 YELLOW - IntStream.range(0, random.nextInt(20)).mapToObj(i -> GREEN) // between 0 and 19 GREEN + IntStream.range(0, 1 + random.nextInt(20)).mapToObj(i -> RED), // at least 1 RED + IntStream.range(0, random.nextInt(20)).mapToObj(i -> YELLOW), // between 0 and 19 YELLOW + IntStream.range(0, random.nextInt(20)).mapToObj(i -> GREEN) // between 0 and 19 GREEN ).flatMap(s -> s) - .forEach(statuses::add); + .forEach(statuses::add); Collections.shuffle(statuses); - HealthCheckerImpl underTest = newHealthCheckerImpl(statuses.stream()); + HealthCheckerImpl underTest = newNodeHealthCheckerImpl(statuses.stream()); assertThat(underTest.checkNode().getStatus()) - .describedAs("%s should have been computed from %s statuses", RED, statuses) - .isEqualTo(RED); + .describedAs("%s should have been computed from %s statuses", RED, statuses) + .isEqualTo(RED); } @Test - public void checkNode_returns_causes_of_all_HealthChecks_whichever_their_status() { + public void checkNode_returns_causes_of_all_NodeHealthCheck_whichever_their_status() { NodeHealthCheck[] nodeHealthChecks = IntStream.range(0, 1 + random.nextInt(20)) - .mapToObj(s -> new HardcodedHealthNodeCheck(IntStream.range(0, random.nextInt(3)).mapToObj(i -> RandomStringUtils.randomAlphanumeric(3)).toArray(String[]::new))) - .map(NodeHealthCheck.class::cast) - .toArray(NodeHealthCheck[]::new); + .mapToObj(s -> new HardcodedHealthNodeCheck(IntStream.range(0, random.nextInt(3)).mapToObj(i -> randomAlphanumeric(3)).toArray(String[]::new))) + .map(NodeHealthCheck.class::cast) + .toArray(NodeHealthCheck[]::new); String[] expected = Arrays.stream(nodeHealthChecks).map(NodeHealthCheck::check).flatMap(s -> s.getCauses().stream()).toArray(String[]::new); - HealthCheckerImpl underTest = new HealthCheckerImpl(nodeHealthChecks); + HealthCheckerImpl underTest = new HealthCheckerImpl(webServer, nodeHealthChecks); assertThat(underTest.checkNode().getCauses()).containsOnly(expected); } - private HealthCheckerImpl newHealthCheckerImpl(Stream<Health.Status> statuses) { + @Test + public void checkCluster_fails_with_ISE_in_standalone() { + when(webServer.isStandalone()).thenReturn(true); + HealthCheckerImpl underTest = new HealthCheckerImpl(webServer, new NodeHealthCheck[0], new ClusterHealthCheck[0], sharedHealthState); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Clustering is not enabled"); + + underTest.checkCluster(); + } + + @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); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("HealthState instance can't be null when clustering is enabled"); + + underTest.checkCluster(); + } + + @Test + public void checkCluster_returns_GREEN_when_there_is_no_ClusterHealthCheck() { + when(webServer.isStandalone()).thenReturn(false); + HealthCheckerImpl underTest = new HealthCheckerImpl(webServer, new NodeHealthCheck[0], new ClusterHealthCheck[0], sharedHealthState); + + assertThat(underTest.checkCluster()).isEqualTo(Health.GREEN); + } + + @Test + public void checkCluster_returns_GREEN_status_if_only_GREEN_statuses_returned_by_ClusterHealthChecks() { + when(webServer.isStandalone()).thenReturn(false); + List<Health.Status> statuses = IntStream.range(1, 1 + random.nextInt(20)).mapToObj(i -> GREEN).collect(Collectors.toList()); + HealthCheckerImpl underTest = newClusterHealthCheckerImpl(statuses.stream()); + + assertThat(underTest.checkCluster().getStatus()) + .describedAs("%s should have been computed from %s statuses", GREEN, statuses) + .isEqualTo(GREEN); + } + + @Test + public void checkCluster_returns_YELLOW_status_if_only_GREEN_and_at_least_one_YELLOW_statuses_returned_by_ClusterHealthChecks() { + when(webServer.isStandalone()).thenReturn(false); + List<Health.Status> statuses = new ArrayList<>(); + Stream.concat( + IntStream.range(0, 1 + random.nextInt(20)).mapToObj(i -> YELLOW), // at least 1 YELLOW + IntStream.range(0, random.nextInt(20)).mapToObj(i -> GREEN)).forEach(statuses::add); // between 0 and 19 GREEN + Collections.shuffle(statuses); + HealthCheckerImpl underTest = newClusterHealthCheckerImpl(statuses.stream()); + + assertThat(underTest.checkCluster().getStatus()) + .describedAs("%s should have been computed from %s statuses", YELLOW, statuses) + .isEqualTo(YELLOW); + } + + @Test + public void checkCluster_returns_RED_status_if_at_least_one_RED_status_returned_by_ClusterHealthChecks() { + when(webServer.isStandalone()).thenReturn(false); + List<Health.Status> statuses = new ArrayList<>(); + Stream.of( + IntStream.range(0, 1 + random.nextInt(20)).mapToObj(i -> RED), // at least 1 RED + IntStream.range(0, random.nextInt(20)).mapToObj(i -> YELLOW), // between 0 and 19 YELLOW + IntStream.range(0, random.nextInt(20)).mapToObj(i -> GREEN) // between 0 and 19 GREEN + ).flatMap(s -> s) + .forEach(statuses::add); + Collections.shuffle(statuses); + HealthCheckerImpl underTest = newClusterHealthCheckerImpl(statuses.stream()); + + assertThat(underTest.checkCluster().getStatus()) + .describedAs("%s should have been computed from %s statuses", RED, statuses) + .isEqualTo(RED); + } + + @Test + public void checkCluster_returns_causes_of_all_ClusterHealthChecks_whichever_their_status() { + when(webServer.isStandalone()).thenReturn(false); + List<String[]> causesGroups = IntStream.range(0, 1 + random.nextInt(20)) + .mapToObj(s -> IntStream.range(0, random.nextInt(3)).mapToObj(i -> randomAlphanumeric(3)).toArray(String[]::new)) + .collect(Collectors.toList()); + ClusterHealthCheck[] clusterHealthChecks = causesGroups.stream() + .map(HardcodedHealthClusterCheck::new) + .map(ClusterHealthCheck.class::cast) + .toArray(ClusterHealthCheck[]::new); + String[] expectedCauses = causesGroups.stream().flatMap(Arrays::stream).collect(Collectors.toSet()).stream().toArray(String[]::new); + + HealthCheckerImpl underTest = new HealthCheckerImpl(webServer, new NodeHealthCheck[0], clusterHealthChecks, sharedHealthState); + + assertThat(underTest.checkCluster().getCauses()).containsOnly(expectedCauses); + } + + @Test + public void checkCluster_passes_set_of_NodeHealth_returns_by_HealthState_to_all_ClusterHealthChecks() { + when(webServer.isStandalone()).thenReturn(false); + ClusterHealthCheck[] mockedClusterHealthChecks = IntStream.range(0, 1 + random.nextInt(3)) + .mapToObj(i -> mock(ClusterHealthCheck.class)) + .toArray(ClusterHealthCheck[]::new); + Set<NodeHealth> nodeHealths = IntStream.range(0, 1 + random.nextInt(4)).mapToObj(i -> randomNodeHealth()).collect(Collectors.toSet()); + when(sharedHealthState.readAll()).thenReturn(nodeHealths); + for (ClusterHealthCheck mockedClusterHealthCheck : mockedClusterHealthChecks) { + when(mockedClusterHealthCheck.check(same(nodeHealths))).thenReturn(Health.GREEN); + } + + HealthCheckerImpl underTest = new HealthCheckerImpl(webServer, new NodeHealthCheck[0], mockedClusterHealthChecks, sharedHealthState); + underTest.checkCluster(); + + for (ClusterHealthCheck mockedClusterHealthCheck : mockedClusterHealthChecks) { + verify(mockedClusterHealthCheck).check(same(nodeHealths)); + } + } + + private NodeHealth randomNodeHealth() { + return newNodeHealthBuilder() + .setStatus(NodeHealth.Status.values()[random.nextInt(NodeHealth.Status.values().length)]) + .setDate(1 + random.nextInt(222)) + .setDetails(newNodeDetailsBuilder() + .setType(random.nextBoolean() ? NodeDetails.Type.APPLICATION : NodeDetails.Type.SEARCH) + .setName(randomAlphanumeric(10)) + .setHost(randomAlphanumeric(5)) + .setPort(1 + random.nextInt(333)) + .setStarted(1 + random.nextInt(444)) + .build()) + .build(); + } + + private HealthCheckerImpl newNodeHealthCheckerImpl(Stream<Health.Status> statuses) { Stream<HardcodedHealthNodeCheck> staticHealthCheckStream = statuses.map(HardcodedHealthNodeCheck::new); - return new HealthCheckerImpl(staticHealthCheckStream.map(NodeHealthCheck.class::cast).toArray(NodeHealthCheck[]::new)); + return new HealthCheckerImpl( + webServer, + staticHealthCheckStream.map(NodeHealthCheck.class::cast).toArray(NodeHealthCheck[]::new)); + } + + private HealthCheckerImpl newClusterHealthCheckerImpl(Stream<Health.Status> statuses) { + Stream<HardcodedHealthClusterCheck> staticHealthCheckStream = statuses.map(HardcodedHealthClusterCheck::new); + return new HealthCheckerImpl( + webServer, + new NodeHealthCheck[0], + staticHealthCheckStream.map(ClusterHealthCheck.class::cast).toArray(ClusterHealthCheck[]::new), + sharedHealthState); } private class HardcodedHealthNodeCheck implements NodeHealthCheck { private final Health health; public HardcodedHealthNodeCheck(Health.Status status) { - this.health = Health.newHealthCheckBuilder().setStatus(status).build(); + this.health = newHealthCheckBuilder().setStatus(status).build(); } public HardcodedHealthNodeCheck(String... causes) { - Health.Builder builder = Health.newHealthCheckBuilder().setStatus(Health.Status.values()[random.nextInt(3)]); + Health.Builder builder = newHealthCheckBuilder().setStatus(Health.Status.values()[random.nextInt(3)]); Stream.of(causes).forEach(builder::addCause); this.health = builder.build(); } @@ -123,4 +277,23 @@ public class HealthCheckerImplTest { return health; } } + + private class HardcodedHealthClusterCheck implements ClusterHealthCheck { + private final Health health; + + public HardcodedHealthClusterCheck(Health.Status status) { + this.health = newHealthCheckBuilder().setStatus(status).build(); + } + + public HardcodedHealthClusterCheck(String... causes) { + Health.Builder builder = newHealthCheckBuilder().setStatus(Health.Status.values()[random.nextInt(3)]); + Stream.of(causes).forEach(builder::addCause); + this.health = builder.build(); + } + + @Override + public Health check(Set<NodeHealth> nodeHealths) { + return health; + } + } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/ws/HealthActionModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/HealthActionModuleTest.java index f4bc59280d4..2ee0421796b 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/platform/ws/HealthActionModuleTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/HealthActionModuleTest.java @@ -26,7 +26,9 @@ import org.junit.Test; import org.picocontainer.ComponentAdapter; import org.sonar.core.platform.ComponentContainer; import org.sonar.server.health.CeStatusNodeCheck; +import org.sonar.server.health.ClusterHealthCheck; import org.sonar.server.health.DbConnectionNodeCheck; +import org.sonar.server.health.EsStatusClusterCheck; import org.sonar.server.health.EsStatusNodeCheck; import org.sonar.server.health.HealthCheckerImpl; import org.sonar.server.health.NodeHealthCheck; @@ -63,6 +65,18 @@ public class HealthActionModuleTest { .contains(CeStatusNodeCheck.class); } + @Test + public void verify_installed_ClusterHealthChecks_implementations() { + ComponentContainer container = new ComponentContainer(); + + underTest.configure(container); + + List<Class<?>> checks = classesAddedToContainer(container).stream().filter(ClusterHealthCheck.class::isAssignableFrom).collect(Collectors.toList()); + assertThat(checks) + .hasSize(1) + .contains(EsStatusClusterCheck.class); + } + private List<Class<?>> classesAddedToContainer(ComponentContainer container) { Collection<ComponentAdapter<?>> componentAdapters = container.getPicoContainer().getComponentAdapters(); return componentAdapters.stream().map(ComponentAdapter::getComponentImplementation).collect(Collectors.toList()); |