Skip to content

Commit 6d98851

Browse files
committed
Use Couchbase's DiagnosticsReport to determine its health
Previously, Couchbase's health was determined by retrieving the bucket info from the cluster info. This retrieval could take over one minute in some cases even when Couchbase is health. This latency is too large for a health check. The Couchbase team have recommended the of a Cluster#diagnostics instead. This provides a much lower latency view of the cluster's health. This commit updates CouchbaseHealthIndicator to use Cluster#diagnostics while retaining support, in a deprecated form, for the old info-based mechanism should anyone want to opt back into that in 2.0.x. Closes gh-14685
1 parent b7b9bf2 commit 6d98851

File tree

6 files changed

+186
-61
lines changed

6 files changed

+186
-61
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/couchbase/CouchbaseHealthIndicatorAutoConfiguration.java

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
import java.util.Map;
2020

21-
import com.couchbase.client.java.Bucket;
21+
import com.couchbase.client.java.Cluster;
2222

2323
import org.springframework.boot.actuate.autoconfigure.health.CompositeHealthIndicatorConfiguration;
2424
import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator;
@@ -31,52 +31,43 @@
3131
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
3232
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
3333
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
34-
import org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration;
35-
import org.springframework.boot.context.properties.EnableConfigurationProperties;
34+
import org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration;
3635
import org.springframework.context.annotation.Bean;
3736
import org.springframework.context.annotation.Configuration;
38-
import org.springframework.data.couchbase.core.CouchbaseOperations;
3937

