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
This commit is contained in:
Andy Wilkinson 2018-10-15 11:37:34 +01:00
parent b7b9bf209e
commit 6d98851fb5
6 changed files with 188 additions and 63 deletions

View File

@ -18,7 +18,7 @@ package org.springframework.boot.actuate.autoconfigure.couchbase;
import java.util.Map;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.Cluster;
import org.springframework.boot.actuate.autoconfigure.health.CompositeHealthIndicatorConfiguration;
import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator;
@ -31,11 +31,9 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.couchbase.core.CouchbaseOperations;
/**
* {@link EnableAutoConfiguration Auto-configuration} for
@ -43,40 +41,33 @@ import org.springframework.data.couchbase.core.CouchbaseOperations;
*
* @author Eddú Meléndez
* @author Stephane Nicoll
* @author Andy Wilkinson Nicoll
* @since 2.0.0
*/
@Configuration
@ConditionalOnClass({ CouchbaseOperations.class, Bucket.class })
@ConditionalOnBean(CouchbaseOperations.class)
@ConditionalOnClass(Cluster.class)
@ConditionalOnBean(Cluster.class)
@ConditionalOnEnabledHealthIndicator("couchbase")
@AutoConfigureBefore(HealthIndicatorAutoConfiguration.class)
@AutoConfigureAfter(CouchbaseDataAutoConfiguration.class)
@EnableConfigurationProperties(CouchbaseHealthIndicatorProperties.class)
public class CouchbaseHealthIndicatorAutoConfiguration extends
CompositeHealthIndicatorConfiguration<CouchbaseHealthIndicator, CouchbaseOperations> {
@AutoConfigureAfter(CouchbaseAutoConfiguration.class)
public class CouchbaseHealthIndicatorAutoConfiguration
extends CompositeHealthIndicatorConfiguration<CouchbaseHealthIndicator, Cluster> {
private final Map<String, CouchbaseOperations> couchbaseOperations;
private final Map<String, Cluster> clusters;
private final CouchbaseHealthIndicatorProperties properties;
public CouchbaseHealthIndicatorAutoConfiguration(
Map<String, CouchbaseOperations> couchbaseOperations,
CouchbaseHealthIndicatorProperties properties) {
this.couchbaseOperations = couchbaseOperations;
this.properties = properties;
public CouchbaseHealthIndicatorAutoConfiguration(Map<String, Cluster> clusters) {
this.clusters = clusters;
}
@Bean
@ConditionalOnMissingBean(name = "couchbaseHealthIndicator")
public HealthIndicator couchbaseHealthIndicator() {
return createHealthIndicator(this.couchbaseOperations);
return createHealthIndicator(this.clusters);
}
@Override
protected CouchbaseHealthIndicator createHealthIndicator(
CouchbaseOperations couchbaseOperations) {
return new CouchbaseHealthIndicator(couchbaseOperations,
this.properties.getTimeout());
protected CouchbaseHealthIndicator createHealthIndicator(Cluster cluster) {
return new CouchbaseHealthIndicator(cluster);
}
}

View File

@ -26,7 +26,9 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
*
* @author Stephane Nicoll
* @since 2.0.5
* @deprecated since 2.0.6
*/
@Deprecated
@ConfigurationProperties(prefix = "management.health.couchbase")
public class CouchbaseHealthIndicatorProperties {

View File

@ -16,6 +16,7 @@
package org.springframework.boot.actuate.autoconfigure.couchbase;
import com.couchbase.client.java.Cluster;
import org.junit.Test;
import org.springframework.boot.actuate.autoconfigure.health.HealthIndicatorAutoConfiguration;
@ -26,8 +27,6 @@ import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
@ -52,17 +51,6 @@ public class CouchbaseHealthIndicatorAutoConfigurationTests {
.doesNotHaveBean(ApplicationHealthIndicator.class));
}
@Test
public void runWithCustomTimeoutShouldCreateIndicator() {
this.contextRunner.withPropertyValues("management.health.couchbase.timeout=2s")
.run((context) -> {
assertThat(context).hasSingleBean(CouchbaseHealthIndicator.class);
assertThat(ReflectionTestUtils.getField(
context.getBean(CouchbaseHealthIndicator.class), "timeout"))
.isEqualTo(2000L);
});
}
@Test
public void runWhenDisabledShouldNotCreateIndicator() {
this.contextRunner.withPropertyValues("management.health.couchbase.enabled:false")
@ -76,8 +64,8 @@ public class CouchbaseHealthIndicatorAutoConfigurationTests {
protected static class CouchbaseConfiguration {
@Bean
public CouchbaseOperations couchbaseOperations() {
return mock(CouchbaseOperations.class);
public Cluster cluster() {
return mock(Cluster.class);
}
}

View File

@ -17,14 +17,22 @@
package org.springframework.boot.actuate.couchbase;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import com.couchbase.client.core.message.internal.DiagnosticsReport;
import com.couchbase.client.core.message.internal.EndpointHealth;
import com.couchbase.client.core.state.LifecycleState;
import com.couchbase.client.java.Cluster;
import com.couchbase.client.java.bucket.BucketInfo;
import com.couchbase.client.java.cluster.ClusterInfo;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Health.Builder;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.util.Assert;
@ -39,23 +47,23 @@ import org.springframework.util.StringUtils;
*/
public class CouchbaseHealthIndicator extends AbstractHealthIndicator {
private final CouchbaseOperations operations;
private final long timeout;
private final HealthCheck healthCheck;
/**
* Create an indicator with the specified {@link CouchbaseOperations} and
* {@code timeout}.
* @param couchbaseOperations the couchbase operations
* @param timeout the request timeout
* @deprecated since 2.0.6 in favour of {@link #CouchbaseHealthIndicator(Cluster)}
*/
@Deprecated
public CouchbaseHealthIndicator(CouchbaseOperations couchbaseOperations,
Duration timeout) {
super("Couchbase health check failed");
Assert.notNull(couchbaseOperations, "CouchbaseOperations must not be null");
Assert.notNull(timeout, "Timeout must not be null");
this.operations = couchbaseOperations;
this.timeout = timeout.toMillis();
this.healthCheck = new OperationsHealthCheck(couchbaseOperations,
timeout.toMillis());
}
/**
@ -69,27 +77,108 @@ public class CouchbaseHealthIndicator extends AbstractHealthIndicator {
this(couchbaseOperations, Duration.ofSeconds(1));
}
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
ClusterInfo cluster = this.operations.getCouchbaseClusterInfo();
BucketInfo bucket = getBucketInfo();
String versions = StringUtils
.collectionToCommaDelimitedString(cluster.getAllVersions());
String nodes = StringUtils.collectionToCommaDelimitedString(bucket.nodeList());
builder.up().withDetail("versions", versions).withDetail("nodes", nodes);
/**
* Create an indicator with the specified {@link Cluster}.
* @param cluster the Couchbase Cluster
* @since 2.0.6
*/
public CouchbaseHealthIndicator(Cluster cluster) {
super("Couchbase health check failed");
Assert.notNull(cluster, "Cluster must not be null");
this.healthCheck = new ClusterHealthCheck(cluster);
}
private BucketInfo getBucketInfo() throws Exception {
try {
return this.operations.getCouchbaseBucket().bucketManager().info(this.timeout,
TimeUnit.MILLISECONDS);
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
this.healthCheck.checkHealth(builder);
}
private interface HealthCheck {
void checkHealth(Builder builder) throws Exception;
}
private static final class OperationsHealthCheck implements HealthCheck {
private final CouchbaseOperations operations;
private final long timeout;
OperationsHealthCheck(CouchbaseOperations operations, long timeout) {
this.operations = operations;
this.timeout = timeout;
}
catch (RuntimeException ex) {
if (ex.getCause() instanceof TimeoutException) {
throw (TimeoutException) ex.getCause();
@Override
public void checkHealth(Builder builder) throws Exception {
ClusterInfo cluster = this.operations.getCouchbaseClusterInfo();
BucketInfo bucket = getBucketInfo();
String versions = StringUtils
.collectionToCommaDelimitedString(cluster.getAllVersions());
String nodes = StringUtils
.collectionToCommaDelimitedString(bucket.nodeList());
builder.up().withDetail("versions", versions).withDetail("nodes", nodes);
}
private BucketInfo getBucketInfo() throws Exception {
try {
return this.operations.getCouchbaseBucket().bucketManager()
.info(this.timeout, TimeUnit.MILLISECONDS);
}
catch (RuntimeException ex) {
if (ex.getCause() instanceof TimeoutException) {
throw (TimeoutException) ex.getCause();
}
throw ex;
}
throw ex;
}
}
private static class ClusterHealthCheck implements HealthCheck {
private final Cluster cluster;
ClusterHealthCheck(Cluster cluster) {
this.cluster = cluster;
}
@Override
public void checkHealth(Builder builder) throws Exception {
DiagnosticsReport diagnostics = this.cluster.diagnostics();
if (isCouchbaseUp(diagnostics)) {
builder.up();
}
else {
builder.down();
}
builder.withDetail("sdk", diagnostics.sdk());
builder.withDetail("endpoints", diagnostics.endpoints().stream()
.map(this::describe).collect(Collectors.toList()));
}
private boolean isCouchbaseUp(DiagnosticsReport diagnostics) {
for (EndpointHealth health : diagnostics.endpoints()) {
LifecycleState state = health.state();
if (state != LifecycleState.CONNECTED && state != LifecycleState.IDLE) {
return false;
}
}
return true;
}
private Map<String, Object> describe(EndpointHealth endpointHealth) {
Map<String, Object> map = new HashMap<>();
map.put("id", endpointHealth.id());
map.put("lastActivity", endpointHealth.lastActivity());
map.put("local", endpointHealth.local().toString());
map.put("remote", endpointHealth.remote().toString());
map.put("state", endpointHealth.state());
map.put("type", endpointHealth.type());
return map;
}
}
}

View File

@ -17,13 +17,22 @@
package org.springframework.boot.actuate.couchbase;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import com.couchbase.client.core.message.internal.DiagnosticsReport;
import com.couchbase.client.core.message.internal.EndpointHealth;
import com.couchbase.client.core.service.ServiceType;
import com.couchbase.client.core.state.LifecycleState;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.Cluster;
import com.couchbase.client.java.bucket.BucketInfo;
import com.couchbase.client.java.bucket.BucketManager;
import com.couchbase.client.java.cluster.ClusterInfo;
@ -49,7 +58,7 @@ import static org.mockito.Mockito.verify;
public class CouchbaseHealthIndicatorTests {
@Test
public void couchbaseIsUp() throws UnknownHostException {
public void couchbaseOperationsIsUp() throws UnknownHostException {
BucketInfo bucketInfo = mock(BucketInfo.class);
given(bucketInfo.nodeList()).willReturn(
Collections.singletonList(InetAddress.getByName("127.0.0.1")));
@ -63,6 +72,7 @@ public class CouchbaseHealthIndicatorTests {
CouchbaseOperations couchbaseOperations = mock(CouchbaseOperations.class);
given(couchbaseOperations.getCouchbaseBucket()).willReturn(bucket);
given(couchbaseOperations.getCouchbaseClusterInfo()).willReturn(clusterInfo);
@SuppressWarnings("deprecation")
CouchbaseHealthIndicator healthIndicator = new CouchbaseHealthIndicator(
couchbaseOperations, Duration.ofSeconds(2));
Health health = healthIndicator.health();
@ -74,7 +84,7 @@ public class CouchbaseHealthIndicatorTests {
}
@Test
public void couchbaseTimeout() {
public void couchbaseOperationsTimeout() {
BucketManager bucketManager = mock(BucketManager.class);
given(bucketManager.info(1500, TimeUnit.MILLISECONDS)).willThrow(
new RuntimeException(new TimeoutException("timeout, expected")));
@ -82,6 +92,7 @@ public class CouchbaseHealthIndicatorTests {
given(bucket.bucketManager()).willReturn(bucketManager);
CouchbaseOperations couchbaseOperations = mock(CouchbaseOperations.class);
given(couchbaseOperations.getCouchbaseBucket()).willReturn(bucket);
@SuppressWarnings("deprecation")
CouchbaseHealthIndicator healthIndicator = new CouchbaseHealthIndicator(
couchbaseOperations, Duration.ofMillis(1500));
Health health = healthIndicator.health();
@ -90,10 +101,11 @@ public class CouchbaseHealthIndicatorTests {
}
@Test
public void couchbaseIsDown() {
public void couchbaseOperationsIsDown() {
CouchbaseOperations couchbaseOperations = mock(CouchbaseOperations.class);
given(couchbaseOperations.getCouchbaseClusterInfo())
.willThrow(new IllegalStateException("test, expected"));
@SuppressWarnings("deprecation")
CouchbaseHealthIndicator healthIndicator = new CouchbaseHealthIndicator(
couchbaseOperations, Duration.ofSeconds(1));
Health health = healthIndicator.health();
@ -102,4 +114,48 @@ public class CouchbaseHealthIndicatorTests {
verify(couchbaseOperations).getCouchbaseClusterInfo();
}
@Test
@SuppressWarnings("unchecked")
public void couchbaseClusterIsUp() {
Cluster cluster = mock(Cluster.class);
CouchbaseHealthIndicator healthIndicator = new CouchbaseHealthIndicator(cluster);
List<EndpointHealth> endpoints = Arrays.asList(new EndpointHealth(
ServiceType.BINARY, LifecycleState.CONNECTED, new InetSocketAddress(0),
new InetSocketAddress(0), 1234, "endpoint-1"));
DiagnosticsReport diagnostics = new DiagnosticsReport(endpoints, "test-sdk",
"test-id");
given(cluster.diagnostics()).willReturn(diagnostics);
Health health = healthIndicator.health();
assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health.getDetails()).containsEntry("sdk", "test-sdk");
assertThat(health.getDetails()).containsKey("endpoints");
assertThat((List<Map<String, Object>>) health.getDetails().get("endpoints"))
.hasSize(1);
verify(cluster).diagnostics();
}
@Test
@SuppressWarnings("unchecked")
public void couchbaseClusterIsDown() {
Cluster cluster = mock(Cluster.class);
CouchbaseHealthIndicator healthIndicator = new CouchbaseHealthIndicator(cluster);
List<EndpointHealth> endpoints = Arrays.asList(
new EndpointHealth(ServiceType.BINARY, LifecycleState.CONNECTED,
new InetSocketAddress(0), new InetSocketAddress(0), 1234,
"endpoint-1"),
new EndpointHealth(ServiceType.BINARY, LifecycleState.CONNECTING,
new InetSocketAddress(0), new InetSocketAddress(0), 1234,
"endpoint-2"));
DiagnosticsReport diagnostics = new DiagnosticsReport(endpoints, "test-sdk",
"test-id");
given(cluster.diagnostics()).willReturn(diagnostics);
Health health = healthIndicator.health();
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
assertThat(health.getDetails()).containsEntry("sdk", "test-sdk");
assertThat(health.getDetails()).containsKey("endpoints");
assertThat((List<Map<String, Object>>) health.getDetails().get("endpoints"))
.hasSize(2);
verify(cluster).diagnostics();
}
}

View File

@ -1284,7 +1284,6 @@ content into your application. Rather, pick only the properties that you need.
management.health.db.enabled=true # Whether to enable database health check.
management.health.cassandra.enabled=true # Whether to enable Cassandra health check.
management.health.couchbase.enabled=true # Whether to enable Couchbase health check.
management.health.couchbase.timeout=1000ms # Timeout for getting the Bucket information from the server.
management.health.defaults.enabled=true # Whether to enable default health indicators.
management.health.diskspace.enabled=true # Whether to enable disk space health check.
management.health.diskspace.path= # Path used to compute the available disk space.