Skip to content

Commit 8e5a041

Browse files
tomekl007snicoll
authored andcommitted
Improve Cassandra health indicator with more robust mechanism
See gh-23041
1 parent f241cd0 commit 8e5a041

File tree

4 files changed

+316
-68
lines changed

4 files changed

+316
-68
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: 141 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,32 @@
1616

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

19+
import java.util.HashMap;
20+
import java.util.Map;
21+
import java.util.UUID;
22+
1923
import com.datastax.oss.driver.api.core.CqlSession;
2024
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;
25+
import com.datastax.oss.driver.api.core.Version;
26+
import com.datastax.oss.driver.api.core.metadata.Metadata;
27+
import com.datastax.oss.driver.api.core.metadata.Node;
28+
import com.datastax.oss.driver.api.core.metadata.NodeState;
2429
import org.junit.jupiter.api.Test;
2530

2631
import org.springframework.boot.actuate.health.Health;
2732
import org.springframework.boot.actuate.health.Status;
2833

2934
import static org.assertj.core.api.Assertions.assertThat;
3035
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
31-
import static org.mockito.ArgumentMatchers.any;
3236
import static org.mockito.BDDMockito.given;
33-
import static org.mockito.Mockito.mock;
37+
import static org.mockito.BDDMockito.mock;
38+
import static org.mockito.BDDMockito.when;
3439

