Realization of Spring Cloud Gateway Retry Mechanism

  • 2021-07-03 00:09:46
  • OfStack

Preface

Try again, I believe everyone is no stranger. When we call the Http interface, it will always fail for some reason. At this time, we can re-request the interface by retrying.

There are many such cases in life, such as making a phone call, the other party is on the phone, the signal is not good, and so on. You will always be unable to get through. When you fail to get through for the first time, you will call for the second time, the third time... and the fourth time.

Retry should also pay attention to the application scenario. The interface for reading data is more suitable for retry scenario, and the interface for writing data should pay attention to the idempotence of the interface. Also, if the number of retries is too much, it will double the number of requests, which will cause greater pressure on the back end. Setting a reasonable retry mechanism is the most critical.

Today, let's take a brief look at the retry mechanism and use in Spring Cloud Gateway.

Use explanation

RetryGatewayFilter is one GatewayFilter Factory provided by Spring Cloud Gateway for request retry.

Configuration mode:


spring:
 cloud:
  gateway:
   routes:
   - id: fsh-house
    uri: lb://fsh-house
    predicates:
    - Path=/house/**
    filters:
    - name: Retry
     args:
      retries: 3
      series:
      - SERVER_ERROR
      statuses:
      - OK
      methods:
      - GET
      - POST
      exceptions:
      - java.io.IOException

Configuration explanation

Configuration class source code org. springframework. cloud. gateway. filter. factory. RetryGatewayFilterFactory. RetryConfig:


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

retries: Number of retries, default is 3

series: Status code configuration (segmentation), the retry logic will be carried out only when a certain section of status code is matched. The default value is SERVER_ERROR, and the value is 5, that is, 5XX (status code beginning with 5), with a total of 5 values:


public enum Series {
  INFORMATIONAL(1),
  SUCCESSFUL(2),
  REDIRECTION(3),
  CLIENT_ERROR(4),
  SERVER_ERROR(5);
}

statuses: Configuration of status code. Different from series, here is the configuration of specific status code. Please refer to: org. springframework. http. HttpStatus

methods: Specifies which method requests require retry logic. The default value is the GET method with the following values:


public enum HttpMethod {
  GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE;
}

exceptions: Specifies which exceptions require retry logic, default is java. io. IOException

Code testing

Just write an interface, record the number of requests in the interface, and then throw an exception simulation 500, and access this interface through the gateway. If you configure the retry number to be 3, the interface will output 4 results, which is correct, proving that the retry is effective.


AtomicInteger ac = new AtomicInteger();

@GetMapping("/data")
public HouseInfo getData(@RequestParam("name") String name) {
  if (StringUtils.isBlank(name)) {
    throw new RuntimeException("error");
  }
  System.err.println(ac.addAndGet(1));
  return new HouseInfo(1L, " Shanghai ", " Hongkou ", "XX Community ");
}

More Spring Cloud codes are available at: https://github.com/yinjihuan/spring-cloud

Appreciation of source code


  @Override
  public GatewayFilter apply(RetryConfig retryConfig) {
    //  Verify that the retry configuration is formatted correctly 
    retryConfig.validate();

    Repeat<ServerWebExchange> statusCodeRepeat = null;
    if (!retryConfig.getStatuses().isEmpty() || !retryConfig.getSeries().isEmpty()) {
      Predicate<RepeatContext<ServerWebExchange>> repeatPredicate = context -> {
        ServerWebExchange exchange = context.applicationContext();
        //  Determines whether the number of retries has reached the configured maximum 
        if (exceedsMaxIterations(exchange, retryConfig)) {
          return false;
        }
        //  Gets the status code of the response 
        HttpStatus statusCode = exchange.getResponse().getStatusCode();
        //  Get the request method type 
        HttpMethod httpMethod = exchange.getRequest().getMethod();
        //  Determining whether the response status code exists in the configuration 
        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));
        }
        //  Determine whether the method is included in the configuration 
        boolean retryableMethod = retryConfig.getMethods().contains(httpMethod);
        //  Decide whether to retry 
        return retryableMethod && retryableStatusCode;
      };

      statusCodeRepeat = Repeat.onlyIf(repeatPredicate)
          .doOnRepeat(context -> reset(context.applicationContext()));
    }

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

    Retry<ServerWebExchange> exceptionRetry = null;
    if (!retryConfig.getExceptions().isEmpty()) {
      Predicate<RetryContext<ServerWebExchange>> retryContextPredicate = context -> {
        if (exceedsMaxIterations(context.applicationContext(), retryConfig)) {
          return false;
        }
        //  Abnormal judgment 
        for (Class<? extends Throwable> clazz : retryConfig.getExceptions()) {       
          if (clazz.isInstance(context.exception())) {
            return true;
          }
        }
        return false;
      };
      //  Use reactor extra Adj. retry Component 
      exceptionRetry = Retry.onlyIf(retryContextPredicate)
          .doOnRetry(context -> reset(context.applicationContext()))
          .retryMax(retryConfig.getRetries());
    }


    return apply(statusCodeRepeat, exceptionRetry);
  }

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

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

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

      // chain.filter returns a Mono<Void>
      Publisher<Void> publisher = chain.filter(exchange)
          //.log("retry-filter", Level.INFO)
          .doOnSuccessOrError((aVoid, throwable) -> {
            //  Gets the number of times that has been retried, the default value is -1
            int iteration = exchange.getAttributeOrDefault(RETRY_ITERATION_KEY, -1);
            //  Increase the number of retries 
            exchange.getAttributes().put(RETRY_ITERATION_KEY, iteration + 1);
          });

      if (retry != null) {
        // retryWhen returns a Mono<Void>
        // retry needs to go before repeat
        publisher = ((Mono<Void>)publisher).retryWhen(retry.withApplicationContext(exchange));
      }
      if (repeat != null) {
        // repeatWhen returns a Flux<Void>
        // so this needs to be last and the variable a Publisher<Void>
        publisher = ((Mono<Void>)publisher).repeatWhen(repeat.withApplicationContext(exchange));
      }

      return Mono.fromDirect(publisher);
    };
  }


Related articles: