From 3bc54ab82cf2272d2ca4149a4494f37ef04ebceb Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Wed, 14 Feb 2018 23:30:13 -0500 Subject: [PATCH] Configuration for retry filter. --- .../factory/RetryGatewayFilterFactory.java | 130 ++++++++++++++++-- .../route/builder/GatewayFilterSpec.java | 37 ++++- 2 files changed, 152 insertions(+), 15 deletions(-) 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 82a2710c..c468bc51 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,30 +17,138 @@ package org.springframework.cloud.gateway.filter.factory; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.function.Predicate; -import org.springframework.http.HttpMethod; import reactor.retry.DefaultRepeat; import reactor.retry.Repeat; import reactor.retry.RepeatContext; import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatus.Series; import org.springframework.tuple.Tuple; +import org.springframework.util.Assert; import org.springframework.web.server.ServerWebExchange; public class RetryGatewayFilterFactory implements GatewayFilterFactory { @Override public GatewayFilter apply(Tuple args) { - return (exchange, chain) -> { - Predicate> predicate = context -> { - ServerWebExchange ex = (ServerWebExchange) context.applicationContext(); - boolean retryableStatusCode = ex.getResponse().getStatusCode().is5xxServerError(); - boolean retryableMethod = ex.getRequest().getMethod().equals(HttpMethod.GET); - return retryableMethod && retryableStatusCode; - }; - Repeat repeat = DefaultRepeat.create(predicate, 4) - .withApplicationContext(exchange); - return chain.filter(exchange).repeatWhen(repeat).next(); + Retry retry = new Retry(); + + if (args.hasFieldName("retries")) { + retry.retries(args.getInt("retries")); + } + + // TODO: list of statusSeries + if (args.hasFieldName("statusSeries")) { + int statusSeries = args.getInt("statusSeries"); + retry.series(Series.valueOf(statusSeries)); + } + + // TODO: list of status + if (args.hasFieldName("status")) { + retry.statuses(ServerWebExchangeUtils.parse(args.getRawString("status"))); + } + + // TODO: list of methods + if (args.hasFieldName("method")) { + retry.methods(HttpMethod.resolve(args.getString("method").toUpperCase())); + } + + return apply(retry); + } + + public GatewayFilter apply(Retry retry) { + retry.validate(); + + Predicate> predicate = context -> { + ServerWebExchange exchange = context.applicationContext(); + HttpStatus statusCode = exchange.getResponse().getStatusCode(); + HttpMethod httpMethod = exchange.getRequest().getMethod(); + + boolean retryableStatusCode = retry.getStatuses().contains(statusCode); + + if (!retryableStatusCode) { + // try the series + retryableStatusCode = retry.getSeries().stream() + .anyMatch(series -> statusCode.series().equals(series)); + } + + boolean retryableMethod = retry.getMethods().contains(httpMethod); + return retryableMethod && retryableStatusCode; }; + + //TODO: use Repeat statics once updated with a create() like method + Repeat repeat = DefaultRepeat.create(predicate, retry.getRetries()); + + //TODO: support timeout, backoff, jitter, etc... in Builder + return apply(repeat); + } + + public GatewayFilter apply(Repeat repeat) { + return (exchange, chain) -> chain.filter(exchange).repeatWhen( + repeat.withApplicationContext(exchange)).next(); + } + + public static class Retry { + private int retries = 3; + + private List series = Collections.singletonList(Series.SERVER_ERROR); + + private List statuses = Collections.emptyList(); + + private List methods = Collections.singletonList(HttpMethod.GET); + + public Retry retries(int retries) { + this.retries = retries; + return this; + } + + public Retry series(Series... series) { + this.series = Arrays.asList(series); + return this; + } + + public Retry statuses(HttpStatus... statuses) { + this.statuses = Arrays.asList(statuses); + return this; + } + + public Retry methods(HttpMethod... methods) { + this.methods = Arrays.asList(methods); + return this; + } + + public Retry allMethods() { + return methods(HttpMethod.values()); + } + + public void validate() { + Assert.isTrue(this.retries > 0, "retries must be greater than 0"); + Assert.isTrue(!this.series.isEmpty() || !this.statuses.isEmpty(), + "series and status may not both be empty"); + Assert.notEmpty(this.methods, "methods may not be empty"); + } + + public int getRetries() { + return retries; + } + + public List getSeries() { + return series; + } + + public List getStatuses() { + return statuses; + } + + public List getMethods() { + return methods; + } } } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java index 74dafbd7..9d89186f 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java @@ -22,8 +22,11 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; +import com.netflix.hystrix.HystrixObservableCommand; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import reactor.retry.Repeat; + import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.OrderedGatewayFilter; @@ -50,10 +53,10 @@ import org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilter import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; import org.springframework.cloud.gateway.route.Route; import org.springframework.core.Ordered; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.tuple.Tuple; - -import com.netflix.hystrix.HystrixObservableCommand; +import org.springframework.web.server.ServerWebExchange; import static org.springframework.tuple.TupleBuilder.tuple; @@ -203,8 +206,34 @@ public class GatewayFilterSpec extends UriSpec { return filter(getBean(RewritePathGatewayFilterFactory.class).apply(regex, replacement)); } - public GatewayFilterSpec retry() { - return filter(getBean(RetryGatewayFilterFactory.class).apply(EMPTY_TUPLE)); + /** + * 5xx errors and GET are retryable + * @param retries max number of retries + */ + public GatewayFilterSpec retry(int retries) { + return filter(getBean(RetryGatewayFilterFactory.class) + .apply(new RetryGatewayFilterFactory.Retry() + .retries(retries))); + } + + /** + * @param retries max number of retries + * @param httpStatusSeries the http status series that is retryable + * @param httpMethod the http method that is retryable + */ + public GatewayFilterSpec retry(int retries, HttpStatus.Series httpStatusSeries, HttpMethod httpMethod) { + return retry(new RetryGatewayFilterFactory.Retry() + .retries(retries) + .series(httpStatusSeries) + .methods(httpMethod)); + } + + public GatewayFilterSpec retry(RetryGatewayFilterFactory.Retry retry) { + return filter(getBean(RetryGatewayFilterFactory.class).apply(retry)); + } + + public GatewayFilterSpec retry(Repeat repeat) { + return filter(getBean(RetryGatewayFilterFactory.class).apply(repeat)); } public GatewayFilterSpec secureHeaders() {