3540
/**
3641
* Tests for {@link CassandraDriverHealthIndicator}.
3742
*
3843
* @author Alexandre Dutra
44+
* @author Tomasz Lelek
3945
* @since 2.4.0
4046
*/
4147
class CassandraDriverHealthIndicatorTests {
@@ -46,29 +52,150 @@ void createWhenCqlSessionIsNullShouldThrowException() {
4652
}
4753

4854
@Test
49-
void healthWithCassandraUp() {
55+
void oneHealthyNodeShouldReturnUp() {
5056
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");
57+
Metadata metadata = mock(Metadata.class);
58+
Node healthyNode = mock(Node.class);
59+
given(healthyNode.getState()).willReturn(NodeState.UP);
60+
given(session.getMetadata()).willReturn(metadata);
61+
given(metadata.getNodes()).willReturn(createNodesMap(healthyNode));
5762
CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session);
5863
Health health = healthIndicator.health();
5964
assertThat(health.getStatus()).isEqualTo(Status.UP);
60-
assertThat(health.getDetails().get("version")).isEqualTo("1.0.0");
65+
}
66+
67+
@Test
68+
void oneUnhealthyNodeShouldReturnDown() {
69+
CqlSession session = mock(CqlSession.class);
70+
Metadata metadata = mock(Metadata.class);
71+
Node unhealthyNode = mock(Node.class);
72+
given(unhealthyNode.getState()).willReturn(NodeState.DOWN);
73+
given(session.getMetadata()).willReturn(metadata);
74+
given(metadata.getNodes()).willReturn(createNodesMap(unhealthyNode));
75+
CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session);
76+
Health health = healthIndicator.health();
77+
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
78+
}
79+
80+
@Test
81+
void oneUnknownNodeShouldReturnDown() {
82+
CqlSession session = mock(CqlSession.class);
83+
Metadata metadata = mock(Metadata.class);
84+
Node unknownNode = mock(Node.class);
85+
given(unknownNode.getState()).willReturn(NodeState.UNKNOWN);
86+
given(session.getMetadata()).willReturn(metadata);
87+
given(metadata.getNodes()).willReturn(createNodesMap(unknownNode));
88+
CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session);
89+
Health health = healthIndicator.health();
90+
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
91+
}
92+
93+
@Test
94+
void oneForcedDownNodeShouldReturnDown() {
95+
CqlSession session = mock(CqlSession.class);
96+
Metadata metadata = mock(Metadata.class);
97+
Node forcedDownNode = mock(Node.class);
98+
given(forcedDownNode.getState()).willReturn(NodeState.FORCED_DOWN);
99+
given(session.getMetadata()).willReturn(metadata);
100+
given(metadata.getNodes()).willReturn(createNodesMap(forcedDownNode));
101+
CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session);
102+
Health health = healthIndicator.health();
103+
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
104+
}
105+
106+
@Test
107+
void oneHealthyNodeAndOneUnhealthyNodeShouldReturnUp() {
108+
CqlSession session = mock(CqlSession.class);
109+
Metadata metadata = mock(Metadata.class);
110+
Node healthyNode = mock(Node.class);
111+
Node unhealthyNode = mock(Node.class);
112+
given(healthyNode.getState()).willReturn(NodeState.UP);
113+
given(unhealthyNode.getState()).willReturn(NodeState.DOWN);
114+
given(session.getMetadata()).willReturn(metadata);
115+
given(metadata.getNodes()).willReturn(createNodesMap(healthyNode, unhealthyNode));
116+
CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session);
117+
Health health = healthIndicator.health();
118+
assertThat(health.getStatus()).isEqualTo(Status.UP);
119+
}
120+
121+
@Test
122+
void oneHealthyNodeAndOneUnknownNodeShouldReturnUp() {
123+
CqlSession session = mock(CqlSession.class);
124+
Metadata metadata = mock(Metadata.class);
125+
Node healthyNode = mock(Node.class);
126+
Node unknownNode = mock(Node.class);
127+
given(healthyNode.getState()).willReturn(NodeState.UP);
128+
given(unknownNode.getState()).willReturn(NodeState.UNKNOWN);
129+
given(session.getMetadata()).willReturn(metadata);
130+
given(metadata.getNodes()).willReturn(createNodesMap(healthyNode, unknownNode));
131+
CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session);
132+
Health health = healthIndicator.health();
133+
assertThat(health.getStatus()).isEqualTo(Status.UP);
134+
}
135+
136+
@Test
137+
void oneHealthyNodeAndOneForcedDownNodeShouldReturnUp() {
138+
CqlSession session = mock(CqlSession.class);
139+
Metadata metadata = mock(Metadata.class);
140+
Node healthyNode = mock(Node.class);
141+
Node forcedDownNode = mock(Node.class);
142+
given(healthyNode.getState()).willReturn(NodeState.UP);
143+
given(forcedDownNode.getState()).willReturn(NodeState.FORCED_DOWN);
144+
given(session.getMetadata()).willReturn(metadata);
145+
given(metadata.getNodes()).willReturn(createNodesMap(healthyNode, forcedDownNode));
146+
CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session);
147+
Health health = healthIndicator.health();
148+
assertThat(health.getStatus()).isEqualTo(Status.UP);
149+
}
150+
151+
@Test
152+
void addVersionToDetailsIfReportedNotNull() {
153+
CqlSession session = mock(CqlSession.class);
154+
Metadata metadata = mock(Metadata.class);
155+
when(session.getMetadata()).thenReturn(metadata);
156+
Node node = mock(Node.class);
157+
when(node.getState()).thenReturn(NodeState.UP);
158+
when(node.getCassandraVersion()).thenReturn(Version.V4_0_0);
159+
when(metadata.getNodes()).thenReturn(createNodesMap(node));
160+
161+
CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session);
162+
Health health = healthIndicator.health();
163+
assertThat(health.getStatus()).isEqualTo(Status.UP);
164+
assertThat(health.getDetails().get("version")).isEqualTo(Version.V4_0_0);
165+
}
166+
167+
@Test
168+
void doNotAddVersionToDetailsIfReportedNull() {
169+
CqlSession session = mock(CqlSession.class);
170+
Metadata metadata = mock(Metadata.class);
171+
when(session.getMetadata()).thenReturn(metadata);
172+
Node node = mock(Node.class);
173+
when(node.getState()).thenReturn(NodeState.UP);
174+
when(metadata.getNodes()).thenReturn(createNodesMap(node));
175+
176+
CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session);
177+
Health health = healthIndicator.health();
178+
assertThat(health.getStatus()).isEqualTo(Status.UP);
179+
assertThat(health.getDetails().get("version")).isNull();
61180
}
62181

63182
@Test
64183
void healthWithCassandraDown() {
65184
CqlSession session = mock(CqlSession.class);
66-
given(session.execute(any(SimpleStatement.class))).willThrow(new DriverTimeoutException("Test Exception"));
185+
given(session.getMetadata()).willThrow(new DriverTimeoutException("Test Exception"));
67186
CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session);
68187
Health health = healthIndicator.health();
69188
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
70189
assertThat(health.getDetails().get("error"))
71190
.isEqualTo(DriverTimeoutException.class.getName() + ": Test Exception");
72191
}
73192

193+
private static Map<UUID, Node> createNodesMap(Node... nodes) {
194+
Map<UUID, Node> nodesMap = new HashMap<>();
195+
for (Node n : nodes) {
196+
nodesMap.put(UUID.randomUUID(), n);
197+
}
198+
return nodesMap;
199+
}
200+
74201
}

0 commit comments

Comments
 (0)