Adds ability for the TTL check to take into account the current application health status (#690)

Fixes gh-676

Co-authored-by: Spencer Gibb <spencer@gibb.us>
This commit is contained in:
Chris Bono
2022-09-23 12:21:39 -05:00
committed by GitHub
parent d077ef4f7e
commit f3be7d7947
16 changed files with 988 additions and 177 deletions

View File

@@ -32,9 +32,11 @@
|spring.cloud.consul.discovery.health-check-timeout | | Timeout for health check (e.g. 10s).
|spring.cloud.consul.discovery.health-check-tls-skip-verify | | Skips certificate verification during service checks if true, otherwise runs certificate verification.
|spring.cloud.consul.discovery.health-check-url | | Custom health check url to override default.
|spring.cloud.consul.discovery.heartbeat.actuator-health-group | | The actuator health group to use (`null` for the root group) when determining system health via Actuator.
|spring.cloud.consul.discovery.heartbeat.enabled | `+++false+++` |
|spring.cloud.consul.discovery.heartbeat.interval-ratio | |
|spring.cloud.consul.discovery.heartbeat.reregister-service-on-failure | `+++false+++` |
|spring.cloud.consul.discovery.heartbeat.use-actuator-health | `true` | Whether or not to take the current system health (as reported via the Actuator Health endpoint) into account when reporting the application status to the Consul TTL check. Actuator Health endpoint also has to be available to the application.
|spring.cloud.consul.discovery.heartbeat.ttl | `+++30s+++` |
|spring.cloud.consul.discovery.hostname | | Hostname to use when accessing server.
|spring.cloud.consul.discovery.include-hostname-in-instance-id | `+++false+++` | Whether hostname is included into the default instance id when registering service.
@@ -79,4 +81,4 @@
|spring.cloud.consul.tls.key-store-password | | Password to an external keystore.
|spring.cloud.consul.tls.key-store-path | | Path to an external keystore.
|===
|===

View File

@@ -35,7 +35,7 @@ To activate Consul Service Discovery use the starter with group `org.springframe
=== Registering with Consul
When a client registers with Consul, it provides meta-data about itself such as host and port, id, name and tags. An HTTP https://www.consul.io/docs/agent/checks.html[Check] is created by default that Consul hits the `/actuator/health` endpoint every 10 seconds. If the health check fails, the service instance is marked as critical.
When a client registers with Consul, it provides meta-data about itself such as host and port, id, name and tags. An https://www.consul.io/docs/discovery/checks#http-interval[HTTP Check] is created by default that Consul hits the `/actuator/health` endpoint every 10 seconds. If the health check fails, the service instance is marked as critical.
Example Consul client:
@@ -200,6 +200,78 @@ spring:
- "Some other value"
----
==== TTL Health Check
A Consul https://www.consul.io/docs/discovery/checks#ttl[TTL Check] can be used instead of the default configured HTTP check.
The main difference is that the application sends a heartbeat signal to the Consul agent rather than the Consul agent sending a request to the application.
The interval the application uses to send the ping may also be configured. "10s" and "1m" represent 10 seconds and 1 minute respectively.
The default is 30 seconds.
This example illustrates the above (see the `spring.cloud.consul.discovery.heartbeat.*` properties in link:appendix.html[the appendix page] for more options).
.application.yml
----
spring:
cloud:
consul:
discovery:
heartbeat:
enabled: true
ttl: 10s
----
===== TTL Application Status
For a Spring Boot Actuator application the status is determined from its available health endpoint.
When the health endpoint is not available (either disabled or not a Spring Boot Actuator application) it assumes the application is in good health.
When querying the health endpoint, the root https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#production-ready-health-groups[health group] is used by default.
A different health group can be used by setting the following property:
.application.yml
----
spring:
cloud:
consul:
discovery:
heartbeat:
actuator-health-group: <your-custom-group-goes-here>
----
You can disable the use of the health endpoint entirely by setting the following property:
.application.yml
----
spring:
cloud:
consul:
discovery:
heartbeat:
use-actuator-health: false
----
====== Custom TTL Application Status
If you want to configure your own application status mechanism, simply implement the `ApplicationStatusProvider` interface
.MyCustomApplicationStatusProvider.java
----
@Bean
public class MyCustomApplicationStatusProvider implements ApplicationStatusProvider {
public CheckStatus currentStatus() {
return yourMethodToDetermineAppStatusGoesHere();
}
}
----
and make it available to the application context:
----
@Bean
public CustomApplicationStatusProvider customAppStatusProvider() {
return new MyCustomApplicationStatusProvider();
}
----
==== Actuator Health Indicator(s)
If the service instance is a Spring Boot Actuator application, it may be provided the following Actuator health indicators.

View File

@@ -29,9 +29,10 @@ import org.springframework.core.style.ToStringCreator;
import org.springframework.validation.annotation.Validated;
/**
* Properties related to hearbeat verification.
* Properties related to heartbeat verification.
*
* @author Spencer Gibb
* @author Chris Bono
*/
@ConfigurationProperties(prefix = "spring.cloud.consul.discovery.heartbeat")
@Validated
@@ -52,6 +53,19 @@ public class HeartbeatProperties {
private boolean reregisterServiceOnFailure = false;
/**
* Whether or not to take the current system health (as reported via the Actuator
* Health endpoint) into account when reporting the application status to the Consul
* TTL check. Actuator Health endpoint also has to be available to the application.
*/
private boolean useActuatorHealth = true;
/**
* The actuator health group to use (null for the root group) when determining system
* health via Actuator.
*/
private String actuatorHealthGroup;
/**
* @return the computed heartbeat interval
*/
@@ -99,10 +113,27 @@ public class HeartbeatProperties {
this.reregisterServiceOnFailure = reregisterServiceOnFailure;
}
public boolean isUseActuatorHealth() {
return useActuatorHealth;
}
public void setUseActuatorHealth(boolean useActuatorHealth) {
this.useActuatorHealth = useActuatorHealth;
}
public String getActuatorHealthGroup() {
return actuatorHealthGroup;
}
public void setActuatorHealthGroup(String actuatorHealthGroup) {
this.actuatorHealthGroup = actuatorHealthGroup;
}
@Override
public String toString() {
return new ToStringCreator(this).append("enabled", this.enabled).append("ttl", this.ttl)
.append("intervalRatio", this.intervalRatio).toString();
.append("intervalRatio", this.intervalRatio).append("useActuatorHealth", this.useActuatorHealth)
.append("healthGroup", this.actuatorHealthGroup).toString();
}
}

View File

@@ -20,6 +20,7 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledFuture;
import java.util.function.Supplier;
import com.ecwid.consul.v1.ConsulClient;
import com.ecwid.consul.v1.OperationException;
@@ -27,13 +28,18 @@ import com.ecwid.consul.v1.agent.model.NewService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.consul.serviceregistry.ApplicationStatusProvider;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler;
import static com.ecwid.consul.v1.health.model.Check.CheckStatus;
/**
* Created by nicu on 11.03.2015.
*
* @author Stéphane LEROY
* @author Chris Bono
*/
public class TtlScheduler {
@@ -53,12 +59,17 @@ public class TtlScheduler {
private final Map<String, NewService> registeredServices = new ConcurrentHashMap<>();
private ApplicationStatusProvider applicationStatusProvider;
public TtlScheduler(HeartbeatProperties heartbeatProperties, ConsulDiscoveryProperties discoveryProperties,
ConsulClient client, ReregistrationPredicate reregistrationPredicate) {
ConsulClient client, ReregistrationPredicate reregistrationPredicate,
ObjectProvider<ApplicationStatusProvider> applicationStatusProviderFactory) {
this.heartbeatProperties = heartbeatProperties;
this.discoveryProperties = discoveryProperties;
this.client = client;
this.reregistrationPredicate = reregistrationPredicate;
this.applicationStatusProvider = applicationStatusProviderFactory
.getIfAvailable(() -> () -> CheckStatus.PASSING);
}
public void add(final NewService service) {
@@ -71,7 +82,8 @@ public class TtlScheduler {
* @param instanceId instance id
*/
public void add(String instanceId) {
ScheduledFuture task = this.scheduler.scheduleAtFixedRate(new ConsulHeartbeatTask(instanceId, this),
ScheduledFuture task = this.scheduler.scheduleAtFixedRate(
new ConsulHeartbeatTask(instanceId, this, () -> applicationStatusProvider.currentStatus()),
this.heartbeatProperties.computeHeartbeatInterval().toMillis());
ScheduledFuture previousTask = this.serviceHeartbeats.put(instanceId, task);
if (previousTask != null) {
@@ -96,7 +108,9 @@ public class TtlScheduler {
private final TtlScheduler ttlScheduler;
ConsulHeartbeatTask(String serviceId, TtlScheduler ttlScheduler) {
private final Supplier<CheckStatus> statusSupplier;
ConsulHeartbeatTask(String serviceId, TtlScheduler ttlScheduler, Supplier<CheckStatus> statusSupplier) {
this.serviceId = serviceId;
if (!this.serviceId.startsWith("service:")) {
this.checkId = "service:" + this.serviceId;
@@ -104,16 +118,39 @@ public class TtlScheduler {
else {
this.checkId = this.serviceId;
}
this.statusSupplier = statusSupplier;
this.ttlScheduler = ttlScheduler;
}
@Override
public void run() {
ConsulClient client = this.ttlScheduler.client;
CheckStatus status = statusSupplier.get();
switch (status) {
case PASSING:
possiblyReregisterIfFails(() -> client.agentCheckPass(checkId));
logHeartbeatSent(status);
break;
case WARNING:
possiblyReregisterIfFails(() -> client.agentCheckWarn(checkId));
logHeartbeatSent(status);
break;
case CRITICAL:
possiblyReregisterIfFails(() -> client.agentCheckFail(checkId));
logHeartbeatSent(status);
break;
default:
log.debug(String.format("Not sending consul heartbeat for %s (%s)", checkId, status));
}
}
private void logHeartbeatSent(CheckStatus status) {
log.debug(String.format("Sent consul heartbeat for %s (%s)", checkId, status));
}
private void possiblyReregisterIfFails(Runnable consulClientCall) {
try {
this.ttlScheduler.client.agentCheckPass(this.checkId);
if (log.isDebugEnabled()) {
log.debug("Sending consul heartbeat for: " + this.checkId);
}
consulClientCall.run();
}
catch (OperationException e) {
if (this.ttlScheduler.heartbeatProperties.isReregisterServiceOnFailure()

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.consul.serviceregistry;
import java.util.Optional;
import com.ecwid.consul.v1.health.model.Check.CheckStatus;
import org.springframework.boot.actuate.health.HealthComponent;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.Status;
import org.springframework.cloud.consul.discovery.HeartbeatProperties;
import static org.springframework.boot.actuate.health.Status.DOWN;
import static org.springframework.boot.actuate.health.Status.OUT_OF_SERVICE;
import static org.springframework.boot.actuate.health.Status.UP;
/**
* Leverages Spring Boot Actuator health endpoint to determine the current health of the
* application.
*
* @author Chris Bono
*/
public class ActuatorHealthApplicationStatusProvider implements ApplicationStatusProvider {
private HealthEndpoint healthEndpoint;
private HeartbeatProperties heartbeatProperties;
public ActuatorHealthApplicationStatusProvider(HealthEndpoint healthEndpoint,
HeartbeatProperties heartbeatProperties) {
this.healthEndpoint = healthEndpoint;
this.heartbeatProperties = heartbeatProperties;
}
/**
* Gets the current actuator health status and converts to Consul check status using
* the following mapping:
* <ul>
* <li>{@link Status#UP} -> {@link CheckStatus#PASSING}</li>
* <li>{@link Status#DOWN} or {@link Status#OUT_OF_SERVICE} ->
* {@link CheckStatus#CRITICAL}</li>
* <li>Otherwise {@link CheckStatus#UNKNOWN}</li>
* </ul>
* .
* @return the check status based on the actuator health status (see above for
* mapping)
*/
@Override
public CheckStatus currentStatus() {
String healthGroup = heartbeatProperties.getActuatorHealthGroup();
String[] path = healthGroup == null ? new String[0] : new String[] { healthGroup };
return Optional.ofNullable(healthEndpoint.healthForPath(path)).map(HealthComponent::getStatus)
.map(this::healthStatusToCheckStatus).orElse(CheckStatus.UNKNOWN);
}
private CheckStatus healthStatusToCheckStatus(Status healthStatus) {
if (healthStatus == UP) {
return CheckStatus.PASSING;
}
if (healthStatus == DOWN || healthStatus == OUT_OF_SERVICE) {
return CheckStatus.CRITICAL;
}
return CheckStatus.UNKNOWN;
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.consul.serviceregistry;
import com.ecwid.consul.v1.health.model.Check.CheckStatus;
/**
* Provides the current health of the application represented in Consul's
* {@link CheckStatus} so that it can then be sent to Consul TTL checks.
*
* @author Chris Bono
*/
@FunctionalInterface
public interface ApplicationStatusProvider {
/**
* @return the current health of the application
*/
CheckStatus currentStatus();
}

View File

@@ -18,8 +18,12 @@ package org.springframework.cloud.consul.support;
import com.ecwid.consul.v1.ConsulClient;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
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.condition.ConditionalOnProperty;
import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled;
@@ -29,6 +33,8 @@ import org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties;
import org.springframework.cloud.consul.discovery.HeartbeatProperties;
import org.springframework.cloud.consul.discovery.ReregistrationPredicate;
import org.springframework.cloud.consul.discovery.TtlScheduler;
import org.springframework.cloud.consul.serviceregistry.ActuatorHealthApplicationStatusProvider;
import org.springframework.cloud.consul.serviceregistry.ApplicationStatusProvider;
import org.springframework.cloud.consul.serviceregistry.ConsulServiceRegistryAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -37,6 +43,7 @@ import org.springframework.context.annotation.Configuration;
* Auto configuration for the heartbeat.
*
* @author Tim Ysewyn
* @author Chris Bono
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnConsulEnabled
@@ -56,8 +63,10 @@ public class ConsulHeartbeatAutoConfiguration {
@ConditionalOnMissingBean
public TtlScheduler ttlScheduler(HeartbeatProperties heartbeatProperties,
ConsulDiscoveryProperties discoveryProperties, ConsulClient consulClient,
ReregistrationPredicate reRegistrationPredicate) {
return new TtlScheduler(heartbeatProperties, discoveryProperties, consulClient, reRegistrationPredicate);
ReregistrationPredicate reRegistrationPredicate,
ObjectProvider<ApplicationStatusProvider> applicationStatusProvider) {
return new TtlScheduler(heartbeatProperties, discoveryProperties, consulClient, reRegistrationPredicate,
applicationStatusProvider);
}
@Bean
@@ -66,4 +75,20 @@ public class ConsulHeartbeatAutoConfiguration {
return ReregistrationPredicate.DEFAULT;
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HealthEndpoint.class)
@ConditionalOnProperty(value = "spring.cloud.consul.discovery.heartbeat.use-actuator-health", havingValue = "true",
matchIfMissing = true)
static class ActuatorBasedApplicationStatusProviderConfig {
@Bean
@ConditionalOnBean(HealthEndpoint.class)
@ConditionalOnMissingBean
public ApplicationStatusProvider actuatorHealthStatusProvider(HealthEndpoint healthEndpoint,
HeartbeatProperties heartbeatProperties) {
return new ActuatorHealthApplicationStatusProvider(healthEndpoint, heartbeatProperties);
}
}
}

View File

@@ -29,6 +29,12 @@
"type": "java.lang.Boolean",
"description": "Enables Consul Service Registry Auto-registration.",
"defaultValue": "true"
},
{
"name": "spring.cloud.consul.discovery.heartbeat.use-actuator-health",
"type": "java.lang.Boolean",
"description": "Whether or not to take the current system health (as reported via the Actuator Health endpoint) into account when reporting the application status to the Consul TTL check. Actuator Health endpoint also has to be available to the application.",
"defaultValue": "true"
}
]
}

View File

@@ -16,16 +16,22 @@
package org.springframework.cloud.consul.discovery;
import java.util.Collections;
import com.ecwid.consul.v1.ConsulClient;
import com.ecwid.consul.v1.OperationException;
import com.ecwid.consul.v1.agent.model.NewService;
import com.ecwid.consul.v1.health.model.Check;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.support.StaticListableBeanFactory;
import org.springframework.cloud.commons.util.InetUtils;
import org.springframework.cloud.commons.util.InetUtilsProperties;
import org.springframework.cloud.consul.discovery.TtlScheduler.ConsulHeartbeatTask;
import org.springframework.cloud.consul.serviceregistry.ApplicationStatusProvider;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -38,29 +44,43 @@ import static org.mockito.Mockito.verify;
* Test for ConsulHeartbeatTask
*
* @author Toshiaki Maki
* @author Chris Bono
*/
public class ConsulHeartbeatTaskTests {
String serviceId = "service-A";
private String serviceId = "service-A";
HeartbeatProperties heartbeatProperties;
private HeartbeatProperties heartbeatProperties;
ConsulDiscoveryProperties discoveryProperties;
private ConsulDiscoveryProperties discoveryProperties;
ConsulClient consulClient;
private ConsulClient consulClient;
private ObjectProvider<ApplicationStatusProvider> applicationStatusProviders;
private ApplicationStatusProvider applicationStatusProvider;
@Before
public void setUp() {
this.heartbeatProperties = new HeartbeatProperties();
this.discoveryProperties = new ConsulDiscoveryProperties(new InetUtils(new InetUtilsProperties()));
this.consulClient = mock(ConsulClient.class);
setupApplicationStatusProvider(Check.CheckStatus.PASSING);
}
private void setupApplicationStatusProvider(Check.CheckStatus desiredCheckStatus) {
applicationStatusProvider = () -> desiredCheckStatus;
StaticListableBeanFactory beanFactory = new StaticListableBeanFactory(
Collections.singletonMap("applicationStatusProvider", applicationStatusProvider));
this.applicationStatusProviders = beanFactory.getBeanProvider(ApplicationStatusProvider.class);
}
@Test
public void enableReRegistration() {
public void enableReRegistrationForAgentCheckPass() {
TtlScheduler ttlScheduler = new TtlScheduler(heartbeatProperties, discoveryProperties, consulClient,
ReregistrationPredicate.DEFAULT);
ConsulHeartbeatTask consulHeartbeatTask = new ConsulHeartbeatTask(serviceId, ttlScheduler);
ReregistrationPredicate.DEFAULT, applicationStatusProviders);
ConsulHeartbeatTask consulHeartbeatTask = new ConsulHeartbeatTask(serviceId, ttlScheduler,
() -> Check.CheckStatus.PASSING);
heartbeatProperties.setReregisterServiceOnFailure(true);
NewService service = new NewService();
service.setId(serviceId);
@@ -74,11 +94,52 @@ public class ConsulHeartbeatTaskTests {
assertThat(serviceCaptor.getValue()).isSameAs(service);
}
@Test
public void enableReRegistrationForAgentCheckWarn() {
setupApplicationStatusProvider(Check.CheckStatus.WARNING);
TtlScheduler ttlScheduler = new TtlScheduler(heartbeatProperties, discoveryProperties, consulClient,
ReregistrationPredicate.DEFAULT, applicationStatusProviders);
ConsulHeartbeatTask consulHeartbeatTask = new ConsulHeartbeatTask(serviceId, ttlScheduler,
() -> Check.CheckStatus.WARNING);
heartbeatProperties.setReregisterServiceOnFailure(true);
NewService service = new NewService();
service.setId(serviceId);
ttlScheduler.add(service);
given(consulClient.agentCheckWarn("service:" + serviceId)).willThrow(new OperationException(500,
"Internal Server Error", "CheckID \"service:service-A\" does not have associated TTL"));
consulHeartbeatTask.run();
ArgumentCaptor<NewService> serviceCaptor = ArgumentCaptor.forClass(NewService.class);
ArgumentCaptor<String> tokenCaptor = ArgumentCaptor.forClass(String.class);
verify(consulClient, atLeastOnce()).agentServiceRegister(serviceCaptor.capture(), tokenCaptor.capture());
assertThat(serviceCaptor.getValue()).isSameAs(service);
}
@Test
public void enableReRegistrationForAgentCheckFail() {
setupApplicationStatusProvider(Check.CheckStatus.CRITICAL);
TtlScheduler ttlScheduler = new TtlScheduler(heartbeatProperties, discoveryProperties, consulClient,
ReregistrationPredicate.DEFAULT, applicationStatusProviders);
ConsulHeartbeatTask consulHeartbeatTask = new ConsulHeartbeatTask(serviceId, ttlScheduler,
() -> Check.CheckStatus.CRITICAL);
heartbeatProperties.setReregisterServiceOnFailure(true);
NewService service = new NewService();
service.setId(serviceId);
ttlScheduler.add(service);
given(consulClient.agentCheckFail("service:" + serviceId)).willThrow(new OperationException(500,
"Internal Server Error", "CheckID \"service:service-A\" does not have associated TTL"));
consulHeartbeatTask.run();
ArgumentCaptor<NewService> serviceCaptor = ArgumentCaptor.forClass(NewService.class);
ArgumentCaptor<String> tokenCaptor = ArgumentCaptor.forClass(String.class);
verify(consulClient, atLeastOnce()).agentServiceRegister(serviceCaptor.capture(), tokenCaptor.capture());
assertThat(serviceCaptor.getValue()).isSameAs(service);
}
@Test
public void notEligibleForReRegistration() {
TtlScheduler ttlScheduler = new TtlScheduler(heartbeatProperties, discoveryProperties, consulClient,
ReregistrationPredicate.DEFAULT);
ConsulHeartbeatTask consulHeartbeatTask = new ConsulHeartbeatTask(serviceId, ttlScheduler);
ReregistrationPredicate.DEFAULT, applicationStatusProviders);
ConsulHeartbeatTask consulHeartbeatTask = new ConsulHeartbeatTask(serviceId, ttlScheduler,
() -> Check.CheckStatus.PASSING);
heartbeatProperties.setReregisterServiceOnFailure(true);
NewService service = new NewService();
service.setId(serviceId);
@@ -92,8 +153,9 @@ public class ConsulHeartbeatTaskTests {
@Test
public void enableReRegistrationWithCustomPredicate() {
TtlScheduler ttlScheduler = new TtlScheduler(heartbeatProperties, discoveryProperties, consulClient,
e -> e.getStatusContent().endsWith("does not have associated TTL"));
ConsulHeartbeatTask consulHeartbeatTask = new ConsulHeartbeatTask(serviceId, ttlScheduler);
e -> e.getStatusContent().endsWith("does not have associated TTL"), applicationStatusProviders);
ConsulHeartbeatTask consulHeartbeatTask = new ConsulHeartbeatTask(serviceId, ttlScheduler,
() -> Check.CheckStatus.PASSING);
heartbeatProperties.setReregisterServiceOnFailure(true);
NewService service = new NewService();
service.setId(serviceId);
@@ -110,8 +172,9 @@ public class ConsulHeartbeatTaskTests {
@Test
public void disableReRegistration() {
TtlScheduler ttlScheduler = new TtlScheduler(heartbeatProperties, discoveryProperties, consulClient,
ReregistrationPredicate.DEFAULT);
ConsulHeartbeatTask consulHeartbeatTask = new ConsulHeartbeatTask(serviceId, ttlScheduler);
ReregistrationPredicate.DEFAULT, applicationStatusProviders);
ConsulHeartbeatTask consulHeartbeatTask = new ConsulHeartbeatTask(serviceId, ttlScheduler,
() -> Check.CheckStatus.PASSING);
heartbeatProperties.setReregisterServiceOnFailure(false);
NewService service = new NewService();
service.setId(serviceId);

View File

@@ -1,98 +0,0 @@
/*
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.consul.discovery;
import java.util.List;
import com.ecwid.consul.v1.ConsulClient;
import com.ecwid.consul.v1.QueryParams;
import com.ecwid.consul.v1.Response;
import com.ecwid.consul.v1.health.HealthChecksForServiceRequest;
import com.ecwid.consul.v1.health.model.Check;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration;
import org.springframework.cloud.consul.ConsulAutoConfiguration;
import org.springframework.cloud.consul.support.ConsulHeartbeatAutoConfiguration;
import org.springframework.cloud.consul.test.ConsulTestcontainers;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import static com.ecwid.consul.v1.health.model.Check.CheckStatus.CRITICAL;
import static com.ecwid.consul.v1.health.model.Check.CheckStatus.PASSING;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
/**
* @author Stéphane Leroy
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TtlSchedulerRemoveTests.TtlSchedulerRemoveTestConfig.class,
properties = { "spring.cloud.consul.discovery.heartbeat.ttl=5s", "spring.application.name=ttlSchedulerRemove",
"spring.cloud.consul.discovery.instance-id=ttlSchedulerRemove-id",
"spring.cloud.consul.discovery.heartbeat.enabled=true",
"spring.cloud.consul.discovery.heartbeat.ttlValue=2" },
webEnvironment = RANDOM_PORT)
@ContextConfiguration(initializers = ConsulTestcontainers.class)
public class TtlSchedulerRemoveTests {
@Autowired
private ConsulClient consul;
@Autowired
private TtlScheduler ttlScheduler;
@Test
public void should_not_send_check_if_service_removed() throws InterruptedException {
await().untilAsserted(() -> {
Check serviceCheck = getCheckForService("ttlSchedulerRemove");
assertThat(serviceCheck.getStatus()).as("Service check is in wrong state").isEqualTo(PASSING);
});
// Remove service from TtlScheduler and wait for TTL to expired.
this.ttlScheduler.remove("ttlSchedulerRemove-id");
await().untilAsserted(() -> {
Check serviceCheck = getCheckForService("ttlSchedulerRemove");
assertThat(serviceCheck.getStatus()).as("Service check is in wrong state").isEqualTo(CRITICAL);
});
}
private Check getCheckForService(String serviceId) {
Response<List<Check>> checkResponse = this.consul.getHealthChecksForService(serviceId,
HealthChecksForServiceRequest.newBuilder().setQueryParams(QueryParams.DEFAULT).build());
if (checkResponse.getValue().size() > 0) {
return checkResponse.getValue().get(0);
}
return null;
}
@Configuration(proxyBeanMethods = false)
@EnableAutoConfiguration
@Import({ AutoServiceRegistrationConfiguration.class, ConsulAutoConfiguration.class,
ConsulDiscoveryClientConfiguration.class, ConsulHeartbeatAutoConfiguration.class })
public static class TtlSchedulerRemoveTestConfig {
}
}

View File

@@ -16,76 +16,99 @@
package org.springframework.cloud.consul.discovery;
import java.util.List;
import java.time.Duration;
import java.util.Collections;
import com.ecwid.consul.v1.ConsulClient;
import com.ecwid.consul.v1.QueryParams;
import com.ecwid.consul.v1.Response;
import com.ecwid.consul.v1.health.HealthChecksForServiceRequest;
import com.ecwid.consul.v1.health.model.Check;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration;
import org.springframework.cloud.consul.ConsulAutoConfiguration;
import org.springframework.cloud.consul.support.ConsulHeartbeatAutoConfiguration;
import org.springframework.cloud.consul.test.ConsulTestcontainers;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.beans.factory.support.StaticListableBeanFactory;
import org.springframework.cloud.consul.serviceregistry.ApplicationStatusProvider;
import static com.ecwid.consul.v1.health.model.Check.CheckStatus;
import static com.ecwid.consul.v1.health.model.Check.CheckStatus.CRITICAL;
import static com.ecwid.consul.v1.health.model.Check.CheckStatus.PASSING;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import static com.ecwid.consul.v1.health.model.Check.CheckStatus.WARNING;
import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Unit tests for {@link TtlScheduler}.
*
* @author Stéphane Leroy
* @author Chris Bono
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TtlSchedulerTests.TtlSchedulerTestConfig.class,
properties = { "spring.application.name=ttlScheduler",
"spring.cloud.consul.discovery.instance-id=ttlScheduler-id",
"spring.cloud.consul.discovery.heartbeat.enabled=true",
"spring.cloud.consul.discovery.heartbeat.ttlValue=2", "management.server.port=0" },
webEnvironment = RANDOM_PORT)
@ContextConfiguration(initializers = ConsulTestcontainers.class)
public class TtlSchedulerTests {
@ExtendWith(MockitoExtension.class)
class TtlSchedulerTests {
@Autowired
private ConsulClient consul;
@Mock
private HeartbeatProperties heartbeatProperties;
@Mock
private ConsulDiscoveryProperties discoveryProperties;
@Mock
private ConsulClient client;
@Mock
private ApplicationStatusProvider applicationStatusProvider;
private TtlScheduler ttlScheduler;
@BeforeEach
void setup() {
StaticListableBeanFactory beanFactory = new StaticListableBeanFactory(
Collections.singletonMap("applicationStatusProvider", applicationStatusProvider));
ttlScheduler = new TtlScheduler(heartbeatProperties, discoveryProperties, client,
ReregistrationPredicate.DEFAULT, beanFactory.getBeanProvider(ApplicationStatusProvider.class));
when(heartbeatProperties.computeHeartbeatInterval()).thenReturn(Duration.ofMillis(2000));
}
@Test
public void should_send_a_check_before_ttl_for_all_services() throws InterruptedException {
Thread.sleep(2100); // Wait for TTL to expired (TTL is set to 2 seconds)
Check serviceCheck = getCheckForService("ttlScheduler");
assertThat(serviceCheck).isNotNull();
assertThat(serviceCheck.getStatus()).isEqualTo(PASSING).as("Service check is in wrong state");
Check serviceManagementCheck = getCheckForService("ttlScheduler-management");
assertThat(serviceManagementCheck).isNotNull();
assertThat(serviceManagementCheck.getStatus()).isEqualTo(PASSING)
.as("Service management check is in wrong state");
void agentCheckIsReportedProperAmountOfTimes() {
String serviceId = addServiceToSchedulerWhenApplicationStatusIs(PASSING);
// Wait for 5s and it should have run 3 times as the interval is 2s and it runs
// immediately when added
awaitFor(Duration.ofSeconds(5));
verify(client, times(3)).agentCheckPass("service:" + serviceId);
}
private Check getCheckForService(String serviceId) {
Response<List<Check>> checkResponse = this.consul.getHealthChecksForService(serviceId,
HealthChecksForServiceRequest.newBuilder().setQueryParams(QueryParams.DEFAULT).build());
if (checkResponse.getValue().size() > 0) {
return checkResponse.getValue().get(0);
}
return null;
@Test
void agentCheckPassGetsCalledWhenApplicationStatusIsPassing() {
String serviceId = addServiceToSchedulerWhenApplicationStatusIs(PASSING);
verify(client).agentCheckPass("service:" + serviceId);
}
@Configuration(proxyBeanMethods = false)
@EnableAutoConfiguration
@Import({ AutoServiceRegistrationConfiguration.class, ConsulAutoConfiguration.class,
ConsulDiscoveryClientConfiguration.class, ConsulHeartbeatAutoConfiguration.class })
public static class TtlSchedulerTestConfig {
@Test
void agentCheckWarnGetsCalledWhenApplicationStatusIsWarning() {
String serviceId = addServiceToSchedulerWhenApplicationStatusIs(WARNING);
verify(client).agentCheckWarn("service:" + serviceId);
}
@Test
void agentCheckFailGetsCalledWhenApplicationStatusIsCritical() {
String serviceId = addServiceToSchedulerWhenApplicationStatusIs(CRITICAL);
verify(client).agentCheckFail("service:" + serviceId);
}
private String addServiceToSchedulerWhenApplicationStatusIs(CheckStatus checkStatus) {
String serviceId = "svc-" + checkStatus.name();
when(applicationStatusProvider.currentStatus()).thenReturn(checkStatus);
ttlScheduler.add(serviceId);
// The scheduler runs immediately after the add(serviceId) - pause for 500ms
awaitFor(Duration.ofMillis(500));
verify(applicationStatusProvider).currentStatus();
return serviceId;
}
private void awaitFor(Duration duration) {
await().pollDelay(duration).until(() -> true);
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.consul.serviceregistry;
import java.util.stream.Stream;
import com.ecwid.consul.v1.health.model.Check;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.actuate.health.HealthComponent;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.Status;
import org.springframework.cloud.consul.discovery.HeartbeatProperties;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.boot.actuate.health.Status.DOWN;
import static org.springframework.boot.actuate.health.Status.OUT_OF_SERVICE;
import static org.springframework.boot.actuate.health.Status.UNKNOWN;
import static org.springframework.boot.actuate.health.Status.UP;
/**
* Unit tests for {@link ActuatorHealthApplicationStatusProvider}.
*
* @author Chris Bono
*/
@ExtendWith(MockitoExtension.class)
class ActuatorHealthApplicationStatusProviderTests {
@Mock
private HealthEndpoint healthEndpoint;
@Mock
private HeartbeatProperties heartbeatProperties;
@Mock
private HealthComponent healthComponent;
@InjectMocks
private ActuatorHealthApplicationStatusProvider applicationStatusProvider;
static Stream<Arguments> currentCheckStatusBasedOnHealthStatusArgs() {
return Stream.of(arguments(UP, Check.CheckStatus.PASSING), arguments(DOWN, Check.CheckStatus.CRITICAL),
arguments(OUT_OF_SERVICE, Check.CheckStatus.CRITICAL), arguments(UNKNOWN, Check.CheckStatus.UNKNOWN));
}
@DisplayName("currentStatus() tests")
@ParameterizedTest(name = "{index} ==> health status ''{0}'' should map to check status ''{1}''")
@MethodSource("currentCheckStatusBasedOnHealthStatusArgs")
void currentCheckStatusBasedOnHealthStatus(Status healthStatus, Check.CheckStatus expectedCheckStatus) {
when(healthComponent.getStatus()).thenReturn(healthStatus);
when(healthEndpoint.healthForPath(new String[0])).thenReturn(healthComponent);
assertThat(applicationStatusProvider.currentStatus()).isEqualTo(expectedCheckStatus);
verify(healthEndpoint).healthForPath(new String[0]);
verify(healthComponent).getStatus();
}
@Test
void currentStatusUsesHealthGroupIfSpecified() {
when(heartbeatProperties.getActuatorHealthGroup()).thenReturn("5150");
when(healthComponent.getStatus()).thenReturn(OUT_OF_SERVICE);
when(healthEndpoint.healthForPath(new String[] { "5150" })).thenReturn(healthComponent);
assertThat(applicationStatusProvider.currentStatus()).isEqualTo(Check.CheckStatus.CRITICAL);
verify(healthEndpoint).healthForPath(new String[] { "5150" });
verify(healthComponent).getStatus();
}
@Test
void currentStatusHandlesNullHealthStatusGracefully() {
when(healthEndpoint.healthForPath(new String[0])).thenReturn(null);
assertThat(applicationStatusProvider.currentStatus()).isEqualTo(Check.CheckStatus.UNKNOWN);
verify(healthEndpoint).healthForPath(new String[0]);
verifyNoInteractions(healthComponent);
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.consul.serviceregistry;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration;
import org.springframework.cloud.consul.ConsulAutoConfiguration;
import org.springframework.cloud.consul.support.ConsulHeartbeatAutoConfiguration;
import org.springframework.context.annotation.Configuration;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration(proxyBeanMethods = false)
@EnableAutoConfiguration
@ImportAutoConfiguration({ AutoServiceRegistrationConfiguration.class, ConsulAutoConfiguration.class,
ConsulAutoServiceRegistrationAutoConfiguration.class, ConsulHeartbeatAutoConfiguration.class })
public @interface ConsulAutoServiceRegistrationIntegrationTestConfig {
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2013-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.consul.serviceregistry;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.TimeUnit;
import com.ecwid.consul.v1.ConsulClient;
import com.ecwid.consul.v1.QueryParams;
import com.ecwid.consul.v1.Response;
import com.ecwid.consul.v1.health.HealthChecksForServiceRequest;
import com.ecwid.consul.v1.health.model.Check;
import com.ecwid.consul.v1.health.model.Check.CheckStatus;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.consul.discovery.TtlScheduler;
import org.springframework.cloud.consul.test.ConsulTestcontainers;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.ContextConfiguration;
import static com.ecwid.consul.v1.health.model.Check.CheckStatus.CRITICAL;
import static com.ecwid.consul.v1.health.model.Check.CheckStatus.PASSING;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
/**
* Integration test that verifies the TTL checks basic functionality.
*
* @author Chris Bono
*/
@SpringBootTest(classes = ConsulAutoServiceRegistrationTtlCheckTests.TestConfig.class,
properties = { "spring.application.name=" + ConsulAutoServiceRegistrationTtlCheckTests.SERVICE_NAME,
"spring.cloud.consul.discovery.instance-id=" + ConsulAutoServiceRegistrationTtlCheckTests.INSTANCE_ID,
"spring.cloud.consul.discovery.heartbeat.enabled=true",
"spring.cloud.consul.discovery.heartbeat.ttl=2s", "management.server.port=0" },
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = ConsulTestcontainers.class)
class ConsulAutoServiceRegistrationTtlCheckTests {
// Visible for constant in SpringBootTest.properties
static final String SERVICE_NAME = "ttl-check-test-svc";
// Visible for constant in SpringBootTest.properties
static final String INSTANCE_ID = SERVICE_NAME + "-001";
private static final String MGMT_SERVICE_NAME = SERVICE_NAME + "-management";
@Autowired
private ConsulClient consul;
@Autowired
private TtlScheduler ttlScheduler;
@Test
void serviceAndManagementTtlChecksRegisteredAndInPasssingStatusInitially() {
assertThatConsulTtlCheckIsInStatus(SERVICE_NAME, PASSING);
assertThatConsulTtlCheckIsInStatus(MGMT_SERVICE_NAME, PASSING);
// Wait for TTL to expire and make sure it actually got renewed prior
await().pollDelay(Duration.ofMillis(2500)).until(() -> true);
assertThatConsulTtlCheckIsInStatus(SERVICE_NAME, PASSING);
assertThatConsulTtlCheckIsInStatus(MGMT_SERVICE_NAME, PASSING);
}
@Test
void serviceGoesIntoCriticalStatusWhenRemovedFromTheTtlScheduler() {
assertThatConsulTtlCheckIsInStatus(SERVICE_NAME, PASSING);
// Remove service from TtlScheduler which should quit sending updates to Consul
this.ttlScheduler.remove(INSTANCE_ID);
// Wait for TTL to expire and make sure it did not get renewed
await().pollDelay(Duration.ofMillis(2500)).until(() -> true);
assertThatConsulTtlCheckIsInStatus(SERVICE_NAME, CRITICAL);
}
private void assertThatConsulTtlCheckIsInStatus(String serviceName, CheckStatus expectedStatus) {
await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> assertThat(getCheckForService(serviceName)).isNotNull()
.extracting(Check::getStatus).isEqualTo(expectedStatus));
}
private Check getCheckForService(String serviceName) {
Response<List<Check>> checkResponse = this.consul.getHealthChecksForService(serviceName,
HealthChecksForServiceRequest.newBuilder().setQueryParams(QueryParams.DEFAULT).build());
if (checkResponse.getValue() == null || checkResponse.getValue().isEmpty()) {
return null;
}
return checkResponse.getValue().get(0);
}
@ConsulAutoServiceRegistrationIntegrationTestConfig
static class TestConfig {
@Bean
ApplicationStatusProvider alwaysPassingApplicationStatusProvider() {
return () -> CheckStatus.PASSING;
}
}
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright 2013-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.consul.serviceregistry;
import java.util.List;
import java.util.concurrent.TimeUnit;
import com.ecwid.consul.v1.ConsulClient;
import com.ecwid.consul.v1.QueryParams;
import com.ecwid.consul.v1.Response;
import com.ecwid.consul.v1.health.HealthChecksForServiceRequest;
import com.ecwid.consul.v1.health.model.Check;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.Status;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.consul.test.ConsulTestcontainers;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.ContextConfiguration;
import static com.ecwid.consul.v1.health.model.Check.CheckStatus.CRITICAL;
import static com.ecwid.consul.v1.health.model.Check.CheckStatus.PASSING;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
/**
* Integration test that verifies the TTL checks when the
* {@link ApplicationStatusProvider} is an
* {@link ActuatorHealthApplicationStatusProvider}.
*
* @author Chris Bono
*/
@SpringBootTest(classes = ConsulAutoServiceRegistrationTtlCheckWithActuatorHealthTests.TestConfig.class, properties = {
"spring.application.name=" + ConsulAutoServiceRegistrationTtlCheckWithActuatorHealthTests.SERVICE_NAME,
"spring.cloud.consul.discovery.heartbeat.enabled=true", "spring.cloud.consul.discovery.heartbeat.ttl=2s" },
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = ConsulTestcontainers.class)
public class ConsulAutoServiceRegistrationTtlCheckWithActuatorHealthTests {
// Visible for constant in SpringBootTest.properties
static final String SERVICE_NAME = "ttl-check-actuator-health-test-svc";
@Autowired
private ConsulClient consul;
@Autowired
private StaticHealthIndicator ttlStatusControllingIndicator;
@Test
void serviceRegisteredWithApplicationHealthRespectingTtlCheck() {
switchHealthIndicatorStatusTo(Status.UP);
assertThatConsulTtlCheckIsInStatus(PASSING);
switchHealthIndicatorStatusTo(Status.DOWN);
assertThatConsulTtlCheckIsInStatus(CRITICAL);
switchHealthIndicatorStatusTo(Status.UP);
assertThatConsulTtlCheckIsInStatus(PASSING);
switchHealthIndicatorStatusTo(Status.OUT_OF_SERVICE);
assertThatConsulTtlCheckIsInStatus(CRITICAL);
switchHealthIndicatorStatusTo(Status.UP);
assertThatConsulTtlCheckIsInStatus(PASSING);
}
private void switchHealthIndicatorStatusTo(Status newStatus) {
ttlStatusControllingIndicator.setStatus(newStatus);
}
private void assertThatConsulTtlCheckIsInStatus(Check.CheckStatus expectedStatus) {
await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> assertThat(getCheckForService(SERVICE_NAME))
.isNotNull().extracting(Check::getStatus).isEqualTo(expectedStatus));
}
private Check getCheckForService(String serviceName) {
Response<List<Check>> checkResponse = this.consul.getHealthChecksForService(serviceName,
HealthChecksForServiceRequest.newBuilder().setQueryParams(QueryParams.DEFAULT).build());
if (checkResponse.getValue() == null || checkResponse.getValue().isEmpty()) {
return null;
}
return checkResponse.getValue().get(0);
}
@ConsulAutoServiceRegistrationIntegrationTestConfig
static class TestConfig {
@Bean
StaticHealthIndicator ttlStatusControllingIndicator() {
return new StaticHealthIndicator();
}
}
static class StaticHealthIndicator implements HealthIndicator {
private Status status = Status.UP;
void setStatus(Status status) {
this.status = status;
}
@Override
public Health health() {
return Health.status(status).build();
}
}
}

View File

@@ -0,0 +1,144 @@
/*
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.consul.serviceregistry;
import com.ecwid.consul.v1.ConsulClient;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties;
import org.springframework.cloud.consul.discovery.HeartbeatProperties;
import org.springframework.cloud.consul.discovery.TtlScheduler;
import org.springframework.cloud.consul.support.ConsulHeartbeatAutoConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Auto-configuration tests for {@link ConsulHeartbeatAutoConfiguration}
*/
class ConsulHeartbeatAutoConfigurationTests {
private ApplicationContextRunner appContextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ConsulHeartbeatAutoConfiguration.class))
.withBean(ConsulClient.class, () -> mock(ConsulClient.class))
.withBean(HealthEndpoint.class, () -> mock(HealthEndpoint.class))
.withBean(ConsulDiscoveryProperties.class, () -> mock(ConsulDiscoveryProperties.class))
.withPropertyValues("spring.cloud.consul.discovery.heartbeat.enabled=true");
@Test
void heartbeatEnabled() {
appContextRunner.run(this::assertThatHeartbeatConfigured);
}
@Test
void heartbeatDisabled() {
appContextRunner.withPropertyValues("spring.cloud.consul.discovery.heartbeat.enabled=false")
.run(this::assertThatHeartbeatNotConfigured);
}
@Test
void heartbeatEnabledPropertyNotSpecified() {
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(ConsulHeartbeatAutoConfiguration.class))
.withBean(ConsulClient.class, () -> mock(ConsulClient.class))
.withBean(HealthEndpoint.class, () -> mock(HealthEndpoint.class))
.run(this::assertThatHeartbeatNotConfigured);
}
@Test
void heartbeatEnabledButConsulDisabled() {
appContextRunner.withPropertyValues("spring.cloud.consul.enabled=false")
.run(this::assertThatHeartbeatNotConfigured);
}
@Test
void heartbeatEnabledButDiscoveryDisabled() {
appContextRunner.withPropertyValues("spring.cloud.discovery.enabled=false")
.run(this::assertThatHeartbeatNotConfigured);
}
private void assertThatHeartbeatNotConfigured(AssertableApplicationContext context) {
assertThat(context).hasNotFailed().doesNotHaveBean(HeartbeatProperties.class)
.doesNotHaveBean(TtlScheduler.class).doesNotHaveBean(ApplicationStatusProvider.class);
}
private void assertThatHeartbeatConfigured(AssertableApplicationContext context) {
assertThat(context).hasNotFailed().hasSingleBean(HeartbeatProperties.class).hasSingleBean(TtlScheduler.class)
.hasSingleBean(ApplicationStatusProvider.class);
}
@Test
void heartbeatEnabledAndActuatorNotOnClasspath() {
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(ConsulHeartbeatAutoConfiguration.class))
.withBean(ConsulClient.class, () -> mock(ConsulClient.class))
.withBean(ConsulDiscoveryProperties.class, () -> mock(ConsulDiscoveryProperties.class))
.withPropertyValues("spring.cloud.consul.discovery.heartbeat.enabled=true")
.withClassLoader(new FilteredClassLoader(HealthEndpoint.class))
.run(this::assertThatHeartbeatConfiguredWithoutAppStatusProvider);
}
@Test
void heartbeatEnabledAndActuatorOnClasspathButNoHealthEndpointBeanRegistered() {
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(ConsulHeartbeatAutoConfiguration.class))
.withBean(ConsulClient.class, () -> mock(ConsulClient.class))
.withBean(ConsulDiscoveryProperties.class, () -> mock(ConsulDiscoveryProperties.class))
.withPropertyValues("spring.cloud.consul.discovery.heartbeat.enabled=true")
.run(this::assertThatHeartbeatConfiguredWithoutAppStatusProvider);
}
@Test
void heartbeatEnabledButUseActuatorHealthPropertySetToFalse() {
appContextRunner.withPropertyValues("spring.cloud.consul.discovery.heartbeat.use-actuator-health=false")
.run(this::assertThatHeartbeatConfiguredWithoutAppStatusProvider);
}
private void assertThatHeartbeatConfiguredWithoutAppStatusProvider(AssertableApplicationContext context) {
assertThat(context).hasNotFailed().hasSingleBean(HeartbeatProperties.class).hasSingleBean(TtlScheduler.class)
.doesNotHaveBean(ApplicationStatusProvider.class);
}
@Test
void customHeartbeatPropertiesRespected() {
HeartbeatProperties customHeartbeatProps = mock(HeartbeatProperties.class);
appContextRunner.withBean(HeartbeatProperties.class, () -> customHeartbeatProps)
.run(context -> assertThat(context).hasNotFailed().hasSingleBean(HeartbeatProperties.class)
.getBean(HeartbeatProperties.class).isSameAs(customHeartbeatProps));
}
@Test
void customTtlSchedulerRespected() {
TtlScheduler customTtlScheduler = mock(TtlScheduler.class);
appContextRunner.withBean(TtlScheduler.class, () -> customTtlScheduler)
.run(context -> assertThat(context).hasNotFailed().hasSingleBean(TtlScheduler.class)
.getBean(TtlScheduler.class).isSameAs(customTtlScheduler));
}
@Test
void customApplicationStatusProviderRespected() {
ApplicationStatusProvider customAppStatusProvider = mock(ApplicationStatusProvider.class);
appContextRunner.withBean(ApplicationStatusProvider.class, () -> customAppStatusProvider)
.run(context -> assertThat(context).hasNotFailed().hasSingleBean(ApplicationStatusProvider.class)
.getBean(ApplicationStatusProvider.class).isSameAs(customAppStatusProvider));
}
}