Skip to content

Commit 7f23cb8

Browse files
Add Reactive retries for SC LoadBalancer (#847)
* Implement retry logic. * Fix retrying on next instance when RetryExhausted in same instance. * Fix retrying on next instance when RetryExhausted in same instance. * Fix retrying on next instance when RetryExhausted in same instance. * Move duplicated methods to utility class. Fix checkstyle. * Fix test. * Add more tests. * Fix test. * Add autoConfiguration. * Refactor and add javadocs. * Add javadocs. * Use RetryAwareServiceInstanceListSupplier with reactive retries. * Update properties. * Fix the docs. * Rename utility class. * Verify interactions in order.
1 parent 1195581 commit 7f23cb8

20 files changed

+1257
-71
lines changed

docs/src/main/asciidoc/_configprops.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
|spring.cloud.loadbalancer.health-check.path | |
3333
|spring.cloud.loadbalancer.hint | | Allows setting the value of <code>hint</code> that is passed on to the LoadBalancer request and can subsequently be used in {@link ReactiveLoadBalancer} implementations.
3434
|spring.cloud.loadbalancer.retry.avoid-previous-instance | true | Enables wrapping ServiceInstanceListSupplier beans with `RetryAwareServiceInstanceListSupplier` if Spring-Retry is in the classpath.
35+
|spring.cloud.loadbalancer.retry.backoff.enabled | false | Indicates whether Reactor Retry backoffs should be applied.
36+
|spring.cloud.loadbalancer.retry.backoff.jitter | 0.5 | Used to set {@link RetryBackoffSpec#jitter}.
37+
|spring.cloud.loadbalancer.retry.backoff.max-backoff | | Used to set {@link RetryBackoffSpec#maxBackoff}.
38+
|spring.cloud.loadbalancer.retry.backoff.min-backoff | 5ms | Used to set {@link RetryBackoffSpec#minBackoff}.
3539
|spring.cloud.loadbalancer.retry.enabled | true |
3640
|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.
3741
|spring.cloud.loadbalancer.retry.max-retries-on-same-service-instance | 0 | Number of retries to be executed on the same <code>ServiceInstance</code>.

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -437,18 +437,27 @@ Then, `ReactiveLoadBalancer` is used underneath.
437437

438438
A load-balanced `RestTemplate` can be configured to retry failed requests.
439439
By default, this logic is disabled.
440-
You can enable it by adding link:https://github.com/spring-projects/spring-retry[Spring Retry] to your application's classpath.
440+
For the non-reactive version (with `RestTemplate`), you can enable it by adding link:https://github.com/spring-projects/spring-retry[Spring Retry] to your application's classpath. For the reactive version (with `WebTestClient), you need to set `spring.cloud.loadbalancer.retry.enabled=false`.
441441

442-
If you would like to disable the retry logic with Spring Retry on the classpath, you can set `spring.cloud.loadbalancer.retry.enabled=false`.
442+
If you would like to disable the retry logic with Spring Retry or Reactive Retry on the classpath, you can set `spring.cloud.loadbalancer.retry.enabled=false`.
443443

444-
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.
444+
For the non-reactive implementation, 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.
445+
446+
For the reactive implementation, you just need to enable it by setting `spring.cloud.loadbalancer.retry.backoff.enabled` to `false`.
445447

446448
You can set:
447449

448450
- `spring.cloud.loadbalancer.retry.maxRetriesOnSameServiceInstance` - indicates how many times a request should be retried on the same `ServiceInstance` (counted separately for every selected instance)
449451
- `spring.cloud.loadbalancer.retry.maxRetriesOnNextServiceInstance` - indicates how many times a request should be retried a newly selected `ServiceInstance`
450452
- `spring.cloud.loadbalancer.retry.retryableStatusCodes` - the status codes on which to always retry a failed request.
451453

454+
For the reactive implementation, you can additionally set:
455+
- `spring.cloud.loadbalancer.retry.backoff.minBackoff` - Sets the minimum backoff duration (by default, 5 milliseconds)
456+
- `spring.cloud.loadbalancer.retry.backoff.maxBackoff` - Sets the maximum backoff duration (by default, max long value of milliseconds)
457+
- `spring.cloud.loadbalancer.retry.backoff.jitter` - Sets the jitter used for calculationg the actual backoff duration for each call (by default, 0.5).
458+
459+
For the reactive implementation, you can also implement your own `LoadBalancerRetryPolicy` to have more detailed control over the load-balanced call retries.
460+
452461
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`.
453462

454463
====

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

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

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

19+
import org.springframework.http.HttpMethod;
1920
import org.springframework.web.reactive.function.client.ClientRequest;
2021

2122
/**
@@ -36,4 +37,8 @@ public ClientRequest getClientRequest() {
3637
return (ClientRequest) super.getClientRequest();
3738
}
3839

40+
public HttpMethod method() {
41+
return ((ClientRequest) super.getClientRequest()).method();
42+
}
43+
3944
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.client.loadbalancer.reactive;
18+
19+
import java.net.URI;
20+
import java.util.Map;
21+
22+
import org.springframework.web.reactive.function.client.ClientRequest;
23+
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
24+
25+
/**
26+
* A utility class for load-balanced {@link ExchangeFilterFunction} instances.
27+
*
28+
* @author Olga Maciaszek-Sharma
29+
* @since 3.0.0
30+
*/
31+
public final class ExchangeFilterFunctionUtils {
32+
33+
private ExchangeFilterFunctionUtils() {
34+
throw new IllegalStateException("Can't instantiate a utility class.");
35+
}
36+
37+
static String getHint(String serviceId, Map<String, String> hints) {
38+
String defaultHint = hints.getOrDefault("default", "default");
39+
String hintPropertyValue = hints.get(serviceId);
40+
return hintPropertyValue != null ? hintPropertyValue : defaultHint;
41+
}
42+
43+
static ClientRequest buildClientRequest(ClientRequest request, URI uri) {
44+
return ClientRequest.create(request.method(), uri).headers(headers -> headers.addAll(request.headers()))
45+
.cookies(cookies -> cookies.addAll(request.cookies()))
46+
.attributes(attributes -> attributes.putAll(request.attributes())).body(request.body()).build();
47+
}
48+
49+
static String serviceInstanceUnavailableMessage(String serviceId) {
50+
return "LoadBalancer does not contain an instance for the service " + serviceId;
51+
}
52+
53+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.client.loadbalancer.reactive;
18+
19+
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
20+
21+
/**
22+
* A marker interface for load-balanced {@link ExchangeFilterFunction} instances.
23+
*
24+
* @author Olga Maciaszek-Sharma
25+
* @since 3.0.0
26+
*/
27+
public interface LoadBalancedExchangeFilterFunction extends ExchangeFilterFunction {
28+
29+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ protected static class ReactorDeferringLoadBalancerFilterConfig {
5858

5959
@Bean
6060
@Primary
61-
DeferringLoadBalancerExchangeFilterFunction<ReactorLoadBalancerExchangeFilterFunction> reactorDeferringLoadBalancerExchangeFilterFunction(
62-
ObjectProvider<ReactorLoadBalancerExchangeFilterFunction> exchangeFilterFunctionProvider) {
61+
DeferringLoadBalancerExchangeFilterFunction<LoadBalancedExchangeFilterFunction> reactorDeferringLoadBalancerExchangeFilterFunction(
62+
ObjectProvider<LoadBalancedExchangeFilterFunction> exchangeFilterFunctionProvider) {
6363
return new DeferringLoadBalancerExchangeFilterFunction<>(exchangeFilterFunctionProvider);
6464
}
6565

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

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import java.util.Map;
2222
import java.util.Set;
2323

24+
import reactor.util.retry.RetryBackoffSpec;
25+
2426
import org.springframework.boot.context.properties.ConfigurationProperties;
2527
import org.springframework.http.HttpMethod;
2628
import org.springframework.util.LinkedCaseInsensitiveMap;
@@ -47,7 +49,7 @@ public class LoadBalancerProperties {
4749
private Map<String, String> hint = new LinkedCaseInsensitiveMap<>();
4850

4951
/**
50-
* Properties for Spring-Retry support in Spring Cloud LoadBalancer.
52+
* Properties for Spring-Retry and Reactor Retry support in Spring Cloud LoadBalancer.
5153
*/
5254
private Retry retry = new Retry();
5355

@@ -141,6 +143,11 @@ public static class Retry {
141143
*/
142144
private Set<Integer> retryableStatusCodes = new HashSet<>();
143145

146+
/**
147+
* Properties for Reactor Retry backoffs in Spring Cloud LoadBalancer.
148+
*/
149+
private Backoff backoff = new Backoff();
150+
144151
/**
145152
* Returns true if the load balancer should retry failed requests.
146153
* @return True if the load balancer should retry failed requests; false
@@ -190,6 +197,70 @@ public void setRetryableStatusCodes(Set<Integer> retryableStatusCodes) {
190197
this.retryableStatusCodes = retryableStatusCodes;
191198
}
192199

200+
public Backoff getBackoff() {
201+
return backoff;
202+
}
203+
204+
public void setBackoff(Backoff backoff) {
205+
this.backoff = backoff;
206+
}
207+
208+
public static class Backoff {
209+
210+
/**
211+
* Indicates whether Reactor Retry backoffs should be applied.
212+
*/
213+
private boolean enabled = false;
214+
215+
/**
216+
* Used to set {@link RetryBackoffSpec#minBackoff}.
217+
*/
218+
private Duration minBackoff = Duration.ofMillis(5);
219+
220+
/**
221+
* Used to set {@link RetryBackoffSpec#maxBackoff}.
222+
*/
223+
private Duration maxBackoff = Duration.ofMillis(Long.MAX_VALUE);
224+
225+
/**
226+
* Used to set {@link RetryBackoffSpec#jitter}.
227+
*/
228+
private double jitter = 0.5d;
229+
230+
public Duration getMinBackoff() {
231+
return minBackoff;
232+
}
233+
234+
public void setMinBackoff(Duration minBackoff) {
235+
this.minBackoff = minBackoff;
236+
}
237+
238+
public Duration getMaxBackoff() {
239+
return maxBackoff;
240+
}
241+
242+
public void setMaxBackoff(Duration maxBackoff) {
243+
this.maxBackoff = maxBackoff;
244+
}
245+
246+
public double getJitter() {
247+
return jitter;
248+
}
249+
250+
public void setJitter(double jitter) {
251+
this.jitter = jitter;
252+
}
253+
254+
public boolean isEnabled() {
255+
return enabled;
256+
}
257+
258+
public void setEnabled(boolean enabled) {
259+
this.enabled = enabled;
260+
}
261+
262+
}
263+
193264
}
194265

195266
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.client.loadbalancer.reactive;
18+
19+
import org.springframework.http.HttpMethod;
20+
import org.springframework.web.reactive.function.client.ClientRequest;
21+
import org.springframework.web.reactive.function.client.ClientResponse;
22+
23+
/**
24+
* Stores the data for a load-balanced call that is being retried.
25+
*
26+
* @author Olga Maciaszek-Sharma
27+
* @since 3.0.0
28+
*/
29+
class LoadBalancerRetryContext {
30+
31+
private final ClientRequest request;
32+
33+
private ClientResponse clientResponse;
34+
35+
private Integer retriesSameServiceInstance = 0;
36+
37+
private Integer retriesNextServiceInstance = 0;
38+
39+
LoadBalancerRetryContext(ClientRequest request) {
40+
this.request = request;
41+
}
42+
43+
ClientRequest getRequest() {
44+
return request;
45+
}
46+
47+
ClientResponse getClientResponse() {
48+
return clientResponse;
49+
}
50+
51+
void setClientResponse(ClientResponse clientResponse) {
52+
this.clientResponse = clientResponse;
53+
}
54+
55+
Integer getRetriesSameServiceInstance() {
56+
return retriesSameServiceInstance;
57+
}
58+
59+
void incrementRetriesSameServiceInstance() {
60+
retriesSameServiceInstance++;
61+
}
62+
63+
void resetRetriesSameServiceInstance() {
64+
retriesSameServiceInstance = 0;
65+
}
66+
67+
Integer getRetriesNextServiceInstance() {
68+
return retriesNextServiceInstance;
69+
}
70+
71+
void incrementRetriesNextServiceInstance() {
72+
retriesNextServiceInstance++;
73+
}
74+
75+
Integer getResponseStatusCode() {
76+
return clientResponse.statusCode().value();
77+
}
78+
79+
HttpMethod getRequestMethod() {
80+
return request.method();
81+
}
82+
83+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.client.loadbalancer.reactive;
18+
19+
import org.springframework.http.HttpMethod;
20+
21+
/**
22+
* Pluggable policy used to establish whether a given load-balanced call should be
23+
* retried.
24+
*
25+
* @author Olga Maciaszek-Sharma
26+
* @since 3.0.0
27+
*/
28+
public interface LoadBalancerRetryPolicy {
29+
30+
/**
31+
* Return <code>true</code> to retry on the same service instance.
32+
* @param context the context for the retry operation
33+
* @return true to retry on the same service instance
34+
*/
35+
boolean canRetrySameServiceInstance(LoadBalancerRetryContext context);
36+
37+
/**
38+
* Return <code>true</code> to retry on the next service instance.
39+
* @param context the context for the retry operation
40+
* @return true to retry on the same service instance
41+
*/
42+
boolean canRetryNextServiceInstance(LoadBalancerRetryContext context);
43+
44+
/**
45+
* Return <code>true</code> to retry on the provided HTTP status code.
46+
* @param statusCode the HTTP status code
47+
* @return true to retry on the provided HTTP status code
48+
*/
49+
boolean retryableStatusCode(int statusCode);
50+
51+
/**
52+
* Return <code>true</code> to retry on the provided HTTP method.
53+
* @param method the HTTP request method
54+
* @return true to retry on the provided HTTP method
55+
*/
56+
boolean canRetryOnMethod(HttpMethod method);
57+
58+
}

0 commit comments

Comments
 (0)