From 7d78365648ec7daa7a59e2611907b10fc136a601 Mon Sep 17 00:00:00 2001 From: Anastasiia Smirnova Date: Sun, 17 Feb 2019 23:51:16 +0200 Subject: [PATCH] Adds exponential backoff config options to retry filter. --- .../main/asciidoc/spring-cloud-gateway.adoc | 24 ++++- .../factory/RetryGatewayFilterFactory.java | 96 +++++++++++++++++++ ...yGatewayFilterFactoryIntegrationTests.java | 22 +++++ 3 files changed, 139 insertions(+), 3 deletions(-) diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 1244d3fa1..e4d5d4509 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -1049,12 +1049,25 @@ spring: When a request is made through the gateway to `/name/bar/foo` the request made to `nameservice` will look like `http://nameservice/foo`. === Retry GatewayFilter Factory -The Retry GatewayFilter Factory takes `retries`, `statuses`, `methods`, and `series` as parameters. + +The Retry GatewayFilter Factory support following set of parameters: * `retries`: the number of retries that should be attempted * `statuses`: the HTTP status codes that should be retried, represented using `org.springframework.http.HttpStatus` * `methods`: the HTTP methods that should be retried, represented using `org.springframework.http.HttpMethod` * `series`: the series of status codes to be retried, represented using `org.springframework.http.HttpStatus.Series` +* `exceptions`: list of exceptions thrown that should be retried +* `backoff`: configured exponential backoff for the retries. Retries are performed after a backoff interval of `firstBackoff * (factor ^ n)` where `n` is the iteration. +If `maxBackoff` is configured, the maximum backoff applied will be limited to `maxBackoff`. +If `basedOnPreviousValue` is true, backoff will be calculated using `prevBackoff * factor`. + +The following defaults are configured for `Retry` filter if enabled: + +* `retries` -- 3 times +* `series` -- 5XX series +* `methods` -- GET method +* `exceptions` -- `IOException` and `TimeoutException` +* `backoff` -- disabled .application.yml [source,yaml] @@ -1072,6 +1085,11 @@ spring: args: retries: 3 statuses: BAD_GATEWAY + backoff: + firstBackoff: 10ms + maxBackoff: 50ms + factor: 2 + basedOnPreviousValue: false ---- NOTE: The retry filter does not currently support retrying with a body (e.g. for POST or PUT requests with a body). @@ -1311,9 +1329,9 @@ To enable Gateway Metrics add spring-boot-starter-actuator as a project dependen * `httpStatusCode`: Http Status of the request returned to the client * `httpMethod`: The Http method used for the request -These metrics are then available to be scraped from ``/actuator/metrics/gateway.requests`` and can be easily integated with Prometheus to create a link:images/gateway-grafana-dashboard.jpeg[Grafana] link:gateway-grafana-dashboard.json[dashboard]. +These metrics are then available to be scraped from ``/actuator/metrics/gateway.requests`` and can be easily integrated with Prometheus to create a link:images/gateway-grafana-dashboard.jpeg[Grafana] link:gateway-grafana-dashboard.json[dashboard]. -NOTE: To enable the pometheus endpoint add micrometer-registry-prometheus as a project dependency. +NOTE: To enable the prometheus endpoint add micrometer-registry-prometheus as a project dependency. === Marking An Exchange As Routed diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java index 567a589d5..7c49b5b6f 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java @@ -17,6 +17,7 @@ package org.springframework.cloud.gateway.filter.factory; import java.io.IOException; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -28,6 +29,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; +import reactor.retry.Backoff; import reactor.retry.Repeat; import reactor.retry.RepeatContext; import reactor.retry.Retry; @@ -105,6 +107,11 @@ public class RetryGatewayFilterFactory statusCodeRepeat = Repeat.onlyIf(repeatPredicate) .doOnRepeat(context -> reset(context.applicationContext())); + + BackoffConfig backoff = retryConfig.getBackoff(); + if (backoff != null) { + statusCodeRepeat = statusCodeRepeat.backoff(getBackoff(backoff)); + } } // TODO: support timeout, backoff, jitter, etc... in Builder @@ -132,6 +139,10 @@ public class RetryGatewayFilterFactory exceptionRetry = Retry.onlyIf(retryContextPredicate) .doOnRetry(context -> reset(context.applicationContext())) .retryMax(retryConfig.getRetries()); + BackoffConfig backoff = retryConfig.getBackoff(); + if (backoff != null) { + exceptionRetry = exceptionRetry.backoff(getBackoff(backoff)); + } } GatewayFilter gatewayFilter = apply(retryConfig.getRouteId(), statusCodeRepeat, @@ -155,6 +166,11 @@ public class RetryGatewayFilterFactory }; } + private Backoff getBackoff(BackoffConfig backoff) { + return Backoff.exponential(backoff.firstBackoff, backoff.maxBackoff, + backoff.factor, backoff.basedOnPreviousValue); + } + public boolean exceedsMaxIterations(ServerWebExchange exchange, RetryConfig retryConfig) { Integer iteration = exchange.getAttribute(RETRY_ITERATION_KEY); @@ -240,6 +256,8 @@ public class RetryGatewayFilterFactory private List> exceptions = toList(IOException.class, TimeoutException.class); + private BackoffConfig backoff; + public RetryConfig allMethods() { return setMethods(HttpMethod.values()); } @@ -251,6 +269,25 @@ public class RetryGatewayFilterFactory || !this.exceptions.isEmpty(), "series, status and exceptions may not all be empty"); Assert.notEmpty(this.methods, "methods may not be empty"); + if (this.backoff != null) { + this.backoff.validate(); + } + } + + public BackoffConfig getBackoff() { + return backoff; + } + + public RetryConfig setBackoff(BackoffConfig backoff) { + this.backoff = backoff; + return this; + } + + public RetryConfig setBackoff(Duration firstBackoff, Duration maxBackoff, + int factor, boolean basedOnPreviousValue) { + this.backoff = new BackoffConfig(firstBackoff, maxBackoff, factor, + basedOnPreviousValue); + return this; } @Override @@ -310,4 +347,63 @@ public class RetryGatewayFilterFactory } + public static class BackoffConfig { + + private Duration firstBackoff = Duration.ofMillis(5); + + private Duration maxBackoff; + + private int factor = 2; + + private boolean basedOnPreviousValue = true; + + public BackoffConfig() { + } + + public BackoffConfig(Duration firstBackoff, Duration maxBackoff, int factor, + boolean basedOnPreviousValue) { + this.firstBackoff = firstBackoff; + this.maxBackoff = maxBackoff; + this.factor = factor; + this.basedOnPreviousValue = basedOnPreviousValue; + } + + public void validate() { + Assert.notNull(this.firstBackoff, "firstBackoff must be present"); + } + + public Duration getFirstBackoff() { + return firstBackoff; + } + + public void setFirstBackoff(Duration firstBackoff) { + this.firstBackoff = firstBackoff; + } + + public Duration getMaxBackoff() { + return maxBackoff; + } + + public void setMaxBackoff(Duration maxBackoff) { + this.maxBackoff = maxBackoff; + } + + public int getFactor() { + return factor; + } + + public void setFactor(int factor) { + this.factor = factor; + } + + public boolean isBasedOnPreviousValue() { + return basedOnPreviousValue; + } + + public void setBasedOnPreviousValue(boolean basedOnPreviousValue) { + this.basedOnPreviousValue = basedOnPreviousValue; + } + + } + } diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactoryIntegrationTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactoryIntegrationTests.java index ca431a138..d5349c3b0 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactoryIntegrationTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactoryIntegrationTests.java @@ -26,6 +26,7 @@ import com.netflix.loadbalancer.Server; import com.netflix.loadbalancer.ServerList; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.hamcrest.CoreMatchers; import org.junit.Test; import org.junit.runner.RunWith; @@ -78,6 +79,18 @@ public class RetryGatewayFilterFactoryIntegrationTests extends BaseWebClientTest }); } + @Test + public void retryWithBackoff() { + // @formatter:off + testClient.get() + .uri("/retry?key=retry-with-backoff&count=3") + .header(HttpHeaders.HOST, "www.retrywithbackoff.org") + .exchange() + .expectStatus().isOk() + .expectHeader().value("X-Retry-Count", CoreMatchers.equalTo("3")); + // @formatter:on + } + @Test public void retryFilterGetJavaDsl() { testClient.get().uri("/retry?key=getjava&count=2") @@ -193,6 +206,15 @@ public class RetryGatewayFilterFactoryIntegrationTests extends BaseWebClientTest .retry(config -> config.setRetries(2) .setMethods(HttpMethod.POST, HttpMethod.GET))) .uri(uri)) + + .route("retry_with_backoff", 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_loadbalancer", r -> r.host("**.retrywithloadbalancer.org") .filters(f -> f.prefixPath("/httpbin")