Skip to content

Commit 5456d6d

Browse files
committed
Merge branch '4.2.x'
2 parents 014a851 + fe3ea71 commit 5456d6d

File tree

5 files changed

+153
-6
lines changed

5 files changed

+153
-6
lines changed

docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/retry-factory.adoc

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ The `Retry` `GatewayFilter` factory supports the following parameters:
1212
Retries are performed after a backoff interval of `firstBackoff * (factor ^ n)`, where `n` is the iteration.
1313
If `maxBackoff` is configured, the maximum backoff applied is limited to `maxBackoff`.
1414
If `basedOnPreviousValue` is true, the backoff is calculated by using `prevBackoff * factor`.
15+
* `jitter`: The configured random jitter for the retries.
16+
Generating a backoff between `[backoff - backoff*randomFactor, backoff + backoff*randomFactor]`
17+
* `timeout`: The configured timeout for the retries.
1518

1619
The following defaults are configured for `Retry` filter, if enabled:
1720

@@ -20,6 +23,8 @@ The following defaults are configured for `Retry` filter, if enabled:
2023
* `methods`: GET method
2124
* `exceptions`: `IOException` and `TimeoutException`
2225
* `backoff`: disabled
26+
* `jitter`: disabled
27+
* `timeout`: unlimited
2328

2429
The following listing configures a Retry `GatewayFilter`:
2530

@@ -45,6 +50,9 @@ spring:
4550
maxBackoff: 50ms
4651
factor: 2
4752
basedOnPreviousValue: false
53+
jitter:
54+
randomFactor: 0.5
55+
timeout: 100ms
4856
----
4957

5058
NOTE: When using the retry filter with a `forward:` prefixed URL, the target endpoint should be written carefully so that, in case of an error, it does not do anything that could result in a response being sent to the client and committed.
@@ -79,10 +87,13 @@ spring:
7987
maxBackoff: 50ms
8088
factor: 2
8189
basedOnPreviousValue: false
90+
jitter:
91+
randomFactor: 0.5
92+
timeout: 100ms
8293
8394
- id: retryshortcut_route
8495
uri: https://example.org
8596
filters:
86-
- Retry=3,INTERNAL_SERVER_ERROR,GET,10ms,50ms,2,false
97+
- Retry=3,INTERNAL_SERVER_ERROR,GET,10ms,50ms,2,false,0.5,100ms
8798
----
8899

spring-cloud-gateway-integration-tests/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<version>4.3.0-SNAPSHOT</version>
2020
<relativePath>..</relativePath> <!-- lookup parent from repository -->
2121
</parent>
22-
22+
2323
<modules>
2424
<module>grpc</module>
2525
<module>http2</module>

spring-cloud-gateway-server-mvc/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,4 @@
136136
<scope>test</scope>
137137
</dependency>
138138
</dependencies>
139-
</project>
139+
</project>

spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import reactor.core.publisher.Mono;
3131
import reactor.netty.Connection;
3232
import reactor.retry.Backoff;
33+
import reactor.retry.Jitter;
3334
import reactor.retry.Repeat;
3435
import reactor.retry.RepeatContext;
3536
import reactor.retry.Retry;
@@ -40,6 +41,7 @@
4041
import org.springframework.cloud.gateway.support.HasRouteId;
4142
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
4243
import org.springframework.cloud.gateway.support.TimeoutException;
44+
import org.springframework.core.style.ToStringCreator;
4345
import org.springframework.http.HttpMethod;
4446
import org.springframework.http.HttpStatus;
4547
import org.springframework.http.HttpStatus.Series;
@@ -69,7 +71,7 @@ private static <T> List<T> toList(T... items) {
6971
@Override
7072
public List<String> shortcutFieldOrder() {
7173
return Arrays.asList("retries", "statuses", "methods", "backoff.firstBackoff", "backoff.maxBackoff",
72-
"backoff.factor", "backoff.basedOnPreviousValue");
74+
"backoff.factor", "backoff.basedOnPreviousValue", "jitter.randomFactor", "timeout");
7375
}
7476

