diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/retry-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/retry-factory.adoc index b9b9abe5..b593ac00 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/retry-factory.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway/gatewayfilter-factories/retry-factory.adoc @@ -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 ---- diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java index 814a6ecf..f073796d 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java @@ -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 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 exceptionRetry = null; if (!retryConfig.getExceptions().isEmpty()) { Predicate> retryContextPredicate = context -> { @@ -162,6 +170,14 @@ public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory= 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= 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(); + + } + } } diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactoryIntegrationTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactoryIntegrationTests.java index 192411b1..fb1439d1 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactoryIntegrationTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactoryIntegrationTests.java @@ -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")