diff --git a/docs/src/main/asciidoc/spring-cloud-commons.adoc b/docs/src/main/asciidoc/spring-cloud-commons.adoc index ac6232d93..cb2cbf674 100644 --- a/docs/src/main/asciidoc/spring-cloud-commons.adoc +++ b/docs/src/main/asciidoc/spring-cloud-commons.adoc @@ -468,6 +468,8 @@ For the reactive implementation, you can additionally set: For the reactive implementation, you can also implement your own `LoadBalancerRetryPolicy` to have more detailed control over the load-balanced call retries. +NOTE: Individual Loadbalancer clients may be configured individually with the same properties as above except the prefix is `spring.cloud.loadbalancer.clients..*` where `clientId` is the name of the loadbalancer. + NOTE: For load-balanced retries, by default, we wrap the `ServiceInstanceListSupplier` bean with `RetryAwareServiceInstanceListSupplier` to select a different instance from the one previously chosen, if available. You can disable this behavior by setting the value of `spring.cloud.loadbalancer.retry.avoidPreviousInstance` to `false`. ==== @@ -1246,6 +1248,27 @@ NOTE: The meters are registered in the registry when at least one record is adde TIP: You can further configure the behavior of those metrics (for example, add https://micrometer.io/docs/concepts#_histograms_and_percentiles[publishing percentiles and histograms]) by https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-metrics-per-meter-properties[adding `MeterFilters`]. +=== Configuring Individual LoadBalancerClients + +Individual Loadbalancer clients may be configured individually with a different prefix `spring.cloud.loadbalancer.clients..*` where `clientId` is the name of the loadbalancer. Default configuration values may be set in the `spring.cloud.loadbalancer.*` namespace and will be merged with the client specific values taking precedence + +.application.yml +==== +---- +spring: + cloud: + loadbalancer: + health-check: + initial-delay: 1s + clients: + myclient: + health-check: + interval: 30s +---- +==== + +The above example will result in a merged health-check `@ConfigurationProperties` object with `initial-delay=1s` and `interval=30s`. + == Spring Cloud Circuit Breaker include::spring-cloud-circuitbreaker.adoc[leveloffset=+1] diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfiguration.java index ccafa07ee..7fdc57da5 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfiguration.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfiguration.java @@ -51,7 +51,7 @@ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(RestTemplate.class) @ConditionalOnBean(LoadBalancerClient.class) -@EnableConfigurationProperties(LoadBalancerProperties.class) +@EnableConfigurationProperties(LoadBalancerClientsProperties.class) public class LoadBalancerAutoConfiguration { @LoadBalanced @@ -147,11 +147,10 @@ public static class RetryInterceptorAutoConfiguration { @Bean @ConditionalOnMissingBean public RetryLoadBalancerInterceptor loadBalancerInterceptor(LoadBalancerClient loadBalancerClient, - LoadBalancerProperties properties, LoadBalancerRequestFactory requestFactory, - LoadBalancedRetryFactory loadBalancedRetryFactory, + LoadBalancerRequestFactory requestFactory, LoadBalancedRetryFactory loadBalancedRetryFactory, ReactiveLoadBalancer.Factory loadBalancerFactory) { - return new RetryLoadBalancerInterceptor(loadBalancerClient, properties, requestFactory, - loadBalancedRetryFactory, loadBalancerFactory); + return new RetryLoadBalancerInterceptor(loadBalancerClient, requestFactory, loadBalancedRetryFactory, + loadBalancerFactory); } @Bean diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerClientsProperties.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerClientsProperties.java new file mode 100644 index 000000000..240829850 --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerClientsProperties.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013-2021 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.client.loadbalancer; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * A {@link ConfigurationProperties} bean for Spring Cloud Loadbalancer. + * + * Individual clients are configured via the {@link LoadBalancerClientsProperties#clients} + * field. Defaults and other properties are located in the {@link LoadBalancerProperties} + * base class. + * + * @author Spencer Gibb + * @since 3.1.0 + */ +@ConfigurationProperties("spring.cloud.loadbalancer") +public class LoadBalancerClientsProperties extends LoadBalancerProperties { + + private Map clients = new HashMap<>(); + + public Map getClients() { + return this.clients; + } + +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerDefaultMappingsProviderAutoConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerDefaultMappingsProviderAutoConfiguration.java new file mode 100644 index 000000000..501f1c4bf --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerDefaultMappingsProviderAutoConfiguration.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2020 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.client.loadbalancer; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.cloud.commons.config.DefaultsBindHandlerAdvisor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Spencer Gibb + * @since 3.1.0 + */ +@Configuration(proxyBeanMethods = false) +@AutoConfigureBefore(name = "org.springframework.cloud.commons.config.CommonsConfigAutoConfiguration") +public class LoadBalancerDefaultMappingsProviderAutoConfiguration { + + @Bean + public DefaultsBindHandlerAdvisor.MappingsProvider loadBalancerClientsDefaultsMappingsProvider() { + return () -> { + Map mappings = new HashMap<>(); + mappings.put(ConfigurationPropertyName.of("spring.cloud.loadbalancer.clients"), + ConfigurationPropertyName.of("spring.cloud.loadbalancer")); + return mappings; + }; + } + +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerProperties.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerProperties.java index bbefb715d..644970c77 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerProperties.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerProperties.java @@ -29,12 +29,14 @@ import org.springframework.util.LinkedCaseInsensitiveMap; /** - * A {@link ConfigurationProperties} bean for Spring Cloud LoadBalancer. + * The base configuration bean for Spring Cloud LoadBalancer. + * + * See {@link LoadBalancerClientsProperties} for the {@link ConfigurationProperties} + * annotation. * * @author Olga Maciaszek-Sharma * @since 2.2.1 */ -@ConfigurationProperties("spring.cloud.loadbalancer") public class LoadBalancerProperties { /** diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerInterceptor.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerInterceptor.java index e56922b72..fc20c0557 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerInterceptor.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerInterceptor.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.net.URI; +import java.util.Map; import java.util.Set; import org.apache.commons.logging.Log; @@ -50,19 +51,26 @@ public class RetryLoadBalancerInterceptor implements ClientHttpRequestIntercepto private final LoadBalancerClient loadBalancer; - private final LoadBalancerProperties properties; - private final LoadBalancerRequestFactory requestFactory; private final LoadBalancedRetryFactory lbRetryFactory; private final ReactiveLoadBalancer.Factory loadBalancerFactory; + @Deprecated public RetryLoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerProperties properties, LoadBalancerRequestFactory requestFactory, LoadBalancedRetryFactory lbRetryFactory, ReactiveLoadBalancer.Factory loadBalancerFactory) { this.loadBalancer = loadBalancer; - this.properties = properties; + this.requestFactory = requestFactory; + this.lbRetryFactory = lbRetryFactory; + this.loadBalancerFactory = loadBalancerFactory; + } + + public RetryLoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory, + LoadBalancedRetryFactory lbRetryFactory, + ReactiveLoadBalancer.Factory loadBalancerFactory) { + this.loadBalancer = loadBalancer; this.requestFactory = requestFactory; this.lbRetryFactory = lbRetryFactory; this.loadBalancerFactory = loadBalancerFactory; @@ -159,14 +167,17 @@ private RetryTemplate createRetryTemplate(String serviceName, HttpRequest reques if (retryListeners != null && retryListeners.length != 0) { template.setListeners(retryListeners); } - template.setRetryPolicy(!properties.getRetry().isEnabled() || retryPolicy == null ? new NeverRetryPolicy() - : new InterceptorRetryPolicy(request, retryPolicy, loadBalancer, serviceName)); + template.setRetryPolicy( + !loadBalancerFactory.getProperties(serviceName).getRetry().isEnabled() || retryPolicy == null + ? new NeverRetryPolicy() + : new InterceptorRetryPolicy(request, retryPolicy, loadBalancer, serviceName)); return template; } private String getHint(String serviceId) { - String defaultHint = properties.getHint().getOrDefault("default", "default"); - String hintPropertyValue = properties.getHint().get(serviceId); + Map hint = loadBalancerFactory.getProperties(serviceId).getHint(); + String defaultHint = hint.getOrDefault("default", "default"); + String hintPropertyValue = hint.get(serviceId); return hintPropertyValue != null ? hintPropertyValue : defaultHint; } diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerRetryPolicy.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerRetryPolicy.java index 0d2edad87..0da54842f 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerRetryPolicy.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerRetryPolicy.java @@ -16,6 +16,7 @@ package org.springframework.cloud.client.loadbalancer.reactive; +import org.springframework.cglib.core.internal.Function; import org.springframework.http.HttpMethod; /** @@ -55,4 +56,8 @@ public interface LoadBalancerRetryPolicy { */ boolean canRetryOnMethod(HttpMethod method); + interface Factory extends Function { + + } + } diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactiveLoadBalancer.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactiveLoadBalancer.java index 9f762ca2a..9d4613f85 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactiveLoadBalancer.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactiveLoadBalancer.java @@ -22,6 +22,7 @@ import org.springframework.cloud.client.loadbalancer.DefaultRequest; import org.springframework.cloud.client.loadbalancer.DefaultRequestContext; +import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties; import org.springframework.cloud.client.loadbalancer.Request; import org.springframework.cloud.client.loadbalancer.Response; @@ -53,6 +54,10 @@ default Publisher> choose() { // conflicting name interface Factory { + default LoadBalancerProperties getProperties(String serviceId) { + return null; + } + ReactiveLoadBalancer getInstance(String serviceId); /** diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerClientAutoConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerClientAutoConfiguration.java index 42135a3cd..a132b73be 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerClientAutoConfiguration.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerClientAutoConfiguration.java @@ -24,9 +24,10 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.LoadBalanced; -import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.client.WebClient; @@ -42,6 +43,7 @@ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(WebClient.class) @ConditionalOnBean(ReactiveLoadBalancer.Factory.class) +@EnableConfigurationProperties(LoadBalancerClientsProperties.class) public class ReactorLoadBalancerClientAutoConfiguration { @ConditionalOnMissingBean @@ -49,9 +51,9 @@ public class ReactorLoadBalancerClientAutoConfiguration { matchIfMissing = true) @Bean public ReactorLoadBalancerExchangeFilterFunction loadBalancerExchangeFilterFunction( - ReactiveLoadBalancer.Factory loadBalancerFactory, LoadBalancerProperties properties, + ReactiveLoadBalancer.Factory loadBalancerFactory, ObjectProvider> transformers) { - return new ReactorLoadBalancerExchangeFilterFunction(loadBalancerFactory, properties, + return new ReactorLoadBalancerExchangeFilterFunction(loadBalancerFactory, transformers.getIfAvailable(Collections::emptyList)); } @@ -59,18 +61,19 @@ public ReactorLoadBalancerExchangeFilterFunction loadBalancerExchangeFilterFunct @ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true") @Bean public RetryableLoadBalancerExchangeFilterFunction retryableLoadBalancerExchangeFilterFunction( - ReactiveLoadBalancer.Factory loadBalancerFactory, LoadBalancerProperties properties, - LoadBalancerRetryPolicy retryPolicy, + ReactiveLoadBalancer.Factory loadBalancerFactory, + LoadBalancerRetryPolicy.Factory retryPolicyFactory, ObjectProvider> transformers) { - return new RetryableLoadBalancerExchangeFilterFunction(retryPolicy, loadBalancerFactory, properties, + return new RetryableLoadBalancerExchangeFilterFunction(retryPolicyFactory, loadBalancerFactory, transformers.getIfAvailable(Collections::emptyList)); } @ConditionalOnMissingBean @ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true") @Bean - public LoadBalancerRetryPolicy loadBalancerRetryPolicy(LoadBalancerProperties properties) { - return new RetryableExchangeFilterFunctionLoadBalancerRetryPolicy(properties); + public LoadBalancerRetryPolicy.Factory loadBalancerRetryPolicy( + ReactiveLoadBalancer.Factory loadBalancerFactory) { + return new RetryableExchangeFilterFunctionLoadBalancerRetryPolicy.Factory(loadBalancerFactory); } } diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerExchangeFilterFunction.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerExchangeFilterFunction.java index d5735b66d..528caffa8 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerExchangeFilterFunction.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerExchangeFilterFunction.java @@ -61,8 +61,6 @@ public class ReactorLoadBalancerExchangeFilterFunction implements LoadBalancedEx private final ReactiveLoadBalancer.Factory loadBalancerFactory; - private final LoadBalancerProperties properties; - private final List transformers; /** @@ -77,10 +75,16 @@ public ReactorLoadBalancerExchangeFilterFunction(ReactiveLoadBalancer.Factory loadBalancerFactory, LoadBalancerProperties properties, List transformers) { this.loadBalancerFactory = loadBalancerFactory; - this.properties = properties; + this.transformers = transformers; + } + + public ReactorLoadBalancerExchangeFilterFunction(ReactiveLoadBalancer.Factory loadBalancerFactory, + List transformers) { + this.loadBalancerFactory = loadBalancerFactory; this.transformers = transformers; } @@ -99,7 +103,7 @@ public Mono filter(ClientRequest clientRequest, ExchangeFunction .getSupportedLifecycleProcessors( loadBalancerFactory.getInstances(serviceId, LoadBalancerLifecycle.class), RequestDataContext.class, ResponseData.class, ServiceInstance.class); - String hint = getHint(serviceId, properties.getHint()); + String hint = getHint(serviceId, loadBalancerFactory.getProperties(serviceId).getHint()); RequestData requestData = new RequestData(clientRequest); DefaultRequest lbRequest = new DefaultRequest<>(new RequestDataContext(requestData, hint)); supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest)); @@ -120,7 +124,8 @@ public Mono filter(ClientRequest clientRequest, ExchangeFunction LOG.debug(String.format("LoadBalancer has retrieved the instance for service %s: %s", serviceId, instance.getUri())); } - LoadBalancerProperties.StickySession stickySessionProperties = properties.getStickySession(); + LoadBalancerProperties.StickySession stickySessionProperties = loadBalancerFactory.getProperties(serviceId) + .getStickySession(); ClientRequest newRequest = buildClientRequest(clientRequest, instance, stickySessionProperties.getInstanceIdCookieName(), stickySessionProperties.isAddServiceInstanceCookie(), transformers); diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableExchangeFilterFunctionLoadBalancerRetryPolicy.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableExchangeFilterFunctionLoadBalancerRetryPolicy.java index e04e0ede6..1a1eb6e5e 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableExchangeFilterFunctionLoadBalancerRetryPolicy.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableExchangeFilterFunctionLoadBalancerRetryPolicy.java @@ -16,6 +16,7 @@ package org.springframework.cloud.client.loadbalancer.reactive; +import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties; import org.springframework.http.HttpMethod; @@ -53,4 +54,20 @@ public boolean canRetryOnMethod(HttpMethod method) { return HttpMethod.GET.equals(method) || properties.getRetry().isRetryOnAllOperations(); } + static class Factory implements LoadBalancerRetryPolicy.Factory { + + final ReactiveLoadBalancer.Factory loadBalancerFactory; + + Factory(ReactiveLoadBalancer.Factory loadBalancerFactory) { + this.loadBalancerFactory = loadBalancerFactory; + } + + @Override + public LoadBalancerRetryPolicy apply(String serviceId) { + return new RetryableExchangeFilterFunctionLoadBalancerRetryPolicy( + loadBalancerFactory.getProperties(serviceId)); + } + + } + } diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunction.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunction.java index 288c719a3..5256284ea 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunction.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunction.java @@ -69,9 +69,7 @@ public class RetryableLoadBalancerExchangeFilterFunction implements LoadBalanced private static final List> exceptions = Arrays.asList(IOException.class, TimeoutException.class, RetryableStatusCodeException.class); - private final LoadBalancerRetryPolicy retryPolicy; - - private final LoadBalancerProperties properties; + private final LoadBalancerRetryPolicy.Factory retryPolicyFactory; private final ReactiveLoadBalancer.Factory loadBalancerFactory; @@ -87,22 +85,26 @@ public RetryableLoadBalancerExchangeFilterFunction(LoadBalancerRetryPolicy retry this(retryPolicy, loadBalancerFactory, properties, Collections.emptyList()); } + @Deprecated public RetryableLoadBalancerExchangeFilterFunction(LoadBalancerRetryPolicy retryPolicy, ReactiveLoadBalancer.Factory loadBalancerFactory, LoadBalancerProperties properties, List transformers) { - this.retryPolicy = retryPolicy; + this.retryPolicyFactory = s -> retryPolicy; + this.loadBalancerFactory = loadBalancerFactory; + this.transformers = transformers; + } + + public RetryableLoadBalancerExchangeFilterFunction(LoadBalancerRetryPolicy.Factory retryPolicyFactory, + ReactiveLoadBalancer.Factory loadBalancerFactory, + List transformers) { + this.retryPolicyFactory = retryPolicyFactory; this.loadBalancerFactory = loadBalancerFactory; - this.properties = properties; this.transformers = transformers; } @SuppressWarnings({ "rawtypes", "unchecked" }) @Override public Mono filter(ClientRequest clientRequest, ExchangeFunction next) { - LoadBalancerRetryContext loadBalancerRetryContext = new LoadBalancerRetryContext(clientRequest); - Retry exchangeRetry = buildRetrySpec(properties.getRetry().getMaxRetriesOnSameServiceInstance(), true); - Retry filterRetry = buildRetrySpec(properties.getRetry().getMaxRetriesOnNextServiceInstance(), false); - URI originalUrl = clientRequest.url(); String serviceId = originalUrl.getHost(); if (serviceId == null) { @@ -112,6 +114,15 @@ public Mono filter(ClientRequest clientRequest, ExchangeFunction } return Mono.just(ClientResponse.create(HttpStatus.BAD_REQUEST).body(message).build()); } + LoadBalancerRetryContext loadBalancerRetryContext = new LoadBalancerRetryContext(clientRequest); + LoadBalancerProperties properties = loadBalancerFactory.getProperties(serviceId); + + Retry exchangeRetry = buildRetrySpec(properties.getRetry().getMaxRetriesOnSameServiceInstance(), true, + properties.getRetry()); + Retry filterRetry = buildRetrySpec(properties.getRetry().getMaxRetriesOnNextServiceInstance(), false, + properties.getRetry()); + LoadBalancerRetryPolicy retryPolicy = retryPolicyFactory.apply(serviceId); + Set supportedLifecycleProcessors = LoadBalancerLifecycleValidator .getSupportedLifecycleProcessors( loadBalancerFactory.getInstances(serviceId, LoadBalancerLifecycle.class), @@ -154,7 +165,7 @@ public Mono filter(ClientRequest clientRequest, ExchangeFunction lbRequest, lbResponse, new ResponseData(clientResponse, requestData))))) .map(clientResponse -> { loadBalancerRetryContext.setClientResponse(clientResponse); - if (shouldRetrySameServiceInstance(loadBalancerRetryContext)) { + if (shouldRetrySameServiceInstance(retryPolicy, loadBalancerRetryContext)) { if (LOG.isDebugEnabled()) { LOG.debug(String.format("Retrying on status code: %d", clientResponse.statusCode().value())); @@ -166,7 +177,7 @@ lbRequest, lbResponse, new ResponseData(clientResponse, requestData))))) }); }).map(clientResponse -> { loadBalancerRetryContext.setClientResponse(clientResponse); - if (shouldRetryNextServiceInstance(loadBalancerRetryContext)) { + if (shouldRetryNextServiceInstance(retryPolicy, loadBalancerRetryContext)) { if (LOG.isDebugEnabled()) { LOG.debug(String.format("Retrying on status code: %d", clientResponse.statusCode().value())); } @@ -177,8 +188,11 @@ lbRequest, lbResponse, new ResponseData(clientResponse, requestData))))) }).retryWhen(exchangeRetry)).retryWhen(filterRetry); } - private Retry buildRetrySpec(int max, boolean transientErrors) { - LoadBalancerProperties.Retry.Backoff backoffProperties = properties.getRetry().getBackoff(); + private Retry buildRetrySpec(int max, boolean transientErrors, LoadBalancerProperties.Retry retry) { + if (!retry.isEnabled()) { + return Retry.max(0).filter(this::isRetryException).transientErrors(transientErrors); + } + LoadBalancerProperties.Retry.Backoff backoffProperties = retry.getBackoff(); if (backoffProperties.isEnabled()) { return RetrySpec.backoff(max, backoffProperties.getMinBackoff()).filter(this::isRetryException) .maxBackoff(backoffProperties.getMaxBackoff()).jitter(backoffProperties.getJitter()) @@ -187,7 +201,8 @@ private Retry buildRetrySpec(int max, boolean transientErrors) { return RetrySpec.max(max).filter(this::isRetryException).transientErrors(transientErrors); } - private boolean shouldRetrySameServiceInstance(LoadBalancerRetryContext loadBalancerRetryContext) { + private boolean shouldRetrySameServiceInstance(LoadBalancerRetryPolicy retryPolicy, + LoadBalancerRetryContext loadBalancerRetryContext) { boolean shouldRetry = retryPolicy.retryableStatusCode(loadBalancerRetryContext.getResponseStatusCode()) && retryPolicy.canRetryOnMethod(loadBalancerRetryContext.getRequestMethod()) && retryPolicy.canRetrySameServiceInstance(loadBalancerRetryContext); @@ -197,7 +212,8 @@ private boolean shouldRetrySameServiceInstance(LoadBalancerRetryContext loadBala return shouldRetry; } - private boolean shouldRetryNextServiceInstance(LoadBalancerRetryContext loadBalancerRetryContext) { + private boolean shouldRetryNextServiceInstance(LoadBalancerRetryPolicy retryPolicy, + LoadBalancerRetryContext loadBalancerRetryContext) { boolean shouldRetry = retryPolicy.retryableStatusCode(loadBalancerRetryContext.getResponseStatusCode()) && retryPolicy.canRetryOnMethod(loadBalancerRetryContext.getRequestMethod()) && retryPolicy.canRetryNextServiceInstance(loadBalancerRetryContext); diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/ConfigDataMissingEnvironmentPostProcessor.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/ConfigDataMissingEnvironmentPostProcessor.java index dc7a3b99b..8d204d0a0 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/ConfigDataMissingEnvironmentPostProcessor.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/ConfigDataMissingEnvironmentPostProcessor.java @@ -43,6 +43,7 @@ /** * @author Ryan Baxter */ +// TODO: 4.0.0 move to org.springframework.cloud.commons.config public abstract class ConfigDataMissingEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { /** diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/config/CommonsConfigAutoConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/config/CommonsConfigAutoConfiguration.java new file mode 100644 index 000000000..95647158b --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/config/CommonsConfigAutoConfiguration.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2020 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.commons.config; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * @author Spencer Gibb + * @since 3.1.0 + */ +@Configuration(proxyBeanMethods = false) +public class CommonsConfigAutoConfiguration { + + @Bean + public DefaultsBindHandlerAdvisor defaultsBindHandlerAdvisor( + @Nullable DefaultsBindHandlerAdvisor.MappingsProvider[] providers) { + Map additionalMappings = new HashMap<>(); + if (!ObjectUtils.isEmpty(providers)) { + for (int i = 0; i < providers.length; i++) { + DefaultsBindHandlerAdvisor.MappingsProvider mappingsProvider = providers[i]; + additionalMappings.putAll(mappingsProvider.getDefaultMappings()); + } + } + return new DefaultsBindHandlerAdvisor(additionalMappings); + } + +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/config/DefaultsBindHandlerAdvisor.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/config/DefaultsBindHandlerAdvisor.java new file mode 100644 index 000000000..7afe89594 --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/config/DefaultsBindHandlerAdvisor.java @@ -0,0 +1,90 @@ +/* + * Copyright 2013-2021 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.commons.config; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationPropertiesBindHandlerAdvisor; +import org.springframework.boot.context.properties.bind.AbstractBindHandler; +import org.springframework.boot.context.properties.bind.BindContext; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.BindResult; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form; +import org.springframework.util.CollectionUtils; + +/** + * @author Oleg Zhurakousky + * @since 3.1.0 + */ +public class DefaultsBindHandlerAdvisor implements ConfigurationPropertiesBindHandlerAdvisor { + + private final Map mappings; + + public DefaultsBindHandlerAdvisor(Map additionalMappings) { + this.mappings = new LinkedHashMap<>(); + if (!CollectionUtils.isEmpty(additionalMappings)) { + this.mappings.putAll(additionalMappings); + } + } + + @Override + public BindHandler apply(BindHandler bindHandler) { + + BindHandler handler = new AbstractBindHandler(bindHandler) { + @Override + public Bindable onStart(ConfigurationPropertyName name, Bindable target, BindContext context) { + ConfigurationPropertyName defaultName = getDefaultName(name); + if (defaultName != null) { + BindResult result = context.getBinder().bind(defaultName, target); + if (result.isBound()) { + return target.withExistingValue(result.get()); + } + } + return bindHandler.onStart(name, target, context); + } + }; + return handler; + } + + private ConfigurationPropertyName getDefaultName(ConfigurationPropertyName name) { + for (Map.Entry mapping : this.mappings.entrySet()) { + ConfigurationPropertyName from = mapping.getKey(); + ConfigurationPropertyName to = mapping.getValue(); + if ((from.isAncestorOf(name) && name.getNumberOfElements() > from.getNumberOfElements())) { + ConfigurationPropertyName defaultName = to; + for (int i = from.getNumberOfElements() + 1; i < name.getNumberOfElements(); i++) { + defaultName = defaultName.append(name.getElement(i, Form.UNIFORM)); + } + return defaultName; + } + } + return null; + } + + /** + * Provides mappings including the default mappings. + */ + public interface MappingsProvider { + + Map getDefaultMappings(); + + } + +} diff --git a/spring-cloud-commons/src/main/resources/META-INF/spring.factories b/spring-cloud-commons/src/main/resources/META-INF/spring.factories index 016abdd69..81723a805 100644 --- a/spring-cloud-commons/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-commons/src/main/resources/META-INF/spring.factories @@ -9,6 +9,7 @@ org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscove org.springframework.cloud.client.hypermedia.CloudHypermediaAutoConfiguration,\ org.springframework.cloud.client.loadbalancer.AsyncLoadBalancerAutoConfiguration,\ org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration,\ +org.springframework.cloud.client.loadbalancer.LoadBalancerDefaultMappingsProviderAutoConfiguration,\ org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerBeanPostProcessorAutoConfiguration,\ org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerClientAutoConfiguration,\ org.springframework.cloud.client.serviceregistry.ServiceRegistryAutoConfiguration,\ @@ -16,7 +17,8 @@ org.springframework.cloud.commons.httpclient.HttpClientConfiguration,\ org.springframework.cloud.commons.util.UtilAutoConfiguration,\ org.springframework.cloud.configuration.CompatibilityVerifierAutoConfiguration,\ org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration,\ -org.springframework.cloud.commons.security.ResourceServerTokenRelayAutoConfiguration +org.springframework.cloud.commons.security.ResourceServerTokenRelayAutoConfiguration,\ +org.springframework.cloud.commons.config.CommonsConfigAutoConfiguration # Environment Post Processors org.springframework.boot.env.EnvironmentPostProcessor=\ org.springframework.cloud.client.HostInfoEnvironmentPostProcessor diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/AbstractLoadBalancerAutoConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/AbstractLoadBalancerAutoConfigurationTests.java index 4e64b044a..1a0d21b8d 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/AbstractLoadBalancerAutoConfigurationTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/AbstractLoadBalancerAutoConfigurationTests.java @@ -103,8 +103,8 @@ LoadBalancerClient loadBalancerClient() { } @Bean - ReactiveLoadBalancer.Factory loadBalancerFactory() { - return new TestLoadBalancerFactory(); + ReactiveLoadBalancer.Factory loadBalancerFactory(LoadBalancerProperties properties) { + return new TestLoadBalancerFactory(properties); } } @@ -176,6 +176,12 @@ public URI reconstructURI(ServiceInstance instance, URI original) { private static class TestLoadBalancerFactory implements ReactiveLoadBalancer.Factory { + private final LoadBalancerProperties properties; + + TestLoadBalancerFactory(LoadBalancerProperties properties) { + this.properties = properties; + } + @Override public ReactiveLoadBalancer getInstance(String serviceId) { throw new UnsupportedOperationException("Not implemented."); @@ -191,6 +197,11 @@ public Map getInstances(String name, Class type) { throw new UnsupportedOperationException("Not implemented."); } + @Override + public LoadBalancerProperties getProperties(String serviceId) { + return properties; + } + } } diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerInterceptorTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerInterceptorTests.java index 94ad3f8e8..579a25bcf 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerInterceptorTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerInterceptorTests.java @@ -62,6 +62,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; /** * @author Ryan Baxter @@ -88,7 +89,8 @@ public void setUp() { client = mock(LoadBalancerClient.class); lbRequestFactory = mock(LoadBalancerRequestFactory.class); properties = new LoadBalancerProperties(); - lbFactory = mock(ReactiveLoadBalancer.Factory.class); + lbFactory = mock(ReactiveLoadBalancer.Factory.class, withSettings().lenient()); + when(lbFactory.getProperties(any())).thenReturn(properties); } @AfterEach @@ -105,8 +107,8 @@ public void interceptDisableRetry() throws Throwable { when(client.execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class))) .thenThrow(new IOException()); properties.getRetry().setEnabled(false); - RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, properties, - lbRequestFactory, loadBalancedRetryFactory, lbFactory); + RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory, + loadBalancedRetryFactory, lbFactory); byte[] body = new byte[] {}; ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class); @@ -123,8 +125,8 @@ public void interceptInvalidHost() throws Throwable { HttpRequest request = mock(HttpRequest.class); when(request.getURI()).thenReturn(new URI("http://foo_underscore")); properties.getRetry().setEnabled(true); - RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, properties, - lbRequestFactory, loadBalancedRetryFactory, lbFactory); + RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory, + loadBalancedRetryFactory, lbFactory); byte[] body = new byte[] {}; ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class); Assertions.assertThrows(IllegalStateException.class, () -> { @@ -143,8 +145,8 @@ public void interceptNeverRetry() throws Throwable { .thenReturn(clientHttpResponse); when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class)); properties.getRetry().setEnabled(true); - RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, properties, - lbRequestFactory, loadBalancedRetryFactory, lbFactory); + RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory, + loadBalancedRetryFactory, lbFactory); byte[] body = new byte[] {}; ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class); interceptor.intercept(request, body, execution); @@ -163,8 +165,8 @@ public void interceptSuccess() throws Throwable { .thenReturn(clientHttpResponse); when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class)); properties.getRetry().setEnabled(true); - RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, properties, - lbRequestFactory, new MyLoadBalancedRetryFactory(policy), lbFactory); + RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory, + new MyLoadBalancedRetryFactory(policy), lbFactory); byte[] body = new byte[] {}; ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class); ClientHttpResponse rsp = interceptor.intercept(request, body, execution); @@ -189,8 +191,8 @@ public void interceptRetryOnStatusCode() throws Throwable { when(client.execute(eq("foo"), eq(serviceInstance), nullable(LoadBalancerRequest.class))) .thenReturn(clientHttpResponseNotFound).thenReturn(clientHttpResponseOk); properties.getRetry().setEnabled(true); - RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, properties, - lbRequestFactory, new MyLoadBalancedRetryFactory(policy), lbFactory); + RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory, + new MyLoadBalancedRetryFactory(policy), lbFactory); byte[] body = new byte[] {}; ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class); ClientHttpResponse rsp = interceptor.intercept(request, body, execution); @@ -222,8 +224,8 @@ public void interceptRetryFailOnStatusCode() throws Throwable { properties.getRetry().setEnabled(true); byte[] body = new byte[] {}; ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class); - RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, properties, - lbRequestFactory, new MyLoadBalancedRetryFactory(policy), lbFactory); + RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory, + new MyLoadBalancedRetryFactory(policy), lbFactory); ClientHttpResponse rsp = interceptor.intercept(request, body, execution); verify(client, times(1)).execute(eq("foo"), eq(serviceInstance), @@ -252,8 +254,8 @@ public void interceptRetry() throws Throwable { .thenThrow(new IOException()).thenReturn(clientHttpResponse); when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class)); properties.getRetry().setEnabled(true); - RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, properties, - lbRequestFactory, new MyLoadBalancedRetryFactory(policy, backOffPolicy), lbFactory); + RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory, + new MyLoadBalancedRetryFactory(policy, backOffPolicy), lbFactory); byte[] body = new byte[] {}; ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class); ClientHttpResponse rsp = interceptor.intercept(request, body, execution); @@ -276,8 +278,8 @@ public void interceptFailedRetry() throws Exception { .thenThrow(new IOException()).thenReturn(clientHttpResponse); when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class)); properties.getRetry().setEnabled(true); - RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, properties, - lbRequestFactory, new MyLoadBalancedRetryFactory(policy), lbFactory); + RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory, + new MyLoadBalancedRetryFactory(policy), lbFactory); byte[] body = new byte[] {}; ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class); Assertions.assertThrows(IOException.class, () -> { @@ -305,8 +307,7 @@ public void retryListenerTest() throws Throwable { properties.getRetry().setEnabled(true); MyRetryListener retryListener = new MyRetryListener(); when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class)); - RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, properties, - lbRequestFactory, + RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory, new MyLoadBalancedRetryFactory(policy, backOffPolicy, new RetryListener[] { retryListener }), lbFactory); byte[] body = new byte[] {}; @@ -333,8 +334,8 @@ public void retryWithDefaultConstructorTest() throws Throwable { .thenThrow(new IOException()).thenReturn(clientHttpResponse); properties.getRetry().setEnabled(true); when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class)); - RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, properties, - lbRequestFactory, new MyLoadBalancedRetryFactory(policy, backOffPolicy), lbFactory); + RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory, + new MyLoadBalancedRetryFactory(policy, backOffPolicy), lbFactory); byte[] body = new byte[] {}; ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class); ClientHttpResponse rsp = interceptor.intercept(request, body, execution); @@ -357,8 +358,7 @@ public boolean open(RetryContext context, RetryCallback return false; } }; - RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, properties, - lbRequestFactory, + RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory, new MyLoadBalancedRetryFactory(policy, backOffPolicy, new RetryListener[] { myRetryListener }), lbFactory); ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class); @@ -376,8 +376,8 @@ public void shouldNotDuplicateLifecycleCalls() throws IOException, URISyntaxExce HttpRequest request = mock(HttpRequest.class); when(request.getURI()).thenReturn(new URI("http://test")); TestLoadBalancerClient client = new TestLoadBalancerClient(); - RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, properties, - lbRequestFactory, loadBalancedRetryFactory, lbFactory); + RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory, + loadBalancedRetryFactory, lbFactory); interceptor.intercept(request, new byte[] {}, mock(ClientHttpRequestExecution.class)); diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerClientRequestTransformerTest.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerClientRequestTransformerTest.java index 5e4b27f90..57a7e5d9a 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerClientRequestTransformerTest.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerClientRequestTransformerTest.java @@ -63,6 +63,7 @@ class LoadBalancerClientRequestTransformerTest { @BeforeEach void setUp() { when(factory.getInstance("testServiceId")).thenReturn(new TestReactiveLoadBalancer()); + when(factory.getProperties(any())).thenReturn(properties); when(clientRequest.method()).thenReturn(HttpMethod.GET); when(clientRequest.url()).thenReturn(URI.create("http://testServiceId")); when(clientRequest.headers()).thenReturn(new HttpHeaders()); @@ -75,7 +76,7 @@ void setUp() { void transformReactorLoadBalancerExchangeFilterFunction() { ArgumentCaptor captor = ArgumentCaptor.forClass(ClientRequest.class); ReactorLoadBalancerExchangeFilterFunction filterFunction = new ReactorLoadBalancerExchangeFilterFunction( - factory, properties, Arrays.asList(new Transformer1(), new Transformer2())); + factory, Arrays.asList(new Transformer1(), new Transformer2())); filterFunction.filter(clientRequest, next).subscribe(); verify(next).exchange(captor.capture()); HttpHeaders headers = captor.getValue().headers(); @@ -87,7 +88,7 @@ void transformReactorLoadBalancerExchangeFilterFunction() { void transformRetryableLoadBalancerExchangeFilterFunction() { ArgumentCaptor captor = ArgumentCaptor.forClass(ClientRequest.class); RetryableLoadBalancerExchangeFilterFunction filterFunction = new RetryableLoadBalancerExchangeFilterFunction( - policy, factory, properties, Arrays.asList(new Transformer1(), new Transformer2())); + s -> policy, factory, Arrays.asList(new Transformer1(), new Transformer2())); filterFunction.filter(clientRequest, next).subscribe(); verify(next).exchange(captor.capture()); HttpHeaders headers = captor.getValue().headers(); diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerClientAutoConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerClientAutoConfigurationTests.java index b5ffe50ca..1e2eebcdb 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerClientAutoConfigurationTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerClientAutoConfigurationTests.java @@ -16,14 +16,19 @@ package org.springframework.cloud.client.loadbalancer.reactive; +import java.time.Duration; import java.util.Map; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties; import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; @@ -128,16 +133,39 @@ void noCustomWebClientBuilders() { then(getFilters(builder)).isNullOrEmpty(); } + @Test + void defaultPropertiesWorks() { + ConfigurableApplicationContext context = new SpringApplicationBuilder().web(WebApplicationType.NONE) + .sources(OneWebClientBuilder.class, DefaulConfig.class) + .properties("spring.cloud.loadbalancer.health-check.initial-delay=1s", + "spring.cloud.loadbalancer.clients.myclient.health-check.interval=30s") + .run(); + LoadBalancerClientsProperties properties = context.getBean(LoadBalancerClientsProperties.class); + + then(properties.getClients()).containsKey("myclient"); + LoadBalancerProperties clientProperties = properties.getClients().get("myclient"); + // default value + then(clientProperties.getHealthCheck().getInitialDelay()).isEqualTo(Duration.ofSeconds(1)); + // client specific value + then(clientProperties.getHealthCheck().getInterval()).isEqualTo(Duration.ofSeconds(30)); + } + private ConfigurableApplicationContext init(Class config) { return LoadBalancerTestUtils.init(config, ReactorLoadBalancerClientAutoConfiguration.class, LoadBalancerBeanPostProcessorAutoConfiguration.class); } + @Configuration + @EnableAutoConfiguration + protected static class DefaulConfig { + + } + @Configuration protected static class NoWebClientBuilder { @Bean - ReactiveLoadBalancer.Factory reactiveLoadBalancerFactory() { + ReactiveLoadBalancer.Factory reactiveLoadBalancerFactory(LoadBalancerProperties properties) { return new ReactiveLoadBalancer.Factory() { @Override public ReactiveLoadBalancer getInstance(String serviceId) { @@ -153,6 +181,11 @@ public Map getInstances(String name, Class type) { public X getInstance(String name, Class clazz, Class... generics) { throw new UnsupportedOperationException("Not implemented."); } + + @Override + public LoadBalancerProperties getProperties(String serviceId) { + return properties; + } }; } @@ -162,11 +195,6 @@ LoadBalancedRetryFactory loadBalancedRetryFactory() { }; } - @Bean - LoadBalancerProperties loadBalancerProperties() { - return new LoadBalancerProperties(); - } - } @Configuration diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerExchangeFilterFunctionTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerExchangeFilterFunctionTests.java index 963403d6f..502db407c 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerExchangeFilterFunctionTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerExchangeFilterFunctionTests.java @@ -171,7 +171,8 @@ String callbackTestResult() { } @Bean - ReactiveLoadBalancer.Factory reactiveLoadBalancerFactory(DiscoveryClient discoveryClient) { + ReactiveLoadBalancer.Factory reactiveLoadBalancerFactory(DiscoveryClient discoveryClient, + LoadBalancerProperties properties) { return new ReactiveLoadBalancer.Factory() { private final TestLoadBalancerLifecycle testLoadBalancerLifecycle = new TestLoadBalancerLifecycle(); @@ -198,12 +199,12 @@ public Map getInstances(String name, Class type) { public X getInstance(String name, Class clazz, Class... generics) { return null; } - }; - } - @Bean - LoadBalancerProperties loadBalancerProperties() { - return new LoadBalancerProperties(); + @Override + public LoadBalancerProperties getProperties(String serviceId) { + return properties; + } + }; } } diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunctionIntegrationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunctionIntegrationTests.java index a1c3eb3ca..0363e82fe 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunctionIntegrationTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunctionIntegrationTests.java @@ -228,7 +228,8 @@ String exception() { } @Bean - ReactiveLoadBalancer.Factory reactiveLoadBalancerFactory(DiscoveryClient discoveryClient) { + ReactiveLoadBalancer.Factory reactiveLoadBalancerFactory(DiscoveryClient discoveryClient, + LoadBalancerProperties properties) { return new ReactiveLoadBalancer.Factory() { private final TestLoadBalancerLifecycle testLoadBalancerLifecycle = new TestLoadBalancerLifecycle(); @@ -256,12 +257,12 @@ public Map getInstances(String name, Class type) { public X getInstance(String name, Class clazz, Class... generics) { return null; } - }; - } - @Bean - LoadBalancerProperties loadBalancerProperties() { - return new LoadBalancerProperties(); + @Override + public LoadBalancerProperties getProperties(String serviceId) { + return properties; + } + }; } @Bean diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunctionTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunctionTests.java index 5a183c8d0..d78395d07 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunctionTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunctionTests.java @@ -74,6 +74,7 @@ void setUp() { properties.getRetry().getRetryableStatusCodes().add(404); when(clientRequest.url()).thenReturn(URI.create("http://test")); when(factory.getInstance("test")).thenReturn(new TestReactiveLoadBalancer()); + when(factory.getProperties(any())).thenReturn(properties); when(clientRequest.headers()).thenReturn(new HttpHeaders()); when(clientRequest.cookies()).thenReturn(new HttpHeaders()); @@ -138,7 +139,7 @@ void shouldRetryOnMethodOtherThanGetWhenEnabled() { properties.getRetry().getRetryableStatusCodes().add(404); LoadBalancerRetryPolicy policy = new RetryableExchangeFilterFunctionLoadBalancerRetryPolicy(properties); RetryableLoadBalancerExchangeFilterFunction filterFunction = new RetryableLoadBalancerExchangeFilterFunction( - policy, factory, properties, Collections.emptyList()); + s -> policy, factory, Collections.emptyList()); when(clientRequest.method()).thenReturn(HttpMethod.POST); when(clientResponse.statusCode()).thenReturn(HttpStatus.NOT_FOUND); when(next.exchange(any())).thenReturn(Mono.just(clientResponse)); diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/blocking/retry/BlockingLoadBalancedRetryFactory.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/blocking/retry/BlockingLoadBalancedRetryFactory.java index 92a063756..6a8bc07c1 100644 --- a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/blocking/retry/BlockingLoadBalancedRetryFactory.java +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/blocking/retry/BlockingLoadBalancedRetryFactory.java @@ -16,10 +16,11 @@ package org.springframework.cloud.loadbalancer.blocking.retry; +import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicy; -import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties; import org.springframework.cloud.client.loadbalancer.ServiceInstanceChooser; +import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer; import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient; /** @@ -31,15 +32,15 @@ */ public class BlockingLoadBalancedRetryFactory implements LoadBalancedRetryFactory { - private final LoadBalancerProperties loadBalancerProperties; + private final ReactiveLoadBalancer.Factory loadBalancerFactory; - public BlockingLoadBalancedRetryFactory(LoadBalancerProperties loadBalancerProperties) { - this.loadBalancerProperties = loadBalancerProperties; + public BlockingLoadBalancedRetryFactory(ReactiveLoadBalancer.Factory loadBalancerFactory) { + this.loadBalancerFactory = loadBalancerFactory; } @Override public LoadBalancedRetryPolicy createRetryPolicy(String serviceId, ServiceInstanceChooser serviceInstanceChooser) { - return new BlockingLoadBalancedRetryPolicy(loadBalancerProperties); + return new BlockingLoadBalancedRetryPolicy(loadBalancerFactory.getProperties(serviceId)); } } diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/BlockingLoadBalancerClientAutoConfiguration.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/BlockingLoadBalancerClientAutoConfiguration.java index f9fad6065..2da800e2f 100644 --- a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/BlockingLoadBalancerClientAutoConfiguration.java +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/BlockingLoadBalancerClientAutoConfiguration.java @@ -23,10 +23,13 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.AsyncLoadBalancerAutoConfiguration; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties; import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties; +import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer; import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients; import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient; import org.springframework.cloud.loadbalancer.blocking.retry.BlockingLoadBalancedRetryFactory; @@ -70,13 +73,14 @@ public LoadBalancerServiceInstanceCookieTransformer loadBalancerServiceInstanceC @Configuration @ConditionalOnClass(RetryTemplate.class) - @EnableConfigurationProperties(LoadBalancerProperties.class) + @EnableConfigurationProperties(LoadBalancerClientsProperties.class) protected static class BlockingLoadBalancerRetryConfig { @Bean @ConditionalOnMissingBean - LoadBalancedRetryFactory loadBalancedRetryFactory(LoadBalancerProperties properties) { - return new BlockingLoadBalancedRetryFactory(properties); + LoadBalancedRetryFactory loadBalancedRetryFactory( + ReactiveLoadBalancer.Factory loadBalancerFactory) { + return new BlockingLoadBalancedRetryFactory(loadBalancerFactory); } } 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 bdde78608..e3f7519d6 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 @@ -24,7 +24,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties; import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerBeanPostProcessorAutoConfiguration; import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerClientAutoConfiguration; import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientSpecification; @@ -40,7 +40,7 @@ */ @Configuration(proxyBeanMethods = false) @LoadBalancerClients -@EnableConfigurationProperties(LoadBalancerProperties.class) +@EnableConfigurationProperties(LoadBalancerClientsProperties.class) @AutoConfigureBefore({ ReactorLoadBalancerClientAutoConfiguration.class, LoadBalancerBeanPostProcessorAutoConfiguration.class }) @ConditionalOnProperty(value = "spring.cloud.loadbalancer.enabled", havingValue = "true", matchIfMissing = true) @@ -60,8 +60,8 @@ public LoadBalancerZoneConfig zoneConfig(Environment environment) { @ConditionalOnMissingBean @Bean - public LoadBalancerClientFactory loadBalancerClientFactory() { - LoadBalancerClientFactory clientFactory = new LoadBalancerClientFactory(); + public LoadBalancerClientFactory loadBalancerClientFactory(LoadBalancerClientsProperties properties) { + LoadBalancerClientFactory clientFactory = new LoadBalancerClientFactory(properties); clientFactory.setConfigurations(this.configurations.getIfAvailable(Collections::emptyList)); return clientFactory; } diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/LoadBalancerClientFactory.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/LoadBalancerClientFactory.java index 518e1a981..d2cd44423 100644 --- a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/LoadBalancerClientFactory.java +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/support/LoadBalancerClientFactory.java @@ -16,7 +16,12 @@ package org.springframework.cloud.loadbalancer.support; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties; +import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties; import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer; import org.springframework.cloud.context.named.NamedContextFactory; import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientConfiguration; @@ -36,6 +41,8 @@ public class LoadBalancerClientFactory extends NamedContextFactory implements ReactiveLoadBalancer.Factory { + private static final Log log = LogFactory.getLog(LoadBalancerClientFactory.class); + /** * Property source name for load balancer. */ @@ -46,8 +53,16 @@ public class LoadBalancerClientFactory extends NamedContextFactory getInstance(String serviceId) { return getInstance(serviceId, ReactorServiceInstanceLoadBalancer.class); } + @Override + public LoadBalancerProperties getProperties(String serviceId) { + if (properties == null) { + if (log.isWarnEnabled()) { + log.warn("LoadBalancerClientsProperties is null. Please use the new constructor."); + } + return null; + } + if (!properties.getClients().containsKey(serviceId)) { + // no specific client properties, return default + return properties; + } + // because specifics are overlayed on top of defaults, everything in `properties`, + // unless overridden, is in `clientsProperties` + return properties.getClients().get(serviceId); + } + } diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/CachingServiceInstanceListSupplierTests.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/CachingServiceInstanceListSupplierTests.java index 91251b46e..282aafa24 100644 --- a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/CachingServiceInstanceListSupplierTests.java +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/CachingServiceInstanceListSupplierTests.java @@ -26,6 +26,7 @@ import org.springframework.cloud.client.DefaultServiceInstance; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties; import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties; import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient; import org.springframework.cloud.loadbalancer.cache.LoadBalancerCacheManager; @@ -100,8 +101,8 @@ ReactorLoadBalancer reactorLoadBalancer(ObjectProvider