Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/src/main/asciidoc/spring-cloud-commons.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,9 @@ public class CustomLoadBalancerConfiguration {
}
----

TIP: For the non-reactive stack, create this supplier with the `withBlockingHealthChecks()`.
You can also pass your own `WebClient` or `RestTemplate` instance to be used for the checks.

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`.

=== Same instance preference for LoadBalancer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;

/**
* @author Spencer Gibb
Expand Down Expand Up @@ -90,7 +92,7 @@ public ServiceInstanceListSupplier zonePreferenceDiscoveryClientServiceInstanceL
}

@Bean
@ConditionalOnBean(ReactiveDiscoveryClient.class)
@ConditionalOnBean({ ReactiveDiscoveryClient.class, WebClient.Builder.class })
@ConditionalOnMissingBean
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations", havingValue = "health-check")
public ServiceInstanceListSupplier healthCheckDiscoveryClientServiceInstanceListSupplier(
Expand Down Expand Up @@ -148,12 +150,12 @@ public ServiceInstanceListSupplier zonePreferenceDiscoveryClientServiceInstanceL
}

@Bean
@ConditionalOnBean(DiscoveryClient.class)
@ConditionalOnBean({ DiscoveryClient.class, RestTemplate.class })
@ConditionalOnMissingBean
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations", havingValue = "health-check")
public ServiceInstanceListSupplier healthCheckDiscoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().withHealthChecks()
return ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().withBlockingHealthChecks()
.build(context);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.BiFunction;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
Expand All @@ -31,14 +32,11 @@
import org.springframework.beans.factory.InitializingBean;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponentsBuilder;

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

private final LoadBalancerProperties.HealthCheck healthCheck;

private final WebClient webClient;

private final String defaultHealthCheckPath;

private final Flux<List<ServiceInstance>> aliveInstancesReplay;

private Disposable healthCheckDisposable;

private final BiFunction<ServiceInstance, String, Mono<Boolean>> aliveFunction;

public HealthCheckServiceInstanceListSupplier(ServiceInstanceListSupplier delegate,
LoadBalancerProperties.HealthCheck healthCheck, WebClient webClient) {
LoadBalancerProperties.HealthCheck healthCheck,
BiFunction<ServiceInstance, String, Mono<Boolean>> aliveFunction) {
super(delegate);
defaultHealthCheckPath = healthCheck.getPath().getOrDefault("default", "/actuator/health");
this.webClient = webClient;
this.aliveFunction = aliveFunction;
this.healthCheck = healthCheck;
Repeat<Object> aliveInstancesReplayRepeat = Repeat
.onlyIf(repeatContext -> this.healthCheck.getRefetchInstances())
Expand Down Expand Up @@ -129,10 +128,7 @@ public Flux<List<ServiceInstance>> get() {
protected Mono<Boolean> isAlive(ServiceInstance serviceInstance) {
String healthCheckPropertyValue = healthCheck.getPath().get(serviceInstance.getServiceId());
String healthCheckPath = healthCheckPropertyValue != null ? healthCheckPropertyValue : defaultHealthCheckPath;
return webClient.get()
.uri(UriComponentsBuilder.fromUri(serviceInstance.getUri()).path(healthCheckPath).build().toUri())
.exchange().flatMap(clientResponse -> clientResponse.releaseBody()
.thenReturn(HttpStatus.OK.value() == clientResponse.rawStatusCode()));
return aliveFunction.apply(serviceInstance, healthCheckPath);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@

package org.springframework.cloud.loadbalancer.core;

import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.discovery.DiscoveryClient;
Expand All @@ -31,8 +33,11 @@
import org.springframework.cloud.loadbalancer.cache.LoadBalancerCacheManager;
import org.springframework.cloud.loadbalancer.config.LoadBalancerZoneConfig;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponentsBuilder;

/**
* A Builder for creating a {@link ServiceInstanceListSupplier} hierarchy to be used in
Expand Down Expand Up @@ -110,7 +115,22 @@ public ServiceInstanceListSupplierBuilder withHealthChecks() {
DelegateCreator creator = (context, delegate) -> {
LoadBalancerProperties properties = context.getBean(LoadBalancerProperties.class);
WebClient.Builder webClient = context.getBean(WebClient.Builder.class);
return new HealthCheckServiceInstanceListSupplier(delegate, properties.getHealthCheck(), webClient.build());
return healthCheckServiceInstanceListSupplier(webClient.build(), delegate, properties);
};
this.creators.add(creator);
return this;
}

/**
* Adds a {@link HealthCheckServiceInstanceListSupplier} that uses user-provided
* {@link WebClient} instance to the {@link ServiceInstanceListSupplier} hierarchy.
* @param webClient a user-provided {@link WebClient} instance
* @return the {@link ServiceInstanceListSupplierBuilder} object
*/
public ServiceInstanceListSupplierBuilder withHealthChecks(WebClient webClient) {
DelegateCreator creator = (context, delegate) -> {
LoadBalancerProperties properties = context.getBean(LoadBalancerProperties.class);
return healthCheckServiceInstanceListSupplier(webClient, delegate, properties);
};
this.creators.add(creator);
return this;
Expand All @@ -130,14 +150,29 @@ public ServiceInstanceListSupplierBuilder withSameInstancePreference() {

/**
* Adds a {@link HealthCheckServiceInstanceListSupplier} that uses user-provided
* {@link WebClient} instance to the {@link ServiceInstanceListSupplier} hierarchy.
* @param webClient a user-provided {@link WebClient} instance
* {@link RestTemplate} instance to the {@link ServiceInstanceListSupplier} hierarchy.
* @return the {@link ServiceInstanceListSupplierBuilder} object
*/
public ServiceInstanceListSupplierBuilder withHealthChecks(WebClient webClient) {
public ServiceInstanceListSupplierBuilder withBlockingHealthChecks() {
DelegateCreator creator = (context, delegate) -> {
RestTemplate restTemplate = context.getBean(RestTemplate.class);
LoadBalancerProperties properties = context.getBean(LoadBalancerProperties.class);
return blockingHealthCheckServiceInstanceListSupplier(restTemplate, delegate, properties);
};
this.creators.add(creator);
return this;
}

/**
* Adds a {@link HealthCheckServiceInstanceListSupplier} that uses user-provided
* {@link RestTemplate} instance to the {@link ServiceInstanceListSupplier} hierarchy.
* @param restTemplate a user-provided {@link RestTemplate} instance
* @return the {@link ServiceInstanceListSupplierBuilder} object
*/
public ServiceInstanceListSupplierBuilder withBlockingHealthChecks(RestTemplate restTemplate) {
DelegateCreator creator = (context, delegate) -> {
LoadBalancerProperties properties = context.getBean(LoadBalancerProperties.class);
return new HealthCheckServiceInstanceListSupplier(delegate, properties.getHealthCheck(), webClient);
return blockingHealthCheckServiceInstanceListSupplier(restTemplate, delegate, properties);
};
this.creators.add(creator);
return this;
Expand Down Expand Up @@ -225,6 +260,32 @@ public ServiceInstanceListSupplier build(ConfigurableApplicationContext context)
return supplier;
}

private ServiceInstanceListSupplier healthCheckServiceInstanceListSupplier(WebClient webClient,
ServiceInstanceListSupplier delegate, LoadBalancerProperties properties) {
return new HealthCheckServiceInstanceListSupplier(delegate, properties.getHealthCheck(),
(serviceInstance, healthCheckPath) -> webClient.get()
.uri(UriComponentsBuilder.fromUri(serviceInstance.getUri()).path(healthCheckPath).build()
.toUri())
.exchange().flatMap(clientResponse -> clientResponse.releaseBody()
.thenReturn(HttpStatus.OK.value() == clientResponse.rawStatusCode())));
}

private ServiceInstanceListSupplier blockingHealthCheckServiceInstanceListSupplier(RestTemplate restTemplate,
ServiceInstanceListSupplier delegate, LoadBalancerProperties properties) {
return new HealthCheckServiceInstanceListSupplier(delegate, properties.getHealthCheck(),
(serviceInstance, healthCheckPath) -> Mono.defer(() -> {
URI uri = UriComponentsBuilder.fromUri(serviceInstance.getUri()).path(healthCheckPath).build()
.toUri();
try {
return Mono
.just(HttpStatus.OK.equals(restTemplate.getForEntity(uri, Void.class).getStatusCode()));
}
catch (Exception ignored) {
return Mono.just(false);
}
}));
}

/**
* Allows creating a {@link ServiceInstanceListSupplier} instance based on provided
* {@link ConfigurableApplicationContext}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;

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

}

@Test
void shouldInstantiateBlockingHealthCheckServiceInstanceListSupplier() {
blockingDiscoveryClientRunner.withUserConfiguration(RestTemplateTestConfig.class)
.withPropertyValues("spring.cloud.loadbalancer.configurations=health-check").run(context -> {
ServiceInstanceListSupplier supplier = context.getBean(ServiceInstanceListSupplier.class);
then(supplier).isInstanceOf(HealthCheckServiceInstanceListSupplier.class);
then(((DelegatingServiceInstanceListSupplier) supplier).getDelegate())
.isInstanceOf(DiscoveryClientServiceInstanceListSupplier.class);
});
}

@Configuration
protected static class TestConfig {

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

}

@Configuration
protected static class RestTemplateTestConfig {

@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

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

/**
* Tests for {@link CachingServiceInstanceListSupplier}.
Expand Down Expand Up @@ -140,7 +141,7 @@ private static class TestHealthCheckServiceInstanceListSupplier extends HealthCh

TestHealthCheckServiceInstanceListSupplier(ServiceInstanceListSupplier delegate,
LoadBalancerProperties.HealthCheck healthCheck, WebClient webClient) {
super(delegate, healthCheck, webClient);
super(delegate, healthCheck, healthCheckFunction(webClient));
}

@Override
Expand Down
Loading