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
pull/14914/head
Andy Wilkinson 6 years ago
parent b7b9bf209e
commit 6d98851fb5

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

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

@ -16,6 +16,7 @@
package org.springframework.boot.actuate.autoconfigure.couchbase; package org.springframework.boot.actuate.autoconfigure.couchbase;
import com.couchbase.client.java.Cluster;
import org.junit.Test; import org.junit.Test;
import org.springframework.boot.actuate.autoconfigure.health.HealthIndicatorAutoConfiguration; 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.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@ -52,17 +51,6 @@ public class CouchbaseHealthIndicatorAutoConfigurationTests {
.doesNotHaveBean(ApplicationHealthIndicator.class)); .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 @Test
public void runWhenDisabledShouldNotCreateIndicator() { public void runWhenDisabledShouldNotCreateIndicator() {
this.contextRunner.withPropertyValues("management.health.couchbase.enabled:false") this.contextRunner.withPropertyValues("management.health.couchbase.enabled:false")
@ -76,8 +64,8 @@ public class CouchbaseHealthIndicatorAutoConfigurationTests {
protected static class CouchbaseConfiguration { protected static class CouchbaseConfiguration {
@Bean @Bean
public CouchbaseOperations couchbaseOperations() { public Cluster cluster() {
return mock(CouchbaseOperations.class); return mock(Cluster.class);
} }
} }

@ -17,14 +17,22 @@
package org.springframework.boot.actuate.couchbase; package org.springframework.boot.actuate.couchbase;
import java.time.Duration; import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; 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.bucket.BucketInfo;
import com.couchbase.client.java.cluster.ClusterInfo; import com.couchbase.client.java.cluster.ClusterInfo;
import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health; 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.boot.actuate.health.HealthIndicator;
import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -39,23 +47,23 @@ import org.springframework.util.StringUtils;
*/ */
public class CouchbaseHealthIndicator extends AbstractHealthIndicator { public class CouchbaseHealthIndicator extends AbstractHealthIndicator {
private final CouchbaseOperations operations; private final HealthCheck healthCheck;
private final long timeout;
/** /**
* Create an indicator with the specified {@link CouchbaseOperations} and * Create an indicator with the specified {@link CouchbaseOperations} and
* {@code timeout}. * {@code timeout}.
* @param couchbaseOperations the couchbase operations * @param couchbaseOperations the couchbase operations
* @param timeout the request timeout * @param timeout the request timeout
* @deprecated since 2.0.6 in favour of {@link #CouchbaseHealthIndicator(Cluster)}
*/ */
@Deprecated
public CouchbaseHealthIndicator(CouchbaseOperations couchbaseOperations, public CouchbaseHealthIndicator(CouchbaseOperations couchbaseOperations,
Duration timeout) { Duration timeout) {
super("Couchbase health check failed"); super("Couchbase health check failed");
Assert.notNull(couchbaseOperations, "CouchbaseOperations must not be null"); Assert.notNull(couchbaseOperations, "CouchbaseOperations must not be null");
Assert.notNull(timeout, "Timeout must not be null"); Assert.notNull(timeout, "Timeout must not be null");
this.operations = couchbaseOperations; this.healthCheck = new OperationsHealthCheck(couchbaseOperations,
this.timeout = timeout.toMillis(); timeout.toMillis());
} }
/** /**
@ -69,27 +77,108 @@ public class CouchbaseHealthIndicator extends AbstractHealthIndicator {
this(couchbaseOperations, Duration.ofSeconds(1)); this(couchbaseOperations, Duration.ofSeconds(1));
} }
/**
* 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);
}
@Override @Override
protected void doHealthCheck(Health.Builder builder) throws Exception { protected void doHealthCheck(Health.Builder builder) throws Exception {
ClusterInfo cluster = this.operations.getCouchbaseClusterInfo(); this.healthCheck.checkHealth(builder);
BucketInfo bucket = getBucketInfo(); }
String versions = StringUtils
.collectionToCommaDelimitedString(cluster.getAllVersions()); private interface HealthCheck {
String nodes = StringUtils.collectionToCommaDelimitedString(bucket.nodeList());
builder.up().withDetail("versions", versions).withDetail("nodes", nodes); 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;
}
@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;
}
}
} }
private BucketInfo getBucketInfo() throws Exception { private static class ClusterHealthCheck implements HealthCheck {
try {
return this.operations.getCouchbaseBucket().bucketManager().info(this.timeout, private final Cluster cluster;
TimeUnit.MILLISECONDS);
ClusterHealthCheck(Cluster cluster) {
this.cluster = cluster;
} }
catch (RuntimeException ex) {
if (ex.getCause() instanceof TimeoutException) { @Override
throw (TimeoutException) ex.getCause(); 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;
}
} }
throw ex; 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;
} }
} }
} }

@ -17,13 +17,22 @@
package org.springframework.boot.actuate.couchbase; package org.springframework.boot.actuate.couchbase;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.time.Duration; import java.time.Duration;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; 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.Bucket;
import com.couchbase.client.java.Cluster;
import com.couchbase.client.java.bucket.BucketInfo; import com.couchbase.client.java.bucket.BucketInfo;
import com.couchbase.client.java.bucket.BucketManager; import com.couchbase.client.java.bucket.BucketManager;
import com.couchbase.client.java.cluster.ClusterInfo; import com.couchbase.client.java.cluster.ClusterInfo;
@ -49,7 +58,7 @@ import static org.mockito.Mockito.verify;
public class CouchbaseHealthIndicatorTests { public class CouchbaseHealthIndicatorTests {
@Test @Test
public void couchbaseIsUp() throws UnknownHostException { public void couchbaseOperationsIsUp() throws UnknownHostException {
BucketInfo bucketInfo = mock(BucketInfo.class); BucketInfo bucketInfo = mock(BucketInfo.class);
given(bucketInfo.nodeList()).willReturn( given(bucketInfo.nodeList()).willReturn(
Collections.singletonList(InetAddress.getByName("127.0.0.1"))); Collections.singletonList(InetAddress.getByName("127.0.0.1")));
@ -63,6 +72,7 @@ public class CouchbaseHealthIndicatorTests {
CouchbaseOperations couchbaseOperations = mock(CouchbaseOperations.class); CouchbaseOperations couchbaseOperations = mock(CouchbaseOperations.class);
given(couchbaseOperations.getCouchbaseBucket()).willReturn(bucket); given(couchbaseOperations.getCouchbaseBucket()).willReturn(bucket);
given(couchbaseOperations.getCouchbaseClusterInfo()).willReturn(clusterInfo); given(couchbaseOperations.getCouchbaseClusterInfo()).willReturn(clusterInfo);
@SuppressWarnings("deprecation")
CouchbaseHealthIndicator healthIndicator = new CouchbaseHealthIndicator( CouchbaseHealthIndicator healthIndicator = new CouchbaseHealthIndicator(
couchbaseOperations, Duration.ofSeconds(2)); couchbaseOperations, Duration.ofSeconds(2));
Health health = healthIndicator.health(); Health health = healthIndicator.health();
@ -74,7 +84,7 @@ public class CouchbaseHealthIndicatorTests {
} }
@Test @Test
public void couchbaseTimeout() { public void couchbaseOperationsTimeout() {
BucketManager bucketManager = mock(BucketManager.class); BucketManager bucketManager = mock(BucketManager.class);
given(bucketManager.info(1500, TimeUnit.MILLISECONDS)).willThrow( given(bucketManager.info(1500, TimeUnit.MILLISECONDS)).willThrow(
new RuntimeException(new TimeoutException("timeout, expected"))); new RuntimeException(new TimeoutException("timeout, expected")));
@ -82,6 +92,7 @@ public class CouchbaseHealthIndicatorTests {
given(bucket.bucketManager()).willReturn(bucketManager); given(bucket.bucketManager()).willReturn(bucketManager);
CouchbaseOperations couchbaseOperations = mock(CouchbaseOperations.class); CouchbaseOperations couchbaseOperations = mock(CouchbaseOperations.class);
given(couchbaseOperations.getCouchbaseBucket()).willReturn(bucket); given(couchbaseOperations.getCouchbaseBucket()).willReturn(bucket);
@SuppressWarnings("deprecation")
CouchbaseHealthIndicator healthIndicator = new CouchbaseHealthIndicator( CouchbaseHealthIndicator healthIndicator = new CouchbaseHealthIndicator(
couchbaseOperations, Duration.ofMillis(1500)); couchbaseOperations, Duration.ofMillis(1500));
Health health = healthIndicator.health(); Health health = healthIndicator.health();
@ -90,10 +101,11 @@ public class CouchbaseHealthIndicatorTests {
} }
@Test @Test
public void couchbaseIsDown() { public void couchbaseOperationsIsDown() {
CouchbaseOperations couchbaseOperations = mock(CouchbaseOperations.class); CouchbaseOperations couchbaseOperations = mock(CouchbaseOperations.class);
given(couchbaseOperations.getCouchbaseClusterInfo()) given(couchbaseOperations.getCouchbaseClusterInfo())
.willThrow(new IllegalStateException("test, expected")); .willThrow(new IllegalStateException("test, expected"));
@SuppressWarnings("deprecation")
CouchbaseHealthIndicator healthIndicator = new CouchbaseHealthIndicator( CouchbaseHealthIndicator healthIndicator = new CouchbaseHealthIndicator(
couchbaseOperations, Duration.ofSeconds(1)); couchbaseOperations, Duration.ofSeconds(1));
Health health = healthIndicator.health(); Health health = healthIndicator.health();
@ -102,4 +114,48 @@ public class CouchbaseHealthIndicatorTests {
verify(couchbaseOperations).getCouchbaseClusterInfo(); 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();
}
} }

@ -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.db.enabled=true # Whether to enable database health check.
management.health.cassandra.enabled=true # Whether to enable Cassandra 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.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.defaults.enabled=true # Whether to enable default health indicators.
management.health.diskspace.enabled=true # Whether to enable disk space health check. management.health.diskspace.enabled=true # Whether to enable disk space health check.
management.health.diskspace.path= # Path used to compute the available disk space. management.health.diskspace.path= # Path used to compute the available disk space.

Loading…
Cancel
Save