7577
@Override
@@ -123,10 +125,16 @@ public GatewayFilter apply(RetryConfig retryConfig) {
123125
if (backoff != null) {
124126
statusCodeRepeat = statusCodeRepeat.backoff(getBackoff(backoff));
125127
}
128+
JitterConfig jitter = retryConfig.getJitter();
129+
if (jitter != null) {
130+
statusCodeRepeat = statusCodeRepeat.jitter(getJitter(jitter));
131+
}
132+
Duration timeout = retryConfig.getTimeout();
133+
if (timeout != null) {
134+
statusCodeRepeat = statusCodeRepeat.timeout(timeout);
135+
}
126136
}
127137

128-
// TODO: support timeout, backoff, jitter, etc... in Builder
129-
130138
Retry<ServerWebExchange> exceptionRetry = null;
131139
if (!retryConfig.getExceptions().isEmpty()) {
132140
Predicate<RetryContext<ServerWebExchange>> retryContextPredicate = context -> {
@@ -162,6 +170,14 @@ public GatewayFilter apply(RetryConfig retryConfig) {
162170
if (backoff != null) {
163171
exceptionRetry = exceptionRetry.backoff(getBackoff(backoff));
164172
}
173+
JitterConfig jitter = retryConfig.getJitter();
174+
if (jitter != null) {
175+
exceptionRetry = exceptionRetry.jitter(getJitter(jitter));
176+
}
177+
Duration timeout = retryConfig.getTimeout();
178+
if (timeout != null) {
179+
exceptionRetry = exceptionRetry.timeout(timeout);
180+
}
165181
}
166182

167183
GatewayFilter gatewayFilter = apply(retryConfig.getRouteId(), statusCodeRepeat, exceptionRetry);
@@ -179,6 +195,9 @@ public String toString() {
179195
.append("statuses", retryConfig.getStatuses())
180196
.append("methods", retryConfig.getMethods())
181197
.append("exceptions", retryConfig.getExceptions())
198+
.append("backoff", retryConfig.getBackoff())
199+
.append("jitter", retryConfig.getJitter())
200+
.append("timeout", retryConfig.getTimeout())
182201
.toString();
183202
}
184203
};
@@ -203,6 +222,10 @@ private Backoff getBackoff(BackoffConfig backoff) {
203222
backoff.basedOnPreviousValue);
204223
}
205224

