diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index 1d313be67..9c3d46bb0 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -28,10 +28,13 @@ |spring.cloud.inetutils.timeout-seconds | 1 | Timeout, in seconds, for calculating hostname. |spring.cloud.inetutils.use-only-site-local-interfaces | false | Whether to use only interfaces with site local addresses. See {@link InetAddress#isSiteLocalAddress()} for more details. |spring.cloud.loadbalancer.cache.caffeine.spec | | The spec to use to create caches. See CaffeineSpec for more details on the spec format. +|spring.cloud.loadbalancer.cache.capacity | 256 | Initial cache capacity expressed as int. |spring.cloud.loadbalancer.cache.ttl | 30s | Time To Live - time counted from writing of the record, after which cache entries are expired, expressed as a {@link Duration}. The property {@link String} has to be in keeping with the appropriate syntax as specified in Spring Boot StringToDurationConverter. @see StringToDurationConverter.java +|spring.cloud.loadbalancer.health-check.initial-delay | 0 | Initial delay value for the HealthCheck scheduler. +|spring.cloud.loadbalancer.health-check.interval | 30s | Interval for rerunning the HealthCheck scheduler. +|spring.cloud.loadbalancer.health-check.path | | |spring.cloud.loadbalancer.retry.enabled | true | |spring.cloud.loadbalancer.ribbon.enabled | true | Causes `RibbonLoadBalancerClient` to be used by default. -|spring.cloud.loadbalancer.zone | | A {@link String} representation of the zone used for filtering instances by zoned load-balancing implementations. |spring.cloud.refresh.enabled | true | Enables autoconfiguration for the refresh scope and associated features. |spring.cloud.refresh.extra-refreshable | true | Additional class names for beans to post process into refresh scope. |spring.cloud.service-registry.auto-registration.enabled | true | Whether service auto-registration is enabled. Defaults to true. diff --git a/docs/src/main/asciidoc/spring-cloud-commons.adoc b/docs/src/main/asciidoc/spring-cloud-commons.adoc index 7a4356606..b85851b9b 100644 --- a/docs/src/main/asciidoc/spring-cloud-commons.adoc +++ b/docs/src/main/asciidoc/spring-cloud-commons.adoc @@ -906,12 +906,71 @@ public class CustomLoadBalancerConfiguration { @Bean public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier( ReactiveDiscoveryClient discoveryClient, Environment environment, - LoadBalancerProperties loadBalancerProperties, + LoadBalancerZoneConfig zoneConfig, ApplicationContext context) { DiscoveryClientServiceInstanceListSupplier firstDelegate = new DiscoveryClientServiceInstanceListSupplier( discoveryClient, environment); ZonePreferenceServiceInstanceListSupplier delegate = new ZonePreferenceServiceInstanceListSupplier(firstDelegate, - loadBalancerProperties); + zoneConfig); + ObjectProvider cacheManagerProvider = context + .getBeanProvider(LoadBalancerCacheManager.class); + if (cacheManagerProvider.getIfAvailable() != null) { + return new CachingServiceInstanceListSupplier(delegate, + cacheManagerProvider.getIfAvailable()); + } + return delegate; + } + } +---- + +=== Instance Health-Check for LoadBalancer + +It is possible to enable a scheduled HealthCheck for the LoadBalancer. The `HealthCheckServiceInstanceListSupplier` +is provided for that. It regularly verifies if the instances provided by a delegate +`ServiceInstanceListSupplier` are still alive and only returns the healthy instances, +unless there are none - then it returns all the retrieved instances. + +TIP: This mechanism is particularly helpful while using the `SimpleDiscoveryClient`. For the +clients backed by an actual Service Registry, it's not necessary to use, as we already get +healthy instances after querying the external ServiceDiscovery. + +The `HealthCheckServiceInstanceListSupplier` uses `InstanceHealthChecker` to verify if the instances are +alive. We provide a default `PingHealthChecker` instance. It uses `WebClient` to execute +requests against the `health` endpoint of the instance. You can also provide your own implementation +of `InstanceHealthChecker` instead. + +`HealthCheckServiceInstanceListSupplier` uses properties prefixed with +`spring.cloud.loadbalancer.healthcheck`. You can set the `initialDelay` and `interval` +for the scheduler. + +For the `PingHealthChecker`, you can set the default path for the healthcheck URL by setting +the value of the `spring.cloud.loadbalancer.healthcheck.path.default`. You can also set a specific value +for any given service by setting the value of the `spring.cloud.loadbalancer.healthcheck.path.[SERVICE_ID]`, +substituting the `[SERVICE_ID]` with the correct ID of your service. If the path is not set, +`/actuator/health` is used by default. + +In order to use the health-check scheduler approach, you will have to instantiate a `HealthCheckServiceInstanceListSupplier` bean in a <>. + +We use delegates to work with `ServiceInstanceListSupplier` beans. +We suggest passing a `DiscoveryClientServiceInstanceListSupplier` delegate in the constructor of `HealthCheckServiceInstanceListSupplier` and, in turn, wrapping the latter with a `CachingServiceInstanceListSupplier` to leverage <>. + +You could use this sample configuration to set it up: + +[[zoned-based-custom-loadbalancer-configuration]] +[source,java,indent=0] +---- +public class CustomLoadBalancerConfiguration { + + @Bean + public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier( + ReactiveDiscoveryClient discoveryClient, Environment environment, + LoadBalancerProperties loadBalancerProperties, + ApplicationContext context, + InstanceHealthChecker healthChecker) { + DiscoveryClientServiceInstanceListSupplier firstDelegate = new DiscoveryClientServiceInstanceListSupplier( + discoveryClient, environment); + HealthCheckServiceInstanceListSupplier delegate = new HealthCheckServiceInstanceListSupplier(firstDelegate, + loadBalancerProperties, healthChecker); ObjectProvider cacheManagerProvider = context .getBeanProvider(LoadBalancerCacheManager.class); if (cacheManagerProvider.getIfAvailable() != null) { diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerProperties.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerProperties.java index 989310f2b..3e7f99a3f 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerProperties.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerProperties.java @@ -16,7 +16,11 @@ package org.springframework.cloud.client.loadbalancer.reactive; +import java.time.Duration; +import java.util.Map; + import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.LinkedCaseInsensitiveMap; /** * A {@link ConfigurationProperties} bean for Spring Cloud LoadBalancer. @@ -28,17 +32,56 @@ public class LoadBalancerProperties { /** - * A {@link String} representation of the zone used for filtering - * instances by zoned load-balancing implementations. + * Properties for HealthCheckServiceInstanceListSupplier. */ - private String zone; + private HealthCheck healthCheck = new HealthCheck(); + + public HealthCheck getHealthCheck() { + return healthCheck; + } - public String getZone() { - return zone; + public void setHealthCheck(HealthCheck healthCheck) { + this.healthCheck = healthCheck; } - public void setZone(String zone) { - this.zone = zone; + public static class HealthCheck { + + /** + * Initial delay value for the HealthCheck scheduler. + */ + private int initialDelay = 0; + + /** + * Interval for rerunning the HealthCheck scheduler. + */ + private Duration interval = Duration.ofSeconds(30); + + private Map path = new LinkedCaseInsensitiveMap<>(); + + public int getInitialDelay() { + return initialDelay; + } + + public void setInitialDelay(int initialDelay) { + this.initialDelay = initialDelay; + } + + public Map getPath() { + return path; + } + + public void setPath(Map path) { + this.path = path; + } + + public Duration getInterval() { + return interval; + } + + public void setInterval(Duration interval) { + this.interval = interval; + } + } } diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java index 62d5b9a71..7c6f42828 100644 --- a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java @@ -19,14 +19,12 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.client.ConditionalOnBlockingDiscoveryEnabled; import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled; import org.springframework.cloud.client.ConditionalOnReactiveDiscoveryEnabled; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; -import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties; import org.springframework.cloud.loadbalancer.cache.LoadBalancerCacheManager; import org.springframework.cloud.loadbalancer.core.CachingServiceInstanceListSupplier; import org.springframework.cloud.loadbalancer.core.CachingServiceInstanceSupplier; @@ -49,18 +47,11 @@ * @author Tim Ysewyn */ @Configuration(proxyBeanMethods = false) -@EnableConfigurationProperties(LoadBalancerProperties.class) @ConditionalOnDiscoveryEnabled public class LoadBalancerClientConfiguration { private static final int REACTIVE_SERVICE_INSTANCE_SUPPLIER_ORDER = 193827465; - @Bean - @ConditionalOnMissingBean - LoadBalancerProperties loadBalancerProperties() { - return new LoadBalancerProperties(); - } - @Bean @ConditionalOnMissingBean public ReactorLoadBalancer reactorServiceInstanceLoadBalancer( diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/LoadBalancerAutoConfiguration.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/LoadBalancerAutoConfiguration.java index 23d8a79bf..6afa79f74 100644 --- a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/LoadBalancerAutoConfiguration.java +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/LoadBalancerAutoConfiguration.java @@ -22,7 +22,9 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerBeanPostProcessorAutoConfiguration; +import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties; import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancerAutoConfiguration; import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerClientAutoConfiguration; import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientSpecification; @@ -30,6 +32,7 @@ import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; /** * @author Spencer Gibb @@ -37,6 +40,7 @@ */ @Configuration(proxyBeanMethods = false) @LoadBalancerClients +@EnableConfigurationProperties(LoadBalancerProperties.class) @AutoConfigureBefore({ ReactorLoadBalancerClientAutoConfiguration.class, LoadBalancerBeanPostProcessorAutoConfiguration.class, ReactiveLoadBalancerAutoConfiguration.class }) @@ -49,6 +53,13 @@ public LoadBalancerAutoConfiguration( this.configurations = configurations; } + @Bean + @ConditionalOnMissingBean + public LoadBalancerZoneConfig zoneConfig(Environment environment) { + return new LoadBalancerZoneConfig( + environment.getProperty("spring.cloud.loadbalancer.zone")); + } + @ConditionalOnMissingBean @Bean public LoadBalancerClientFactory loadBalancerClientFactory() { diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/LoadBalancerZoneConfig.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/LoadBalancerZoneConfig.java new file mode 100644 index 000000000..befc05dfc --- /dev/null +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/LoadBalancerZoneConfig.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-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.loadbalancer.config; + +/** + * @author Olga Maciaszek-Sharma + */ +public class LoadBalancerZoneConfig { + + /** + * A {@link String} representation of the zone used for filtering + * instances by zoned load-balancing implementations. + */ + private String zone; + + public LoadBalancerZoneConfig(String zone) { + this.zone = zone; + } + + public String getZone() { + return zone; + } + + public void setZone(String zone) { + this.zone = zone; + } + +} diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/HealthCheckServiceInstanceListSupplier.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/HealthCheckServiceInstanceListSupplier.java new file mode 100644 index 000000000..0374a00d8 --- /dev/null +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/HealthCheckServiceInstanceListSupplier.java @@ -0,0 +1,136 @@ +/* + * Copyright 2012-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.loadbalancer.core; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxSink; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.reactive.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 health endpoint of the instances. + * + * @author Olga Maciaszek-Sharma + * @since 2.2.0 + */ +public class HealthCheckServiceInstanceListSupplier + implements ServiceInstanceListSupplier { + + private static final Log LOG = LogFactory + .getLog(HealthCheckServiceInstanceListSupplier.class); + + private final ServiceInstanceListSupplier delegate; + + private final LoadBalancerProperties.HealthCheck healthCheck; + + private final WebClient webClient; + + private final String defaultHealthCheckPath; + + private List instances = Collections + .synchronizedList(new ArrayList<>()); + + private List healthyInstances = Collections + .synchronizedList(new ArrayList<>()); + + public HealthCheckServiceInstanceListSupplier(ServiceInstanceListSupplier delegate, + LoadBalancerProperties.HealthCheck healthCheck, WebClient webClient) { + this.delegate = delegate; + this.healthCheck = healthCheck; + defaultHealthCheckPath = healthCheck.getPath().getOrDefault("default", + "/actuator/health"); + this.webClient = webClient; + initInstances(); + + } + + private void initInstances() { + delegate.get().subscribe(delegateInstances -> { + instances.clear(); + instances.addAll(delegateInstances); + }); + + Flux> healthCheckFlux = healthCheckFlux(); + + healthCheckFlux.subscribe(verifiedInstances -> { + healthyInstances.clear(); + healthyInstances.addAll(verifiedInstances); + }); + } + + protected Flux> healthCheckFlux() { + return Flux.create(emitter -> Schedulers + .newSingle("Health Check Verifier: " + getServiceId(), true) + .schedulePeriodically(() -> { + List verifiedInstances = new ArrayList<>(); + Flux.fromIterable(instances).filterWhen(this::isAlive) + .subscribe(serviceInstance -> { + verifiedInstances.add(serviceInstance); + emitter.next(verifiedInstances); + }); + }, healthCheck.getInitialDelay(), healthCheck.getInterval().toMillis(), + TimeUnit.MILLISECONDS), + FluxSink.OverflowStrategy.LATEST); + } + + @Override + public String getServiceId() { + return delegate.getServiceId(); + } + + @Override + public Flux> get() { + if (!healthyInstances.isEmpty()) { + return Flux.defer(() -> Flux.fromIterable(healthyInstances).collectList()); + } + // If there are no healthy instances, it might be better to still retry on all of + // them + if (LOG.isWarnEnabled()) { + LOG.warn( + "No verified healthy instances were found, returning all listed instances."); + } + return Flux.defer(() -> Flux.fromIterable(instances).collectList()); + } + + protected Mono 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() + .map(clientResponse -> HttpStatus.OK.equals(clientResponse.statusCode())); + } + +} diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ZonePreferenceServiceInstanceListSupplier.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ZonePreferenceServiceInstanceListSupplier.java index 19192d592..7a2b222d5 100644 --- a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ZonePreferenceServiceInstanceListSupplier.java +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ZonePreferenceServiceInstanceListSupplier.java @@ -23,7 +23,7 @@ import reactor.core.publisher.Flux; import org.springframework.cloud.client.ServiceInstance; -import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties; +import org.springframework.cloud.loadbalancer.config.LoadBalancerZoneConfig; /** * An implementation of {@link ServiceInstanceListSupplier} that filters instances @@ -42,14 +42,14 @@ public class ZonePreferenceServiceInstanceListSupplier private final ServiceInstanceListSupplier delegate; - private final LoadBalancerProperties loadBalancerProperties; + private final LoadBalancerZoneConfig zoneConfig; private String zone; public ZonePreferenceServiceInstanceListSupplier(ServiceInstanceListSupplier delegate, - LoadBalancerProperties loadBalancerProperties) { + LoadBalancerZoneConfig zoneConfig) { this.delegate = delegate; - this.loadBalancerProperties = loadBalancerProperties; + this.zoneConfig = zoneConfig; } @Override @@ -64,7 +64,7 @@ public Flux> get() { private List filteredByZone(List serviceInstances) { if (zone == null) { - zone = loadBalancerProperties.getZone(); + zone = zoneConfig.getZone(); } if (zone != null) { List filteredInstances = new ArrayList<>(); diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/HealthCheckServiceInstanceListSupplierTests.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/HealthCheckServiceInstanceListSupplierTests.java new file mode 100644 index 000000000..8f6961a81 --- /dev/null +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/HealthCheckServiceInstanceListSupplierTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2012-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.loadbalancer.core; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link HealthCheckServiceInstanceListSupplier}. + * + * @author Olga Maciaszek-Sharma + */ +@ExtendWith(SpringExtension.class) +@SpringBootTest( + classes = HealthCheckServiceInstanceListSupplierTests.TestApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class HealthCheckServiceInstanceListSupplierTests { + + @LocalServerPort + private int port; + + private final WebClient webClient = WebClient.create(); + + private LoadBalancerProperties.HealthCheck healthCheck = new LoadBalancerProperties.HealthCheck(); + + @SuppressWarnings("ConstantConditions") + @Test + void shouldCheckInstanceWithProvidedHealthCheckPath() { + healthCheck.getPath().put("ignored-service", "/health"); + HealthCheckServiceInstanceListSupplier listSupplier = new HealthCheckServiceInstanceListSupplier( + ServiceInstanceListSupplier.FixedServiceInstanceListSupplier + .with(new MockEnvironment()).build(), + healthCheck, webClient); + ServiceInstance serviceInstance = new DefaultServiceInstance("ignored-service-1", + "ignored-service", "127.0.0.1", port, false); + + boolean alive = listSupplier.isAlive(serviceInstance).block(); + + assertThat(alive).isTrue(); + } + + @SuppressWarnings("ConstantConditions") + @Test + void shouldCheckInstanceWithDefaultHealthCheckPath() { + HealthCheckServiceInstanceListSupplier listSupplier = new HealthCheckServiceInstanceListSupplier( + ServiceInstanceListSupplier.FixedServiceInstanceListSupplier + .with(new MockEnvironment()).build(), + healthCheck, webClient); + ServiceInstance serviceInstance = new DefaultServiceInstance("ignored-service-1", + "ignored-service", "127.0.0.1", port, false); + + boolean alive = listSupplier.isAlive(serviceInstance).block(); + + assertThat(alive).isTrue(); + } + + @SuppressWarnings("ConstantConditions") + @Test + void shouldReturnFalseIfEndpointNotFound() { + healthCheck.getPath().put("ignored-service", "/test"); + HealthCheckServiceInstanceListSupplier listSupplier = new HealthCheckServiceInstanceListSupplier( + ServiceInstanceListSupplier.FixedServiceInstanceListSupplier + .with(new MockEnvironment()).build(), + healthCheck, webClient); + ServiceInstance serviceInstance = new DefaultServiceInstance("ignored-service-1", + "ignored-service", "127.0.0.1", port, false); + + boolean alive = listSupplier.isAlive(serviceInstance).block(); + + assertThat(alive).isFalse(); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + @RestController + static class TestApplication { + + public static void main(String[] args) { + SpringApplication.run( + HealthCheckServiceInstanceListSupplierTests.TestApplication.class, + args); + } + + @GetMapping("/health") + void healthCheck() { + + } + + @GetMapping("/actuator/health") + void defaultHealthCheck() { + + } + + } + +} diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/ZonePreferenceServiceInstanceListSupplierTests.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/ZonePreferenceServiceInstanceListSupplierTests.java index bdc1f56d5..b62ab753c 100644 --- a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/ZonePreferenceServiceInstanceListSupplierTests.java +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/ZonePreferenceServiceInstanceListSupplierTests.java @@ -27,7 +27,7 @@ import org.springframework.cloud.client.DefaultServiceInstance; import org.springframework.cloud.client.ServiceInstance; -import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties; +import org.springframework.cloud.loadbalancer.config.LoadBalancerZoneConfig; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; @@ -44,10 +44,10 @@ class ZonePreferenceServiceInstanceListSupplierTests { private DiscoveryClientServiceInstanceListSupplier delegate = mock( DiscoveryClientServiceInstanceListSupplier.class); - private LoadBalancerProperties loadBalancerProperties = new LoadBalancerProperties(); + private LoadBalancerZoneConfig zoneConfig = new LoadBalancerZoneConfig(null); private ZonePreferenceServiceInstanceListSupplier supplier = new ZonePreferenceServiceInstanceListSupplier( - delegate, loadBalancerProperties); + delegate, zoneConfig); private ServiceInstance first = serviceInstance("test-1", buildZoneMetadata("zone1")); @@ -63,7 +63,7 @@ class ZonePreferenceServiceInstanceListSupplierTests { @Test void shouldFilterInstancesByZone() { - loadBalancerProperties.setZone("zone1"); + zoneConfig.setZone("zone1"); when(delegate.get()).thenReturn( Flux.just(Arrays.asList(first, second, third, fourth, fifth))); @@ -78,7 +78,7 @@ void shouldFilterInstancesByZone() { @Test void shouldReturnAllInstancesIfNoZoneInstances() { - loadBalancerProperties.setZone("zone1"); + zoneConfig.setZone("zone1"); when(delegate.get()).thenReturn(Flux.just(Arrays.asList(third, fourth))); List filtered = supplier.get().blockFirst(); @@ -89,7 +89,7 @@ void shouldReturnAllInstancesIfNoZoneInstances() { @Test void shouldNotThrowNPEIfNullInstanceMetadata() { - loadBalancerProperties.setZone("zone1"); + zoneConfig.setZone("zone1"); when(delegate.get()).thenReturn( Flux.just(Collections.singletonList(serviceInstance("test-6", null)))); assertThatCode(() -> supplier.get().blockFirst()).doesNotThrowAnyException(); @@ -97,7 +97,7 @@ void shouldNotThrowNPEIfNullInstanceMetadata() { @Test void shouldReturnAllInstancesIfNoZone() { - loadBalancerProperties.setZone(null); + zoneConfig.setZone(null); when(delegate.get()) .thenReturn(Flux.just(Arrays.asList(first, second, third, fourth)));