diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc
index 68d83b6d4..fdb3d1d4d 100644
--- a/docs/src/main/asciidoc/_configprops.adoc
+++ b/docs/src/main/asciidoc/_configprops.adoc
@@ -27,6 +27,8 @@
|spring.cloud.loadbalancer.cache.capacity | `256` | Initial cache capacity expressed as int.
|spring.cloud.loadbalancer.cache.enabled | `true` | Enables Spring Cloud LoadBalancer caching mechanism.
|spring.cloud.loadbalancer.cache.ttl | `35s` | 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.client | |
+|spring.cloud.loadbalancer.client.configuration.enabled | `false` | Enables per client LoadBalancer configuration.
|spring.cloud.loadbalancer.configurations | `default` | Enables a predefined LoadBalancer configuration.
|spring.cloud.loadbalancer.health-check.initial-delay | `0` | Initial delay value for the HealthCheck scheduler.
|spring.cloud.loadbalancer.health-check.interval | `25s` | Interval for rerunning the HealthCheck scheduler.
diff --git a/docs/src/main/asciidoc/spring-cloud-commons.adoc b/docs/src/main/asciidoc/spring-cloud-commons.adoc
index 635afc045..0fea4bf80 100644
--- a/docs/src/main/asciidoc/spring-cloud-commons.adoc
+++ b/docs/src/main/asciidoc/spring-cloud-commons.adoc
@@ -1228,6 +1228,44 @@ 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`].
+=== Spring Cloud LoadBalancer per client configuration
+By default, to preserve backward compatibility, Spring Cloud LoadBalancer uses general `spring.cloud.loadbalancer` configuration.
+To enable per client configuration, set the `spring.cloud.loadbalancer.client.configuration.enabled` property to `true`.
+
+When per client configuration is enabled, you can configure multiple loadBalancer clients by using
+`spring.cloud.loadbalancer.client.services[]` properties:
+====
+[source,yaml]
+----
+spring:
+ cloud:
+ loadBalancer:
+ client:
+ foo1:
+ retry:
+ retryOnAllOperations: true
+ foo2:
+ healthCheck:
+ refetchInstances: true
+----
+====
+
+You can also configure default properties for LoadBalancer:
+====
+[source,yaml]
+----
+spring:
+ cloud:
+ loadBalancer:
+ client:
+ default:
+ retry:
+ retryOnAllOperations: true
+ healthCheck:
+ refetchInstances: true
+----
+====
+
== 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..528679c00 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
@@ -23,6 +23,7 @@
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -47,13 +48,22 @@
* @author Will Tran
* @author Gang Li
* @author Olga Maciaszek-Sharma
+ * @author Andrii Bohutskyi
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
-@EnableConfigurationProperties(LoadBalancerProperties.class)
+@EnableConfigurationProperties({ LoadBalancerProperties.class, LoadBalancerClientProperties.class })
public class LoadBalancerAutoConfiguration {
+ @Bean
+ @ConditionalOnMissingBean
+ public LoadBalancerPropertiesFactory loadBalancerPropertiesFactory(LoadBalancerProperties globalProperties,
+ LoadBalancerClientProperties servicesProperties,
+ @Value("${spring.cloud.loadbalancer.service.configuration.enabled:false}") boolean isServiceProperties) {
+ return new LoadBalancerPropertiesFactory(globalProperties, servicesProperties, isServiceProperties);
+ }
+
@LoadBalanced
@Autowired(required = false)
private List restTemplates = Collections.emptyList();
@@ -149,9 +159,10 @@ public static class RetryInterceptorAutoConfiguration {
public RetryLoadBalancerInterceptor loadBalancerInterceptor(LoadBalancerClient loadBalancerClient,
LoadBalancerProperties properties, LoadBalancerRequestFactory requestFactory,
LoadBalancedRetryFactory loadBalancedRetryFactory,
- ReactiveLoadBalancer.Factory loadBalancerFactory) {
+ ReactiveLoadBalancer.Factory loadBalancerFactory,
+ LoadBalancerPropertiesFactory propertiesFactory) {
return new RetryLoadBalancerInterceptor(loadBalancerClient, properties, requestFactory,
- loadBalancedRetryFactory, loadBalancerFactory);
+ loadBalancedRetryFactory, loadBalancerFactory, propertiesFactory);
}
@Bean
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerClientProperties.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerClientProperties.java
new file mode 100644
index 000000000..6cbc9aec1
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerClientProperties.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2012-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 client LoadBalancer.
+ *
+ * @author Andrii Bohutskyi
+ */
+@ConfigurationProperties("spring.cloud.loadbalancer")
+public class LoadBalancerClientProperties {
+
+ private Map client = new HashMap<>();
+
+ public Map getClient() {
+ return client;
+ }
+
+ public void setClient(Map client) {
+ this.client = client;
+ }
+
+ public LoadBalancerProperties getDefaultClientProperties() {
+ return client.getOrDefault("default", new LoadBalancerProperties());
+ }
+
+}
diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerPropertiesFactory.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerPropertiesFactory.java
new file mode 100644
index 000000000..bf5fbf061
--- /dev/null
+++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerPropertiesFactory.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2012-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;
+
+/**
+ * Factory class used to provide client properties.
+ *
+ * @author Andrii Bohutskyi
+ */
+public class LoadBalancerPropertiesFactory {
+
+ private final LoadBalancerProperties globalProperties;
+
+ private final LoadBalancerClientProperties servicesProperties;
+
+ private final boolean isServiceProperties;
+
+ public LoadBalancerPropertiesFactory(LoadBalancerProperties globalProperties,
+ LoadBalancerClientProperties servicesProperties, boolean isServiceProperties) {
+ this.globalProperties = globalProperties;
+ this.servicesProperties = servicesProperties;
+ this.isServiceProperties = isServiceProperties;
+ }
+
+ public LoadBalancerProperties getLoadBalancerProperties(String serviceName) {
+ return isServiceProperties ? getLoadBalancerServiceProperties(serviceName) : globalProperties;
+ }
+
+ public LoadBalancerProperties getGlobalLoadBalancerProperties() {
+ return globalProperties;
+ }
+
+ private LoadBalancerProperties getLoadBalancerServiceProperties(String serviceName) {
+ return servicesProperties.getClient().getOrDefault(serviceName,
+ servicesProperties.getDefaultClientProperties());
+ }
+
+}
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..266dc4c49 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
@@ -42,6 +42,7 @@
* @author Will Tran
* @author Gang Li
* @author Olga Maciaszek-Sharma
+ * @author Andrii Bohutskyi
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public class RetryLoadBalancerInterceptor implements ClientHttpRequestInterceptor {
@@ -58,6 +59,13 @@ public class RetryLoadBalancerInterceptor implements ClientHttpRequestIntercepto
private final ReactiveLoadBalancer.Factory loadBalancerFactory;
+ private LoadBalancerPropertiesFactory propertiesFactory;
+
+ /**
+ * @deprecated Deprecated in favor of
+ * {@link #RetryLoadBalancerInterceptor(LoadBalancerClient, LoadBalancerProperties, LoadBalancerRequestFactory, LoadBalancedRetryFactory, ReactiveLoadBalancer.Factory, LoadBalancerPropertiesFactory)}.
+ */
+ @Deprecated
public RetryLoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerProperties properties,
LoadBalancerRequestFactory requestFactory, LoadBalancedRetryFactory lbRetryFactory,
ReactiveLoadBalancer.Factory loadBalancerFactory) {
@@ -68,6 +76,14 @@ public RetryLoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalance
this.loadBalancerFactory = loadBalancerFactory;
}
+ public RetryLoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerProperties properties,
+ LoadBalancerRequestFactory requestFactory, LoadBalancedRetryFactory lbRetryFactory,
+ ReactiveLoadBalancer.Factory loadBalancerFactory,
+ LoadBalancerPropertiesFactory propertiesFactory) {
+ this(loadBalancer, properties, requestFactory, lbRetryFactory, loadBalancerFactory);
+ this.propertiesFactory = propertiesFactory;
+ }
+
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
@@ -159,15 +175,24 @@ 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()
+ template.setRetryPolicy(!getLoadBalancerProperties(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);
+ String defaultHint = getLoadBalancerProperties(serviceId).getHint().getOrDefault("default", "default");
+ String hintPropertyValue = getLoadBalancerProperties(serviceId).getHint().get(serviceId);
return hintPropertyValue != null ? hintPropertyValue : defaultHint;
}
+ @Deprecated
+ private LoadBalancerProperties getLoadBalancerProperties(String serviceId) {
+ if (propertiesFactory != null) {
+ return propertiesFactory.getLoadBalancerProperties(serviceId);
+ } else {
+ return properties;
+ }
+ }
+
}
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..415dee65a 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
@@ -23,6 +23,7 @@
* retried.
*
* @author Olga Maciaszek-Sharma
+ * @author Andrii Bohutskyi
* @since 3.0.0
*/
public interface LoadBalancerRetryPolicy {
@@ -31,28 +32,78 @@ public interface LoadBalancerRetryPolicy {
* Return true to retry on the same service instance.
* @param context the context for the retry operation
* @return true to retry on the same service instance
+ * @deprecated Deprecated in favor of
+ * {@link #canRetrySameServiceInstance(String, LoadBalancerRetryContext)}
*/
+ @Deprecated
boolean canRetrySameServiceInstance(LoadBalancerRetryContext context);
+ /**
+ * Return true to retry on the same service instance.
+ * @param serviceId the serviceId for the retry operation
+ * @param context the context for the retry operation
+ * @return true to retry on the same service instance
+ */
+ default boolean canRetrySameServiceInstance(String serviceId, LoadBalancerRetryContext context) {
+ return canRetrySameServiceInstance(context);
+ }
+
/**
* Return true to retry on the next service instance.
* @param context the context for the retry operation
* @return true to retry on the same service instance
+ * @deprecated Deprecated in favor of
+ * {@link #canRetryNextServiceInstance(String, LoadBalancerRetryContext)}
*/
+ @Deprecated
boolean canRetryNextServiceInstance(LoadBalancerRetryContext context);
+ /**
+ * Return true to retry on the next service instance.
+ * @param serviceId the serviceId for the retry operation
+ * @param context the context for the retry operation
+ * @return true to retry on the same service instance
+ */
+ default boolean canRetryNextServiceInstance(String serviceId, LoadBalancerRetryContext context) {
+ return canRetryNextServiceInstance(context);
+ }
+
/**
* Return true to retry on the provided HTTP status code.
* @param statusCode the HTTP status code
* @return true to retry on the provided HTTP status code
+ * @deprecated Deprecated in favor of {@link #retryableStatusCode(String, int)}
*/
+ @Deprecated
boolean retryableStatusCode(int statusCode);
+ /**
+ * Return true to retry on the provided HTTP status code.
+ * @param serviceId the serviceId for the retry operation
+ * @param statusCode the HTTP status code
+ * @return true to retry on the provided HTTP status code
+ */
+ default boolean retryableStatusCode(String serviceId, int statusCode) {
+ return retryableStatusCode(statusCode);
+ }
+
/**
* Return true to retry on the provided HTTP method.
* @param method the HTTP request method
* @return true to retry on the provided HTTP method
+ * @deprecated Deprecated in favor of {@link #canRetryOnMethod(String, HttpMethod)}
*/
+ @Deprecated
boolean canRetryOnMethod(HttpMethod method);
+ /**
+ * Return true to retry on the provided HTTP method.
+ * @param serviceId the serviceId for the retry operation
+ * @param method the HTTP request method
+ * @return true to retry on the provided HTTP method
+ */
+ default boolean canRetryOnMethod(String serviceId, HttpMethod method) {
+ return canRetryOnMethod(method);
+ }
+
}
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..daf800fe5 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
@@ -27,6 +27,7 @@
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.LoadBalancerPropertiesFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
@@ -37,6 +38,7 @@
* {@link ReactiveLoadBalancer} used under the hood.
*
* @author Olga Maciaszek-Sharma
+ * @author Andrii Bohutskyi
* @since 2.2.0
*/
@Configuration(proxyBeanMethods = false)
@@ -50,9 +52,10 @@ public class ReactorLoadBalancerClientAutoConfiguration {
@Bean
public ReactorLoadBalancerExchangeFilterFunction loadBalancerExchangeFilterFunction(
ReactiveLoadBalancer.Factory loadBalancerFactory, LoadBalancerProperties properties,
- ObjectProvider> transformers) {
+ ObjectProvider> transformers,
+ LoadBalancerPropertiesFactory propertiesFactory) {
return new ReactorLoadBalancerExchangeFilterFunction(loadBalancerFactory, properties,
- transformers.getIfAvailable(Collections::emptyList));
+ transformers.getIfAvailable(Collections::emptyList), propertiesFactory);
}
@ConditionalOnMissingBean
@@ -61,16 +64,18 @@ public ReactorLoadBalancerExchangeFilterFunction loadBalancerExchangeFilterFunct
public RetryableLoadBalancerExchangeFilterFunction retryableLoadBalancerExchangeFilterFunction(
ReactiveLoadBalancer.Factory loadBalancerFactory, LoadBalancerProperties properties,
LoadBalancerRetryPolicy retryPolicy,
- ObjectProvider> transformers) {
+ ObjectProvider> transformers,
+ LoadBalancerPropertiesFactory propertiesFactory) {
return new RetryableLoadBalancerExchangeFilterFunction(retryPolicy, loadBalancerFactory, properties,
- transformers.getIfAvailable(Collections::emptyList));
+ transformers.getIfAvailable(Collections::emptyList), propertiesFactory);
}
@ConditionalOnMissingBean
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true")
@Bean
- public LoadBalancerRetryPolicy loadBalancerRetryPolicy(LoadBalancerProperties properties) {
- return new RetryableExchangeFilterFunctionLoadBalancerRetryPolicy(properties);
+ public LoadBalancerRetryPolicy loadBalancerRetryPolicy(LoadBalancerProperties properties,
+ LoadBalancerPropertiesFactory propertiesFactory) {
+ return new RetryableExchangeFilterFunctionLoadBalancerRetryPolicy(properties, propertiesFactory);
}
}
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..7ebb907b3 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
@@ -32,6 +32,7 @@
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycleValidator;
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
+import org.springframework.cloud.client.loadbalancer.LoadBalancerPropertiesFactory;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.RequestData;
import org.springframework.cloud.client.loadbalancer.RequestDataContext;
@@ -52,6 +53,7 @@
* requests against a correct {@link ServiceInstance}.
*
* @author Olga Maciaszek-Sharma
+ * @author Andrii Bohutskyi
* @since 2.2.0
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
@@ -65,6 +67,8 @@ public class ReactorLoadBalancerExchangeFilterFunction implements LoadBalancedEx
private final List transformers;
+ private LoadBalancerPropertiesFactory propertiesFactory;
+
/**
* @deprecated Deprecated in favor of
* {@link #ReactorLoadBalancerExchangeFilterFunction(ReactiveLoadBalancer.Factory, LoadBalancerProperties, List)}.
@@ -77,6 +81,11 @@ public ReactorLoadBalancerExchangeFilterFunction(ReactiveLoadBalancer.Factory loadBalancerFactory,
LoadBalancerProperties properties, List transformers) {
this.loadBalancerFactory = loadBalancerFactory;
@@ -84,6 +93,19 @@ public ReactorLoadBalancerExchangeFilterFunction(ReactiveLoadBalancer.Factory loadBalancerFactory,
+ LoadBalancerProperties properties, List transformers,
+ LoadBalancerPropertiesFactory propertiesFactory) {
+ this(loadBalancerFactory, properties, transformers);
+ this.propertiesFactory = propertiesFactory;
+ }
+
@Override
public Mono filter(ClientRequest clientRequest, ExchangeFunction next) {
URI originalUrl = clientRequest.url();
@@ -99,7 +121,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, getLoadBalancerProperties(serviceId).getHint());
RequestData requestData = new RequestData(clientRequest);
DefaultRequest lbRequest = new DefaultRequest<>(new RequestDataContext(requestData, hint));
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
@@ -120,7 +142,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 = getLoadBalancerProperties(serviceId)
+ .getStickySession();
ClientRequest newRequest = buildClientRequest(clientRequest, instance,
stickySessionProperties.getInstanceIdCookieName(),
stickySessionProperties.isAddServiceInstanceCookie(), transformers);
@@ -143,4 +166,14 @@ protected Mono> choose(String serviceId, Request transformers;
+ private LoadBalancerPropertiesFactory propertiesFactory;
+
/**
* @deprecated Deprecated in favor of
* {@link #RetryableLoadBalancerExchangeFilterFunction(LoadBalancerRetryPolicy, ReactiveLoadBalancer.Factory, LoadBalancerProperties, List)}.
@@ -87,6 +91,11 @@ public RetryableLoadBalancerExchangeFilterFunction(LoadBalancerRetryPolicy retry
this(retryPolicy, loadBalancerFactory, properties, Collections.emptyList());
}
+ /**
+ * @deprecated Deprecated in favor of
+ * {@link #RetryableLoadBalancerExchangeFilterFunction(LoadBalancerRetryPolicy, ReactiveLoadBalancer.Factory, LoadBalancerProperties, List, LoadBalancerPropertiesFactory)}
+ */
+ @Deprecated
public RetryableLoadBalancerExchangeFilterFunction(LoadBalancerRetryPolicy retryPolicy,
ReactiveLoadBalancer.Factory loadBalancerFactory, LoadBalancerProperties properties,
List transformers) {
@@ -96,13 +105,16 @@ public RetryableLoadBalancerExchangeFilterFunction(LoadBalancerRetryPolicy retry
this.transformers = transformers;
}
+ public RetryableLoadBalancerExchangeFilterFunction(LoadBalancerRetryPolicy retryPolicy,
+ ReactiveLoadBalancer.Factory loadBalancerFactory, LoadBalancerProperties properties,
+ List transformers, LoadBalancerPropertiesFactory propertiesFactory) {
+ this(retryPolicy, loadBalancerFactory, properties, transformers);
+ this.propertiesFactory = propertiesFactory;
+ }
+
@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,11 +124,23 @@ public Mono filter(ClientRequest clientRequest, ExchangeFunction
}
return Mono.just(ClientResponse.create(HttpStatus.BAD_REQUEST).body(message).build());
}
+
+ if (!getLoadBalancerProperties(serviceId).getRetry().isEnabled()) {
+ return new ReactorLoadBalancerExchangeFilterFunction(loadBalancerFactory, properties, transformers,
+ propertiesFactory).filter(clientRequest, next);
+ }
+
+ LoadBalancerRetryContext loadBalancerRetryContext = new LoadBalancerRetryContext(clientRequest);
+ Retry exchangeRetry = buildRetrySpec(serviceId,
+ getLoadBalancerProperties(serviceId).getRetry().getMaxRetriesOnSameServiceInstance(), true);
+ Retry filterRetry = buildRetrySpec(serviceId,
+ getLoadBalancerProperties(serviceId).getRetry().getMaxRetriesOnNextServiceInstance(), false);
+
Set supportedLifecycleProcessors = LoadBalancerLifecycleValidator
.getSupportedLifecycleProcessors(
loadBalancerFactory.getInstances(serviceId, LoadBalancerLifecycle.class),
RetryableRequestContext.class, ResponseData.class, ServiceInstance.class);
- String hint = getHint(serviceId, properties.getHint());
+ String hint = getHint(serviceId, getLoadBalancerProperties(serviceId).getHint());
RequestData requestData = new RequestData(clientRequest);
DefaultRequest lbRequest = new DefaultRequest<>(
new RetryableRequestContext(null, requestData, hint));
@@ -140,7 +164,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 = getLoadBalancerProperties(serviceId)
+ .getStickySession();
ClientRequest newRequest = buildClientRequest(clientRequest, instance,
stickySessionProperties.getInstanceIdCookieName(),
stickySessionProperties.isAddServiceInstanceCookie(), transformers);
@@ -154,7 +179,7 @@ public Mono filter(ClientRequest clientRequest, ExchangeFunction
lbRequest, lbResponse, new ResponseData(clientResponse, requestData)))))
.map(clientResponse -> {
loadBalancerRetryContext.setClientResponse(clientResponse);
- if (shouldRetrySameServiceInstance(loadBalancerRetryContext)) {
+ if (shouldRetrySameServiceInstance(serviceId, loadBalancerRetryContext)) {
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("Retrying on status code: %d",
clientResponse.statusCode().value()));
@@ -166,7 +191,7 @@ lbRequest, lbResponse, new ResponseData(clientResponse, requestData)))))
});
}).map(clientResponse -> {
loadBalancerRetryContext.setClientResponse(clientResponse);
- if (shouldRetryNextServiceInstance(loadBalancerRetryContext)) {
+ if (shouldRetryNextServiceInstance(serviceId, loadBalancerRetryContext)) {
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("Retrying on status code: %d", clientResponse.statusCode().value()));
}
@@ -177,8 +202,9 @@ 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(String serviceId, int max, boolean transientErrors) {
+ LoadBalancerProperties.Retry.Backoff backoffProperties = getLoadBalancerProperties(serviceId).getRetry()
+ .getBackoff();
if (backoffProperties.isEnabled()) {
return RetrySpec.backoff(max, backoffProperties.getMinBackoff()).filter(this::isRetryException)
.maxBackoff(backoffProperties.getMaxBackoff()).jitter(backoffProperties.getJitter())
@@ -187,20 +213,24 @@ private Retry buildRetrySpec(int max, boolean transientErrors) {
return RetrySpec.max(max).filter(this::isRetryException).transientErrors(transientErrors);
}
- private boolean shouldRetrySameServiceInstance(LoadBalancerRetryContext loadBalancerRetryContext) {
- boolean shouldRetry = retryPolicy.retryableStatusCode(loadBalancerRetryContext.getResponseStatusCode())
- && retryPolicy.canRetryOnMethod(loadBalancerRetryContext.getRequestMethod())
- && retryPolicy.canRetrySameServiceInstance(loadBalancerRetryContext);
+ private boolean shouldRetrySameServiceInstance(String serviceId,
+ LoadBalancerRetryContext loadBalancerRetryContext) {
+ boolean shouldRetry = retryPolicy.retryableStatusCode(serviceId,
+ loadBalancerRetryContext.getResponseStatusCode())
+ && retryPolicy.canRetryOnMethod(serviceId, loadBalancerRetryContext.getRequestMethod())
+ && retryPolicy.canRetrySameServiceInstance(serviceId, loadBalancerRetryContext);
if (shouldRetry) {
loadBalancerRetryContext.incrementRetriesSameServiceInstance();
}
return shouldRetry;
}
- private boolean shouldRetryNextServiceInstance(LoadBalancerRetryContext loadBalancerRetryContext) {
- boolean shouldRetry = retryPolicy.retryableStatusCode(loadBalancerRetryContext.getResponseStatusCode())
- && retryPolicy.canRetryOnMethod(loadBalancerRetryContext.getRequestMethod())
- && retryPolicy.canRetryNextServiceInstance(loadBalancerRetryContext);
+ private boolean shouldRetryNextServiceInstance(String serviceId,
+ LoadBalancerRetryContext loadBalancerRetryContext) {
+ boolean shouldRetry = retryPolicy.retryableStatusCode(serviceId,
+ loadBalancerRetryContext.getResponseStatusCode())
+ && retryPolicy.canRetryOnMethod(serviceId, loadBalancerRetryContext.getRequestMethod())
+ && retryPolicy.canRetryNextServiceInstance(serviceId, loadBalancerRetryContext);
if (shouldRetry) {
loadBalancerRetryContext.incrementRetriesNextServiceInstance();
loadBalancerRetryContext.resetRetriesSameServiceInstance();
@@ -223,4 +253,14 @@ protected Mono> choose(String serviceId, Request lbFactory;
+
+ @Before
+ public void setUp() {
+ client = mock(LoadBalancerClient.class);
+ lbRequestFactory = mock(LoadBalancerRequestFactory.class);
+ serviceProperties = new LoadBalancerClientProperties();
+ serviceProperties.getClient().put("foo", new LoadBalancerProperties());
+ propertiesFactory = new LoadBalancerPropertiesFactory(new LoadBalancerProperties(), serviceProperties, true);
+ lbFactory = mock(ReactiveLoadBalancer.Factory.class);
+ }
+
+ @After
+ public void tearDown() {
+ client = null;
+ }
+
+ @Test(expected = IOException.class)
+ public void interceptDisableRetry() throws Throwable {
+ HttpRequest request = mock(HttpRequest.class);
+ when(request.getURI()).thenReturn(new URI("http://foo"));
+ ServiceInstance serviceInstance = mock(ServiceInstance.class);
+ when(client.choose(eq("foo"), any())).thenReturn(serviceInstance);
+ when(client.execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class)))
+ .thenThrow(new IOException());
+ serviceProperties.getClient().get("foo").getRetry().setEnabled(false);
+ RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client,
+ new LoadBalancerProperties(), lbRequestFactory, loadBalancedRetryFactory, lbFactory, propertiesFactory);
+ byte[] body = new byte[] {};
+ ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
+
+ when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
+
+ interceptor.intercept(request, body, execution);
+ verify(lbRequestFactory).createRequest(request, body, execution);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void interceptInvalidHost() throws Throwable {
+ HttpRequest request = mock(HttpRequest.class);
+ when(request.getURI()).thenReturn(new URI("http://foo_underscore"));
+ serviceProperties.getClient().get("foo").getRetry().setEnabled(true);
+ RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client,
+ new LoadBalancerProperties(), lbRequestFactory, loadBalancedRetryFactory, lbFactory, propertiesFactory);
+ byte[] body = new byte[] {};
+ ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
+ interceptor.intercept(request, body, execution);
+ }
+
+ @Test
+ public void interceptNeverRetry() throws Throwable {
+ HttpRequest request = mock(HttpRequest.class);
+ when(request.getURI()).thenReturn(new URI("http://foo"));
+ ClientHttpResponse clientHttpResponse = new MockClientHttpResponse(new byte[] {}, HttpStatus.OK);
+ ServiceInstance serviceInstance = mock(ServiceInstance.class);
+ when(client.choose(eq("foo"), any())).thenReturn(serviceInstance);
+ when(client.execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class)))
+ .thenReturn(clientHttpResponse);
+ when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
+ serviceProperties.getClient().get("foo").getRetry().setEnabled(true);
+ RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client,
+ new LoadBalancerProperties(), lbRequestFactory, loadBalancedRetryFactory, lbFactory, propertiesFactory);
+ byte[] body = new byte[] {};
+ ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
+ interceptor.intercept(request, body, execution);
+ verify(lbRequestFactory).createRequest(request, body, execution);
+ }
+
+ @Test
+ public void interceptSuccess() throws Throwable {
+ HttpRequest request = mock(HttpRequest.class);
+ when(request.getURI()).thenReturn(new URI("http://foo"));
+ ClientHttpResponse clientHttpResponse = new MockClientHttpResponse(new byte[] {}, HttpStatus.OK);
+ LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
+ ServiceInstance serviceInstance = mock(ServiceInstance.class);
+ when(client.choose(eq("foo"), any())).thenReturn(serviceInstance);
+ when(client.execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class)))
+ .thenReturn(clientHttpResponse);
+ when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
+ serviceProperties.getClient().get("foo").getRetry().setEnabled(true);
+ RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client,
+ new LoadBalancerProperties(), lbRequestFactory, new MyLoadBalancedRetryFactory(policy), lbFactory,
+ propertiesFactory);
+ byte[] body = new byte[] {};
+ ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
+ ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
+ then(rsp).isEqualTo(clientHttpResponse);
+ verify(lbRequestFactory).createRequest(request, body, execution);
+ }
+
+ @Test
+ public void interceptRetryOnStatusCode() throws Throwable {
+ HttpRequest request = mock(HttpRequest.class);
+ when(request.getURI()).thenReturn(new URI("http://foo"));
+ InputStream notFoundStream = mock(InputStream.class);
+ when(notFoundStream.read(any(byte[].class))).thenReturn(-1);
+ ClientHttpResponse clientHttpResponseNotFound = new MockClientHttpResponse(notFoundStream,
+ HttpStatus.NOT_FOUND);
+ ClientHttpResponse clientHttpResponseOk = new MockClientHttpResponse(new byte[] {}, HttpStatus.OK);
+ LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
+ when(policy.retryableStatusCode(eq(HttpStatus.NOT_FOUND.value()))).thenReturn(true);
+ when(policy.canRetryNextServer(any(LoadBalancedRetryContext.class))).thenReturn(true);
+ ServiceInstance serviceInstance = mock(ServiceInstance.class);
+ when(client.choose(eq("foo"), any())).thenReturn(serviceInstance);
+ when(client.execute(eq("foo"), eq(serviceInstance), nullable(LoadBalancerRequest.class)))
+ .thenReturn(clientHttpResponseNotFound).thenReturn(clientHttpResponseOk);
+ serviceProperties.getClient().get("foo").getRetry().setEnabled(true);
+ RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client,
+ new LoadBalancerProperties(), lbRequestFactory, new MyLoadBalancedRetryFactory(policy), lbFactory,
+ propertiesFactory);
+ byte[] body = new byte[] {};
+ ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
+ ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
+ verify(client, times(2)).execute(eq("foo"), eq(serviceInstance), nullable(LoadBalancerRequest.class));
+ verify(notFoundStream, times(1)).close();
+ then(rsp).isEqualTo(clientHttpResponseOk);
+ verify(lbRequestFactory, times(2)).createRequest(request, body, execution);
+ }
+
+ @Test
+ public void interceptRetryFailOnStatusCode() throws Throwable {
+ HttpRequest request = mock(HttpRequest.class);
+ when(request.getURI()).thenReturn(new URI("http://foo"));
+
+ InputStream notFoundStream = new ByteArrayInputStream("foo".getBytes());
+ ClientHttpResponse clientHttpResponseNotFound = new MockClientHttpResponse(notFoundStream,
+ HttpStatus.NOT_FOUND);
+
+ LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
+ when(policy.retryableStatusCode(eq(HttpStatus.NOT_FOUND.value()))).thenReturn(true);
+ when(policy.canRetryNextServer(any(LoadBalancedRetryContext.class))).thenReturn(false);
+
+ ServiceInstance serviceInstance = mock(ServiceInstance.class);
+ when(client.choose(eq("foo"), any())).thenReturn(serviceInstance);
+ when(client.execute(eq("foo"), eq(serviceInstance),
+ ArgumentMatchers.>any()))
+ .thenReturn(clientHttpResponseNotFound);
+
+ serviceProperties.getClient().get("foo").getRetry().setEnabled(true);
+ byte[] body = new byte[] {};
+ ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
+ RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client,
+ new LoadBalancerProperties(), lbRequestFactory, new MyLoadBalancedRetryFactory(policy), lbFactory,
+ propertiesFactory);
+ ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
+
+ verify(client, times(1)).execute(eq("foo"), eq(serviceInstance),
+ ArgumentMatchers.>any());
+ verify(lbRequestFactory, times(1)).createRequest(request, body, execution);
+ verify(policy, times(2)).canRetryNextServer(any(LoadBalancedRetryContext.class));
+
+ // call twice in a retry attempt
+ byte[] content = new byte[1024];
+ int length = rsp.getBody().read(content);
+ then(length).isEqualTo("foo".getBytes().length);
+ then(new String(content, 0, length)).isEqualTo("foo");
+ }
+
+ @Test
+ public void interceptRetry() throws Throwable {
+ HttpRequest request = mock(HttpRequest.class);
+ when(request.getURI()).thenReturn(new URI("http://foo"));
+ ClientHttpResponse clientHttpResponse = new MockClientHttpResponse(new byte[] {}, HttpStatus.OK);
+ LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
+ when(policy.canRetryNextServer(any(LoadBalancedRetryContext.class))).thenReturn(true);
+ MyBackOffPolicy backOffPolicy = new MyBackOffPolicy();
+ ServiceInstance serviceInstance = mock(ServiceInstance.class);
+ when(client.choose(eq("foo"), any())).thenReturn(serviceInstance);
+ when(client.execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class)))
+ .thenThrow(new IOException()).thenReturn(clientHttpResponse);
+ when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
+ serviceProperties.getClient().get("foo").getRetry().setEnabled(true);
+ RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client,
+ new LoadBalancerProperties(), lbRequestFactory, new MyLoadBalancedRetryFactory(policy, backOffPolicy),
+ lbFactory, propertiesFactory);
+ byte[] body = new byte[] {};
+ ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
+ ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
+ verify(client, times(2)).execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class));
+ then(rsp).isEqualTo(clientHttpResponse);
+ verify(lbRequestFactory, times(2)).createRequest(request, body, execution);
+ then(backOffPolicy.getBackoffAttempts()).isEqualTo(1);
+ }
+
+ @Test(expected = IOException.class)
+ public void interceptFailedRetry() throws Exception {
+ HttpRequest request = mock(HttpRequest.class);
+ when(request.getURI()).thenReturn(new URI("http://foo"));
+ ClientHttpResponse clientHttpResponse = new MockClientHttpResponse(new byte[] {}, HttpStatus.OK);
+ LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
+ when(policy.canRetryNextServer(any(LoadBalancedRetryContext.class))).thenReturn(false);
+ ServiceInstance serviceInstance = mock(ServiceInstance.class);
+ when(client.choose(eq("foo"), any())).thenReturn(serviceInstance);
+ when(client.execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class)))
+ .thenThrow(new IOException()).thenReturn(clientHttpResponse);
+ when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
+ serviceProperties.getClient().get("foo").getRetry().setEnabled(true);
+ RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client,
+ new LoadBalancerProperties(), lbRequestFactory, new MyLoadBalancedRetryFactory(policy), lbFactory,
+ propertiesFactory);
+ byte[] body = new byte[] {};
+ ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
+ interceptor.intercept(request, body, execution);
+ verify(lbRequestFactory).createRequest(request, body, execution);
+ }
+
+ private static ServiceInstance defaultServiceInstance() {
+ return new DefaultServiceInstance("testInstance", "test", "testHost", 80, false);
+ }
+
+ @Test
+ public void retryListenerTest() throws Throwable {
+ HttpRequest request = mock(HttpRequest.class);
+ when(request.getURI()).thenReturn(new URI("http://listener"));
+ ClientHttpResponse clientHttpResponse = new MockClientHttpResponse(new byte[] {}, HttpStatus.OK);
+ LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
+ when(policy.canRetryNextServer(any(LoadBalancedRetryContext.class))).thenReturn(true);
+ MyBackOffPolicy backOffPolicy = new MyBackOffPolicy();
+ ServiceInstance serviceInstance = mock(ServiceInstance.class);
+ when(client.choose(eq("listener"), any())).thenReturn(serviceInstance);
+ when(client.execute(eq("listener"), eq(serviceInstance), any(LoadBalancerRequest.class)))
+ .thenThrow(new IOException()).thenReturn(clientHttpResponse);
+ serviceProperties.getClient().get("foo").getRetry().setEnabled(true);
+ MyRetryListener retryListener = new MyRetryListener();
+ when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
+ RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client,
+ new LoadBalancerProperties(), lbRequestFactory,
+ new MyLoadBalancedRetryFactory(policy, backOffPolicy, new RetryListener[] { retryListener }), lbFactory,
+ propertiesFactory);
+ byte[] body = new byte[] {};
+ ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
+ ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
+ verify(client, times(2)).execute(eq("listener"), eq(serviceInstance), any(LoadBalancerRequest.class));
+ then(rsp).isEqualTo(clientHttpResponse);
+ verify(lbRequestFactory, times(2)).createRequest(request, body, execution);
+ then(backOffPolicy.getBackoffAttempts()).isEqualTo(1);
+ then(retryListener.getOnError()).isEqualTo(1);
+ }
+
+ @Test
+ public void retryWithDefaultConstructorTest() throws Throwable {
+ HttpRequest request = mock(HttpRequest.class);
+ when(request.getURI()).thenReturn(new URI("http://default"));
+ ClientHttpResponse clientHttpResponse = new MockClientHttpResponse(new byte[] {}, HttpStatus.OK);
+ LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
+ when(policy.canRetryNextServer(any(LoadBalancedRetryContext.class))).thenReturn(true);
+ MyBackOffPolicy backOffPolicy = new MyBackOffPolicy();
+ ServiceInstance serviceInstance = mock(ServiceInstance.class);
+ when(client.choose(eq("default"), any())).thenReturn(serviceInstance);
+ when(client.execute(eq("default"), eq(serviceInstance), any(LoadBalancerRequest.class)))
+ .thenThrow(new IOException()).thenReturn(clientHttpResponse);
+ serviceProperties.getClient().get("foo").getRetry().setEnabled(true);
+ when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
+ RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client,
+ new LoadBalancerProperties(), lbRequestFactory, new MyLoadBalancedRetryFactory(policy, backOffPolicy),
+ lbFactory, propertiesFactory);
+ byte[] body = new byte[] {};
+ ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
+ ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
+ verify(client, times(2)).execute(eq("default"), eq(serviceInstance), any(LoadBalancerRequest.class));
+ then(rsp).isEqualTo(clientHttpResponse);
+ verify(lbRequestFactory, times(2)).createRequest(request, body, execution);
+ then(backOffPolicy.getBackoffAttempts()).isEqualTo(1);
+ }
+
+ @Test(expected = TerminatedRetryException.class)
+ public void retryListenerTestNoRetry() throws Throwable {
+ HttpRequest request = mock(HttpRequest.class);
+ when(request.getURI()).thenReturn(new URI("http://noRetry"));
+ LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
+ MyBackOffPolicy backOffPolicy = new MyBackOffPolicy();
+ serviceProperties.getClient().put("noRetry", new LoadBalancerProperties());
+ serviceProperties.getClient().get("noRetry").getRetry().setEnabled(true);
+ RetryListener myRetryListener = new RetryListenerSupport() {
+ @Override
+ public boolean open(RetryContext context, RetryCallback callback) {
+ return false;
+ }
+ };
+ RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client,
+ new LoadBalancerProperties(), lbRequestFactory,
+ new MyLoadBalancedRetryFactory(policy, backOffPolicy, new RetryListener[] { myRetryListener }),
+ lbFactory, propertiesFactory);
+ ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
+ interceptor.intercept(request, new byte[] {}, execution);
+ }
+
+ @Test
+ public void shouldNotDuplicateLifecycleCalls() throws IOException, URISyntaxException {
+ Map lifecycleProcessors = new HashMap<>();
+ lifecycleProcessors.put("testLifecycle", new TestLoadBalancerLifecycle());
+ lifecycleProcessors.put("anotherLifecycle", new AnotherLoadBalancerLifecycle());
+ when(lbFactory.getInstances("test", LoadBalancerLifecycle.class)).thenReturn(lifecycleProcessors);
+ HttpRequest request = mock(HttpRequest.class);
+ when(request.getURI()).thenReturn(new URI("http://test"));
+ TestLoadBalancerClient client = new TestLoadBalancerClient();
+ RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client,
+ new LoadBalancerProperties(), lbRequestFactory, loadBalancedRetryFactory, lbFactory, propertiesFactory);
+
+ interceptor.intercept(request, new byte[] {}, mock(ClientHttpRequestExecution.class));
+
+ assertThat(((TestLoadBalancerLifecycle) lifecycleProcessors.get("testLifecycle")).getStartLog()).hasSize(1);
+ assertThat(((TestLoadBalancerLifecycle) lifecycleProcessors.get("testLifecycle")).getStartRequestLog())
+ .hasSize(0);
+ assertThat(((TestLoadBalancerLifecycle) lifecycleProcessors.get("testLifecycle")).getCompleteLog()).hasSize(0);
+ assertThat(((TestLoadBalancerLifecycle) lifecycleProcessors.get("anotherLifecycle")).getStartLog()).hasSize(1);
+ assertThat(((TestLoadBalancerLifecycle) lifecycleProcessors.get("anotherLifecycle")).getStartRequestLog())
+ .hasSize(0);
+ assertThat(((TestLoadBalancerLifecycle) lifecycleProcessors.get("anotherLifecycle")).getCompleteLog())
+ .hasSize(0);
+ assertThat(((TestLoadBalancerLifecycle) client.getLifecycleProcessors().get("testLifecycle")).getStartLog())
+ .hasSize(0);
+ assertThat(
+ ((TestLoadBalancerLifecycle) client.getLifecycleProcessors().get("testLifecycle")).getStartRequestLog())
+ .hasSize(1);
+ assertThat(((TestLoadBalancerLifecycle) client.getLifecycleProcessors().get("testLifecycle")).getCompleteLog())
+ .hasSize(1);
+ assertThat(((TestLoadBalancerLifecycle) client.getLifecycleProcessors().get("anotherLifecycle")).getStartLog())
+ .hasSize(0);
+ assertThat(
+ ((TestLoadBalancerLifecycle) client.getLifecycleProcessors().get("testLifecycle")).getStartRequestLog())
+ .hasSize(1);
+ assertThat(
+ ((TestLoadBalancerLifecycle) client.getLifecycleProcessors().get("anotherLifecycle")).getCompleteLog())
+ .hasSize(1);
+ }
+
+ static class MyLoadBalancedRetryFactory implements LoadBalancedRetryFactory {
+
+ private final LoadBalancedRetryPolicy loadBalancedRetryPolicy;
+
+ private BackOffPolicy backOffPolicy;
+
+ private RetryListener[] retryListeners;
+
+ MyLoadBalancedRetryFactory(LoadBalancedRetryPolicy loadBalancedRetryPolicy) {
+ this.loadBalancedRetryPolicy = loadBalancedRetryPolicy;
+ }
+
+ MyLoadBalancedRetryFactory(LoadBalancedRetryPolicy loadBalancedRetryPolicy, BackOffPolicy backOffPolicy) {
+ this(loadBalancedRetryPolicy);
+ this.backOffPolicy = backOffPolicy;
+ }
+
+ MyLoadBalancedRetryFactory(LoadBalancedRetryPolicy loadBalancedRetryPolicy, BackOffPolicy backOffPolicy,
+ RetryListener[] retryListeners) {
+ this(loadBalancedRetryPolicy, backOffPolicy);
+ this.retryListeners = retryListeners;
+ }
+
+ @Override
+ public LoadBalancedRetryPolicy createRetryPolicy(String service,
+ ServiceInstanceChooser serviceInstanceChooser) {
+ return loadBalancedRetryPolicy;
+ }
+
+ @Override
+ public BackOffPolicy createBackOffPolicy(String service) {
+ if (backOffPolicy == null) {
+ return new NoBackOffPolicy();
+ }
+ else {
+ return backOffPolicy;
+ }
+ }
+
+ @Override
+ public RetryListener[] createRetryListeners(String service) {
+ if (retryListeners == null) {
+ return new RetryListener[0];
+ }
+ else {
+ return retryListeners;
+ }
+ }
+
+ }
+
+ static class MyBackOffPolicy implements BackOffPolicy {
+
+ private int backoffAttempts = 0;
+
+ @Override
+ public BackOffContext start(RetryContext retryContext) {
+ return new BackOffContext() {
+ @Override
+ protected Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
+ };
+ }
+
+ @Override
+ public void backOff(BackOffContext backOffContext) throws BackOffInterruptedException {
+ backoffAttempts++;
+ }
+
+ int getBackoffAttempts() {
+ return backoffAttempts;
+ }
+
+ }
+
+ static class MyRetryListener extends RetryListenerSupport {
+
+ private int onError = 0;
+
+ @Override
+ public void onError(RetryContext retryContext, RetryCallback retryCallback,
+ Throwable throwable) {
+ onError++;
+ }
+
+ int getOnError() {
+ return onError;
+ }
+
+ }
+
+ protected static class TestLoadBalancerClient implements LoadBalancerClient {
+
+ private final Map lifecycleProcessors = new HashMap<>();
+
+ TestLoadBalancerClient() {
+ lifecycleProcessors.put("testLifecycle", new TestLoadBalancerLifecycle());
+ lifecycleProcessors.put("anotherLifecycle", new AnotherLoadBalancerLifecycle());
+ }
+
+ @Override
+ public T execute(String serviceId, LoadBalancerRequest request) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest request) {
+ Set supportedLoadBalancerProcessors = LoadBalancerLifecycleValidator
+ .getSupportedLifecycleProcessors(lifecycleProcessors, DefaultRequestContext.class, Object.class,
+ ServiceInstance.class);
+ supportedLoadBalancerProcessors.forEach(lifecycle -> lifecycle.onStartRequest(new DefaultRequest<>(),
+ new DefaultResponse(serviceInstance)));
+ T response = (T) new MockClientHttpResponse(new byte[] {}, HttpStatus.OK);
+ supportedLoadBalancerProcessors
+ .forEach(lifecycle -> lifecycle.onComplete(new CompletionContext(CompletionContext.Status.SUCCESS,
+ new DefaultRequest<>(), new DefaultResponse(defaultServiceInstance()))));
+ return response;
+ }
+
+ @Override
+ public URI reconstructURI(ServiceInstance instance, URI original) {
+ throw new UnsupportedOperationException("Please, implement me.");
+ }
+
+ @Override
+ public ServiceInstance choose(String serviceId) {
+ return defaultServiceInstance();
+ }
+
+ @Override
+ public ServiceInstance choose(String serviceId, Request request) {
+ return defaultServiceInstance();
+ }
+
+ Map getLifecycleProcessors() {
+ return lifecycleProcessors;
+ }
+
+ }
+
+ protected static class TestLoadBalancerLifecycle implements LoadBalancerLifecycle