Spring Cloud Gateway再試験機構の実現


前言
もう一度試してみます。みんなはよく知っていると思います。私たちがHttpインターフェースを呼び出す時、いつも何らかの理由で呼び出しが失敗しました。この時、私たちはもう一度試してみます。インターフェースを再要求します。
生活の中でこのような事例が多いです。例えば電話をかけています。相手は通話中です。電波が悪いからです。
再試行も応用シーンに注意してください。データを読むインターフェースは再試行の場面に適しています。データを書くインターフェースはインタフェースのべき乗などに注意しなければなりません。また、再試行の回数が多すぎると要求量が倍になり、バックエンドに大きなストレスを与え、合理的な再試行体制を設けることが一番重要です。
今日はSpring Cloud Gatewayにおける再試行機構と使用について簡単に調べてみます。
使用説明
RetryGatewayFilterは、Spring Cloud Gatewayが再試行を要求して提供するGateway Filter Factoryである。
設定:

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
説明の配置
配置のソースコードorg.springframe eweet.cloud.gateway.filter.factory.RetryGateway FilterFactory.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:再試行回数、デフォルト値は3回です。
series:状態コードの配置(セグメント)は、該当する部分の状態コードが再試行論理を行います。標準値はSERVER(u)です。ERROR、値は5、つまり5 XX(5先頭の状態コード)で、5つの値があります。

public enum Series {
  INFORMATIONAL(1),
  SUCCESSFUL(2),
  REDIRECTION(3),
  CLIENT_ERROR(4),
  SERVER_ERROR(5);
}
statuses:状態コードの配置は、seriesとは違ってこちらが具体的な状態コードの配置です。値を取ってください。
methods:どのような方法を指定する要求が再試行ロジックを必要としますか?

public enum HttpMethod {
  GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE;
}
exceptions:どの異常を指定するかを再試行するロジックで、デフォルト値はjava.io.IOExceptionです。
コードテスト
インターフェースを書き込み、要求回数をインターフェースに記録し、異常シミュレーション500を抛り出し、ゲートウェイを通じてこのインターフェースにアクセスする場合、再試行回数が3であるとインターフェースから4回の結果が出力されます。再試行の有効性が証明されます。

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, "  ", "  ", "XX  ");
}

もっと多くのSpring Cloudコードがあります。https://github.com/yinjihuan/spring-cloud
ソースの鑑賞

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

    Repeat<ServerWebExchange> statusCodeRepeat = null;
    if (!retryConfig.getStatuses().isEmpty() || !retryConfig.getSeries().isEmpty()) {
      Predicate<RepeatContext<ServerWebExchange>> repeatPredicate = 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;
      };

      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;
        }
        //     
        for (Class<? extends Throwable> clazz : retryConfig.getExceptions()) {       
          if (clazz.isInstance(context.exception())) {
            return true;
          }
        }
        return false;
      };
      //   reactor extra retry  
      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) -> {
            //          ,    -1
            int iteration = exchange.getAttributeOrDefault(RETRY_ITERATION_KEY, -1);
            //       
            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);
    };
  }

以上が本文の全部です。皆さんの勉強に役に立つように、私たちを応援してください。