Merge branch '4.1.x' into 4.2.x

This commit is contained in:
Ryan Baxter
2025-03-20 10:10:39 -04:00
3 changed files with 151 additions and 4 deletions

View File

@@ -12,6 +12,9 @@ The `Retry` `GatewayFilter` factory supports the following parameters:
Retries are performed after a backoff interval of `firstBackoff * (factor ^ n)`, where `n` is the iteration.
If `maxBackoff` is configured, the maximum backoff applied is limited to `maxBackoff`.
If `basedOnPreviousValue` is true, the backoff is calculated by using `prevBackoff * factor`.
* `jitter`: The configured random jitter for the retries.
Generating a backoff between `[backoff - backoff*randomFactor, backoff + backoff*randomFactor]`
* `timeout`: The configured timeout for the retries.
The following defaults are configured for `Retry` filter, if enabled:
@@ -20,6 +23,8 @@ The following defaults are configured for `Retry` filter, if enabled:
* `methods`: GET method
* `exceptions`: `IOException` and `TimeoutException`
* `backoff`: disabled
* `jitter`: disabled
* `timeout`: unlimited
The following listing configures a Retry `GatewayFilter`:
@@ -45,6 +50,9 @@ spring:
maxBackoff: 50ms
factor: 2
basedOnPreviousValue: false
jitter:
randomFactor: 0.5
timeout: 100ms
----
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:
maxBackoff: 50ms
factor: 2
basedOnPreviousValue: false
jitter:
randomFactor: 0.5
timeout: 100ms
- id: retryshortcut_route
uri: https://example.org
filters:
- Retry=3,INTERNAL_SERVER_ERROR,GET,10ms,50ms,2,false
- Retry=3,INTERNAL_SERVER_ERROR,GET,10ms,50ms,2,false,0.5,100ms
----

View File

@@ -30,6 +30,7 @@ import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import reactor.netty.Connection;
import reactor.retry.Backoff;
import reactor.retry.Jitter;
import reactor.retry.Repeat;
import reactor.retry.RepeatContext;
import reactor.retry.Retry;
@@ -40,6 +41,7 @@ import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.support.HasRouteId;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.cloud.gateway.support.TimeoutException;
import org.springframework.core.style.ToStringCreator;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatus.Series;
@@ -69,7 +71,7 @@ public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory<Retr
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("retries", "statuses", "methods", "backoff.firstBackoff", "backoff.maxBackoff",
"backoff.factor", "backoff.basedOnPreviousValue");
"backoff.factor", "backoff.basedOnPreviousValue", "jitter.randomFactor", "timeout");
}
@Override
@@ -123,10 +125,16 @@ public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory<Retr
if (backoff != null) {
statusCodeRepeat = statusCodeRepeat.backoff(getBackoff(backoff));
}
JitterConfig jitter = retryConfig.getJitter();
if (jitter != null) {
statusCodeRepeat = statusCodeRepeat.jitter(getJitter(jitter));
}
Duration timeout = retryConfig.getTimeout();
if (timeout != null) {
statusCodeRepeat = statusCodeRepeat.timeout(timeout);
}
}
// TODO: support timeout, backoff, jitter, etc... in Builder
Retry<ServerWebExchange> exceptionRetry = null;
if (!retryConfig.getExceptions().isEmpty()) {
Predicate<RetryContext<ServerWebExchange>> retryContextPredicate = context -> {
@@ -162,6 +170,14 @@ public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory<Retr
if (backoff != null) {
exceptionRetry = exceptionRetry.backoff(getBackoff(backoff));
}
JitterConfig jitter = retryConfig.getJitter();
if (jitter != null) {
exceptionRetry = exceptionRetry.jitter(getJitter(jitter));
}
Duration timeout = retryConfig.getTimeout();
if (timeout != null) {
exceptionRetry = exceptionRetry.timeout(timeout);
}
}
GatewayFilter gatewayFilter = apply(retryConfig.getRouteId(), statusCodeRepeat, exceptionRetry);
@@ -179,6 +195,9 @@ public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory<Retr
.append("statuses", retryConfig.getStatuses())
.append("methods", retryConfig.getMethods())
.append("exceptions", retryConfig.getExceptions())
.append("backoff", retryConfig.getBackoff())
.append("jitter", retryConfig.getJitter())
.append("timeout", retryConfig.getTimeout())
.toString();
}
};
@@ -203,6 +222,10 @@ public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory<Retr
backoff.basedOnPreviousValue);
}
private Jitter getJitter(JitterConfig jitter) {
return Jitter.random(jitter.randomFactor);
}
public boolean exceedsMaxIterations(ServerWebExchange exchange, RetryConfig retryConfig) {
Integer iteration = exchange.getAttribute(RETRY_ITERATION_KEY);
@@ -291,6 +314,10 @@ public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory<Retr
private BackoffConfig backoff;
private JitterConfig jitter;
private Duration timeout;
public RetryConfig allMethods() {
return setMethods(HttpMethod.values());
}
@@ -303,6 +330,35 @@ public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory<Retr
if (this.backoff != null) {
this.backoff.validate();
}
if (this.jitter != null) {
this.jitter.validate();
}
if (this.timeout != null) {
Assert.isTrue(!timeout.isNegative(), "timeout should be >= 0");
}
}
public Duration getTimeout() {
return timeout;
}
public RetryConfig setTimeout(Duration timeout) {
this.timeout = timeout;
return this;
}
public JitterConfig getJitter() {
return jitter;
}
public RetryConfig setJitter(JitterConfig jitter) {
this.jitter = jitter;
return this;
}
public RetryConfig setJitter(double randomFactor) {
this.jitter = new JitterConfig(randomFactor);
return this;
}
public BackoffConfig getBackoff() {
@@ -433,6 +489,47 @@ public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory<Retr
this.basedOnPreviousValue = basedOnPreviousValue;
}
@Override
public String toString() {
return new ToStringCreator(this).append("firstBackoff", firstBackoff)
.append("maxBackoff", maxBackoff)
.append("factor", factor)
.append("basedOnPreviousValue", basedOnPreviousValue)
.toString();
}
}
public static class JitterConfig {
private double randomFactor = 0.5;
public void validate() {
Assert.isTrue(randomFactor >= 0 && randomFactor <= 1,
"random factor must be between 0 and 1 (default 0.5)");
}
public JitterConfig() {
}
public JitterConfig(double randomFactor) {
this.randomFactor = randomFactor;
}
public double getRandomFactor() {
return randomFactor;
}
public void setRandomFactor(double randomFactor) {
this.randomFactor = randomFactor;
}
@Override
public String toString() {
return new ToStringCreator(this).append("randomFactor", randomFactor).toString();
}
}
}