4038
/**
4139
* {@link EnableAutoConfiguration Auto-configuration} for
4240
* {@link CouchbaseHealthIndicator}.
4341
*
4442
* @author Eddú Meléndez
4543
* @author Stephane Nicoll
44+
* @author Andy Wilkinson Nicoll
4645
* @since 2.0.0
4746
*/
4847
@Configuration
49-
@ConditionalOnClass({ CouchbaseOperations.class, Bucket.class })
50-
@ConditionalOnBean(CouchbaseOperations.class)
48+
@ConditionalOnClass(Cluster.class)
49+
@ConditionalOnBean(Cluster.class)
5150
@ConditionalOnEnabledHealthIndicator("couchbase")
5251
@AutoConfigureBefore(HealthIndicatorAutoConfiguration.class)
53-
@AutoConfigureAfter(CouchbaseDataAutoConfiguration.class)
54-
@EnableConfigurationProperties(CouchbaseHealthIndicatorProperties.class)
55-
public class CouchbaseHealthIndicatorAutoConfiguration extends
56-
CompositeHealthIndicatorConfiguration<CouchbaseHealthIndicator, CouchbaseOperations> {
52+
@AutoConfigureAfter(CouchbaseAutoConfiguration.class)
53+
public class CouchbaseHealthIndicatorAutoConfiguration
54+
extends CompositeHealthIndicatorConfiguration<CouchbaseHealthIndicator, Cluster> {
5755

58-
private final Map<String, CouchbaseOperations> couchbaseOperations;
56+
private final Map<String, Cluster> clusters;
5957

60-
private final CouchbaseHealthIndicatorProperties properties;
61-
62-
public CouchbaseHealthIndicatorAutoConfiguration(
63-
Map<String, CouchbaseOperations> couchbaseOperations,
64-
CouchbaseHealthIndicatorProperties properties) {
65-
this.couchbaseOperations = couchbaseOperations;
66-
this.properties = properties;
58+
public CouchbaseHealthIndicatorAutoConfiguration(Map<String, Cluster> clusters) {
59+
this.clusters = clusters;
6760
}
6861

6962
@Bean
7063
@ConditionalOnMissingBean(name = "couchbaseHealthIndicator")
7164
public HealthIndicator couchbaseHealthIndicator() {
72-
return createHealthIndicator(this.couchbaseOperations);
65+
return createHealthIndicator(this.clusters);
7366
}
7467

7568
@Override
76-
protected CouchbaseHealthIndicator createHealthIndicator(
77-
CouchbaseOperations couchbaseOperations) {
78-
return new CouchbaseHealthIndicator(couchbaseOperations,
79-
this.properties.getTimeout());
69+
protected CouchbaseHealthIndicator createHealthIndicator(Cluster cluster) {
70+
return new CouchbaseHealthIndicator(cluster);
8071
}
8172

8273
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/couchbase/CouchbaseHealthIndicatorProperties.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
*
2727
* @author Stephane Nicoll
2828
* @since 2.0.5
29+
* @deprecated since 2.0.6
2930
*/
31+
@Deprecated
3032
@ConfigurationProperties(prefix = "management.health.couchbase")
3133
public class CouchbaseHealthIndicatorProperties {
3234

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/couchbase/CouchbaseHealthIndicatorAutoConfigurationTests.java

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.couchbase;
1818

19+
import com.couchbase.client.java.Cluster;
1920
import org.junit.Test;
2021

2122
import org.springframework.boot.actuate.autoconfigure.health.HealthIndicatorAutoConfiguration;
@@ -26,8 +27,6 @@
2627
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
2728
import org.springframework.context.annotation.Bean;
2829
import org.springframework.context.annotation.Configuration;
29-
import org.springframework.data.couchbase.core.CouchbaseOperations;
30-
import org.springframework.test.util.ReflectionTestUtils;
3130

3231
import static org.assertj.core.api.Assertions.assertThat;
3332
import static org.mockito.Mockito.mock;
@@ -52,17 +51,6 @@ public void runShouldCreateIndicator() {
5251
.doesNotHaveBean(ApplicationHealthIndicator.class));
5352
}
5453

55-
@Test
56-
public void runWithCustomTimeoutShouldCreateIndicator() {
57-
this.contextRunner.withPropertyValues("management.health.couchbase.timeout=2s")
58-
.run((context) -> {
59-
assertThat(context).hasSingleBean(CouchbaseHealthIndicator.class);
60-
assertThat(ReflectionTestUtils.getField(
61-
context.getBean(CouchbaseHealthIndicator.class), "timeout"))
62-
.isEqualTo(2000L);
63-
});
64-
}
65-
6654
@Test
6755
public void runWhenDisabledShouldNotCreateIndicator() {
6856
this.contextRunner.withPropertyValues("management.health.couchbase.enabled:false")
@@ -76,8 +64,8 @@ public void runWhenDisabledShouldNotCreateIndicator() {
7664
protected static class CouchbaseConfiguration {
7765

7866
@Bean
79-
public CouchbaseOperations couchbaseOperations() {
80-
return mock(CouchbaseOperations.class);
67+
public Cluster cluster() {
68+
return mock(Cluster.class);
8169
}
8270

8371
}

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/couchbase/CouchbaseHealthIndicator.java

Lines changed: 108 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,22 @@
1717
package org.springframework.boot.actuate.couchbase;
1818

1919
import java.time.Duration;
20+
import java.util.HashMap;
21+
import java.util.Map;
2022
import java.util.concurrent.TimeUnit;
2123
import java.util.concurrent.TimeoutException;
24+
import java.util.stream.Collectors;
2225

26+
import com.couchbase.client.core.message.internal.DiagnosticsReport;
27+
import com.couchbase.client.core.message.internal.EndpointHealth;
28+
import com.couchbase.client.core.state.LifecycleState;
29+
import com.couchbase.client.java.Cluster;
2330
import com.couchbase.client.java.bucket.BucketInfo;
2431
import com.couchbase.client.java.cluster.ClusterInfo;
2532

2633
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
2734
import org.springframework.boot.actuate.health.Health;
35+
import org.springframework.boot.actuate.health.Health.Builder;
2836
import org.springframework.boot.actuate.health.HealthIndicator;
2937
import org.springframework.data.couchbase.core.CouchbaseOperations;
3038
import org.springframework.util.Assert;
@@ -39,23 +47,23 @@
3947
*/
4048
public class CouchbaseHealthIndicator extends AbstractHealthIndicator {
4149

42-
private final CouchbaseOperations operations;
43-
44-
private final long timeout;
50+
private final HealthCheck healthCheck;
4551

4652
/**
4753
* Create an indicator with the specified {@link CouchbaseOperations} and
4854
* {@code timeout}.
4955
* @param couchbaseOperations the couchbase operations
5056
* @param timeout the request timeout
57+
* @deprecated since 2.0.6 in favour of {@link #CouchbaseHealthIndicator(Cluster)}
5158
*/
59+
@Deprecated
5260
public CouchbaseHealthIndicator(CouchbaseOperations couchbaseOperations,
5361
Duration timeout) {
5462
super("Couchbase health check failed");
5563
Assert.notNull(couchbaseOperations, "CouchbaseOperations must not be null");
5664
Assert.notNull(timeout, "Timeout must not be null");
57-
this.operations = couchbaseOperations;
58-
this.timeout = timeout.toMillis();
65+
this.healthCheck = new OperationsHealthCheck(couchbaseOperations,
66+
timeout.toMillis());
5967
}
6068

6169
/**
@@ -69,27 +77,108 @@ public CouchbaseHealthIndicator(CouchbaseOperations couchbaseOperations) {
6977
this(couchbaseOperations, Duration.ofSeconds(1));
7078
}
7179

80+
/**
81+
* Create an indicator with the specified {@link Cluster}.
82+
* @param cluster the Couchbase Cluster
83+
* @since 2.0.6
84+
*/
85+
public CouchbaseHealthIndicator(Cluster cluster) {
86+
super("Couchbase health check failed");
87+
Assert.notNull(cluster, "Cluster must not be null");
88+
this.healthCheck = new ClusterHealthCheck(cluster);
89+
}
90+
7291
@Override
7392
protected void doHealthCheck(Health.Builder builder) throws Exception {
74-
ClusterInfo cluster = this.operations.getCouchbaseClusterInfo();
75-
BucketInfo bucket = getBucketInfo();
76-
String versions = StringUtils
77-
.collectionToCommaDelimitedString(cluster.getAllVersions());
78-
String nodes = StringUtils.collectionToCommaDelimitedString(bucket.nodeList());
79-
builder.up().withDetail("versions", versions).withDetail("nodes", nodes);
93+
this.healthCheck.checkHealth(builder);
94+
}
95+
96+
private interface HealthCheck {
97+
98+
void checkHealth(Builder builder) throws Exception;
99+
100+
}
101+
102+
private static final class OperationsHealthCheck implements HealthCheck {
103+
104+
private final CouchbaseOperations operations;
105+
106+
private final long timeout;
107+
108+
OperationsHealthCheck(CouchbaseOperations operations, long timeout) {
109+
this.operations = operations;
110+
this.timeout = timeout;
111+
}
112+
113+
@Override
114+
public void checkHealth(Builder builder) throws Exception {
115+
ClusterInfo cluster = this.operations.getCouchbaseClusterInfo();
116+
BucketInfo bucket = getBucketInfo();
117+
String versions = StringUtils
118+
.collectionToCommaDelimitedString(cluster.getAllVersions());
119+
String nodes = StringUtils
120+
.collectionToCommaDelimitedString(bucket.nodeList());
121+
builder.up().withDetail("versions", versions).withDetail("nodes", nodes);
122+
}
123+
124+
private BucketInfo getBucketInfo() throws Exception {
125+
try {
126+
return this.operations.getCouchbaseBucket().bucketManager()
127+
.info(this.timeout, TimeUnit.MILLISECONDS);
128+
}
129+
catch (RuntimeException ex) {
130+
if (ex.getCause() instanceof TimeoutException) {
131+
throw (TimeoutException) ex.getCause();
132+
}
133+
throw ex;
134+
}
135+
}
136+
80137
}
81138

82-
private BucketInfo getBucketInfo() throws Exception {
83-
try {
84-
return this.operations.getCouchbaseBucket().bucketManager().info(this.timeout,
85-
TimeUnit.MILLISECONDS);
139+
private static class ClusterHealthCheck implements HealthCheck {
140+
141+
private final Cluster cluster;
142+
143+
ClusterHealthCheck(Cluster cluster) {
144+
this.cluster = cluster;
86145
}
87-
catch (RuntimeException ex) {
88-
if (ex.getCause() instanceof TimeoutException) {
89-
throw (TimeoutException) ex.getCause();
146+
147+
@Override
148+
public void checkHealth(Builder builder) throws Exception {
149+
DiagnosticsReport diagnostics = this.cluster.diagnostics();
150+
if (isCouchbaseUp(diagnostics)) {
151+
builder.up();
152+
}
153+
else {
154+
builder.down();
155+
}
156+
builder.withDetail("sdk", diagnostics.sdk());
157+
builder.withDetail("endpoints", diagnostics.endpoints().stream()
158+
.map(this::describe).collect(Collectors.toList()));
159+
}
160+
161+
private boolean isCouchbaseUp(DiagnosticsReport diagnostics) {
162+
for (EndpointHealth health : diagnostics.endpoints()) {
163+
LifecycleState state = health.state();
164+
if (state != LifecycleState.CONNECTED && state != LifecycleState.IDLE) {
165+
return false;
166+
}
90167
}
91-
throw ex;
168+
return true;
169+
}
170+
171+
private Map<String, Object> describe(EndpointHealth endpointHealth) {
172+
Map<String, Object> map = new HashMap<>();
173+
map.put("id", endpointHealth.id());
174+
map.put("lastActivity", endpointHealth.lastActivity());
175+
map.put("local", endpointHealth.local().toString());
176+
map.put("remote", endpointHealth.remote().toString());
177+
map.put("state", endpointHealth.state());
178+
map.put("type", endpointHealth.type());
179+
return map;
92180
}
181+
93182
}
94183

95184
}

0 commit comments

Comments
 (0)