Skip to content

Commit 5bfd461

Browse files
Backport retryable lb exchange filter function (#902)
1 parent 28932e5 commit 5bfd461

14 files changed

+1086
-28
lines changed

README.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Extract the files into the JDK/jre/lib/security folder for whichever version of
2727

2828
== Building
2929

30-
:jdkversion: 1.7
30+
:jdkversion: 1.8
3131

3232
=== Basic Compile and Test
3333

docs/src/main/asciidoc/_configprops.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
|spring.cloud.loadbalancer.health-check.refetch-instances | false | Indicates whether the instances should be refetched by the <code>HealthCheckServiceInstanceListSupplier</code>. This can be used if the instances can be updated and the underlying delegate does not provide an ongoing flux.
3434
|spring.cloud.loadbalancer.health-check.refetch-instances-interval | 25s | Interval for refetching available service instances.
3535
|spring.cloud.loadbalancer.health-check.repeat-health-check | true | Indicates whether health checks should keep repeating. It might be useful to set it to <code>false</code> if periodically refetching the instances, as every refetch will also trigger a healthcheck.
36+
|spring.cloud.loadbalancer.retry.backoff.enabled | false | Indicates whether Reactor Retry backoffs should be applied.
37+
|spring.cloud.loadbalancer.retry.backoff.jitter | 0.5 | Used to set {@link RetryBackoffSpec#jitter}.
38+
|spring.cloud.loadbalancer.retry.backoff.max-backoff | | Used to set {@link RetryBackoffSpec#maxBackoff}.
39+
|spring.cloud.loadbalancer.retry.backoff.min-backoff | 5ms | Used to set {@link RetryBackoffSpec#minBackoff}.
3640
|spring.cloud.loadbalancer.retry.enabled | true |
3741
|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.
3842
|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: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -452,11 +452,67 @@ set the `spring.cloud.loadbalancer.ribbon.enabled` property to `false`.
452452

453453
A load-balanced `RestTemplate` can be configured to retry failed requests.
454454
By default, this logic is disabled.
455-
You can enable it by adding link:https://github.com/spring-projects/spring-retry[Spring Retry] to your application's classpath.
455+
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.
456+
457+
To use the reactive version of load-balanced retries in the Hoxton release train, you will need to instantiate your own `RetryableLoadBalancerExchangeFilterFunction` bean:
458+
459+
[source,java,indent=0]
460+
----
461+
@Configuration
462+
public class MyConfiguration {
463+
464+
@Bean
465+
RetryableLoadBalancerExchangeFilterFunction retryableLoadBalancerExchangeFilterFunction(
466+
LoadBalancerRetryProperties properties,
467+
ReactiveLoadBalancer.Factory<ServiceInstance> factory) {
468+
return new RetryableLoadBalancerExchangeFilterFunction(
469+
new RetryableExchangeFilterFunctionLoadBalancerRetryPolicy(
470+
properties),
471+
factory, properties);
472+
}
473+
}
474+
----
475+
476+
Then you can use it as a filter while building `webClient` instances:
477+
478+
[source,java,indent=0]
479+
----
480+
public class MyClass {
481+
@Autowired
482+
private RetryableLoadBalancerExchangeFilterFunction retryableLbFunction;
483+
484+
public Mono<String> doOtherStuff() {
485+
return WebClient.builder().baseUrl("http://stores")
486+
.filter(retryableLbFunction)
487+
.build()
488+
.get()
489+
.uri("/stores")
490+
.retrieve()
491+
.bodyToMono(String.class);
492+
}
493+
}
494+
----
456495

457496
If you would like to disable the retry logic with Spring Retry on the classpath, you can set `spring.cloud.loadbalancer.retry.enabled=false`.
458497

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.
498+
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.
499+
500+
For the reactive implementation, you just need to enable it by setting `spring.cloud.loadbalancer.retry.backoff.enabled` to `false`.
501+
502+
You can set:
503+
504+
- `spring.cloud.loadbalancer.retry.maxRetriesOnSameServiceInstance` - indicates how many times a request should be retried on the same `ServiceInstance` (counted separately for every selected instance)
505+
- `spring.cloud.loadbalancer.retry.maxRetriesOnNextServiceInstance` - indicates how many times a request should be retried a newly selected `ServiceInstance`
506+
- `spring.cloud.loadbalancer.retry.retryableStatusCodes` - the status codes on which to always retry a failed request.
507+
508+
For the reactive implementation, you can additionally set:
509+
- `spring.cloud.loadbalancer.retry.backoff.minBackoff` - Sets the minimum backoff duration (by default, 5 milliseconds)
510+
- `spring.cloud.loadbalancer.retry.backoff.maxBackoff` - Sets the maximum backoff duration (by default, max long value of milliseconds)
511+
- `spring.cloud.loadbalancer.retry.backoff.jitter` - Sets the jitter used for calculationg the actual backoff duration for each call (by default, 0.5).
512+
513+
For the reactive implementation, you can also implement your own `LoadBalancerRetryPolicy` to have more detailed control over the load-balanced call retries.
514+
515+
WARN:: For the non-reactive version, 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`.
460516

461517
===== Ribbon-based retries
462518

@@ -475,23 +531,6 @@ For the Spring Cloud LoadBalancer-backed implementation, you can set:
475531

476532
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`.
477533

478-
====
479-
[source,java,indent=0]
480-
----
481-
@Configuration
482-
public class MyConfiguration {
483-
@Bean
484-
LoadBalancedRetryFactory retryFactory() {
485-
return new LoadBalancedRetryFactory() {
486-
@Override
487-
public BackOffPolicy createBackOffPolicy(String service) {
488-
return new ExponentialBackOffPolicy();
489-
}
490-
};
491-
}
492-
}
493-
----
494-
====
495534

496535
NOTE: `client` in the preceding examples should be replaced with your Ribbon client's name.
497536

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

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616

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

19+
import java.time.Duration;
1920
import java.util.HashSet;
2021
import java.util.Set;
2122

23+
import reactor.util.retry.RetryBackoffSpec;
24+
2225
import org.springframework.boot.context.properties.ConfigurationProperties;
2326
import org.springframework.http.HttpMethod;
2427

@@ -54,6 +57,11 @@ public class LoadBalancerRetryProperties {
5457
*/
5558
private Set<Integer> retryableStatusCodes = new HashSet<>();
5659

60+
/**
61+
* Properties for Reactor Retry backoffs in Spring Cloud LoadBalancer.
62+
*/
63+
private Backoff backoff = new Backoff();
64+
5765
/**
5866
* Returns true if the load balancer should retry failed requests.
5967
* @return True if the load balancer should retry failed requests; false otherwise.
@@ -102,4 +110,68 @@ public void setRetryableStatusCodes(Set<Integer> retryableStatusCodes) {
102110
this.retryableStatusCodes = retryableStatusCodes;
103111
}
104112

113+
public Backoff getBackoff() {
114+
return backoff;
115+
}
116+
117+
public void setBackoff(Backoff backoff) {
118+
this.backoff = backoff;
119+
}
120+
121+
public static class Backoff {
122+
123+
/**
124+
* Indicates whether Reactor Retry backoffs should be applied.
125+
*/
126+
private boolean enabled = false;
127+
128+
/**
129+
* Used to set {@link RetryBackoffSpec#minBackoff}.
130+
*/
131+
private Duration minBackoff = Duration.ofMillis(5);
132+
133+
/**
134+
* Used to set {@link RetryBackoffSpec#maxBackoff}.
135+
*/
136+
private Duration maxBackoff = Duration.ofMillis(Long.MAX_VALUE);
137+
138+
/**
139+
* Used to set {@link RetryBackoffSpec#jitter}.
140+
*/
141+
private double jitter = 0.5d;
142+
143+
public Duration getMinBackoff() {
144+
return minBackoff;
145+
}
146+
147+
public void setMinBackoff(Duration minBackoff) {
148+
this.minBackoff = minBackoff;
149+
}
150+
151+
public Duration getMaxBackoff() {
152+
return maxBackoff;
153+
}
154+
155+
public void setMaxBackoff(Duration maxBackoff) {
156+
this.maxBackoff = maxBackoff;
157+
}
158+
159+
public double getJitter() {
160+
return jitter;
161+
}
162+
163+
public void setJitter(double jitter) {
164+
this.jitter = jitter;
165+
}
166+
167+
public boolean isEnabled() {
168+
return enabled;
169+
}
170+
171+
public void setEnabled(boolean enabled) {
172+
this.enabled = enabled;
173+
}
174+
175+
}
176+
105177
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2012-2021 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;
18+
19+
import java.util.Objects;
20+
21+
import org.springframework.cloud.client.ServiceInstance;
22+
import org.springframework.core.style.ToStringCreator;
23+
24+
/**
25+
* A request context object that allows storing information on previously used service
26+
* instances.
27+
*
28+
* @author Olga Maciaszek-Sharma
29+
* @since 2.2.7
30+
*/
31+
public class RetryableRequestContext extends DefaultRequestContext {
32+
33+
private ServiceInstance previousServiceInstance;
34+
35+
public RetryableRequestContext(ServiceInstance previousServiceInstance) {
36+
this.previousServiceInstance = previousServiceInstance;
37+
}
38+
39+
public RetryableRequestContext(ServiceInstance previousServiceInstance, String hint) {
40+
super(hint);
41+
this.previousServiceInstance = previousServiceInstance;
42+
}
43+
44+
public ServiceInstance getPreviousServiceInstance() {
45+
return previousServiceInstance;
46+
}
47+
48+
public void setPreviousServiceInstance(ServiceInstance previousServiceInstance) {
49+
this.previousServiceInstance = previousServiceInstance;
50+
}
51+
52+
@Override
53+
public String toString() {
54+
ToStringCreator to = new ToStringCreator(this);
55+
to.append("previousServiceInstance", previousServiceInstance);
56+
return to.toString();
57+
}
58+
59+
@Override
60+
public boolean equals(Object o) {
61+
if (this == o) {
62+
return true;
63+
}
64+
if (!(o instanceof RetryableRequestContext)) {
65+
return false;
66+
}
67+
if (!super.equals(o)) {
68+
return false;
69+
}
70+
RetryableRequestContext context = (RetryableRequestContext) o;
71+
return Objects.equals(previousServiceInstance, context.previousServiceInstance);
72+
}
73+
74+
@Override
75+
public int hashCode() {
76+
return Objects.hash(super.hashCode(), previousServiceInstance);
77+
}
78+
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2012-2021 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+
21+
import org.springframework.web.reactive.function.client.ClientRequest;
22+
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
23+
24+
/**
25+
* A utility class for load-balanced {@link ExchangeFilterFunction} instances.
26+
*
27+
* @author Olga Maciaszek-Sharma
28+
* @since 2.2.7
29+
*/
30+
public final class ExchangeFilterFunctionUtils {
31+
32+
private ExchangeFilterFunctionUtils() {
33+
throw new IllegalStateException("Can't instantiate a utility class.");
34+
}
35+
36+
static ClientRequest buildClientRequest(ClientRequest request, URI uri) {
37+
return ClientRequest.create(request.method(), uri)
38+
.headers(headers -> headers.addAll(request.headers()))
39+
.cookies(cookies -> cookies.addAll(request.cookies()))
40+
.attributes(attributes -> attributes.putAll(request.attributes()))
41+
.body(request.body()).build();
42+
}
43+
44+
}

0 commit comments

Comments
 (0)