Talk about RetryGatewayFilter of spring cloud gateway.

  springcloud

Order

This article mainly studies spring cloud gateway’s RetryGatewayFilter.

GatewayAutoConfiguration

spring-cloud-gateway-core-2.0.0.RC2-sources.jar! /org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java

@Configuration
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
@EnableConfigurationProperties
@AutoConfigureBefore(HttpHandlerAutoConfiguration.class)
@AutoConfigureAfter({GatewayLoadBalancerClientAutoConfiguration.class, GatewayClassPathWarningAutoConfiguration.class})
@ConditionalOnClass(DispatcherHandler.class)
public class GatewayAutoConfiguration {
    //......
    @Bean
    public RetryGatewayFilterFactory retryGatewayFilterFactory() {
        return new RetryGatewayFilterFactory();
    }
    //......
}

RetryGatewayFilterFactory is enabled by default

RetryGatewayFilterFactory

spring-cloud-gateway-core-2.0.0.RC2-sources.jar! /org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java

public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory<RetryGatewayFilterFactory.RetryConfig> {
    private static final Log log = LogFactory.getLog(RetryGatewayFilterFactory.class);

    public RetryGatewayFilterFactory() {
        super(RetryConfig.class);
    }

    @Override
    public GatewayFilter apply(RetryConfig retryConfig) {
        retryConfig.validate();

        Predicate<? super RepeatContext<ServerWebExchange>> predicate = context -> {
            ServerWebExchange exchange = context.applicationContext();
            if (exceedsMaxIterations(exchange, retryConfig)) {
                return false;
            }

            HttpStatus statusCode = exchange.getResponse().getStatusCode();
            HttpMethod httpMethod = exchange.getRequest().getMethod();

            boolean retryableStatusCode = retryConfig.getStatuses().contains(statusCode);

            if (!retryableStatusCode && statusCode != null) { // null status code might mean a network exception?
                // try the series
                retryableStatusCode = retryConfig.getSeries().stream()
                        .anyMatch(series -> statusCode.series().equals(series));
            }

            boolean retryableMethod = retryConfig.getMethods().contains(httpMethod);
            return retryableMethod && retryableStatusCode;
        };

        Repeat<ServerWebExchange> repeat = Repeat.onlyIf(predicate)
                .doOnRepeat(context -> reset(context.applicationContext()));

        //TODO: support timeout, backoff, jitter, etc... in Builder

        Predicate<RetryContext<ServerWebExchange>> retryContextPredicate = context -> {
            if (exceedsMaxIterations(context.applicationContext(), retryConfig)) {
                return false;
            }

            for (Class<? extends Throwable> clazz : retryConfig.getExceptions()) {
                if (clazz.isInstance(context.exception())) {
                    return true;
                }
            }
            return false;
        };

        Retry<ServerWebExchange> reactorRetry = Retry.onlyIf(retryContextPredicate)
                .doOnRetry(context -> reset(context.applicationContext()))
                .retryMax(retryConfig.getRetries());
        return apply(repeat, reactorRetry);
    }

    public boolean exceedsMaxIterations(ServerWebExchange exchange, RetryConfig retryConfig) {
        Integer iteration = exchange.getAttribute("retry_iteration");

        //TODO: deal with null iteration
        return iteration != null && iteration >= retryConfig.getRetries();
    }

    public void reset(ServerWebExchange exchange) {
        //TODO: what else to do to reset SWE?
        exchange.getAttributes().remove(ServerWebExchangeUtils.GATEWAY_ALREADY_ROUTED_ATTR);
    }

    @Deprecated
    public GatewayFilter apply(Repeat<ServerWebExchange> repeat) {
        return apply(repeat, Retry.onlyIf(ctxt -> false));
    }

    public GatewayFilter apply(Repeat<ServerWebExchange> repeat, Retry<ServerWebExchange> retry) {
        return (exchange, chain) -> {
            log.trace("Entering retry-filter");

            int iteration = exchange.getAttributeOrDefault("retry_iteration", -1);
            exchange.getAttributes().put("retry_iteration", iteration + 1);

            return Mono.fromDirect(chain.filter(exchange)
                    .log("retry-filter", Level.INFO)
                    .retryWhen(retry.withApplicationContext(exchange))
                    .repeatWhen(repeat.withApplicationContext(exchange)));
        };
    }
    //......
}
  • It can be seen that this filter uses the Retry component of reactor and adds retry_iteration to the attribues of exchange to record the number of retri es. this value starts from -1 by default, and when it is first executed, retry_iteration+1 is 0. After each retry, add 1.
  • Filter’s application receives two parameters, one is Repeat<ServerWebExchange > and the other is Retry<ServerWebExchange >.
  • The difference between repeat and retry is that repeat retries when it is onCompleted, while retry retries when it is onError. Since it is not always possible to try again when it is abnormal, repeat is added here.

RetryConfig

spring-cloud-gateway-core-2.0.0.RC2-sources.jar! /org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java

    public static class RetryConfig {
        private int retries = 3;
        
        private List<Series> series = toList(Series.SERVER_ERROR);
        
        private List<HttpStatus> statuses = new ArrayList<>();
        
        private List<HttpMethod> methods = toList(HttpMethod.GET);

        private List<Class<? extends Throwable>> exceptions = toList(IOException.class);

        //......

        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");
        }
        //......

    }

You can see that the configuration file has 5 attributes, as follows:

  • Retries, which is 3 by default, identifies the number of retries
  • Series, used to specify which segments of the status code need to be retried, the default SERVER_ERROR is 5xx
  • Statuses, used to specify which states need to be retried. The default is blank. It must specify at least one with series.
  • Methods, used to specify that requests for those methods need to be retried; the default is GET
  • Exceptions, used to specify which exceptions need to be retried, default is java.io.IOException

Example

spring:
  cloud:
    gateway:
      routes:
      - id: retry-demo
        uri: http://localhost:9090
        predicates:
        - Path=/retry/**
        filters:
        - name: Retry
          args:
           retries: 15
           series:
            - SERVER_ERROR
            - CLIENT_ERROR
           methods:
            - GET
            - POST
           exceptions:
            - java.io.IOException
            - java.util.concurrent.TimeoutException

Here, the GET or POST methods for 4xx and 5xx, or IOException or TimeoutException are specified for retry, with the number of retries being 15.

Summary

RetryGatewayFilter retried with the help of the retry component of reactor-addons, mainly using Mono’s repeatWhen and retryWhen methods, the former triggered when onCompleted and the latter when onError.

doc