225+
private Jitter getJitter(JitterConfig jitter) {
226+
return Jitter.random(jitter.randomFactor);
227+
}
228+
206229
public boolean exceedsMaxIterations(ServerWebExchange exchange, RetryConfig retryConfig) {
207230
Integer iteration = exchange.getAttribute(RETRY_ITERATION_KEY);
208231

@@ -291,6 +314,10 @@ public static class RetryConfig implements HasRouteId {
291314

292315
private BackoffConfig backoff;
293316

317+
private JitterConfig jitter;
318+
319+
private Duration timeout;
320+
294321
public RetryConfig allMethods() {
295322
return setMethods(HttpMethod.values());
296323
}
@@ -303,6 +330,35 @@ public void validate() {
303330
if (this.backoff != null) {
304331
this.backoff.validate();
305332
}
333+
if (this.jitter != null) {
334+
this.jitter.validate();
335+
}
336+
if (this.timeout != null) {
337+
Assert.isTrue(!timeout.isNegative(), "timeout should be >= 0");
338+
}
339+
}
340+
341+
public Duration getTimeout() {
342+
return timeout;
343+
}
344+
345+
public RetryConfig setTimeout(Duration timeout) {
346+
this.timeout = timeout;
347+
return this;
348+
}
349+
350+
public JitterConfig getJitter() {
351+
return jitter;
352+
}
353+
354+
public RetryConfig setJitter(JitterConfig jitter) {
355+
this.jitter = jitter;
356+
return this;
357+
}
358+
359+
public RetryConfig setJitter(double randomFactor) {
360+
this.jitter = new JitterConfig(randomFactor);
361+
return this;
306362
}
307363

308364
public BackoffConfig getBackoff() {
@@ -433,6 +489,47 @@ public void setBasedOnPreviousValue(boolean basedOnPreviousValue) {
433489
this.basedOnPreviousValue = basedOnPreviousValue;
434490
}
435491

492+
@Override
493+
public String toString() {
494+
return new ToStringCreator(this).append("firstBackoff", firstBackoff)
495+
.append("maxBackoff", maxBackoff)
496+
.append("factor", factor)
497+
.append("basedOnPreviousValue", basedOnPreviousValue)
498+
.toString();
499+
}
500+
501+
}
502+
503+
public static class JitterConfig {
504+
505+
private double randomFactor = 0.5;
506+
507+
public void validate() {
508+
Assert.isTrue(randomFactor >= 0 && randomFactor <= 1,
509+
"random factor must be between 0 and 1 (default 0.5)");
510+
}
511+
512+
public JitterConfig() {
513+
}
514+
515+
public JitterConfig(double randomFactor) {
516+
this.randomFactor = randomFactor;
517+
}
518+
519+
public double getRandomFactor() {
520+
return randomFactor;
521+
}
522+
523+
public void setRandomFactor(double randomFactor) {
524+
this.randomFactor = randomFactor;
525+
}
526+
527+
@Override
528+
public String toString() {
529+
return new ToStringCreator(this).append("randomFactor", randomFactor).toString();
530+
531+
}
532+
436533
}
437534

438535
}

spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactoryIntegrationTests.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,30 @@ public void retryWithBackoff() {
109109
// @formatter:on
110110
}
111111

112+
@Test
113+
public void retryWithBackoffJitterTimeout() {
114+
// @formatter:off
115+
testClient.get()
116+
.uri("/retry?key=retry-with-backoff-jitter-timeout&count=3")
117+
.header(HttpHeaders.HOST, "www.retrywithbackoffjittertimeout.org")
118+
.exchange()
119+
.expectStatus().isOk()
120+
.expectHeader().value("X-Retry-Count", CoreMatchers.equalTo("3"));
121+
// @formatter:on
122+
}
123+
124+
@Test
125+
public void retryWithBackoffTimeout() {
126+
// backoff > timeout
127+
testClient.get()
128+
.uri("/retry?key=retry-with-backoff-timeout&count=3")
129+
.header(HttpHeaders.HOST, "www.retrywithbackofftimeout.org")
130+
.exchange()
131+
.expectStatus()
132+
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
133+
assertThat(TestConfig.map.get("retry-with-backoff-timeout")).isNotNull().hasValue(2);
134+
}
135+
112136
@Test
113137
public void retryFilterGetJavaDsl() {
114138
testClient.get()
@@ -363,6 +387,21 @@ public RouteLocator hystrixRouteLocator(RouteLocatorBuilder builder) {
363387
r -> r.host("**.retrywithbackoff.org").filters(f -> f.prefixPath("/httpbin").retry(config -> {
364388
config.setRetries(2).setBackoff(Duration.ofMillis(100), null, 2, true);
365389
})).uri(uri))
390+
.route("retry_with_backoff_jitter_timeout_test", r -> r.host("**.retrywithbackoffjittertimeout.org")
391+
.filters(f -> f.prefixPath("/httpbin").retry(config -> {
392+
config.setRetries(3)
393+
.setBackoff(Duration.ofMillis(50), Duration.ofMillis(100), 2, true)
394+
.setJitter(0.1)
395+
.setTimeout(Duration.ofMillis(1000));
396+
}))
397+
.uri(uri))
398+
.route("retry_with_backoff_timeout_test", r -> r.host("**.retrywithbackofftimeout.org")
399+
.filters(f -> f.prefixPath("/httpbin").retry(config -> {
400+
config.setRetries(3)
401+
.setBackoff(Duration.ofMillis(100), null, 2, true)
402+
.setTimeout(Duration.ofMillis(200));
403+
}))
404+
.uri(uri))
366405

367406
.route("retry_with_loadbalancer",
368407
r -> r.host("**.retrywithloadbalancer.org")

0 commit comments

Comments
 (0)