View File

@@ -109,6 +109,30 @@ public class RetryGatewayFilterFactoryIntegrationTests extends BaseWebClientTest
// @formatter:on
}
@Test
public void retryWithBackoffJitterTimeout() {
// @formatter:off
testClient.get()
.uri("/retry?key=retry-with-backoff-jitter-timeout&count=3")
.header(HttpHeaders.HOST, "www.retrywithbackoffjittertimeout.org")
.exchange()
.expectStatus().isOk()
.expectHeader().value("X-Retry-Count", CoreMatchers.equalTo("3"));
// @formatter:on
}
@Test
public void retryWithBackoffTimeout() {
// backoff > timeout
testClient.get()
.uri("/retry?key=retry-with-backoff-timeout&count=3")
.header(HttpHeaders.HOST, "www.retrywithbackofftimeout.org")
.exchange()
.expectStatus()
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
assertThat(TestConfig.map.get("retry-with-backoff-timeout")).isNotNull().hasValue(2);
}
@Test
public void retryFilterGetJavaDsl() {
testClient.get()
@@ -363,6 +387,21 @@ public class RetryGatewayFilterFactoryIntegrationTests extends BaseWebClientTest
r -> r.host("**.retrywithbackoff.org").filters(f -> f.prefixPath("/httpbin").retry(config -> {
config.setRetries(2).setBackoff(Duration.ofMillis(100), null, 2, true);
})).uri(uri))
.route("retry_with_backoff_jitter_timeout_test", r -> r.host("**.retrywithbackoffjittertimeout.org")
.filters(f -> f.prefixPath("/httpbin").retry(config -> {
config.setRetries(3)
.setBackoff(Duration.ofMillis(50), Duration.ofMillis(100), 2, true)
.setJitter(0.1)
.setTimeout(Duration.ofMillis(1000));
}))
.uri(uri))
.route("retry_with_backoff_timeout_test", r -> r.host("**.retrywithbackofftimeout.org")
.filters(f -> f.prefixPath("/httpbin").retry(config -> {
config.setRetries(3)
.setBackoff(Duration.ofMillis(100), null, 2, true)
.setTimeout(Duration.ofMillis(200));
}))
.uri(uri))
.route("retry_with_loadbalancer",
r -> r.host("**.retrywithloadbalancer.org")