Skip to content

Commit 48c2f4d

Browse files
committed
Merge pull request #23041 from tomekl007
* pr/23041: Polish "Improve Cassandra health indicator with more robust mechanism" Improve Cassandra health indicator with more robust mechanism Closes gh-23041
2 parents f241cd0 + 6f08e97 commit 48c2f4d

File tree

4 files changed

+248
-72
lines changed

4 files changed

+248
-72
lines changed

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicator.java

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,29 @@
1616

1717
package org.springframework.boot.actuate.cassandra;
1818

19-
import com.datastax.oss.driver.api.core.ConsistencyLevel;
19+
import java.util.Collection;
20+
import java.util.Optional;
21+
2022
import com.datastax.oss.driver.api.core.CqlSession;
21-
import com.datastax.oss.driver.api.core.cql.Row;
22-
import com.datastax.oss.driver.api.core.cql.SimpleStatement;
23+
import com.datastax.oss.driver.api.core.metadata.Node;
24+
import com.datastax.oss.driver.api.core.metadata.NodeState;
2325

2426
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
2527
import org.springframework.boot.actuate.health.Health;
2628
import org.springframework.boot.actuate.health.HealthIndicator;
29+
import org.springframework.boot.actuate.health.Status;
2730
import org.springframework.util.Assert;
2831

