Compare commits

...

2 Commits

Author SHA1 Message Date
Olga Maciaszek-Sharma ca813ec0d3 Add documentation. Reformat. 3 years ago
Olga Maciaszek-Sharma a8cde98d99 Implement logic for retrying on custom exceptions. Add tests. 3 years ago
  1. 6
      docs/src/main/asciidoc/spring-cloud-commons.adoc
  2. 2
      spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/ClientHttpResponseStatusCodeException.java
  3. 3
      spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/InterceptorRetryPolicy.java
  4. 8
      spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancedRetryPolicy.java
  5. 34
      spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerProperties.java
  6. 3
      spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerInterceptor.java
  7. 7
      spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerRetryPolicy.java
  8. 10
      spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableExchangeFilterFunctionLoadBalancerRetryPolicy.java
  9. 33
      spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunction.java
  10. 2
      spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableStatusCodeException.java
  11. 25
      spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/InterceptorRetryPolicyTest.java
  12. 8
      spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerInterceptorTests.java
  13. 1
      spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunctionIntegrationTests.java
  14. 58
      spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunctionTests.java
  15. 12
      spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/blocking/retry/BlockingLoadBalancedRetryPolicy.java

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

@ -464,10 +464,14 @@ You can set: @@ -464,10 +464,14 @@ You can set:
For the reactive implementation, you can additionally set:
- `spring.cloud.loadbalancer.retry.backoff.minBackoff` - Sets the minimum backoff duration (by default, 5 milliseconds)
- `spring.cloud.loadbalancer.retry.backoff.maxBackoff` - Sets the maximum backoff duration (by default, max long value of milliseconds)
- `spring.cloud.loadbalancer.retry.backoff.jitter` - Sets the jitter used for calculationg the actual backoff duration for each call (by default, 0.5).
- `spring.cloud.loadbalancer.retry.backoff.jitter` - Sets the jitter used for calculating the actual backoff duration for each call (by default, 0.5).
For the reactive implementation, you can also implement your own `LoadBalancerRetryPolicy` to have more detailed control over the load-balanced call retries.
For both implementations, you can also set the exceptions that will trigger the replies by adding a list of values under the `spring.cloud.loadbalancer.[serviceId].retry.retryable-exceptions` property. If you do, we'll make sure to add `RetryableStatusCodeExceptions` to the list of exceptions provided by you, so that we also retry on retryable status codes. If you do not specify any exceptions via properties, the exceptions we'll use by default are `IOException`, `TimeoutException` and `RetryableStatusCodeException`. You can also enable retrying on all exceptions by setting `spring.cloud.loadbalancer.[serviceId].retry.retry-on-all-exceptions` to `true`.
WARNING: If you are using the blocking implementation with Spring Retries, if you want to keep the behaviour from previous releases, make sure to set `spring.cloud.loadbalancer.[serviceId].retry.retry-on-all-exceptions` to `true` as that used to be the default mode for the blocking implementation.
NOTE: Individual Loadbalancer clients may be configured individually with the same properties as above except the prefix is `spring.cloud.loadbalancer.clients.<clientId>.*` 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`.

2
spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/ClientHttpResponseStatusCodeException.java

@ -79,7 +79,7 @@ public class ClientHttpResponseStatusCodeException extends RetryableStatusCodeEx @@ -79,7 +79,7 @@ public class ClientHttpResponseStatusCodeException extends RetryableStatusCodeEx
}
@Override
public InputStream getBody() throws IOException {
public InputStream getBody() {
return new ByteArrayInputStream(this.body);
}

3
spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/InterceptorRetryPolicy.java

@ -54,6 +54,9 @@ public class InterceptorRetryPolicy implements RetryPolicy { @@ -54,6 +54,9 @@ public class InterceptorRetryPolicy implements RetryPolicy {
@Override
public boolean canRetry(RetryContext context) {
if (!policy.retryableException(context.getLastThrowable())) {
return false;
}
LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext) context;
if (lbContext.getRetryCount() == 0 && lbContext.getServiceInstance() == null) {
// We haven't even tried to make the request yet so return true so we do

8
spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancedRetryPolicy.java

@ -20,6 +20,7 @@ package org.springframework.cloud.client.loadbalancer; @@ -20,6 +20,7 @@ package org.springframework.cloud.client.loadbalancer;
* Retry logic to use for the {@link LoadBalancerClient}.
*
* @author Ryan Baxter
* @author Olga Maciaszek-Sharma
*/
public interface LoadBalancedRetryPolicy {
@ -65,4 +66,11 @@ public interface LoadBalancedRetryPolicy { @@ -65,4 +66,11 @@ public interface LoadBalancedRetryPolicy {
*/
boolean retryableStatusCode(int statusCode);
/**
* Return <code>true</code> to retry if the provided exception is thrown.
* @param exception the {@link Throwable} to evaluate
* @return true to retry on the provided exception
*/
boolean retryableException(Throwable exception);
}

34
spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerProperties.java

@ -16,10 +16,13 @@ @@ -16,10 +16,13 @@
package org.springframework.cloud.client.loadbalancer;
import java.io.IOException;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import reactor.util.retry.RetryBackoffSpec;
@ -282,6 +285,12 @@ public class LoadBalancerProperties { @@ -282,6 +285,12 @@ public class LoadBalancerProperties {
*/
private boolean retryOnAllOperations = false;
/**
* Indicates retries should be attempted for all exceptions, not only those
* specified in {@code retryableExceptions}.
*/
private boolean retryOnAllExceptions = false;
/**
* Number of retries to be executed on the same <code>ServiceInstance</code>.
*/
@ -298,6 +307,13 @@ public class LoadBalancerProperties { @@ -298,6 +307,13 @@ public class LoadBalancerProperties {
*/
private Set<Integer> retryableStatusCodes = new HashSet<>();
/**
* A {@link Set} of {@link Throwable} classes that should trigger a retry.
*/
private Set<Class<? extends Throwable>> retryableExceptions = new HashSet<>(
Arrays.asList(IOException.class, TimeoutException.class, RetryableStatusCodeException.class,
org.springframework.cloud.client.loadbalancer.reactive.RetryableStatusCodeException.class));
/**
* Properties for Reactor Retry backoffs in Spring Cloud LoadBalancer.
*/
@ -352,6 +368,16 @@ public class LoadBalancerProperties { @@ -352,6 +368,16 @@ public class LoadBalancerProperties {
this.retryableStatusCodes = retryableStatusCodes;
}
public Set<Class<? extends Throwable>> getRetryableExceptions() {
return retryableExceptions;
}
public void setRetryableExceptions(Set<Class<? extends Throwable>> retryableExceptions) {
retryableExceptions
.add(org.springframework.cloud.client.loadbalancer.reactive.RetryableStatusCodeException.class);
this.retryableExceptions = retryableExceptions;
}
public Backoff getBackoff() {
return backoff;
}
@ -360,6 +386,14 @@ public class LoadBalancerProperties { @@ -360,6 +386,14 @@ public class LoadBalancerProperties {
this.backoff = backoff;
}
public boolean isRetryOnAllExceptions() {
return retryOnAllExceptions;
}
public void setRetryOnAllExceptions(boolean retryOnAllExceptions) {
this.retryOnAllExceptions = retryOnAllExceptions;
}
public static class Backoff {
/**

3
spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerInterceptor.java

@ -138,8 +138,7 @@ public class RetryLoadBalancerInterceptor implements ClientHttpRequestIntercepto @@ -138,8 +138,7 @@ public class RetryLoadBalancerInterceptor implements ClientHttpRequestIntercepto
requestFactory.createRequest(request, body, execution),
new RetryableRequestContext(null, new RequestData(request), hint));
ServiceInstance finalServiceInstance = serviceInstance;
ClientHttpResponse response = RetryLoadBalancerInterceptor.this.loadBalancer.execute(serviceName,
finalServiceInstance, lbRequest);
ClientHttpResponse response = loadBalancer.execute(serviceName, finalServiceInstance, lbRequest);
int statusCode = response.getRawStatusCode();
if (retryPolicy != null && retryPolicy.retryableStatusCode(statusCode)) {
if (LOG.isDebugEnabled()) {

7
spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerRetryPolicy.java

@ -49,6 +49,13 @@ public interface LoadBalancerRetryPolicy { @@ -49,6 +49,13 @@ public interface LoadBalancerRetryPolicy {
*/
boolean retryableStatusCode(int statusCode);
/**
* Return <code>true</code> to retry if the provided exception is thrown.
* @param exception the {@link Throwable} to evaluate
* @return true to retry on the provided exception
*/
boolean retryableException(Throwable exception);
/**
* Return <code>true</code> to retry on the provided HTTP method.
* @param method the HTTP request method

10
spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableExchangeFilterFunctionLoadBalancerRetryPolicy.java

@ -49,6 +49,16 @@ public class RetryableExchangeFilterFunctionLoadBalancerRetryPolicy implements L @@ -49,6 +49,16 @@ public class RetryableExchangeFilterFunctionLoadBalancerRetryPolicy implements L
return properties.getRetry().getRetryableStatusCodes().contains(statusCode);
}
@Override
public boolean retryableException(Throwable throwable) {
if (properties.getRetry().isRetryOnAllExceptions()) {
return true;
}
return properties.getRetry().getRetryableExceptions().stream()
.anyMatch(exception -> exception.isInstance(throwable)
|| throwable != null && exception.isInstance(throwable.getCause()));
}
@Override
public boolean canRetryOnMethod(HttpMethod method) {
return HttpMethod.GET.equals(method) || properties.getRetry().isRetryOnAllOperations();

33
spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunction.java

@ -16,13 +16,10 @@ @@ -16,13 +16,10 @@
package org.springframework.cloud.client.loadbalancer.reactive;
import java.io.IOException;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@ -66,9 +63,6 @@ public class RetryableLoadBalancerExchangeFilterFunction implements LoadBalanced @@ -66,9 +63,6 @@ public class RetryableLoadBalancerExchangeFilterFunction implements LoadBalanced
private static final Log LOG = LogFactory.getLog(RetryableLoadBalancerExchangeFilterFunction.class);
private static final List<Class<? extends Throwable>> exceptions = Arrays.asList(IOException.class,
TimeoutException.class, RetryableStatusCodeException.class);
private final LoadBalancerRetryPolicy.Factory retryPolicyFactory;
private final ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory;
@ -121,11 +115,11 @@ public class RetryableLoadBalancerExchangeFilterFunction implements LoadBalanced @@ -121,11 +115,11 @@ public class RetryableLoadBalancerExchangeFilterFunction implements LoadBalanced
LoadBalancerRetryContext loadBalancerRetryContext = new LoadBalancerRetryContext(clientRequest);
LoadBalancerProperties properties = loadBalancerFactory.getProperties(serviceId);
LoadBalancerRetryPolicy retryPolicy = retryPolicyFactory.apply(serviceId);
Retry exchangeRetry = buildRetrySpec(properties.getRetry().getMaxRetriesOnSameServiceInstance(), true,
properties.getRetry());
properties.getRetry(), retryPolicy);
Retry filterRetry = buildRetrySpec(properties.getRetry().getMaxRetriesOnNextServiceInstance(), false,
properties.getRetry());
LoadBalancerRetryPolicy retryPolicy = retryPolicyFactory.apply(serviceId);
properties.getRetry(), retryPolicy);
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator
.getSupportedLifecycleProcessors(
@ -192,17 +186,21 @@ public class RetryableLoadBalancerExchangeFilterFunction implements LoadBalanced @@ -192,17 +186,21 @@ public class RetryableLoadBalancerExchangeFilterFunction implements LoadBalanced
}).retryWhen(exchangeRetry)).retryWhen(filterRetry);
}
private Retry buildRetrySpec(int max, boolean transientErrors, LoadBalancerProperties.Retry retry) {
private Retry buildRetrySpec(int max, boolean transientErrors, LoadBalancerProperties.Retry retry,
LoadBalancerRetryPolicy retryPolicy) {
if (!retry.isEnabled()) {
return Retry.max(0).filter(this::isRetryException).transientErrors(transientErrors);
return Retry.max(0).filter(throwable -> isRetryException(throwable, retryPolicy))
.transientErrors(transientErrors);
}
LoadBalancerProperties.Retry.Backoff backoffProperties = retry.getBackoff();
if (backoffProperties.isEnabled()) {
return RetrySpec.backoff(max, backoffProperties.getMinBackoff()).filter(this::isRetryException)
return RetrySpec.backoff(max, backoffProperties.getMinBackoff())
.filter(throwable -> isRetryException(throwable, retryPolicy))
.maxBackoff(backoffProperties.getMaxBackoff()).jitter(backoffProperties.getJitter())
.transientErrors(transientErrors);
}
return RetrySpec.max(max).filter(this::isRetryException).transientErrors(transientErrors);
return RetrySpec.max(max).filter(throwable -> isRetryException(throwable, retryPolicy))
.transientErrors(transientErrors);
}
private boolean shouldRetrySameServiceInstance(LoadBalancerRetryPolicy retryPolicy,
@ -228,11 +226,10 @@ public class RetryableLoadBalancerExchangeFilterFunction implements LoadBalanced @@ -228,11 +226,10 @@ public class RetryableLoadBalancerExchangeFilterFunction implements LoadBalanced
return shouldRetry;
}
private boolean isRetryException(Throwable throwable) {
return exceptions.stream()
.anyMatch(exception -> exception.isInstance(throwable)
|| throwable != null && exception.isInstance(throwable.getCause())
|| Exceptions.isRetryExhausted(throwable));
private boolean isRetryException(Throwable throwable, LoadBalancerRetryPolicy retryPolicy) {
return retryPolicy.retryableException(throwable)
|| (throwable != null && retryPolicy.retryableException(throwable.getCause()))
|| Exceptions.isRetryExhausted(throwable);
}
protected Mono<Response<ServiceInstance>> choose(String serviceId, Request<RetryableRequestContext> request) {

2
spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableStatusCodeException.java

@ -23,6 +23,6 @@ package org.springframework.cloud.client.loadbalancer.reactive; @@ -23,6 +23,6 @@ package org.springframework.cloud.client.loadbalancer.reactive;
* @author Olga Maciaszek-Sharma
* @since 3.0.0
*/
class RetryableStatusCodeException extends IllegalStateException {
public class RetryableStatusCodeException extends IllegalStateException {
}

25
spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/InterceptorRetryPolicyTest.java

@ -26,6 +26,7 @@ import org.springframework.http.HttpRequest; @@ -26,6 +26,7 @@ import org.springframework.http.HttpRequest;
import org.springframework.retry.RetryContext;
import static org.assertj.core.api.BDDAssertions.then;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
@ -65,13 +66,14 @@ public class InterceptorRetryPolicyTest { @@ -65,13 +66,14 @@ public class InterceptorRetryPolicyTest {
@Test
public void canRetryBeforeExecution() {
when(policy.retryableException(any())).thenReturn(true);
InterceptorRetryPolicy interceptorRetryPolicy = new InterceptorRetryPolicy(request, policy,
serviceInstanceChooser, serviceName);
LoadBalancedRetryContext context = mock(LoadBalancedRetryContext.class);
when(context.getRetryCount()).thenReturn(0);
then(interceptorRetryPolicy.canRetry(context)).isTrue();
verify(context, times(1)).setServiceInstance(eq(null));
}
@Test
@ -81,15 +83,29 @@ public class InterceptorRetryPolicyTest { @@ -81,15 +83,29 @@ public class InterceptorRetryPolicyTest {
LoadBalancedRetryContext context = mock(LoadBalancedRetryContext.class);
when(context.getRetryCount()).thenReturn(1);
when(policy.canRetryNextServer(eq(context))).thenReturn(true);
when(policy.retryableException(any())).thenReturn(true);
then(interceptorRetryPolicy.canRetry(context)).isTrue();
}
@Test
public void cannotRetryOnException() {
InterceptorRetryPolicy interceptorRetryPolicy = new InterceptorRetryPolicy(request, policy,
serviceInstanceChooser, serviceName);
LoadBalancedRetryContext context = mock(LoadBalancedRetryContext.class);
when(policy.retryableException(any())).thenReturn(false);
then(interceptorRetryPolicy.canRetry(context)).isFalse();
}
@Test
public void cannotRetry() {
when(policy.retryableException(any())).thenReturn(true);
InterceptorRetryPolicy interceptorRetryPolicy = new InterceptorRetryPolicy(request, policy,
serviceInstanceChooser, serviceName);
LoadBalancedRetryContext context = mock(LoadBalancedRetryContext.class);
when(context.getRetryCount()).thenReturn(1);
then(interceptorRetryPolicy.canRetry(context)).isFalse();
}
@ -97,7 +113,9 @@ public class InterceptorRetryPolicyTest { @@ -97,7 +113,9 @@ public class InterceptorRetryPolicyTest {
public void open() {
InterceptorRetryPolicy interceptorRetryPolicy = new InterceptorRetryPolicy(request, policy,
serviceInstanceChooser, serviceName);
RetryContext context = interceptorRetryPolicy.open(null);
then(context).isInstanceOf(LoadBalancedRetryContext.class);
}
@ -106,7 +124,9 @@ public class InterceptorRetryPolicyTest { @@ -106,7 +124,9 @@ public class InterceptorRetryPolicyTest {
InterceptorRetryPolicy interceptorRetryPolicy = new InterceptorRetryPolicy(request, policy,
serviceInstanceChooser, serviceName);
LoadBalancedRetryContext context = mock(LoadBalancedRetryContext.class);
interceptorRetryPolicy.close(context);
verify(policy, times(1)).close(eq(context));
}
@ -116,7 +136,9 @@ public class InterceptorRetryPolicyTest { @@ -116,7 +136,9 @@ public class InterceptorRetryPolicyTest {
serviceInstanceChooser, serviceName);
LoadBalancedRetryContext context = mock(LoadBalancedRetryContext.class);
Throwable thrown = new Exception();
interceptorRetryPolicy.registerThrowable(context, thrown);
verify(context, times(1)).registerThrowable(eq(thrown));
verify(policy, times(1)).registerThrowable(eq(context), eq(thrown));
}
@ -125,6 +147,7 @@ public class InterceptorRetryPolicyTest { @@ -125,6 +147,7 @@ public class InterceptorRetryPolicyTest {
public void equals() {
InterceptorRetryPolicy interceptorRetryPolicy = new InterceptorRetryPolicy(request, policy,
serviceInstanceChooser, serviceName);
then(interceptorRetryPolicy.equals(null)).isFalse();
then(interceptorRetryPolicy.equals(new Object())).isFalse();
then(interceptorRetryPolicy.equals(interceptorRetryPolicy)).isTrue();

8
spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerInterceptorTests.java

@ -89,6 +89,7 @@ public class RetryLoadBalancerInterceptorTests { @@ -89,6 +89,7 @@ public class RetryLoadBalancerInterceptorTests {
client = mock(LoadBalancerClient.class);
lbRequestFactory = mock(LoadBalancerRequestFactory.class);
properties = new LoadBalancerProperties();
properties.getRetry().setRetryOnAllExceptions(true);
lbFactory = mock(ReactiveLoadBalancer.Factory.class, withSettings().lenient());
when(lbFactory.getProperties(any())).thenReturn(properties);
}
@ -159,6 +160,7 @@ public class RetryLoadBalancerInterceptorTests { @@ -159,6 +160,7 @@ public class RetryLoadBalancerInterceptorTests {
when(request.getURI()).thenReturn(new URI("http://foo"));
ClientHttpResponse clientHttpResponse = new MockClientHttpResponse(new byte[] {}, HttpStatus.OK);
LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
when(policy.retryableException(any())).thenReturn(true);
ServiceInstance serviceInstance = mock(ServiceInstance.class);
when(client.choose(eq("foo"), any())).thenReturn(serviceInstance);
when(client.execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class)))
@ -186,6 +188,7 @@ public class RetryLoadBalancerInterceptorTests { @@ -186,6 +188,7 @@ public class RetryLoadBalancerInterceptorTests {
LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
when(policy.retryableStatusCode(eq(HttpStatus.NOT_FOUND.value()))).thenReturn(true);
when(policy.canRetryNextServer(any(LoadBalancedRetryContext.class))).thenReturn(true);
when(policy.retryableException(any())).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)))
@ -214,6 +217,7 @@ public class RetryLoadBalancerInterceptorTests { @@ -214,6 +217,7 @@ public class RetryLoadBalancerInterceptorTests {
LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
when(policy.retryableStatusCode(eq(HttpStatus.NOT_FOUND.value()))).thenReturn(true);
when(policy.canRetryNextServer(any(LoadBalancedRetryContext.class))).thenReturn(false);
when(policy.retryableException(any())).thenReturn(true);
ServiceInstance serviceInstance = mock(ServiceInstance.class);
when(client.choose(eq("foo"), any())).thenReturn(serviceInstance);
@ -247,6 +251,7 @@ public class RetryLoadBalancerInterceptorTests { @@ -247,6 +251,7 @@ public class RetryLoadBalancerInterceptorTests {
ClientHttpResponse clientHttpResponse = new MockClientHttpResponse(new byte[] {}, HttpStatus.OK);
LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
when(policy.canRetryNextServer(any(LoadBalancedRetryContext.class))).thenReturn(true);
when(policy.retryableException(any())).thenReturn(true);
MyBackOffPolicy backOffPolicy = new MyBackOffPolicy();
ServiceInstance serviceInstance = mock(ServiceInstance.class);
when(client.choose(eq("foo"), any())).thenReturn(serviceInstance);
@ -272,6 +277,7 @@ public class RetryLoadBalancerInterceptorTests { @@ -272,6 +277,7 @@ public class RetryLoadBalancerInterceptorTests {
ClientHttpResponse clientHttpResponse = new MockClientHttpResponse(new byte[] {}, HttpStatus.OK);
LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
when(policy.canRetryNextServer(any(LoadBalancedRetryContext.class))).thenReturn(false);
when(policy.retryableException(any())).thenReturn(true);
ServiceInstance serviceInstance = mock(ServiceInstance.class);
when(client.choose(eq("foo"), any())).thenReturn(serviceInstance);
when(client.execute(eq("foo"), eq(serviceInstance), any(LoadBalancerRequest.class)))
@ -299,6 +305,7 @@ public class RetryLoadBalancerInterceptorTests { @@ -299,6 +305,7 @@ public class RetryLoadBalancerInterceptorTests {
ClientHttpResponse clientHttpResponse = new MockClientHttpResponse(new byte[] {}, HttpStatus.OK);
LoadBalancedRetryPolicy policy = mock(LoadBalancedRetryPolicy.class);
when(policy.canRetryNextServer(any(LoadBalancedRetryContext.class))).thenReturn(true);
when(policy.retryableException(any())).thenReturn(true);
MyBackOffPolicy backOffPolicy = new MyBackOffPolicy();
ServiceInstance serviceInstance = mock(ServiceInstance.class);
when(client.choose(eq("listener"), any())).thenReturn(serviceInstance);
@ -334,6 +341,7 @@ public class RetryLoadBalancerInterceptorTests { @@ -334,6 +341,7 @@ public class RetryLoadBalancerInterceptorTests {
.thenThrow(new IOException()).thenReturn(clientHttpResponse);
properties.getRetry().setEnabled(true);
when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
when(policy.retryableException(any())).thenReturn(true);
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
new MyLoadBalancedRetryFactory(policy, backOffPolicy), lbFactory);
byte[] body = new byte[] {};

1
spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunctionIntegrationTests.java

@ -101,7 +101,6 @@ class RetryableLoadBalancerExchangeFilterFunctionIntegrationTests { @@ -101,7 +101,6 @@ class RetryableLoadBalancerExchangeFilterFunctionIntegrationTests {
void loadBalancerLifecycleCallbacksExecuted() {
final String callbackTestHint = "callbackTestHint";
loadBalancerProperties.getHint().put("testservice", "callbackTestHint");
final String result = "callbackTestResult";
ClientResponse clientResponse = WebClient.builder().baseUrl("http://testservice")
.filter(this.loadBalancerFunction).build().get().uri("/callback").exchange().block();

58
spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunctionTests.java

@ -19,6 +19,7 @@ package org.springframework.cloud.client.loadbalancer.reactive; @@ -19,6 +19,7 @@ package org.springframework.cloud.client.loadbalancer.reactive;
import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import java.util.HashSet;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -152,4 +153,61 @@ class RetryableLoadBalancerExchangeFilterFunctionTests { @@ -152,4 +153,61 @@ class RetryableLoadBalancerExchangeFilterFunctionTests {
inOrder.verify(next, times(2)).exchange(any());
}
@Test
void shouldNotRetryNotConfiguredException() {
when(clientRequest.method()).thenReturn(HttpMethod.GET);
when(next.exchange(any())).thenThrow(new TestException());
filterFunction.filter(clientRequest, next).subscribe();
inOrder.verify(factory, times(1)).getInstance(any());
inOrder.verify(next, times(1)).exchange(any());
}
@Test
void shouldRetryOnNotConfiguredExceptionWhenRetryOnAllExceptions() {
properties.getRetry().setRetryOnAllExceptions(true);
when(clientRequest.method()).thenReturn(HttpMethod.GET);
when(next.exchange(any())).thenThrow(new TestException());
filterFunction.filter(clientRequest, next).subscribe();
inOrder.verify(factory, times(1)).getInstance(any());
inOrder.verify(next, times(2)).exchange(any());
inOrder.verify(factory, times(1)).getInstance(any());
inOrder.verify(next, times(2)).exchange(any());
}
@Test
void shouldRetryOnConfiguredException() {
properties.getRetry().setRetryableExceptions(new HashSet<>(Collections.singletonList(TestException.class)));
when(clientRequest.method()).thenReturn(HttpMethod.GET);
when(next.exchange(any())).thenThrow(new TestException());
filterFunction.filter(clientRequest, next).subscribe();
inOrder.verify(factory, times(1)).getInstance(any());
inOrder.verify(next, times(2)).exchange(any());
inOrder.verify(factory, times(1)).getInstance(any());
inOrder.verify(next, times(2)).exchange(any());
}
@Test
void shouldRetryOnRetryableStatusCodeExceptionEvenWhenCustomExceptionsConfigured() {
properties.getRetry().setRetryableExceptions(new HashSet<>(Collections.singletonList(TestException.class)));
when(clientRequest.method()).thenReturn(HttpMethod.GET);
when(next.exchange(any())).thenThrow(new RetryableStatusCodeException());
filterFunction.filter(clientRequest, next).subscribe();
inOrder.verify(factory, times(1)).getInstance(any());
inOrder.verify(next, times(2)).exchange(any());
inOrder.verify(factory, times(1)).getInstance(any());
inOrder.verify(next, times(2)).exchange(any());
}
protected static class TestException extends RuntimeException {
}
}

12
spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/blocking/retry/BlockingLoadBalancedRetryPolicy.java

@ -89,4 +89,16 @@ public class BlockingLoadBalancedRetryPolicy implements LoadBalancedRetryPolicy @@ -89,4 +89,16 @@ public class BlockingLoadBalancedRetryPolicy implements LoadBalancedRetryPolicy
return properties.getRetry().getRetryableStatusCodes().contains(statusCode);
}
@Override
public boolean retryableException(Throwable throwable) {
if (throwable == null) {
return true;
}
if (properties.getRetry().isRetryOnAllExceptions()) {
return true;
}
return properties.getRetry().getRetryableExceptions().stream()
.anyMatch(exception -> exception.isInstance(throwable) || exception.isInstance(throwable.getCause()));
}
}

Loading…
Cancel
Save