Skip to content

Commit ddd370c

Browse files
Add function-based health check. Add restTemplate-based health-check beans to default config (#866)
* Switch to a function-based HealthCheckServiceInstanceListSupplier. * Handle exception for restTemplate based function. Add default blocking health-check configuration. * Add docs.
1 parent c6bd75e commit ddd370c

File tree

8 files changed

+232
-36
lines changed

8 files changed

+232
-36
lines changed

docs/src/main/asciidoc/spring-cloud-commons.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,9 @@ public class CustomLoadBalancerConfiguration {
968968
}
969969
----
970970

971+
TIP: For the non-reactive stack, create this supplier with the `withBlockingHealthChecks()`.
972+
You can also pass your own `WebClient` or `RestTemplate` instance to be used for the checks.
973+
971974
WARNING: `HealthCheckServiceInstanceListSupplier` has its own caching mechanism based on Reactor Flux `replay()`. Therefore, if it's being used, you may want to skip wrapping that supplier with `CachingServiceInstanceListSupplier`.
972975

973976
=== Same instance preference for LoadBalancer

spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
import org.springframework.core.annotation.Order;
4444
import org.springframework.core.env.Environment;
4545
import org.springframework.retry.support.RetryTemplate;
46+
import org.springframework.web.client.RestTemplate;
47+
import org.springframework.web.reactive.function.client.WebClient;
4648

4749
/**
4850
* @author Spencer Gibb
@@ -90,7 +92,7 @@ public ServiceInstanceListSupplier zonePreferenceDiscoveryClientServiceInstanceL
9092
}
9193

9294
@Bean
93-
@ConditionalOnBean(ReactiveDiscoveryClient.class)
95+
@ConditionalOnBean({ ReactiveDiscoveryClient.class, WebClient.Builder.class })
9496
@ConditionalOnMissingBean
9597
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations", havingValue = "health-check")
9698
public ServiceInstanceListSupplier healthCheckDiscoveryClientServiceInstanceListSupplier(
@@ -148,12 +150,12 @@ public ServiceInstanceListSupplier zonePreferenceDiscoveryClientServiceInstanceL
148150
}
149151

150152
@Bean
151-
@ConditionalOnBean(DiscoveryClient.class)
153+
@ConditionalOnBean({ DiscoveryClient.class, RestTemplate.class })
152154
@ConditionalOnMissingBean
153155
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations", havingValue = "health-check")
154156
public ServiceInstanceListSupplier healthCheckDiscoveryClientServiceInstanceListSupplier(
155157
ConfigurableApplicationContext context) {
156-
return ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().withHealthChecks()
158+
return ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().withBlockingHealthChecks()
157159
.build(context);
158160
}
159161

spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/HealthCheckServiceInstanceListSupplier.java

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.ArrayList;
2020
import java.util.Collections;
2121
import java.util.List;
22+
import java.util.function.BiFunction;
2223

2324
import org.apache.commons.logging.Log;
2425
import org.apache.commons.logging.LogFactory;
@@ -31,14 +32,11 @@
3132
import org.springframework.beans.factory.InitializingBean;
3233
import org.springframework.cloud.client.ServiceInstance;
3334
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
34-
import org.springframework.http.HttpStatus;
35-
import org.springframework.web.reactive.function.client.WebClient;
36-
import org.springframework.web.util.UriComponentsBuilder;
3735

3836
/**
3937
* A {@link ServiceInstanceListSupplier} implementation that verifies whether the
40-
* instances are alive and only returns the healthy one, unless there are none. Uses
41-
* {@link WebClient} to ping the <code>health</code> endpoint of the instances.
38+
* instances are alive and only returns the healthy one, unless there are none. Uses a
39+
* user-provided function to ping the <code>health</code> endpoint of the instances.
4240
*
4341
* @author Olga Maciaszek-Sharma
4442
* @author Roman Matiushchenko
@@ -51,19 +49,20 @@ public class HealthCheckServiceInstanceListSupplier extends DelegatingServiceIns
5149

5250
private final LoadBalancerProperties.HealthCheck healthCheck;
5351

54-
private final WebClient webClient;
55-
5652
private final String defaultHealthCheckPath;
5753

5854
private final Flux<List<ServiceInstance>> aliveInstancesReplay;
5955

6056
private Disposable healthCheckDisposable;
6157

58+
private final BiFunction<ServiceInstance, String, Mono<Boolean>> aliveFunction;
59+
6260
public HealthCheckServiceInstanceListSupplier(ServiceInstanceListSupplier delegate,
63-
LoadBalancerProperties.HealthCheck healthCheck, WebClient webClient) {
61+
LoadBalancerProperties.HealthCheck healthCheck,
62+
BiFunction<ServiceInstance, String, Mono<Boolean>> aliveFunction) {
6463
super(delegate);
6564
defaultHealthCheckPath = healthCheck.getPath().getOrDefault("default", "/actuator/health");
66-
this.webClient = webClient;
65+
this.aliveFunction = aliveFunction;
6766
this.healthCheck = healthCheck;
6867
Repeat<Object> aliveInstancesReplayRepeat = Repeat
6968
.onlyIf(repeatContext -> this.healthCheck.getRefetchInstances())
@@ -129,10 +128,7 @@ public Flux<List<ServiceInstance>> get() {
129128
protected Mono<Boolean> isAlive(ServiceInstance serviceInstance) {
130129
String healthCheckPropertyValue = healthCheck.getPath().get(serviceInstance.getServiceId());
131130
String healthCheckPath = healthCheckPropertyValue != null ? healthCheckPropertyValue : defaultHealthCheckPath;
132-
return webClient.get()
133-
.uri(UriComponentsBuilder.fromUri(serviceInstance.getUri()).path(healthCheckPath).build().toUri())
134-
.exchange().flatMap(clientResponse -> clientResponse.releaseBody()
135-
.thenReturn(HttpStatus.OK.value() == clientResponse.rawStatusCode()));
131+
return aliveFunction.apply(serviceInstance, healthCheckPath);
136132
}
137133

138134
@Override

spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSupplierBuilder.java

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616

1717
package org.springframework.cloud.loadbalancer.core;
1818

19+
import java.net.URI;
1920
import java.util.ArrayList;
2021
import java.util.List;
2122
import java.util.function.BiFunction;
2223
import java.util.function.Function;
2324

2425
import org.apache.commons.logging.Log;
2526
import org.apache.commons.logging.LogFactory;
27+
import reactor.core.publisher.Mono;
2628

2729
import org.springframework.beans.factory.ObjectProvider;
2830
import org.springframework.cloud.client.discovery.DiscoveryClient;
@@ -31,8 +33,11 @@
3133
import org.springframework.cloud.loadbalancer.cache.LoadBalancerCacheManager;
3234
import org.springframework.cloud.loadbalancer.config.LoadBalancerZoneConfig;
3335
import org.springframework.context.ConfigurableApplicationContext;
36+
import org.springframework.http.HttpStatus;
3437
import org.springframework.util.Assert;
38+
import org.springframework.web.client.RestTemplate;
3539
import org.springframework.web.reactive.function.client.WebClient;
40+
import org.springframework.web.util.UriComponentsBuilder;
3641

3742
/**
3843
* A Builder for creating a {@link ServiceInstanceListSupplier} hierarchy to be used in
@@ -110,7 +115,22 @@ public ServiceInstanceListSupplierBuilder withHealthChecks() {
110115
DelegateCreator creator = (context, delegate) -> {
111116
LoadBalancerProperties properties = context.getBean(LoadBalancerProperties.class);
112117
WebClient.Builder webClient = context.getBean(WebClient.Builder.class);
113-
return new HealthCheckServiceInstanceListSupplier(delegate, properties.getHealthCheck(), webClient.build());
118+
return healthCheckServiceInstanceListSupplier(webClient.build(), delegate, properties);
119+
};
120+
this.creators.add(creator);
121+
return this;
122+
}
123+
124+
/**
125+
* Adds a {@link HealthCheckServiceInstanceListSupplier} that uses user-provided
126+
* {@link WebClient} instance to the {@link ServiceInstanceListSupplier} hierarchy.
127+
* @param webClient a user-provided {@link WebClient} instance
128+
* @return the {@link ServiceInstanceListSupplierBuilder} object
129+
*/
130+
public ServiceInstanceListSupplierBuilder withHealthChecks(WebClient webClient) {
131+
DelegateCreator creator = (context, delegate) -> {
132+
LoadBalancerProperties properties = context.getBean(LoadBalancerProperties.class);
133+
return healthCheckServiceInstanceListSupplier(webClient, delegate, properties);
114134
};
115135
this.creators.add(creator);
116136
return this;
@@ -130,14 +150,29 @@ public ServiceInstanceListSupplierBuilder withSameInstancePreference() {
130150

131151
/**
132152
* Adds a {@link HealthCheckServiceInstanceListSupplier} that uses user-provided
133-
* {@link WebClient} instance to the {@link ServiceInstanceListSupplier} hierarchy.
134-
* @param webClient a user-provided {@link WebClient} instance
153+
* {@link RestTemplate} instance to the {@link ServiceInstanceListSupplier} hierarchy.
135154
* @return the {@link ServiceInstanceListSupplierBuilder} object
136155
*/
137-
public ServiceInstanceListSupplierBuilder withHealthChecks(WebClient webClient) {
156+
public ServiceInstanceListSupplierBuilder withBlockingHealthChecks() {
157+
DelegateCreator creator = (context, delegate) -> {
158+
RestTemplate restTemplate = context.getBean(RestTemplate.class);
159+
LoadBalancerProperties properties = context.getBean(LoadBalancerProperties.class);
160+
return blockingHealthCheckServiceInstanceListSupplier(restTemplate, delegate, properties);
161+
};
162+
this.creators.add(creator);
163+
return this;
164+
}
165+
166+
/**
167+
* Adds a {@link HealthCheckServiceInstanceListSupplier} that uses user-provided
168+
* {@link RestTemplate} instance to the {@link ServiceInstanceListSupplier} hierarchy.
169+
* @param restTemplate a user-provided {@link RestTemplate} instance
170+
* @return the {@link ServiceInstanceListSupplierBuilder} object
171+
*/
172+
public ServiceInstanceListSupplierBuilder withBlockingHealthChecks(RestTemplate restTemplate) {
138173
DelegateCreator creator = (context, delegate) -> {
139174
LoadBalancerProperties properties = context.getBean(LoadBalancerProperties.class);
140-
return new HealthCheckServiceInstanceListSupplier(delegate, properties.getHealthCheck(), webClient);
175+
return blockingHealthCheckServiceInstanceListSupplier(restTemplate, delegate, properties);
141176
};
142177
this.creators.add(creator);
143178
return this;
@@ -225,6 +260,32 @@ public ServiceInstanceListSupplier build(ConfigurableApplicationContext context)
225260
return supplier;
226261
}
227262

263+
private ServiceInstanceListSupplier healthCheckServiceInstanceListSupplier(WebClient webClient,
264+
ServiceInstanceListSupplier delegate, LoadBalancerProperties properties) {
265+
return new HealthCheckServiceInstanceListSupplier(delegate, properties.getHealthCheck(),
266+
(serviceInstance, healthCheckPath) -> webClient.get()
267+
.uri(UriComponentsBuilder.fromUri(serviceInstance.getUri()).path(healthCheckPath).build()
268+
.toUri())
269+
.exchange().flatMap(clientResponse -> clientResponse.releaseBody()
270+
.thenReturn(HttpStatus.OK.value() == clientResponse.rawStatusCode())));
271+
}
272+
273+
private ServiceInstanceListSupplier blockingHealthCheckServiceInstanceListSupplier(RestTemplate restTemplate,
274+
ServiceInstanceListSupplier delegate, LoadBalancerProperties properties) {
275+
return new HealthCheckServiceInstanceListSupplier(delegate, properties.getHealthCheck(),
276+
(serviceInstance, healthCheckPath) -> Mono.defer(() -> {
277+
URI uri = UriComponentsBuilder.fromUri(serviceInstance.getUri()).path(healthCheckPath).build()
278+
.toUri();
279+
try {
280+
return Mono
281+
.just(HttpStatus.OK.equals(restTemplate.getForEntity(uri, Void.class).getStatusCode()));
282+
}
283+
catch (Exception ignored) {
284+
return Mono.just(false);
285+
}
286+
}));
287+
}
288+
228289
/**
229290
* Allows creating a {@link ServiceInstanceListSupplier} instance based on provided
230291
* {@link ConfigurableApplicationContext}.

spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfigurationTests.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.springframework.context.annotation.Bean;
3838
import org.springframework.context.annotation.Configuration;
3939
import org.springframework.retry.support.RetryTemplate;
40+
import org.springframework.web.client.RestTemplate;
4041
import org.springframework.web.reactive.function.client.WebClient;
4142

4243
import static org.assertj.core.api.BDDAssertions.then;
@@ -170,6 +171,17 @@ void shouldNotWrapWithRetryAwareSupplierWhenRetryTemplateOnClasspath() {
170171

171172
}
172173

174+
@Test
175+
void shouldInstantiateBlockingHealthCheckServiceInstanceListSupplier() {
176+
blockingDiscoveryClientRunner.withUserConfiguration(RestTemplateTestConfig.class)
177+
.withPropertyValues("spring.cloud.loadbalancer.configurations=health-check").run(context -> {
178+
ServiceInstanceListSupplier supplier = context.getBean(ServiceInstanceListSupplier.class);
179+
then(supplier).isInstanceOf(HealthCheckServiceInstanceListSupplier.class);
180+
then(((DelegatingServiceInstanceListSupplier) supplier).getDelegate())
181+
.isInstanceOf(DiscoveryClientServiceInstanceListSupplier.class);
182+
});
183+
}
184+
173185
@Configuration
174186
protected static class TestConfig {
175187

@@ -181,4 +193,14 @@ WebClient.Builder webClientBuilder() {
181193

182194
}
183195

196+
@Configuration
197+
protected static class RestTemplateTestConfig {
198+
199+
@Bean
200+
RestTemplate restTemplate() {
201+
return new RestTemplate();
202+
}
203+
204+
}
205+
184206
}

spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/CachingServiceInstanceListSupplierTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141

4242
import static java.time.Duration.ofMillis;
4343
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
44+
import static org.springframework.cloud.loadbalancer.core.ServiceInstanceListSuppliersTestUtils.healthCheckFunction;
4445

4546
/**
4647
* Tests for {@link CachingServiceInstanceListSupplier}.
@@ -140,7 +141,7 @@ private static class TestHealthCheckServiceInstanceListSupplier extends HealthCh
140141

141142
TestHealthCheckServiceInstanceListSupplier(ServiceInstanceListSupplier delegate,
142143
LoadBalancerProperties.HealthCheck healthCheck, WebClient webClient) {
143-
super(delegate, healthCheck, webClient);
144+
super(delegate, healthCheck, healthCheckFunction(webClient));
144145
}
145146

146147
@Override

0 commit comments

Comments
 (0)