2932
/**
3033
* Simple implementation of a {@link HealthIndicator} returning status information for
3134
* Cassandra data stores.
3235
*
3336
* @author Alexandre Dutra
37+
* @author Tomasz Lelek
3438
* @since 2.4.0
3539
*/
3640
public class CassandraDriverHealthIndicator extends AbstractHealthIndicator {
3741

38-
private static final SimpleStatement SELECT = SimpleStatement
39-
.newInstance("SELECT release_version FROM system.local").setConsistencyLevel(ConsistencyLevel.LOCAL_ONE);
40-
4142
private final CqlSession session;
4243

4344
/**
@@ -52,11 +53,10 @@ public CassandraDriverHealthIndicator(CqlSession session) {
5253

5354
@Override
5455
protected void doHealthCheck(Health.Builder builder) throws Exception {
55-
Row row = this.session.execute(SELECT).one();
56-
builder.up();
57-
if (row != null && !row.isNull(0)) {
58-
builder.withDetail("version", row.getString(0));
59-
}
56+
Collection<Node> nodes = this.session.getMetadata().getNodes().values();
57+
Optional<Node> nodeUp = nodes.stream().filter((node) -> node.getState() == NodeState.UP).findAny();
58+
builder.status(nodeUp.isPresent() ? Status.UP : Status.DOWN);
59+
nodeUp.map(Node::getCassandraVersion).ifPresent((version) -> builder.withDetail("version", version));
6060
}
6161

6262
}

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicator.java

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,30 @@
1515
*/
1616
package org.springframework.boot.actuate.cassandra;
1717

18-
import com.datastax.oss.driver.api.core.ConsistencyLevel;
18+
import java.util.Collection;
19+
import java.util.Optional;
20+
1921
import com.datastax.oss.driver.api.core.CqlSession;
20-
import com.datastax.oss.driver.api.core.cql.SimpleStatement;
22+
import com.datastax.oss.driver.api.core.metadata.Node;
23+
import com.datastax.oss.driver.api.core.metadata.NodeState;
2124
import reactor.core.publisher.Mono;
2225

2326
import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator;
2427
import org.springframework.boot.actuate.health.Health;
2528
import org.springframework.boot.actuate.health.ReactiveHealthIndicator;
29+
import org.springframework.boot.actuate.health.Status;
2630
import org.springframework.util.Assert;
2731

2832
/**
2933
* Simple implementation of a {@link ReactiveHealthIndicator} returning status information
3034
* for Cassandra data stores.
3135
*
3236
* @author Alexandre Dutra
37+
* @author Tomasz Lelek
3338
* @since 2.4.0
3439
*/
3540
public class CassandraDriverReactiveHealthIndicator extends AbstractReactiveHealthIndicator {
3641

37-
private static final SimpleStatement SELECT = SimpleStatement
38-
.newInstance("SELECT release_version FROM system.local").setConsistencyLevel(ConsistencyLevel.LOCAL_ONE);
39-
4042
private final CqlSession session;
4143

4244
/**
@@ -51,8 +53,13 @@ public CassandraDriverReactiveHealthIndicator(CqlSession session) {
5153

5254
@Override
5355
protected Mono<Health> doHealthCheck(Health.Builder builder) {
54-
return Mono.from(this.session.executeReactive(SELECT))
55-
.map((row) -> builder.up().withDetail("version", row.getString(0)).build());
56+
return Mono.fromSupplier(() -> {
57+
Collection<Node> nodes = this.session.getMetadata().getNodes().values();
58+
Optional<Node> nodeUp = nodes.stream().filter((node) -> node.getState() == NodeState.UP).findAny();
59+
builder.status(nodeUp.isPresent() ? Status.UP : Status.DOWN);
60+
nodeUp.map(Node::getCassandraVersion).ifPresent((version) -> builder.withDetail("version", version));
61+
return builder.build();
62+
});
5663
}
5764

5865
}

spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicatorTests.java

Lines changed: 107 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,34 @@
1616

1717
package org.springframework.boot.actuate.cassandra;
1818

19+
import java.util.ArrayList;
20+
import java.util.Collections;
21+
import java.util.HashMap;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.UUID;
25+
1926
import com.datastax.oss.driver.api.core.CqlSession;
2027
import com.datastax.oss.driver.api.core.DriverTimeoutException;
21-
import com.datastax.oss.driver.api.core.cql.ResultSet;
22-
import com.datastax.oss.driver.api.core.cql.Row;
23-
import com.datastax.oss.driver.api.core.cql.SimpleStatement;
28+
import com.datastax.oss.driver.api.core.Version;
29+
import com.datastax.oss.driver.api.core.metadata.Metadata;
30+
import com.datastax.oss.driver.api.core.metadata.Node;
31+
import com.datastax.oss.driver.api.core.metadata.NodeState;
2432
import org.junit.jupiter.api.Test;
2533

2634
import org.springframework.boot.actuate.health.Health;
2735
import org.springframework.boot.actuate.health.Status;
2836

2937
import static org.assertj.core.api.Assertions.assertThat;
3038
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
31-
import static org.mockito.ArgumentMatchers.any;
3239
import static org.mockito.BDDMockito.given;
3340
import static org.mockito.Mockito.mock;
3441

3542
/**
3643
* Tests for {@link CassandraDriverHealthIndicator}.
3744
*
3845
* @author Alexandre Dutra
39-
* @since 2.4.0
46+
* @author Stephane Nicoll
4047
*/
4148
class CassandraDriverHealthIndicatorTests {
4249

@@ -46,29 +53,114 @@ void createWhenCqlSessionIsNullShouldThrowException() {
4653
}
4754

4855
@Test
49-
void healthWithCassandraUp() {
56+
void healthWithOneHealthyNodeShouldReturnUp() {
57+
CqlSession session = mockCqlSessionWithNodeState(NodeState.UP);
58+
CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session);
59+
Health health = healthIndicator.health();
60+
assertThat(health.getStatus()).isEqualTo(Status.UP);
61+
}
62+
63+
@Test
64+
void healthWithOneUnhealthyNodeShouldReturnDown() {
65+
CqlSession session = mockCqlSessionWithNodeState(NodeState.DOWN);
66+
CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session);
67+
Health health = healthIndicator.health();
68+
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
69+
}
70+
71+
@Test
72+
void healthWithOneUnknownNodeShouldReturnDown() {
73+
CqlSession session = mockCqlSessionWithNodeState(NodeState.UNKNOWN);
74+
CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session);
75+
Health health = healthIndicator.health();
76+
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
77+
}
78+
79+
@Test
80+
void healthWithOneForcedDownNodeShouldReturnDown() {
81+
CqlSession session = mockCqlSessionWithNodeState(NodeState.FORCED_DOWN);
82+
CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session);
83+
Health health = healthIndicator.health();
84+
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
85+
}
86+
87+
@Test
88+
void healthWithOneHealthyNodeAndOneUnhealthyNodeShouldReturnUp() {
89+
CqlSession session = mockCqlSessionWithNodeState(NodeState.UP, NodeState.DOWN);
90+
CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session);
91+
Health health = healthIndicator.health();
92+
assertThat(health.getStatus()).isEqualTo(Status.UP);
93+
}
94+
95+
@Test
96+
void healthWithOneHealthyNodeAndOneUnknownNodeShouldReturnUp() {
97+
CqlSession session = mockCqlSessionWithNodeState(NodeState.UP, NodeState.UNKNOWN);
98+
CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session);
99+
Health health = healthIndicator.health();
100+
assertThat(health.getStatus()).isEqualTo(Status.UP);
101+
}
102+
103+
@Test
104+
void healthWithOneHealthyNodeAndOneForcedDownNodeShouldReturnUp() {
105+
CqlSession session = mockCqlSessionWithNodeState(NodeState.UP, NodeState.FORCED_DOWN);
106+
CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session);
107+
Health health = healthIndicator.health();
108+
assertThat(health.getStatus()).isEqualTo(Status.UP);
109+
}
110+
111+
@Test
112+
void healthWithNodeVersionShouldAddVersionDetail() {
50113
CqlSession session = mock(CqlSession.class);
51-
ResultSet resultSet = mock(ResultSet.class);
52-
Row row = mock(Row.class);
53-
given(session.execute(any(SimpleStatement.class))).willReturn(resultSet);
54-
given(resultSet.one()).willReturn(row);
55-
given(row.isNull(0)).willReturn(false);
56-
given(row.getString(0)).willReturn("1.0.0");
114+
Metadata metadata = mock(Metadata.class);
115+
given(session.getMetadata()).willReturn(metadata);
116+
Node node = mock(Node.class);
117+
given(node.getState()).willReturn(NodeState.UP);
118+
given(node.getCassandraVersion()).willReturn(Version.V4_0_0);
119+
given(metadata.getNodes()).willReturn(createNodesWithRandomUUID(Collections.singletonList(node)));
57120
CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session);
58121
Health health = healthIndicator.health();
59122
assertThat(health.getStatus()).isEqualTo(Status.UP);
60-
assertThat(health.getDetails().get("version")).isEqualTo("1.0.0");
123+
assertThat(health.getDetails().get("version")).isEqualTo(Version.V4_0_0);
61124
}
62125

