From a678b4560e425cf400291c15aa081a2e21dd7333 Mon Sep 17 00:00:00 2001 From: tomekl007 Date: Fri, 21 Aug 2020 11:16:17 +0200 Subject: [PATCH 1/7] Improve CassandraHealthIndicator with more robust mechanism fixes gh-22901 --- .../CassandraDriverHealthIndicator.java | 27 ++-- ...assandraDriverReactiveHealthIndicator.java | 28 +++- .../cassandra/CassandraHealthIndicator.java | 32 ++++- .../CassandraReactiveHealthIndicator.java | 41 +++++- .../CassandraDriverHealthIndicatorTests.java | 94 ++++++++++++-- ...draDriverReactiveHealthIndicatorTests.java | 120 ++++++++++++------ .../CassandraHealthIndicatorTests.java | 99 +++++++++++++-- ...CassandraReactiveHealthIndicatorTests.java | 116 +++++++++++++++-- 8 files changed, 457 insertions(+), 100 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicator.java index bbb1468c0e71..b18ac9846843 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicator.java @@ -16,10 +16,12 @@ package org.springframework.boot.actuate.cassandra; -import com.datastax.oss.driver.api.core.ConsistencyLevel; +import java.util.Collection; +import java.util.Objects; + import com.datastax.oss.driver.api.core.CqlSession; -import com.datastax.oss.driver.api.core.cql.Row; -import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; @@ -31,13 +33,11 @@ * Cassandra data stores. * * @author Alexandre Dutra + * @author Tomasz Lelek * @since 2.4.0 */ public class CassandraDriverHealthIndicator extends AbstractHealthIndicator { - private static final SimpleStatement SELECT = SimpleStatement - .newInstance("SELECT release_version FROM system.local").setConsistencyLevel(ConsistencyLevel.LOCAL_ONE); - private final CqlSession session; /** @@ -52,11 +52,18 @@ public CassandraDriverHealthIndicator(CqlSession session) { @Override protected void doHealthCheck(Health.Builder builder) throws Exception { - Row row = this.session.execute(SELECT).one(); - builder.up(); - if (row != null && !row.isNull(0)) { - builder.withDetail("version", row.getString(0)); + Collection nodes = this.session.getMetadata().getNodes().values(); + boolean atLeastOneUp = nodes.stream().map(Node::getState).anyMatch((state) -> state == NodeState.UP); + if (atLeastOneUp) { + builder.up(); } + else { + builder.down(); + } + + // fill details with version of the first node (if the version is not null) + nodes.stream().map(Node::getCassandraVersion).filter(Objects::nonNull).findFirst() + .ifPresent((version) -> builder.withDetail("version", version)); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicator.java index 40483ca04ef3..e8dd6c14d506 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicator.java @@ -15,9 +15,12 @@ */ package org.springframework.boot.actuate.cassandra; -import com.datastax.oss.driver.api.core.ConsistencyLevel; +import java.util.Collection; +import java.util.Objects; + import com.datastax.oss.driver.api.core.CqlSession; -import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; import reactor.core.publisher.Mono; import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator; @@ -30,13 +33,11 @@ * for Cassandra data stores. * * @author Alexandre Dutra + * @author Tomasz Lelek * @since 2.4.0 */ public class CassandraDriverReactiveHealthIndicator extends AbstractReactiveHealthIndicator { - private static final SimpleStatement SELECT = SimpleStatement - .newInstance("SELECT release_version FROM system.local").setConsistencyLevel(ConsistencyLevel.LOCAL_ONE); - private final CqlSession session; /** @@ -51,8 +52,21 @@ public CassandraDriverReactiveHealthIndicator(CqlSession session) { @Override protected Mono doHealthCheck(Health.Builder builder) { - return Mono.from(this.session.executeReactive(SELECT)) - .map((row) -> builder.up().withDetail("version", row.getString(0)).build()); + return Mono.fromSupplier(() -> { + Collection nodes = this.session.getMetadata().getNodes().values(); + boolean atLeastOneUp = nodes.stream().map(Node::getState).anyMatch((state) -> state == NodeState.UP); + if (atLeastOneUp) { + builder.up(); + } + else { + builder.down(); + } + + // fill details with version of the first node (if the version is not null) + nodes.stream().map(Node::getCassandraVersion).filter(Objects::nonNull).findFirst() + .ifPresent((version) -> builder.withDetail("version", version)); + return builder.build(); + }); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicator.java index a02d5fe39893..3cffebeffacf 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicator.java @@ -16,8 +16,13 @@ package org.springframework.boot.actuate.cassandra; -import com.datastax.oss.driver.api.core.ConsistencyLevel; -import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import java.util.Collection; +import java.util.Objects; + +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import com.datastax.oss.driver.api.core.session.Session; import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; @@ -31,13 +36,11 @@ * * @author Julien Dubois * @author Alexandre Dutra + * @author Tomasz Lelek * @since 2.0.0 */ public class CassandraHealthIndicator extends AbstractHealthIndicator { - private static final SimpleStatement SELECT = SimpleStatement - .newInstance("SELECT release_version FROM system.local").setConsistencyLevel(ConsistencyLevel.LOCAL_ONE); - private CassandraOperations cassandraOperations; public CassandraHealthIndicator() { @@ -56,8 +59,23 @@ public CassandraHealthIndicator(CassandraOperations cassandraOperations) { @Override protected void doHealthCheck(Health.Builder builder) throws Exception { - String version = this.cassandraOperations.getCqlOperations().queryForObject(SELECT, String.class); - builder.up().withDetail("version", version); + Metadata metadata = this.cassandraOperations.getCqlOperations().execute(Session::getMetadata); + if (metadata == null) { + throw new IllegalStateException("The CqlSession metadata was null; cannot perform health check."); + } + Collection nodes = metadata.getNodes().values(); + boolean atLeastOneUp = nodes.stream().map(Node::getState).anyMatch((state) -> state == NodeState.UP); + if (atLeastOneUp) { + builder.up(); + } + else { + builder.down(); + } + + // fill details with version of the first node (if the version is not null) + nodes.stream().map(Node::getCassandraVersion).filter(Objects::nonNull).findFirst() + .ifPresent((version) -> builder.withDetail("version", version)); + } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicator.java index eed0b2df7180..1745700ea03f 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicator.java @@ -15,27 +15,32 @@ */ package org.springframework.boot.actuate.cassandra; -import com.datastax.oss.driver.api.core.ConsistencyLevel; -import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import java.util.Collection; +import java.util.Objects; + +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import com.datastax.oss.driver.internal.core.context.DefaultDriverContext; import reactor.core.publisher.Mono; import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator; import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Health.Builder; import org.springframework.boot.actuate.health.ReactiveHealthIndicator; import org.springframework.data.cassandra.core.ReactiveCassandraOperations; +import org.springframework.data.cassandra.core.cql.ReactiveSessionCallback; import org.springframework.util.Assert; /** * A {@link ReactiveHealthIndicator} for Cassandra. * * @author Artsiom Yudovin + * @author Tomasz Lelek * @since 2.1.0 */ public class CassandraReactiveHealthIndicator extends AbstractReactiveHealthIndicator { - private static final SimpleStatement SELECT = SimpleStatement - .newInstance("SELECT release_version FROM system.local").setConsistencyLevel(ConsistencyLevel.LOCAL_ONE); - private final ReactiveCassandraOperations reactiveCassandraOperations; /** @@ -50,8 +55,30 @@ public CassandraReactiveHealthIndicator(ReactiveCassandraOperations reactiveCass @Override protected Mono doHealthCheck(Health.Builder builder) { - return this.reactiveCassandraOperations.getReactiveCqlOperations().queryForObject(SELECT, String.class) - .map((version) -> builder.up().withDetail("version", version).build()).single(); + + return this.reactiveCassandraOperations.getReactiveCqlOperations().execute(extractMetadata()).single() + .map((metadata) -> buildHealth(builder, metadata)); + } + + protected Health buildHealth(Builder builder, Metadata metadata) { + Collection nodes = metadata.getNodes().values(); + boolean atLeastOneUp = nodes.stream().map(Node::getState).anyMatch((state) -> state == NodeState.UP); + if (atLeastOneUp) { + builder.up(); + } + else { + builder.down(); + } + + // fill details with version of the first node (if the version is not null) + nodes.stream().map(Node::getCassandraVersion).filter(Objects::nonNull).findFirst() + .ifPresent((version) -> builder.withDetail("version", version)); + return builder.build(); + } + + protected ReactiveSessionCallback extractMetadata() { + return (session) -> Mono + .fromSupplier(() -> ((DefaultDriverContext) session.getContext()).getMetadataManager().getMetadata()); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicatorTests.java index 875edd64c34f..04060b7053f6 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicatorTests.java @@ -16,26 +16,36 @@ package org.springframework.boot.actuate.cassandra; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Stream; + import com.datastax.oss.driver.api.core.CqlSession; import com.datastax.oss.driver.api.core.DriverTimeoutException; -import com.datastax.oss.driver.api.core.cql.ResultSet; -import com.datastax.oss.driver.api.core.cql.Row; -import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.Version; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Status; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import static org.mockito.BDDMockito.mock; +import static org.mockito.BDDMockito.when; /** * Tests for {@link CassandraDriverHealthIndicator}. * * @author Alexandre Dutra + * @author Tomasz Lelek * @since 2.4.0 */ class CassandraDriverHealthIndicatorTests { @@ -45,25 +55,72 @@ void createWhenCqlSessionIsNullShouldThrowException() { assertThatIllegalArgumentException().isThrownBy(() -> new CassandraDriverHealthIndicator(null)); } + @ParameterizedTest + @MethodSource + void reportCassandraHealthCheck(Map nodes, Status expectedStatus) { + CqlSession session = mock(CqlSession.class); + Metadata metadata = mock(Metadata.class); + when(session.getMetadata()).thenReturn(metadata); + when(metadata.getNodes()).thenReturn(nodes); + + CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(expectedStatus); + } + + static Stream reportCassandraHealthCheck() { + Node healthyNode = mock(Node.class); + when(healthyNode.getState()).thenReturn(NodeState.UP); + Node unhealthyNode = mock(Node.class); + when(unhealthyNode.getState()).thenReturn(NodeState.DOWN); + Node unknownNode = mock(Node.class); + when(unknownNode.getState()).thenReturn(NodeState.UNKNOWN); + Node forcedDownNode = mock(Node.class); + when(forcedDownNode.getState()).thenReturn(NodeState.FORCED_DOWN); + return Stream.builder().add(Arguments.arguments(createNodes(healthyNode), Status.UP)) + .add(Arguments.arguments(createNodes(unhealthyNode), Status.DOWN)) + .add(Arguments.arguments(createNodes(unknownNode), Status.DOWN)) + .add(Arguments.arguments(createNodes(forcedDownNode), Status.DOWN)) + .add(Arguments.arguments(createNodes(healthyNode, unhealthyNode), Status.UP)) + .add(Arguments.arguments(createNodes(healthyNode, unknownNode), Status.UP)) + .add(Arguments.arguments(createNodes(healthyNode, forcedDownNode), Status.UP)).build(); + } + @Test - void healthWithCassandraUp() { + void addVersionToDetailsIfReportedNotNull() { CqlSession session = mock(CqlSession.class); - ResultSet resultSet = mock(ResultSet.class); - Row row = mock(Row.class); - given(session.execute(any(SimpleStatement.class))).willReturn(resultSet); - given(resultSet.one()).willReturn(row); - given(row.isNull(0)).willReturn(false); - given(row.getString(0)).willReturn("1.0.0"); + Metadata metadata = mock(Metadata.class); + when(session.getMetadata()).thenReturn(metadata); + Node node = mock(Node.class); + when(node.getState()).thenReturn(NodeState.UP); + when(node.getCassandraVersion()).thenReturn(Version.V4_0_0); + when(metadata.getNodes()).thenReturn(createNodes(node)); + CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session); Health health = healthIndicator.health(); assertThat(health.getStatus()).isEqualTo(Status.UP); - assertThat(health.getDetails().get("version")).isEqualTo("1.0.0"); + assertThat(health.getDetails().get("version")).isEqualTo(Version.V4_0_0); + } + + @Test + void doNotAddVersionToDetailsIfReportedNull() { + CqlSession session = mock(CqlSession.class); + Metadata metadata = mock(Metadata.class); + when(session.getMetadata()).thenReturn(metadata); + Node node = mock(Node.class); + when(node.getState()).thenReturn(NodeState.UP); + when(metadata.getNodes()).thenReturn(createNodes(node)); + + CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails().get("version")).isNull(); } @Test void healthWithCassandraDown() { CqlSession session = mock(CqlSession.class); - given(session.execute(any(SimpleStatement.class))).willThrow(new DriverTimeoutException("Test Exception")); + given(session.getMetadata()).willThrow(new DriverTimeoutException("Test Exception")); CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session); Health health = healthIndicator.health(); assertThat(health.getStatus()).isEqualTo(Status.DOWN); @@ -71,4 +128,13 @@ void healthWithCassandraDown() { .isEqualTo(DriverTimeoutException.class.getName() + ": Test Exception"); } + private static Map createNodes(Node... nodes) { + Map nodesMap = new HashMap<>(); + for (Node n : nodes) { + nodesMap.put(UUID.randomUUID(), n); + } + + return nodesMap; + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicatorTests.java index 6cf98f77ec48..82ed2861c453 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicatorTests.java @@ -15,15 +15,21 @@ */ package org.springframework.boot.actuate.cassandra; -import com.datastax.dse.driver.api.core.cql.reactive.ReactiveResultSet; -import com.datastax.dse.driver.api.core.cql.reactive.ReactiveRow; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Stream; + import com.datastax.oss.driver.api.core.CqlSession; import com.datastax.oss.driver.api.core.DriverTimeoutException; -import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.Version; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; import org.junit.jupiter.api.Test; -import org.mockito.stubbing.Answer; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -32,15 +38,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.doAnswer; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.mock; +import static org.mockito.BDDMockito.when; /** * Tests for {@link CassandraDriverReactiveHealthIndicator}. * * @author Alexandre Dutra + * @author Tomasz Lelek * @since 2.4.0 */ class CassandraDriverReactiveHealthIndicatorTests { @@ -50,29 +56,79 @@ void createWhenCqlSessionIsNullShouldThrowException() { assertThatIllegalArgumentException().isThrownBy(() -> new CassandraDriverReactiveHealthIndicator(null)); } + @ParameterizedTest + @MethodSource + void reportCassandraHealthCheck(Map nodes, Status expectedStatus) { + CqlSession session = mock(CqlSession.class); + Metadata metadata = mock(Metadata.class); + when(session.getMetadata()).thenReturn(metadata); + when(metadata.getNodes()).thenReturn(nodes); + + CassandraDriverReactiveHealthIndicator healthIndicator = new CassandraDriverReactiveHealthIndicator(session); + Mono health = healthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> assertThat(h.getStatus()).isEqualTo(expectedStatus)) + .verifyComplete(); + } + + static Stream reportCassandraHealthCheck() { + Node healthyNode = mock(Node.class); + when(healthyNode.getState()).thenReturn(NodeState.UP); + Node unhealthyNode = mock(Node.class); + when(unhealthyNode.getState()).thenReturn(NodeState.DOWN); + Node unknownNode = mock(Node.class); + when(unknownNode.getState()).thenReturn(NodeState.UNKNOWN); + Node forcedDownNode = mock(Node.class); + when(forcedDownNode.getState()).thenReturn(NodeState.FORCED_DOWN); + return Stream.builder().add(Arguments.arguments(createNodes(healthyNode), Status.UP)) + .add(Arguments.arguments(createNodes(unhealthyNode), Status.DOWN)) + .add(Arguments.arguments(createNodes(unknownNode), Status.DOWN)) + .add(Arguments.arguments(createNodes(forcedDownNode), Status.DOWN)) + .add(Arguments.arguments(createNodes(healthyNode, unhealthyNode), Status.UP)) + .add(Arguments.arguments(createNodes(healthyNode, unknownNode), Status.UP)) + .add(Arguments.arguments(createNodes(healthyNode, forcedDownNode), Status.UP)).build(); + } + @Test - void testCassandraIsUp() { + void addVersionToDetailsIfReportedNotNull() { CqlSession session = mock(CqlSession.class); - ReactiveResultSet results = mock(ReactiveResultSet.class); - ReactiveRow row = mock(ReactiveRow.class); - given(session.executeReactive(any(SimpleStatement.class))).willReturn(results); - doAnswer(mockReactiveResultSetBehavior(row)).when(results).subscribe(any()); - given(row.getString(0)).willReturn("6.0.0"); - CassandraDriverReactiveHealthIndicator cassandraReactiveHealthIndicator = new CassandraDriverReactiveHealthIndicator( - session); - Mono health = cassandraReactiveHealthIndicator.health(); + Metadata metadata = mock(Metadata.class); + when(session.getMetadata()).thenReturn(metadata); + Node node = mock(Node.class); + when(node.getState()).thenReturn(NodeState.UP); + when(node.getCassandraVersion()).thenReturn(Version.V4_0_0); + when(metadata.getNodes()).thenReturn(createNodes(node)); + + CassandraDriverReactiveHealthIndicator healthIndicator = new CassandraDriverReactiveHealthIndicator(session); + Mono health = healthIndicator.health(); StepVerifier.create(health).consumeNextWith((h) -> { assertThat(h.getStatus()).isEqualTo(Status.UP); assertThat(h.getDetails()).containsOnlyKeys("version"); - assertThat(h.getDetails().get("version")).isEqualTo("6.0.0"); + assertThat(h.getDetails().get("version")).isEqualTo(Version.V4_0_0); + }).verifyComplete(); + } + + @Test + void doNotAddVersionToDetailsIfReportedNull() { + CqlSession session = mock(CqlSession.class); + Metadata metadata = mock(Metadata.class); + when(session.getMetadata()).thenReturn(metadata); + Node node = mock(Node.class); + when(node.getState()).thenReturn(NodeState.UP); + when(metadata.getNodes()).thenReturn(createNodes(node)); + + CassandraDriverReactiveHealthIndicator healthIndicator = new CassandraDriverReactiveHealthIndicator(session); + Mono health = healthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> { + assertThat(h.getStatus()).isEqualTo(Status.UP); + assertThat(h.getDetails().get("version")).isNull(); }).verifyComplete(); } @Test void testCassandraIsDown() { CqlSession session = mock(CqlSession.class); - given(session.executeReactive(any(SimpleStatement.class))) - .willThrow(new DriverTimeoutException("Test Exception")); + given(session.getMetadata()).willThrow(new DriverTimeoutException("Test Exception")); + CassandraDriverReactiveHealthIndicator cassandraReactiveHealthIndicator = new CassandraDriverReactiveHealthIndicator( session); Mono health = cassandraReactiveHealthIndicator.health(); @@ -84,23 +140,13 @@ void testCassandraIsDown() { }).verifyComplete(); } - private Answer mockReactiveResultSetBehavior(ReactiveRow row) { - return (invocation) -> { - Subscriber subscriber = invocation.getArgument(0); - Subscription s = new Subscription() { - @Override - public void request(long n) { - subscriber.onNext(row); - subscriber.onComplete(); - } - - @Override - public void cancel() { - } - }; - subscriber.onSubscribe(s); - return null; - }; + private static Map createNodes(Node... nodes) { + Map nodesMap = new HashMap<>(); + for (Node n : nodes) { + nodesMap.put(UUID.randomUUID(), n); + } + + return nodesMap; } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicatorTests.java index d07274bed93d..f47989096eeb 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicatorTests.java @@ -16,45 +16,119 @@ package org.springframework.boot.actuate.cassandra; -import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Stream; + +import com.datastax.oss.driver.api.core.Version; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Status; import org.springframework.data.cassandra.CassandraInternalException; import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.core.cql.CqlOperations; +import org.springframework.data.cassandra.core.cql.SessionCallback; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import static org.mockito.BDDMockito.mock; +import static org.mockito.BDDMockito.when; /** * Tests for {@link CassandraHealthIndicator}. * * @author Oleksii Bondar * @author Stephane Nicoll + * @author Tomasz Lelek */ class CassandraHealthIndicatorTests { @Test - void createWhenCassandraOperationsIsNullShouldThrowException() { + void createWhenCqlSessionIsNullShouldThrowException() { assertThatIllegalArgumentException().isThrownBy(() -> new CassandraHealthIndicator(null)); } - @Test - void healthWithCassandraUp() { + @ParameterizedTest + @MethodSource + @SuppressWarnings("unchecked") + void reportCassandraHealthCheck(Map nodes, Status expectedStatus) { + Metadata metadata = mock(Metadata.class); + when(metadata.getNodes()).thenReturn(nodes); + CqlOperations cqlOperations = mock(CqlOperations.class); CassandraOperations cassandraOperations = mock(CassandraOperations.class); + given(cassandraOperations.getCqlOperations()).willReturn(cqlOperations); + when(cqlOperations.execute(any(SessionCallback.class))).thenReturn(metadata); + + CassandraHealthIndicator healthIndicator = new CassandraHealthIndicator(cassandraOperations); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(expectedStatus); + } + + static Stream reportCassandraHealthCheck() { + Node healthyNode = mock(Node.class); + when(healthyNode.getState()).thenReturn(NodeState.UP); + Node unhealthyNode = mock(Node.class); + when(unhealthyNode.getState()).thenReturn(NodeState.DOWN); + Node unknownNode = mock(Node.class); + when(unknownNode.getState()).thenReturn(NodeState.UNKNOWN); + Node forcedDownNode = mock(Node.class); + when(forcedDownNode.getState()).thenReturn(NodeState.FORCED_DOWN); + return Stream.builder().add(Arguments.arguments(createNodes(healthyNode), Status.UP)) + .add(Arguments.arguments(createNodes(unhealthyNode), Status.DOWN)) + .add(Arguments.arguments(createNodes(unknownNode), Status.DOWN)) + .add(Arguments.arguments(createNodes(forcedDownNode), Status.DOWN)) + .add(Arguments.arguments(createNodes(healthyNode, unhealthyNode), Status.UP)) + .add(Arguments.arguments(createNodes(healthyNode, unknownNode), Status.UP)) + .add(Arguments.arguments(createNodes(healthyNode, forcedDownNode), Status.UP)).build(); + } + + @Test + @SuppressWarnings("unchecked") + void addVersionToDetailsIfReportedNotNull() { + Metadata metadata = mock(Metadata.class); + Node node = mock(Node.class); + when(node.getState()).thenReturn(NodeState.UP); + when(node.getCassandraVersion()).thenReturn(Version.V4_0_0); + when(metadata.getNodes()).thenReturn(createNodes(node)); + CqlOperations cqlOperations = mock(CqlOperations.class); + CassandraOperations cassandraOperations = mock(CassandraOperations.class); + given(cassandraOperations.getCqlOperations()).willReturn(cqlOperations); + when(cqlOperations.execute(any(SessionCallback.class))).thenReturn(metadata); + CassandraHealthIndicator healthIndicator = new CassandraHealthIndicator(cassandraOperations); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails().get("version")).isEqualTo(Version.V4_0_0); + } + + @Test + @SuppressWarnings("unchecked") + void doNotAddVersionToDetailsIfReportedNull() { + Metadata metadata = mock(Metadata.class); + Node node = mock(Node.class); + when(node.getState()).thenReturn(NodeState.UP); + when(metadata.getNodes()).thenReturn(createNodes(node)); + + CqlOperations cqlOperations = mock(CqlOperations.class); + CassandraOperations cassandraOperations = mock(CassandraOperations.class); given(cassandraOperations.getCqlOperations()).willReturn(cqlOperations); - given(cqlOperations.queryForObject(any(SimpleStatement.class), eq(String.class))).willReturn("1.0.0"); + when(cqlOperations.execute(any(SessionCallback.class))).thenReturn(metadata); + + CassandraHealthIndicator healthIndicator = new CassandraHealthIndicator(cassandraOperations); Health health = healthIndicator.health(); assertThat(health.getStatus()).isEqualTo(Status.UP); - assertThat(health.getDetails().get("version")).isEqualTo("1.0.0"); + assertThat(health.getDetails().get("version")).isNull(); } @Test @@ -68,4 +142,13 @@ void healthWithCassandraDown() { .isEqualTo(CassandraInternalException.class.getName() + ": Connection failed"); } + private static Map createNodes(Node... nodes) { + Map nodesMap = new HashMap<>(); + for (Node n : nodes) { + nodesMap.put(UUID.randomUUID(), n); + } + + return nodesMap; + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicatorTests.java index b795e3d783b5..41dfecb0b815 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicatorTests.java @@ -15,8 +15,20 @@ */ package org.springframework.boot.actuate.cassandra; -import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Stream; + +import com.datastax.oss.driver.api.core.Version; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -25,38 +37,113 @@ import org.springframework.data.cassandra.CassandraInternalException; import org.springframework.data.cassandra.core.ReactiveCassandraOperations; import org.springframework.data.cassandra.core.cql.ReactiveCqlOperations; +import org.springframework.data.cassandra.core.cql.ReactiveSessionCallback; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; +import static org.mockito.BDDMockito.mock; +import static org.mockito.BDDMockito.when; /** * Tests for {@link CassandraReactiveHealthIndicator}. * * @author Artsiom Yudovin + * @author Tomasz Lelek */ class CassandraReactiveHealthIndicatorTests { @Test - void testCassandraIsUp() { + void createWhenCqlSessionIsNullShouldThrowException() { + assertThatIllegalArgumentException().isThrownBy(() -> new CassandraReactiveHealthIndicator(null)); + } + + @ParameterizedTest + @MethodSource + @SuppressWarnings("unchecked") + void reportCassandraHealthCheck(Map nodes, Status expectedStatus) { + Metadata metadata = mock(Metadata.class); + when(metadata.getNodes()).thenReturn(nodes); ReactiveCqlOperations reactiveCqlOperations = mock(ReactiveCqlOperations.class); - given(reactiveCqlOperations.queryForObject(any(SimpleStatement.class), eq(String.class))) - .willReturn(Mono.just("6.0.0")); ReactiveCassandraOperations reactiveCassandraOperations = mock(ReactiveCassandraOperations.class); given(reactiveCassandraOperations.getReactiveCqlOperations()).willReturn(reactiveCqlOperations); + when(reactiveCqlOperations.execute(any(ReactiveSessionCallback.class))) + .thenReturn((Flux.from(Mono.fromSupplier(() -> metadata)))); - CassandraReactiveHealthIndicator cassandraReactiveHealthIndicator = new CassandraReactiveHealthIndicator( + CassandraReactiveHealthIndicator healthIndicator = new CassandraReactiveHealthIndicator( reactiveCassandraOperations); - Mono health = cassandraReactiveHealthIndicator.health(); + Mono health = healthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> assertThat(h.getStatus()).isEqualTo(expectedStatus)) + .verifyComplete(); + } + + static Stream reportCassandraHealthCheck() { + Node healthyNode = mock(Node.class); + when(healthyNode.getState()).thenReturn(NodeState.UP); + Node unhealthyNode = mock(Node.class); + when(unhealthyNode.getState()).thenReturn(NodeState.DOWN); + Node unknownNode = mock(Node.class); + when(unknownNode.getState()).thenReturn(NodeState.UNKNOWN); + Node forcedDownNode = mock(Node.class); + when(forcedDownNode.getState()).thenReturn(NodeState.FORCED_DOWN); + return Stream.builder().add(Arguments.arguments(createNodes(healthyNode), Status.UP)) + .add(Arguments.arguments(createNodes(unhealthyNode), Status.DOWN)) + .add(Arguments.arguments(createNodes(unknownNode), Status.DOWN)) + .add(Arguments.arguments(createNodes(forcedDownNode), Status.DOWN)) + .add(Arguments.arguments(createNodes(healthyNode, unhealthyNode), Status.UP)) + .add(Arguments.arguments(createNodes(healthyNode, unknownNode), Status.UP)) + .add(Arguments.arguments(createNodes(healthyNode, forcedDownNode), Status.UP)).build(); + } + + @Test + @SuppressWarnings("unchecked") + void addVersionToDetailsIfReportedNotNull() { + Metadata metadata = mock(Metadata.class); + Node node = mock(Node.class); + when(node.getState()).thenReturn(NodeState.UP); + when(node.getCassandraVersion()).thenReturn(Version.V4_0_0); + when(metadata.getNodes()).thenReturn(createNodes(node)); + + ReactiveCqlOperations reactiveCqlOperations = mock(ReactiveCqlOperations.class); + ReactiveCassandraOperations reactiveCassandraOperations = mock(ReactiveCassandraOperations.class); + given(reactiveCassandraOperations.getReactiveCqlOperations()).willReturn(reactiveCqlOperations); + when(reactiveCqlOperations.execute(any(ReactiveSessionCallback.class))) + .thenReturn((Flux.from(Mono.fromSupplier(() -> metadata)))); + + CassandraReactiveHealthIndicator healthIndicator = new CassandraReactiveHealthIndicator( + reactiveCassandraOperations); + Mono health = healthIndicator.health(); StepVerifier.create(health).consumeNextWith((h) -> { assertThat(h.getStatus()).isEqualTo(Status.UP); - assertThat(h.getDetails()).containsOnlyKeys("version"); - assertThat(h.getDetails().get("version")).isEqualTo("6.0.0"); + assertThat(h.getDetails().get("version")).isEqualTo(Version.V4_0_0); }).verifyComplete(); } + @Test + @SuppressWarnings("unchecked") + void doNotAddVersionToDetailsIfReportedNull() { + Metadata metadata = mock(Metadata.class); + Node node = mock(Node.class); + when(node.getState()).thenReturn(NodeState.UP); + when(metadata.getNodes()).thenReturn(createNodes(node)); + + ReactiveCqlOperations reactiveCqlOperations = mock(ReactiveCqlOperations.class); + ReactiveCassandraOperations reactiveCassandraOperations = mock(ReactiveCassandraOperations.class); + given(reactiveCassandraOperations.getReactiveCqlOperations()).willReturn(reactiveCqlOperations); + when(reactiveCqlOperations.execute(any(ReactiveSessionCallback.class))) + .thenReturn((Flux.from(Mono.fromSupplier(() -> metadata)))); + + CassandraReactiveHealthIndicator healthIndicator = new CassandraReactiveHealthIndicator( + reactiveCassandraOperations); + Mono health = healthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> { + assertThat(h.getStatus()).isEqualTo(Status.UP); + assertThat(h.getDetails().get("version")).isNull(); + }).verifyComplete(); + + } + @Test void testCassandraIsDown() { ReactiveCassandraOperations reactiveCassandraOperations = mock(ReactiveCassandraOperations.class); @@ -74,4 +161,13 @@ void testCassandraIsDown() { }).verifyComplete(); } + private static Map createNodes(Node... nodes) { + Map nodesMap = new HashMap<>(); + for (Node n : nodes) { + nodesMap.put(UUID.randomUUID(), n); + } + + return nodesMap; + } + } From ab809e34ec0aab76589ac0b3b10a7ccd20dd466c Mon Sep 17 00:00:00 2001 From: Alexandre Dutra Date: Thu, 1 Oct 2020 10:29:34 +0200 Subject: [PATCH 2/7] Use ternary operator to set the builder status --- .../actuate/cassandra/CassandraDriverHealthIndicator.java | 8 ++------ .../cassandra/CassandraDriverReactiveHealthIndicator.java | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicator.java index b18ac9846843..7255f4e2a5f5 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicator.java @@ -26,6 +26,7 @@ import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.Status; import org.springframework.util.Assert; /** @@ -54,12 +55,7 @@ public CassandraDriverHealthIndicator(CqlSession session) { protected void doHealthCheck(Health.Builder builder) throws Exception { Collection nodes = this.session.getMetadata().getNodes().values(); boolean atLeastOneUp = nodes.stream().map(Node::getState).anyMatch((state) -> state == NodeState.UP); - if (atLeastOneUp) { - builder.up(); - } - else { - builder.down(); - } + builder.status(atLeastOneUp ? Status.UP : Status.DOWN); // fill details with version of the first node (if the version is not null) nodes.stream().map(Node::getCassandraVersion).filter(Objects::nonNull).findFirst() diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicator.java index e8dd6c14d506..867c6798de22 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicator.java @@ -26,6 +26,7 @@ import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.ReactiveHealthIndicator; +import org.springframework.boot.actuate.health.Status; import org.springframework.util.Assert; /** @@ -55,12 +56,7 @@ protected Mono doHealthCheck(Health.Builder builder) { return Mono.fromSupplier(() -> { Collection nodes = this.session.getMetadata().getNodes().values(); boolean atLeastOneUp = nodes.stream().map(Node::getState).anyMatch((state) -> state == NodeState.UP); - if (atLeastOneUp) { - builder.up(); - } - else { - builder.down(); - } + builder.status(atLeastOneUp ? Status.UP : Status.DOWN); // fill details with version of the first node (if the version is not null) nodes.stream().map(Node::getCassandraVersion).filter(Objects::nonNull).findFirst() From ca04c9e2d7fb564a92bd1f4dab2667bafeb39e71 Mon Sep 17 00:00:00 2001 From: Alexandre Dutra Date: Thu, 1 Oct 2020 10:29:53 +0200 Subject: [PATCH 3/7] Revert changes to deprecated classes --- .../cassandra/CassandraHealthIndicator.java | 32 ++--- .../CassandraReactiveHealthIndicator.java | 41 ++----- .../CassandraHealthIndicatorTests.java | 99 ++------------- ...CassandraReactiveHealthIndicatorTests.java | 116 ++---------------- 4 files changed, 32 insertions(+), 256 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicator.java index 3cffebeffacf..a02d5fe39893 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicator.java @@ -16,13 +16,8 @@ package org.springframework.boot.actuate.cassandra; -import java.util.Collection; -import java.util.Objects; - -import com.datastax.oss.driver.api.core.metadata.Metadata; -import com.datastax.oss.driver.api.core.metadata.Node; -import com.datastax.oss.driver.api.core.metadata.NodeState; -import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; @@ -36,11 +31,13 @@ * * @author Julien Dubois * @author Alexandre Dutra - * @author Tomasz Lelek * @since 2.0.0 */ public class CassandraHealthIndicator extends AbstractHealthIndicator { + private static final SimpleStatement SELECT = SimpleStatement + .newInstance("SELECT release_version FROM system.local").setConsistencyLevel(ConsistencyLevel.LOCAL_ONE); + private CassandraOperations cassandraOperations; public CassandraHealthIndicator() { @@ -59,23 +56,8 @@ public CassandraHealthIndicator(CassandraOperations cassandraOperations) { @Override protected void doHealthCheck(Health.Builder builder) throws Exception { - Metadata metadata = this.cassandraOperations.getCqlOperations().execute(Session::getMetadata); - if (metadata == null) { - throw new IllegalStateException("The CqlSession metadata was null; cannot perform health check."); - } - Collection nodes = metadata.getNodes().values(); - boolean atLeastOneUp = nodes.stream().map(Node::getState).anyMatch((state) -> state == NodeState.UP); - if (atLeastOneUp) { - builder.up(); - } - else { - builder.down(); - } - - // fill details with version of the first node (if the version is not null) - nodes.stream().map(Node::getCassandraVersion).filter(Objects::nonNull).findFirst() - .ifPresent((version) -> builder.withDetail("version", version)); - + String version = this.cassandraOperations.getCqlOperations().queryForObject(SELECT, String.class); + builder.up().withDetail("version", version); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicator.java index 1745700ea03f..eed0b2df7180 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicator.java @@ -15,32 +15,27 @@ */ package org.springframework.boot.actuate.cassandra; -import java.util.Collection; -import java.util.Objects; - -import com.datastax.oss.driver.api.core.metadata.Metadata; -import com.datastax.oss.driver.api.core.metadata.Node; -import com.datastax.oss.driver.api.core.metadata.NodeState; -import com.datastax.oss.driver.internal.core.context.DefaultDriverContext; +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; import reactor.core.publisher.Mono; import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator; import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.Health.Builder; import org.springframework.boot.actuate.health.ReactiveHealthIndicator; import org.springframework.data.cassandra.core.ReactiveCassandraOperations; -import org.springframework.data.cassandra.core.cql.ReactiveSessionCallback; import org.springframework.util.Assert; /** * A {@link ReactiveHealthIndicator} for Cassandra. * * @author Artsiom Yudovin - * @author Tomasz Lelek * @since 2.1.0 */ public class CassandraReactiveHealthIndicator extends AbstractReactiveHealthIndicator { + private static final SimpleStatement SELECT = SimpleStatement + .newInstance("SELECT release_version FROM system.local").setConsistencyLevel(ConsistencyLevel.LOCAL_ONE); + private final ReactiveCassandraOperations reactiveCassandraOperations; /** @@ -55,30 +50,8 @@ public CassandraReactiveHealthIndicator(ReactiveCassandraOperations reactiveCass @Override protected Mono doHealthCheck(Health.Builder builder) { - - return this.reactiveCassandraOperations.getReactiveCqlOperations().execute(extractMetadata()).single() - .map((metadata) -> buildHealth(builder, metadata)); - } - - protected Health buildHealth(Builder builder, Metadata metadata) { - Collection nodes = metadata.getNodes().values(); - boolean atLeastOneUp = nodes.stream().map(Node::getState).anyMatch((state) -> state == NodeState.UP); - if (atLeastOneUp) { - builder.up(); - } - else { - builder.down(); - } - - // fill details with version of the first node (if the version is not null) - nodes.stream().map(Node::getCassandraVersion).filter(Objects::nonNull).findFirst() - .ifPresent((version) -> builder.withDetail("version", version)); - return builder.build(); - } - - protected ReactiveSessionCallback extractMetadata() { - return (session) -> Mono - .fromSupplier(() -> ((DefaultDriverContext) session.getContext()).getMetadataManager().getMetadata()); + return this.reactiveCassandraOperations.getReactiveCqlOperations().queryForObject(SELECT, String.class) + .map((version) -> builder.up().withDetail("version", version).build()).single(); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicatorTests.java index f47989096eeb..d07274bed93d 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicatorTests.java @@ -16,119 +16,45 @@ package org.springframework.boot.actuate.cassandra; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; -import java.util.stream.Stream; - -import com.datastax.oss.driver.api.core.Version; -import com.datastax.oss.driver.api.core.metadata.Metadata; -import com.datastax.oss.driver.api.core.metadata.Node; -import com.datastax.oss.driver.api.core.metadata.NodeState; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Status; import org.springframework.data.cassandra.CassandraInternalException; import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.core.cql.CqlOperations; -import org.springframework.data.cassandra.core.cql.SessionCallback; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.mock; -import static org.mockito.BDDMockito.when; +import static org.mockito.Mockito.mock; /** * Tests for {@link CassandraHealthIndicator}. * * @author Oleksii Bondar * @author Stephane Nicoll - * @author Tomasz Lelek */ class CassandraHealthIndicatorTests { @Test - void createWhenCqlSessionIsNullShouldThrowException() { + void createWhenCassandraOperationsIsNullShouldThrowException() { assertThatIllegalArgumentException().isThrownBy(() -> new CassandraHealthIndicator(null)); } - @ParameterizedTest - @MethodSource - @SuppressWarnings("unchecked") - void reportCassandraHealthCheck(Map nodes, Status expectedStatus) { - Metadata metadata = mock(Metadata.class); - when(metadata.getNodes()).thenReturn(nodes); - CqlOperations cqlOperations = mock(CqlOperations.class); - CassandraOperations cassandraOperations = mock(CassandraOperations.class); - given(cassandraOperations.getCqlOperations()).willReturn(cqlOperations); - when(cqlOperations.execute(any(SessionCallback.class))).thenReturn(metadata); - - CassandraHealthIndicator healthIndicator = new CassandraHealthIndicator(cassandraOperations); - Health health = healthIndicator.health(); - assertThat(health.getStatus()).isEqualTo(expectedStatus); - } - - static Stream reportCassandraHealthCheck() { - Node healthyNode = mock(Node.class); - when(healthyNode.getState()).thenReturn(NodeState.UP); - Node unhealthyNode = mock(Node.class); - when(unhealthyNode.getState()).thenReturn(NodeState.DOWN); - Node unknownNode = mock(Node.class); - when(unknownNode.getState()).thenReturn(NodeState.UNKNOWN); - Node forcedDownNode = mock(Node.class); - when(forcedDownNode.getState()).thenReturn(NodeState.FORCED_DOWN); - return Stream.builder().add(Arguments.arguments(createNodes(healthyNode), Status.UP)) - .add(Arguments.arguments(createNodes(unhealthyNode), Status.DOWN)) - .add(Arguments.arguments(createNodes(unknownNode), Status.DOWN)) - .add(Arguments.arguments(createNodes(forcedDownNode), Status.DOWN)) - .add(Arguments.arguments(createNodes(healthyNode, unhealthyNode), Status.UP)) - .add(Arguments.arguments(createNodes(healthyNode, unknownNode), Status.UP)) - .add(Arguments.arguments(createNodes(healthyNode, forcedDownNode), Status.UP)).build(); - } - @Test - @SuppressWarnings("unchecked") - void addVersionToDetailsIfReportedNotNull() { - Metadata metadata = mock(Metadata.class); - Node node = mock(Node.class); - when(node.getState()).thenReturn(NodeState.UP); - when(node.getCassandraVersion()).thenReturn(Version.V4_0_0); - when(metadata.getNodes()).thenReturn(createNodes(node)); - - CqlOperations cqlOperations = mock(CqlOperations.class); + void healthWithCassandraUp() { CassandraOperations cassandraOperations = mock(CassandraOperations.class); - given(cassandraOperations.getCqlOperations()).willReturn(cqlOperations); - when(cqlOperations.execute(any(SessionCallback.class))).thenReturn(metadata); - - CassandraHealthIndicator healthIndicator = new CassandraHealthIndicator(cassandraOperations); - Health health = healthIndicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.UP); - assertThat(health.getDetails().get("version")).isEqualTo(Version.V4_0_0); - } - - @Test - @SuppressWarnings("unchecked") - void doNotAddVersionToDetailsIfReportedNull() { - Metadata metadata = mock(Metadata.class); - Node node = mock(Node.class); - when(node.getState()).thenReturn(NodeState.UP); - when(metadata.getNodes()).thenReturn(createNodes(node)); - CqlOperations cqlOperations = mock(CqlOperations.class); - CassandraOperations cassandraOperations = mock(CassandraOperations.class); - given(cassandraOperations.getCqlOperations()).willReturn(cqlOperations); - when(cqlOperations.execute(any(SessionCallback.class))).thenReturn(metadata); - CassandraHealthIndicator healthIndicator = new CassandraHealthIndicator(cassandraOperations); + given(cassandraOperations.getCqlOperations()).willReturn(cqlOperations); + given(cqlOperations.queryForObject(any(SimpleStatement.class), eq(String.class))).willReturn("1.0.0"); Health health = healthIndicator.health(); assertThat(health.getStatus()).isEqualTo(Status.UP); - assertThat(health.getDetails().get("version")).isNull(); + assertThat(health.getDetails().get("version")).isEqualTo("1.0.0"); } @Test @@ -142,13 +68,4 @@ void healthWithCassandraDown() { .isEqualTo(CassandraInternalException.class.getName() + ": Connection failed"); } - private static Map createNodes(Node... nodes) { - Map nodesMap = new HashMap<>(); - for (Node n : nodes) { - nodesMap.put(UUID.randomUUID(), n); - } - - return nodesMap; - } - } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicatorTests.java index 41dfecb0b815..b795e3d783b5 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicatorTests.java @@ -15,20 +15,8 @@ */ package org.springframework.boot.actuate.cassandra; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; -import java.util.stream.Stream; - -import com.datastax.oss.driver.api.core.Version; -import com.datastax.oss.driver.api.core.metadata.Metadata; -import com.datastax.oss.driver.api.core.metadata.Node; -import com.datastax.oss.driver.api.core.metadata.NodeState; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -37,111 +25,36 @@ import org.springframework.data.cassandra.CassandraInternalException; import org.springframework.data.cassandra.core.ReactiveCassandraOperations; import org.springframework.data.cassandra.core.cql.ReactiveCqlOperations; -import org.springframework.data.cassandra.core.cql.ReactiveSessionCallback; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.mock; -import static org.mockito.BDDMockito.when; +import static org.mockito.Mockito.mock; /** * Tests for {@link CassandraReactiveHealthIndicator}. * * @author Artsiom Yudovin - * @author Tomasz Lelek */ class CassandraReactiveHealthIndicatorTests { @Test - void createWhenCqlSessionIsNullShouldThrowException() { - assertThatIllegalArgumentException().isThrownBy(() -> new CassandraReactiveHealthIndicator(null)); - } - - @ParameterizedTest - @MethodSource - @SuppressWarnings("unchecked") - void reportCassandraHealthCheck(Map nodes, Status expectedStatus) { - Metadata metadata = mock(Metadata.class); - when(metadata.getNodes()).thenReturn(nodes); - ReactiveCqlOperations reactiveCqlOperations = mock(ReactiveCqlOperations.class); - ReactiveCassandraOperations reactiveCassandraOperations = mock(ReactiveCassandraOperations.class); - given(reactiveCassandraOperations.getReactiveCqlOperations()).willReturn(reactiveCqlOperations); - when(reactiveCqlOperations.execute(any(ReactiveSessionCallback.class))) - .thenReturn((Flux.from(Mono.fromSupplier(() -> metadata)))); - - CassandraReactiveHealthIndicator healthIndicator = new CassandraReactiveHealthIndicator( - reactiveCassandraOperations); - Mono health = healthIndicator.health(); - StepVerifier.create(health).consumeNextWith((h) -> assertThat(h.getStatus()).isEqualTo(expectedStatus)) - .verifyComplete(); - } - - static Stream reportCassandraHealthCheck() { - Node healthyNode = mock(Node.class); - when(healthyNode.getState()).thenReturn(NodeState.UP); - Node unhealthyNode = mock(Node.class); - when(unhealthyNode.getState()).thenReturn(NodeState.DOWN); - Node unknownNode = mock(Node.class); - when(unknownNode.getState()).thenReturn(NodeState.UNKNOWN); - Node forcedDownNode = mock(Node.class); - when(forcedDownNode.getState()).thenReturn(NodeState.FORCED_DOWN); - return Stream.builder().add(Arguments.arguments(createNodes(healthyNode), Status.UP)) - .add(Arguments.arguments(createNodes(unhealthyNode), Status.DOWN)) - .add(Arguments.arguments(createNodes(unknownNode), Status.DOWN)) - .add(Arguments.arguments(createNodes(forcedDownNode), Status.DOWN)) - .add(Arguments.arguments(createNodes(healthyNode, unhealthyNode), Status.UP)) - .add(Arguments.arguments(createNodes(healthyNode, unknownNode), Status.UP)) - .add(Arguments.arguments(createNodes(healthyNode, forcedDownNode), Status.UP)).build(); - } - - @Test - @SuppressWarnings("unchecked") - void addVersionToDetailsIfReportedNotNull() { - Metadata metadata = mock(Metadata.class); - Node node = mock(Node.class); - when(node.getState()).thenReturn(NodeState.UP); - when(node.getCassandraVersion()).thenReturn(Version.V4_0_0); - when(metadata.getNodes()).thenReturn(createNodes(node)); - - ReactiveCqlOperations reactiveCqlOperations = mock(ReactiveCqlOperations.class); - ReactiveCassandraOperations reactiveCassandraOperations = mock(ReactiveCassandraOperations.class); - given(reactiveCassandraOperations.getReactiveCqlOperations()).willReturn(reactiveCqlOperations); - when(reactiveCqlOperations.execute(any(ReactiveSessionCallback.class))) - .thenReturn((Flux.from(Mono.fromSupplier(() -> metadata)))); - - CassandraReactiveHealthIndicator healthIndicator = new CassandraReactiveHealthIndicator( - reactiveCassandraOperations); - Mono health = healthIndicator.health(); - StepVerifier.create(health).consumeNextWith((h) -> { - assertThat(h.getStatus()).isEqualTo(Status.UP); - assertThat(h.getDetails().get("version")).isEqualTo(Version.V4_0_0); - }).verifyComplete(); - } - - @Test - @SuppressWarnings("unchecked") - void doNotAddVersionToDetailsIfReportedNull() { - Metadata metadata = mock(Metadata.class); - Node node = mock(Node.class); - when(node.getState()).thenReturn(NodeState.UP); - when(metadata.getNodes()).thenReturn(createNodes(node)); - + void testCassandraIsUp() { ReactiveCqlOperations reactiveCqlOperations = mock(ReactiveCqlOperations.class); + given(reactiveCqlOperations.queryForObject(any(SimpleStatement.class), eq(String.class))) + .willReturn(Mono.just("6.0.0")); ReactiveCassandraOperations reactiveCassandraOperations = mock(ReactiveCassandraOperations.class); given(reactiveCassandraOperations.getReactiveCqlOperations()).willReturn(reactiveCqlOperations); - when(reactiveCqlOperations.execute(any(ReactiveSessionCallback.class))) - .thenReturn((Flux.from(Mono.fromSupplier(() -> metadata)))); - CassandraReactiveHealthIndicator healthIndicator = new CassandraReactiveHealthIndicator( + CassandraReactiveHealthIndicator cassandraReactiveHealthIndicator = new CassandraReactiveHealthIndicator( reactiveCassandraOperations); - Mono health = healthIndicator.health(); + Mono health = cassandraReactiveHealthIndicator.health(); StepVerifier.create(health).consumeNextWith((h) -> { assertThat(h.getStatus()).isEqualTo(Status.UP); - assertThat(h.getDetails().get("version")).isNull(); + assertThat(h.getDetails()).containsOnlyKeys("version"); + assertThat(h.getDetails().get("version")).isEqualTo("6.0.0"); }).verifyComplete(); - } @Test @@ -161,13 +74,4 @@ void testCassandraIsDown() { }).verifyComplete(); } - private static Map createNodes(Node... nodes) { - Map nodesMap = new HashMap<>(); - for (Node n : nodes) { - nodesMap.put(UUID.randomUUID(), n); - } - - return nodesMap; - } - } From 09eaca8e4c9b0e5b5bfea49ba45ac9c81fd5b15e Mon Sep 17 00:00:00 2001 From: Alexandre Dutra Date: Thu, 1 Oct 2020 10:43:15 +0200 Subject: [PATCH 4/7] Replace parameterized test with simple tests --- .../CassandraDriverHealthIndicatorTests.java | 113 +++++++++++++---- ...draDriverReactiveHealthIndicatorTests.java | 119 ++++++++++++++---- 2 files changed, 180 insertions(+), 52 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicatorTests.java index 04060b7053f6..c7fa02475823 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicatorTests.java @@ -19,7 +19,6 @@ import java.util.HashMap; import java.util.Map; import java.util.UUID; -import java.util.stream.Stream; import com.datastax.oss.driver.api.core.CqlSession; import com.datastax.oss.driver.api.core.DriverTimeoutException; @@ -28,9 +27,6 @@ import com.datastax.oss.driver.api.core.metadata.Node; import com.datastax.oss.driver.api.core.metadata.NodeState; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Status; @@ -55,35 +51,101 @@ void createWhenCqlSessionIsNullShouldThrowException() { assertThatIllegalArgumentException().isThrownBy(() -> new CassandraDriverHealthIndicator(null)); } - @ParameterizedTest - @MethodSource - void reportCassandraHealthCheck(Map nodes, Status expectedStatus) { + @Test + void oneHealthyNodeShouldReturnUp() { CqlSession session = mock(CqlSession.class); Metadata metadata = mock(Metadata.class); - when(session.getMetadata()).thenReturn(metadata); - when(metadata.getNodes()).thenReturn(nodes); + Node healthyNode = mock(Node.class); + given(healthyNode.getState()).willReturn(NodeState.UP); + given(session.getMetadata()).willReturn(metadata); + given(metadata.getNodes()).willReturn(createNodesMap(healthyNode)); + CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + } + + @Test + void oneUnhealthyNodeShouldReturnDown() { + CqlSession session = mock(CqlSession.class); + Metadata metadata = mock(Metadata.class); + Node unhealthyNode = mock(Node.class); + given(unhealthyNode.getState()).willReturn(NodeState.DOWN); + given(session.getMetadata()).willReturn(metadata); + given(metadata.getNodes()).willReturn(createNodesMap(unhealthyNode)); + CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + } + @Test + void oneUnknownNodeShouldReturnDown() { + CqlSession session = mock(CqlSession.class); + Metadata metadata = mock(Metadata.class); + Node unknownNode = mock(Node.class); + given(unknownNode.getState()).willReturn(NodeState.UNKNOWN); + given(session.getMetadata()).willReturn(metadata); + given(metadata.getNodes()).willReturn(createNodesMap(unknownNode)); CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session); Health health = healthIndicator.health(); - assertThat(health.getStatus()).isEqualTo(expectedStatus); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); } - static Stream reportCassandraHealthCheck() { + @Test + void oneForcedDownNodeShouldReturnDown() { + CqlSession session = mock(CqlSession.class); + Metadata metadata = mock(Metadata.class); + Node forcedDownNode = mock(Node.class); + given(forcedDownNode.getState()).willReturn(NodeState.FORCED_DOWN); + given(session.getMetadata()).willReturn(metadata); + given(metadata.getNodes()).willReturn(createNodesMap(forcedDownNode)); + CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + } + + @Test + void oneHealthyNodeAndOneUnhealthyNodeShouldReturnUp() { + CqlSession session = mock(CqlSession.class); + Metadata metadata = mock(Metadata.class); Node healthyNode = mock(Node.class); - when(healthyNode.getState()).thenReturn(NodeState.UP); Node unhealthyNode = mock(Node.class); - when(unhealthyNode.getState()).thenReturn(NodeState.DOWN); + given(healthyNode.getState()).willReturn(NodeState.UP); + given(unhealthyNode.getState()).willReturn(NodeState.DOWN); + given(session.getMetadata()).willReturn(metadata); + given(metadata.getNodes()).willReturn(createNodesMap(healthyNode, unhealthyNode)); + CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + } + + @Test + void oneHealthyNodeAndOneUnknownNodeShouldReturnUp() { + CqlSession session = mock(CqlSession.class); + Metadata metadata = mock(Metadata.class); + Node healthyNode = mock(Node.class); Node unknownNode = mock(Node.class); - when(unknownNode.getState()).thenReturn(NodeState.UNKNOWN); + given(healthyNode.getState()).willReturn(NodeState.UP); + given(unknownNode.getState()).willReturn(NodeState.UNKNOWN); + given(session.getMetadata()).willReturn(metadata); + given(metadata.getNodes()).willReturn(createNodesMap(healthyNode, unknownNode)); + CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + } + + @Test + void oneHealthyNodeAndOneForcedDownNodeShouldReturnUp() { + CqlSession session = mock(CqlSession.class); + Metadata metadata = mock(Metadata.class); + Node healthyNode = mock(Node.class); Node forcedDownNode = mock(Node.class); - when(forcedDownNode.getState()).thenReturn(NodeState.FORCED_DOWN); - return Stream.builder().add(Arguments.arguments(createNodes(healthyNode), Status.UP)) - .add(Arguments.arguments(createNodes(unhealthyNode), Status.DOWN)) - .add(Arguments.arguments(createNodes(unknownNode), Status.DOWN)) - .add(Arguments.arguments(createNodes(forcedDownNode), Status.DOWN)) - .add(Arguments.arguments(createNodes(healthyNode, unhealthyNode), Status.UP)) - .add(Arguments.arguments(createNodes(healthyNode, unknownNode), Status.UP)) - .add(Arguments.arguments(createNodes(healthyNode, forcedDownNode), Status.UP)).build(); + given(healthyNode.getState()).willReturn(NodeState.UP); + given(forcedDownNode.getState()).willReturn(NodeState.FORCED_DOWN); + given(session.getMetadata()).willReturn(metadata); + given(metadata.getNodes()).willReturn(createNodesMap(healthyNode, forcedDownNode)); + CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); } @Test @@ -94,7 +156,7 @@ void addVersionToDetailsIfReportedNotNull() { Node node = mock(Node.class); when(node.getState()).thenReturn(NodeState.UP); when(node.getCassandraVersion()).thenReturn(Version.V4_0_0); - when(metadata.getNodes()).thenReturn(createNodes(node)); + when(metadata.getNodes()).thenReturn(createNodesMap(node)); CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session); Health health = healthIndicator.health(); @@ -109,7 +171,7 @@ void doNotAddVersionToDetailsIfReportedNull() { when(session.getMetadata()).thenReturn(metadata); Node node = mock(Node.class); when(node.getState()).thenReturn(NodeState.UP); - when(metadata.getNodes()).thenReturn(createNodes(node)); + when(metadata.getNodes()).thenReturn(createNodesMap(node)); CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session); Health health = healthIndicator.health(); @@ -128,12 +190,11 @@ void healthWithCassandraDown() { .isEqualTo(DriverTimeoutException.class.getName() + ": Test Exception"); } - private static Map createNodes(Node... nodes) { + private static Map createNodesMap(Node... nodes) { Map nodesMap = new HashMap<>(); for (Node n : nodes) { nodesMap.put(UUID.randomUUID(), n); } - return nodesMap; } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicatorTests.java index 82ed2861c453..2179e622d600 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicatorTests.java @@ -18,7 +18,6 @@ import java.util.HashMap; import java.util.Map; import java.util.UUID; -import java.util.stream.Stream; import com.datastax.oss.driver.api.core.CqlSession; import com.datastax.oss.driver.api.core.DriverTimeoutException; @@ -27,9 +26,6 @@ import com.datastax.oss.driver.api.core.metadata.Node; import com.datastax.oss.driver.api.core.metadata.NodeState; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -56,36 +52,108 @@ void createWhenCqlSessionIsNullShouldThrowException() { assertThatIllegalArgumentException().isThrownBy(() -> new CassandraDriverReactiveHealthIndicator(null)); } - @ParameterizedTest - @MethodSource - void reportCassandraHealthCheck(Map nodes, Status expectedStatus) { + @Test + void oneHealthyNodeShouldReturnUp() { CqlSession session = mock(CqlSession.class); Metadata metadata = mock(Metadata.class); - when(session.getMetadata()).thenReturn(metadata); - when(metadata.getNodes()).thenReturn(nodes); + Node healthyNode = mock(Node.class); + given(healthyNode.getState()).willReturn(NodeState.UP); + given(session.getMetadata()).willReturn(metadata); + given(metadata.getNodes()).willReturn(createNodesMap(healthyNode)); + CassandraDriverReactiveHealthIndicator healthIndicator = new CassandraDriverReactiveHealthIndicator(session); + Mono health = healthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> assertThat(h.getStatus()).isEqualTo(Status.UP)) + .verifyComplete(); + } + @Test + void oneUnhealthyNodeShouldReturnDown() { + CqlSession session = mock(CqlSession.class); + Metadata metadata = mock(Metadata.class); + Node unhealthyNode = mock(Node.class); + given(unhealthyNode.getState()).willReturn(NodeState.DOWN); + given(session.getMetadata()).willReturn(metadata); + given(metadata.getNodes()).willReturn(createNodesMap(unhealthyNode)); + CassandraDriverReactiveHealthIndicator healthIndicator = new CassandraDriverReactiveHealthIndicator(session); + Mono health = healthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> assertThat(h.getStatus()).isEqualTo(Status.DOWN)) + .verifyComplete(); + } + + @Test + void oneUnknownNodeShouldReturnDown() { + CqlSession session = mock(CqlSession.class); + Metadata metadata = mock(Metadata.class); + Node unknownNode = mock(Node.class); + given(unknownNode.getState()).willReturn(NodeState.UNKNOWN); + given(session.getMetadata()).willReturn(metadata); + given(metadata.getNodes()).willReturn(createNodesMap(unknownNode)); + CassandraDriverReactiveHealthIndicator healthIndicator = new CassandraDriverReactiveHealthIndicator(session); + Mono health = healthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> assertThat(h.getStatus()).isEqualTo(Status.DOWN)) + .verifyComplete(); + } + + @Test + void oneForcedDownNodeShouldReturnDown() { + CqlSession session = mock(CqlSession.class); + Metadata metadata = mock(Metadata.class); + Node forcedDownNode = mock(Node.class); + given(forcedDownNode.getState()).willReturn(NodeState.FORCED_DOWN); + given(session.getMetadata()).willReturn(metadata); + given(metadata.getNodes()).willReturn(createNodesMap(forcedDownNode)); CassandraDriverReactiveHealthIndicator healthIndicator = new CassandraDriverReactiveHealthIndicator(session); Mono health = healthIndicator.health(); - StepVerifier.create(health).consumeNextWith((h) -> assertThat(h.getStatus()).isEqualTo(expectedStatus)) + StepVerifier.create(health).consumeNextWith((h) -> assertThat(h.getStatus()).isEqualTo(Status.DOWN)) .verifyComplete(); } - static Stream reportCassandraHealthCheck() { + @Test + void oneHealthyNodeAndOneUnhealthyNodeShouldReturnUp() { + CqlSession session = mock(CqlSession.class); + Metadata metadata = mock(Metadata.class); Node healthyNode = mock(Node.class); - when(healthyNode.getState()).thenReturn(NodeState.UP); Node unhealthyNode = mock(Node.class); - when(unhealthyNode.getState()).thenReturn(NodeState.DOWN); + given(healthyNode.getState()).willReturn(NodeState.UP); + given(unhealthyNode.getState()).willReturn(NodeState.DOWN); + given(session.getMetadata()).willReturn(metadata); + given(metadata.getNodes()).willReturn(createNodesMap(healthyNode, unhealthyNode)); + CassandraDriverReactiveHealthIndicator healthIndicator = new CassandraDriverReactiveHealthIndicator(session); + Mono health = healthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> assertThat(h.getStatus()).isEqualTo(Status.UP)) + .verifyComplete(); + } + + @Test + void oneHealthyNodeAndOneUnknownNodeShouldReturnUp() { + CqlSession session = mock(CqlSession.class); + Metadata metadata = mock(Metadata.class); + Node healthyNode = mock(Node.class); Node unknownNode = mock(Node.class); - when(unknownNode.getState()).thenReturn(NodeState.UNKNOWN); + given(healthyNode.getState()).willReturn(NodeState.UP); + given(unknownNode.getState()).willReturn(NodeState.UNKNOWN); + given(session.getMetadata()).willReturn(metadata); + given(metadata.getNodes()).willReturn(createNodesMap(healthyNode, unknownNode)); + CassandraDriverReactiveHealthIndicator healthIndicator = new CassandraDriverReactiveHealthIndicator(session); + Mono health = healthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> assertThat(h.getStatus()).isEqualTo(Status.UP)) + .verifyComplete(); + } + + @Test + void oneHealthyNodeAndOneForcedDownNodeShouldReturnUp() { + CqlSession session = mock(CqlSession.class); + Metadata metadata = mock(Metadata.class); + Node healthyNode = mock(Node.class); Node forcedDownNode = mock(Node.class); - when(forcedDownNode.getState()).thenReturn(NodeState.FORCED_DOWN); - return Stream.builder().add(Arguments.arguments(createNodes(healthyNode), Status.UP)) - .add(Arguments.arguments(createNodes(unhealthyNode), Status.DOWN)) - .add(Arguments.arguments(createNodes(unknownNode), Status.DOWN)) - .add(Arguments.arguments(createNodes(forcedDownNode), Status.DOWN)) - .add(Arguments.arguments(createNodes(healthyNode, unhealthyNode), Status.UP)) - .add(Arguments.arguments(createNodes(healthyNode, unknownNode), Status.UP)) - .add(Arguments.arguments(createNodes(healthyNode, forcedDownNode), Status.UP)).build(); + given(healthyNode.getState()).willReturn(NodeState.UP); + given(forcedDownNode.getState()).willReturn(NodeState.FORCED_DOWN); + given(session.getMetadata()).willReturn(metadata); + given(metadata.getNodes()).willReturn(createNodesMap(healthyNode, forcedDownNode)); + CassandraDriverReactiveHealthIndicator healthIndicator = new CassandraDriverReactiveHealthIndicator(session); + Mono health = healthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> assertThat(h.getStatus()).isEqualTo(Status.UP)) + .verifyComplete(); } @Test @@ -96,7 +164,7 @@ void addVersionToDetailsIfReportedNotNull() { Node node = mock(Node.class); when(node.getState()).thenReturn(NodeState.UP); when(node.getCassandraVersion()).thenReturn(Version.V4_0_0); - when(metadata.getNodes()).thenReturn(createNodes(node)); + when(metadata.getNodes()).thenReturn(createNodesMap(node)); CassandraDriverReactiveHealthIndicator healthIndicator = new CassandraDriverReactiveHealthIndicator(session); Mono health = healthIndicator.health(); @@ -114,7 +182,7 @@ void doNotAddVersionToDetailsIfReportedNull() { when(session.getMetadata()).thenReturn(metadata); Node node = mock(Node.class); when(node.getState()).thenReturn(NodeState.UP); - when(metadata.getNodes()).thenReturn(createNodes(node)); + when(metadata.getNodes()).thenReturn(createNodesMap(node)); CassandraDriverReactiveHealthIndicator healthIndicator = new CassandraDriverReactiveHealthIndicator(session); Mono health = healthIndicator.health(); @@ -140,12 +208,11 @@ void testCassandraIsDown() { }).verifyComplete(); } - private static Map createNodes(Node... nodes) { + private static Map createNodesMap(Node... nodes) { Map nodesMap = new HashMap<>(); for (Node n : nodes) { nodesMap.put(UUID.randomUUID(), n); } - return nodesMap; } From 0f9ee8d412f4c9e7034fde64cbf5d1c9cd226da5 Mon Sep 17 00:00:00 2001 From: Alexandre Dutra Date: Thu, 1 Oct 2020 11:05:10 +0200 Subject: [PATCH 5/7] Simplify logic to set the Cassandra version --- .../cassandra/CassandraDriverHealthIndicator.java | 11 ++++------- .../CassandraDriverReactiveHealthIndicator.java | 11 ++++------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicator.java index 7255f4e2a5f5..ed42a0bbda6a 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicator.java @@ -17,7 +17,7 @@ package org.springframework.boot.actuate.cassandra; import java.util.Collection; -import java.util.Objects; +import java.util.Optional; import com.datastax.oss.driver.api.core.CqlSession; import com.datastax.oss.driver.api.core.metadata.Node; @@ -54,12 +54,9 @@ public CassandraDriverHealthIndicator(CqlSession session) { @Override protected void doHealthCheck(Health.Builder builder) throws Exception { Collection nodes = this.session.getMetadata().getNodes().values(); - boolean atLeastOneUp = nodes.stream().map(Node::getState).anyMatch((state) -> state == NodeState.UP); - builder.status(atLeastOneUp ? Status.UP : Status.DOWN); - - // fill details with version of the first node (if the version is not null) - nodes.stream().map(Node::getCassandraVersion).filter(Objects::nonNull).findFirst() - .ifPresent((version) -> builder.withDetail("version", version)); + Optional nodeUp = nodes.stream().filter((node) -> node.getState() == NodeState.UP).findAny(); + builder.status(nodeUp.isPresent() ? Status.UP : Status.DOWN); + nodeUp.map(Node::getCassandraVersion).ifPresent((version) -> builder.withDetail("version", version)); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicator.java index 867c6798de22..893faf585568 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicator.java @@ -16,7 +16,7 @@ package org.springframework.boot.actuate.cassandra; import java.util.Collection; -import java.util.Objects; +import java.util.Optional; import com.datastax.oss.driver.api.core.CqlSession; import com.datastax.oss.driver.api.core.metadata.Node; @@ -55,12 +55,9 @@ public CassandraDriverReactiveHealthIndicator(CqlSession session) { protected Mono doHealthCheck(Health.Builder builder) { return Mono.fromSupplier(() -> { Collection nodes = this.session.getMetadata().getNodes().values(); - boolean atLeastOneUp = nodes.stream().map(Node::getState).anyMatch((state) -> state == NodeState.UP); - builder.status(atLeastOneUp ? Status.UP : Status.DOWN); - - // fill details with version of the first node (if the version is not null) - nodes.stream().map(Node::getCassandraVersion).filter(Objects::nonNull).findFirst() - .ifPresent((version) -> builder.withDetail("version", version)); + Optional nodeUp = nodes.stream().filter((node) -> node.getState() == NodeState.UP).findAny(); + builder.status(nodeUp.isPresent() ? Status.UP : Status.DOWN); + nodeUp.map(Node::getCassandraVersion).ifPresent((version) -> builder.withDetail("version", version)); return builder.build(); }); } From 6ab3e81bcfc84eee5838545b51848142b18e854b Mon Sep 17 00:00:00 2001 From: Alexandre Dutra Date: Thu, 1 Oct 2020 17:49:36 +0200 Subject: [PATCH 6/7] Revert changes to deprecated classes (2nd attempt) --- .../boot/actuate/cassandra/CassandraHealthIndicator.java | 2 ++ .../actuate/cassandra/CassandraReactiveHealthIndicator.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicator.java index a02d5fe39893..2b15f4e8b3f1 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicator.java @@ -32,7 +32,9 @@ * @author Julien Dubois * @author Alexandre Dutra * @since 2.0.0 + * @deprecated since 2.4.0 in favor of {@link CassandraDriverHealthIndicator} */ +@Deprecated public class CassandraHealthIndicator extends AbstractHealthIndicator { private static final SimpleStatement SELECT = SimpleStatement diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicator.java index eed0b2df7180..12375aff5ba2 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicator.java @@ -30,7 +30,9 @@ * * @author Artsiom Yudovin * @since 2.1.0 + * @deprecated since 2.4.0 in favor of {@link CassandraDriverHealthIndicator} */ +@Deprecated public class CassandraReactiveHealthIndicator extends AbstractReactiveHealthIndicator { private static final SimpleStatement SELECT = SimpleStatement From ccb7d89e7a65a9e46ee7093bbad273f03d8a3859 Mon Sep 17 00:00:00 2001 From: Alexandre Dutra Date: Thu, 1 Oct 2020 17:50:24 +0200 Subject: [PATCH 7/7] Revert changes to deprecated test classes (2nd attempt) --- .../boot/actuate/cassandra/CassandraHealthIndicatorTests.java | 1 + .../actuate/cassandra/CassandraReactiveHealthIndicatorTests.java | 1 + 2 files changed, 2 insertions(+) diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicatorTests.java index d07274bed93d..8044c99fc327 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicatorTests.java @@ -38,6 +38,7 @@ * @author Oleksii Bondar * @author Stephane Nicoll */ +@Deprecated class CassandraHealthIndicatorTests { @Test diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicatorTests.java index b795e3d783b5..5ac7de6c061e 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicatorTests.java @@ -37,6 +37,7 @@ * * @author Artsiom Yudovin */ +@Deprecated class CassandraReactiveHealthIndicatorTests { @Test