Skip to content

Commit 5401628

Browse files
Add retry support for blocking LoadBalancer (#832)
* Add LoadBalancerProperties and BlockingLoadBalancedRetryPolicy. Add spring-retry dependency in LoadBalancer. * Add BlockingLoadBalancedRetryFactory * Move retry properties to LoadBalancerRetryProperties. * Refactor and remove deprecations, fix checkstyle. * Add BlockingLoadBalancedRetryPolicy to autoconfiguration. Set default retryableStatusCode prop. Fix javadoc. * Allow using @order on LoadBalancedRetryFactory beans. * Add tests. Reformat tests. Add explanatory comments. * Add documentation. * Fix javadoc. * Fix docs after review. * Change field name.
1 parent f7360b0 commit 5401628

File tree

14 files changed

+456
-79
lines changed

14 files changed

+456
-79
lines changed

docs/src/main/asciidoc/_configprops.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@
3535
|spring.cloud.loadbalancer.health-check.interval | 25s | Interval for rerunning the HealthCheck scheduler.
3636
|spring.cloud.loadbalancer.health-check.path | |
3737
|spring.cloud.loadbalancer.retry.enabled | true |
38+
|spring.cloud.loadbalancer.retry.max-retries-on-next-service-instance | 1 | Number of retries to be executed on the next <code>ServiceInstance</code>. A <code>ServiceInstance</code> is chosen before each retry call.
39+
|spring.cloud.loadbalancer.retry.max-retries-on-same-service-instance | 0 | Number of retries to be executed on the same <code>ServiceInstance</code>.
40+
|spring.cloud.loadbalancer.retry.retry-on-all-operations | false | Indicates retries should be attempted on operations other than {@link HttpMethod#GET}.
41+
|spring.cloud.loadbalancer.retry.retryable-status-codes | | A {@link Set} of status codes that should trigger a retry.
3842
|spring.cloud.loadbalancer.ribbon.enabled | true | Causes `RibbonLoadBalancerClient` to be used by default.
3943
|spring.cloud.loadbalancer.service-discovery.timeout | | String representation of Duration of the timeout for calls to service discovery.
4044
|spring.cloud.loadbalancer.zone | | Spring Cloud LoadBalancer zone.

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -453,12 +453,27 @@ set the `spring.cloud.loadbalancer.ribbon.enabled` property to `false`.
453453
A load-balanced `RestTemplate` can be configured to retry failed requests.
454454
By default, this logic is disabled.
455455
You can enable it by adding link:https://github.com/spring-projects/spring-retry[Spring Retry] to your application's classpath.
456-
The load-balanced `RestTemplate` honors some of the Ribbon configuration values related to retrying failed requests.
457-
You can use `client.ribbon.MaxAutoRetries`, `client.ribbon.MaxAutoRetriesNextServer`, and `client.ribbon.OkToRetryOnAllOperations` properties.
456+
458457
If you would like to disable the retry logic with Spring Retry on the classpath, you can set `spring.cloud.loadbalancer.retry.enabled=false`.
458+
459+
If you would like to implement a `BackOffPolicy` in your retries, you need to create a bean of type `LoadBalancedRetryFactory` and override the `createBackOffPolicy()` method.
460+
461+
===== Ribbon-based retries
462+
463+
For the Ribbon-backed implementation, the load-balanced `RestTemplate` honors some of the Ribbon configuration values related to retrying failed requests.
464+
You can use the `client.ribbon.MaxAutoRetries`, `client.ribbon.MaxAutoRetriesNextServer`, and `client.ribbon.OkToRetryOnAllOperations` properties.
465+
459466
See the https://github.com/Netflix/ribbon/wiki/Getting-Started#the-properties-file-sample-clientproperties[Ribbon documentation] for a description of what these properties do.
460467

461-
If you would like to implement a `BackOffPolicy` in your retries, you need to create a bean of type `LoadBalancedRetryFactory` and override the `createBackOffPolicy` method:
468+
===== Spring Cloud LoadBalancer-based retries
469+
470+
For the Spring Cloud LoadBalancer-backed implementation, you can set:
471+
472+
- `spring.cloud.loadbalancer.retry.maxRetriesOnSameServiceInstance` - indicates how many times a request should be retried on the same `ServiceInstance` (counted separately for every selected instance)
473+
- `spring.cloud.loadbalancer.retry.maxRetriesOnNextServiceInstance` - indicates how many times a request should be retried a newly selected `ServiceInstance`
474+
- `spring.cloud.loadbalancer.retry.retryableStatusCodes` - the status codes on which to always retry a failed request.
475+
476+
WARN:: If you chose to override the `LoadBalancedRetryFactory` while using the Spring Cloud LoadBalancer-backed approach, make sure you annotate your bean with `@Order` and set it to a higher precedence than `1000`, which is the order set on the `BlockingLoadBalancedRetryFactory`.
462477

463478
====
464479
[source,java,indent=0]

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

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@
2828
*/
2929
public class InterceptorRetryPolicy implements RetryPolicy {
3030

31-
private HttpRequest request;
31+
private final HttpRequest request;
3232

33-
private LoadBalancedRetryPolicy policy;
33+
private final LoadBalancedRetryPolicy policy;
3434

35-
private ServiceInstanceChooser serviceInstanceChooser;
35+
private final ServiceInstanceChooser serviceInstanceChooser;
3636

37-
private String serviceName;
37+
private final String serviceName;
3838

3939
/**
4040
* Creates a new retry policy.
@@ -56,21 +56,20 @@ public boolean canRetry(RetryContext context) {
5656
LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext) context;
5757
if (lbContext.getRetryCount() == 0 && lbContext.getServiceInstance() == null) {
5858
// We haven't even tried to make the request yet so return true so we do
59-
lbContext.setServiceInstance(
60-
this.serviceInstanceChooser.choose(this.serviceName));
59+
lbContext.setServiceInstance(serviceInstanceChooser.choose(serviceName));
6160
return true;
6261
}
63-
return this.policy.canRetryNextServer(lbContext);
62+
return policy.canRetryNextServer(lbContext);
6463
}
6564

6665
@Override
6766
public RetryContext open(RetryContext parent) {
68-
return new LoadBalancedRetryContext(parent, this.request);
67+
return new LoadBalancedRetryContext(parent, request);
6968
}
7069

7170
@Override
7271
public void close(RetryContext context) {
73-
this.policy.close((LoadBalancedRetryContext) context);
72+
policy.close((LoadBalancedRetryContext) context);
7473
}
7574

7675
@Override
@@ -80,7 +79,7 @@ public void registerThrowable(RetryContext context, Throwable throwable) {
8079
// increases the retry count
8180
lbContext.registerThrowable(throwable);
8281
// let the policy know about the exception as well
83-
this.policy.registerThrowable(lbContext, throwable);
82+
policy.registerThrowable(lbContext, throwable);
8483
}
8584

8685
@Override
@@ -94,25 +93,25 @@ public boolean equals(Object o) {
9493

9594
InterceptorRetryPolicy that = (InterceptorRetryPolicy) o;
9695

97-
if (!this.request.equals(that.request)) {
96+
if (!request.equals(that.request)) {
9897
return false;
9998
}
100-
if (!this.policy.equals(that.policy)) {
99+
if (!policy.equals(that.policy)) {
101100
return false;
102101
}
103-
if (!this.serviceInstanceChooser.equals(that.serviceInstanceChooser)) {
102+
if (!serviceInstanceChooser.equals(that.serviceInstanceChooser)) {
104103
return false;
105104
}
106-
return this.serviceName.equals(that.serviceName);
105+
return serviceName.equals(that.serviceName);
107106

108107
}
109108

110109
@Override
111110
public int hashCode() {
112-
int result = this.request.hashCode();
113-
result = 31 * result + this.policy.hashCode();
114-
result = 31 * result + this.serviceInstanceChooser.hashCode();
115-
result = 31 * result + this.serviceName.hashCode();
111+
int result = request.hashCode();
112+
result = 31 * result + policy.hashCode();
113+
result = 31 * result + serviceInstanceChooser.hashCode();
114+
result = 31 * result + serviceName.hashCode();
116115
return result;
117116
}
118117

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,13 @@
3030
import org.springframework.boot.context.properties.EnableConfigurationProperties;
3131
import org.springframework.context.annotation.Bean;
3232
import org.springframework.context.annotation.Configuration;
33+
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
3334
import org.springframework.http.client.ClientHttpRequestInterceptor;
3435
import org.springframework.retry.support.RetryTemplate;
3536
import org.springframework.web.client.RestTemplate;
3637

3738
/**
38-
* Auto-configuration for Ribbon (client-side load balancing).
39+
* Auto-configuration for blocking client-side load balancing.
3940
*
4041
* @author Spencer Gibb
4142
* @author Dave Syer
@@ -79,7 +80,7 @@ public LoadBalancerRequestFactory loadBalancerRequestFactory(
7980
static class LoadBalancerInterceptorConfig {
8081

8182
@Bean
82-
public LoadBalancerInterceptor ribbonInterceptor(
83+
public LoadBalancerInterceptor loadBalancerInterceptor(
8384
LoadBalancerClient loadBalancerClient,
8485
LoadBalancerRequestFactory requestFactory) {
8586
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
@@ -124,13 +125,14 @@ public static class RetryInterceptorAutoConfiguration {
124125

125126
@Bean
126127
@ConditionalOnMissingBean
127-
public RetryLoadBalancerInterceptor ribbonInterceptor(
128+
public RetryLoadBalancerInterceptor loadBalancerRetryInterceptor(
128129
LoadBalancerClient loadBalancerClient,
129130
LoadBalancerRetryProperties properties,
130131
LoadBalancerRequestFactory requestFactory,
131-
LoadBalancedRetryFactory loadBalancedRetryFactory) {
132+
List<LoadBalancedRetryFactory> loadBalancedRetryFactories) {
133+
AnnotationAwareOrderComparator.sort(loadBalancedRetryFactories);
132134
return new RetryLoadBalancerInterceptor(loadBalancerClient, properties,
133-
requestFactory, loadBalancedRetryFactory);
135+
requestFactory, loadBalancedRetryFactories.get(0));
134136
}
135137

136138
@Bean

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

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616

1717
package org.springframework.cloud.client.loadbalancer;
1818

19+
import java.util.HashSet;
20+
import java.util.Set;
21+
1922
import org.springframework.boot.context.properties.ConfigurationProperties;
23+
import org.springframework.http.HttpMethod;
2024

2125
/**
2226
* Configuration properties for the {@link LoadBalancerClient}.
@@ -28,6 +32,28 @@ public class LoadBalancerRetryProperties {
2832

2933
private boolean enabled = true;
3034

35+
/**
36+
* Indicates retries should be attempted on operations other than
37+
* {@link HttpMethod#GET}.
38+
*/
39+
private boolean retryOnAllOperations = false;
40+
41+
/**
42+
* Number of retries to be executed on the same <code>ServiceInstance</code>.
43+
*/
44+
private int maxRetriesOnSameServiceInstance = 0;
45+
46+
/**
47+
* Number of retries to be executed on the next <code>ServiceInstance</code>. A
48+
* <code>ServiceInstance</code> is chosen before each retry call.
49+
*/
50+
private int maxRetriesOnNextServiceInstance = 1;
51+
52+
/**
53+
* A {@link Set} of status codes that should trigger a retry.
54+
*/
55+
private Set<Integer> retryableStatusCodes = new HashSet<>();
56+
3157
/**
3258
* Returns true if the load balancer should retry failed requests.
3359
* @return True if the load balancer should retry failed requests; false otherwise.
@@ -44,4 +70,36 @@ public void setEnabled(boolean enabled) {
4470
this.enabled = enabled;
4571
}
4672

73+
public boolean isRetryOnAllOperations() {
74+
return retryOnAllOperations;
75+
}
76+
77+
public void setRetryOnAllOperations(boolean retryOnAllOperations) {
78+
this.retryOnAllOperations = retryOnAllOperations;
79+
}
80+
81+
public int getMaxRetriesOnSameServiceInstance() {
82+
return maxRetriesOnSameServiceInstance;
83+
}
84+
85+
public void setMaxRetriesOnSameServiceInstance(int maxRetriesOnSameServiceInstance) {
86+
this.maxRetriesOnSameServiceInstance = maxRetriesOnSameServiceInstance;
87+
}
88+
89+
public int getMaxRetriesOnNextServiceInstance() {
90+
return maxRetriesOnNextServiceInstance;
91+
}
92+
93+
public void setMaxRetriesOnNextServiceInstance(int maxRetriesOnNextServiceInstance) {
94+
this.maxRetriesOnNextServiceInstance = maxRetriesOnNextServiceInstance;
95+
}
96+
97+
public Set<Integer> getRetryableStatusCodes() {
98+
return retryableStatusCodes;
99+
}
100+
101+
public void setRetryableStatusCodes(Set<Integer> retryableStatusCodes) {
102+
this.retryableStatusCodes = retryableStatusCodes;
103+
}
104+
47105
}

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

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@
3939
*/
4040
public class RetryLoadBalancerInterceptor implements ClientHttpRequestInterceptor {
4141

42-
private LoadBalancerClient loadBalancer;
42+
private final LoadBalancerClient loadBalancer;
4343

44-
private LoadBalancerRetryProperties lbProperties;
44+
private final LoadBalancerRetryProperties lbProperties;
4545

46-
private LoadBalancerRequestFactory requestFactory;
46+
private final LoadBalancerRequestFactory requestFactory;
4747

48-
private LoadBalancedRetryFactory lbRetryFactory;
48+
private final LoadBalancedRetryFactory lbRetryFactory;
4949

5050
public RetryLoadBalancerInterceptor(LoadBalancerClient loadBalancer,
5151
LoadBalancerRetryProperties lbProperties,
@@ -65,8 +65,8 @@ public ClientHttpResponse intercept(final HttpRequest request, final byte[] body
6565
final String serviceName = originalUri.getHost();
6666
Assert.state(serviceName != null,
6767
"Request URI does not contain a valid hostname: " + originalUri);
68-
final LoadBalancedRetryPolicy retryPolicy = this.lbRetryFactory
69-
.createRetryPolicy(serviceName, this.loadBalancer);
68+
final LoadBalancedRetryPolicy retryPolicy = lbRetryFactory
69+
.createRetryPolicy(serviceName, loadBalancer);
7070
RetryTemplate template = createRetryTemplate(serviceName, request, retryPolicy);
7171
return template.execute(context -> {
7272
ServiceInstance serviceInstance = null;
@@ -75,11 +75,11 @@ public ClientHttpResponse intercept(final HttpRequest request, final byte[] body
7575
serviceInstance = lbContext.getServiceInstance();
7676
}
7777
if (serviceInstance == null) {
78-
serviceInstance = this.loadBalancer.choose(serviceName);
78+
serviceInstance = loadBalancer.choose(serviceName);
7979
}
80-
ClientHttpResponse response = RetryLoadBalancerInterceptor.this.loadBalancer
81-
.execute(serviceName, serviceInstance,
82-
this.requestFactory.createRequest(request, body, execution));
80+
ClientHttpResponse response = loadBalancer.execute(serviceName,
81+
serviceInstance,
82+
requestFactory.createRequest(request, body, execution));
8383
int statusCode = response.getRawStatusCode();
8484
if (retryPolicy != null && retryPolicy.retryableStatusCode(statusCode)) {
8585
byte[] bodyCopy = StreamUtils.copyToByteArray(response.getBody());
@@ -103,19 +103,17 @@ protected ClientHttpResponse createResponse(ClientHttpResponse response,
103103
private RetryTemplate createRetryTemplate(String serviceName, HttpRequest request,
104104
LoadBalancedRetryPolicy retryPolicy) {
105105
RetryTemplate template = new RetryTemplate();
106-
BackOffPolicy backOffPolicy = this.lbRetryFactory
107-
.createBackOffPolicy(serviceName);
106+
BackOffPolicy backOffPolicy = lbRetryFactory.createBackOffPolicy(serviceName);
108107
template.setBackOffPolicy(
109108
backOffPolicy == null ? new NoBackOffPolicy() : backOffPolicy);
110109
template.setThrowLastExceptionOnExhausted(true);
111-
RetryListener[] retryListeners = this.lbRetryFactory
112-
.createRetryListeners(serviceName);
110+
RetryListener[] retryListeners = lbRetryFactory.createRetryListeners(serviceName);
113111
if (retryListeners != null && retryListeners.length != 0) {
114112
template.setListeners(retryListeners);
115113
}
116-
template.setRetryPolicy(!this.lbProperties.isEnabled() || retryPolicy == null
114+
template.setRetryPolicy(!lbProperties.isEnabled() || retryPolicy == null
117115
? new NeverRetryPolicy() : new InterceptorRetryPolicy(request,
118-
retryPolicy, this.loadBalancer, serviceName));
116+
retryPolicy, loadBalancer, serviceName));
119117
return template;
120118
}
121119

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package org.springframework.cloud.client.loadbalancer;
1818

19-
import java.io.IOException;
2019
import java.net.URI;
2120
import java.util.Collection;
2221
import java.util.Map;
@@ -156,7 +155,7 @@ public <T> T execute(String serviceId, LoadBalancerRequest<T> request) {
156155

157156
@Override
158157
public <T> T execute(String serviceId, ServiceInstance serviceInstance,
159-
LoadBalancerRequest<T> request) throws IOException {
158+
LoadBalancerRequest<T> request) {
160159
try {
161160
return request.apply(choose(serviceId));
162161
}

0 commit comments

Comments
 (0)