63126
@Test
64-
void healthWithCassandraDown() {
127+
void healthWithoutNodeVersionShouldNotAddVersionDetail() {
128+
CqlSession session = mockCqlSessionWithNodeState(NodeState.UP);
129+
CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session);
130+
Health health = healthIndicator.health();
131+
assertThat(health.getStatus()).isEqualTo(Status.UP);
132+
assertThat(health.getDetails().get("version")).isNull();
133+
}
134+
135+
@Test
136+
void healthWithcassandraDownShouldReturnDown() {
65137
CqlSession session = mock(CqlSession.class);
66-
given(session.execute(any(SimpleStatement.class))).willThrow(new DriverTimeoutException("Test Exception"));
138+
given(session.getMetadata()).willThrow(new DriverTimeoutException("Test Exception"));
67139
CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session);
68140
Health health = healthIndicator.health();
69141
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
70142
assertThat(health.getDetails().get("error"))
71143
.isEqualTo(DriverTimeoutException.class.getName() + ": Test Exception");
72144
}
73145

146+
private CqlSession mockCqlSessionWithNodeState(NodeState... nodeStates) {
147+
CqlSession session = mock(CqlSession.class);
148+
Metadata metadata = mock(Metadata.class);
149+
List<Node> nodes = new ArrayList<>();
150+
for (NodeState nodeState : nodeStates) {
151+
Node node = mock(Node.class);
152+
given(node.getState()).willReturn(nodeState);
153+
nodes.add(node);
154+
}
155+
given(session.getMetadata()).willReturn(metadata);
156+
given(metadata.getNodes()).willReturn(createNodesWithRandomUUID(nodes));
157+
return session;
158+
}
159+
160+
private Map<UUID, Node> createNodesWithRandomUUID(List<Node> nodes) {
161+
Map<UUID, Node> indexedNodes = new HashMap<>();
162+
nodes.forEach((node) -> indexedNodes.put(UUID.randomUUID(), node));
163+
return indexedNodes;
164+
}
165+
74166
}

0 commit comments

Comments
 (0)