Java
spring
spring-boot
OpenFeign

Feign(OpenFeign)でリトライ処理をする

前回の記事ではアノテーションだけでHTTPクライアントを作成する方法を紹介しましたが、実際のHTTPクライアントではリトライをする必要が出てくる場面があります。
実際RestTemplateを持つようなHTTPクライアントクラスを作成してそこでリトライ処理をしているプロジェクトなども多いのではないでしょうか

このリトライ処理をOpenFeignでも実現してみます。

仕組み

OpenFeignのリトライ処理は2つのインターフェースを実装することで実現できます。

ErrorDecoder

ErrorDecoderはHTTPリクエストで200番台以外のレスポンスが返ってきた際にどのようなExceptionを投げるのか決めるためのインターフェースです。

例として400の時にIllegalArgumentExceptionを返すクラスを実装してみます。

class IllegalArgumentExceptionOn404Decoder implements ErrorDecoder {
    @Override
    public Exception decode(String methodKey, Response response) {
        if (response.status() == 400) {
            throw new IllegalArgumentException("400だよー。");
        }
        return new ErrorDecoder.Default().decode(methodKey, response);
    }
}

基本的にはresponse.status()をみて動作を変えるようになるかと思います。
decodeの内部でRetryableExceptionを投げると、以下のRetryer.continueOrPropagateが呼ばれます。

Retryer

RetryerRetryableExceptionが投げられた際に呼ばれ、リトライ間隔や回数などを管理するためのインターフェースです。

デフォルトのRetryerRetryer.Defaultは100msで5回試行するものになっています。

リトライ間隔、回数だけを変えるだけであればRetryer.Defaultで十分かと思います。

使い方

上記の2つのインターフェースの実行クラスをapplication.ymlに指定します。

feign:
  client:
    config:
      {{yourFeignName}}:
        errorDecode: com.example.feign.MyErrorDecoder
        retryer: com.example.feign.MyRetryer

yourFeignNameにはFeignClient.nameで指定したものを設定します。
FeignClient.nameを分けるとname毎にリトライ処理を分けることができます。

サンプル

前回のお天気サンプルにリトライ処理を追加してみます。

MyErrorDecoder.java

このサンプルでは503, 504のときにリトライするようにしています。

package com.example.ofc.feign;

import org.springframework.web.client.RestClientException;

import feign.Response;
import feign.RetryableException;
import feign.codec.ErrorDecoder;

public class MyErrorDecoder implements ErrorDecoder {

    @Override
    public Exception decode(String methodKey, Response response) {
        RestClientException cause = new RestClientException(response.toString());

        final int status = response.status();
        if (status == 503 || status == 504) {
            return new RetryableException(methodKey, cause, null);
        }

        return cause;
    }

}

MyRetryer.java

Retryer.Defaultを少し改変しただけです。

package com.example.ofc.feign;

import static java.util.concurrent.TimeUnit.SECONDS;

import feign.RetryableException;
import feign.Retryer;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class MyRetryer implements Retryer {

    private final int maxAttempts;
    private final long period;
    private final long maxPeriod;
    int attempt;
    long sleptForMillis;

    public MyRetryer() {
        this(100, SECONDS.toMillis(1), 3);
    }

    public MyRetryer(long period, long maxPeriod, int maxAttempts) {
        this.period = period;
        this.maxPeriod = maxPeriod;
        this.maxAttempts = maxAttempts;
        this.attempt = 1;
    }

    // visible for testing;
    protected long currentTimeMillis() {
        return System.currentTimeMillis();
    }

    @Override
    public void continueOrPropagate(RetryableException e) {
        log.info("リトライ処理");
        if (attempt++ >= maxAttempts) {
            throw e;
        }

        long interval;
        if (e.retryAfter() != null) {
            interval = e.retryAfter().getTime() - currentTimeMillis();
            if (interval > maxPeriod) {
                interval = maxPeriod;
            }
            if (interval < 0) {
                return;
            }
        } else {
            interval = nextMaxInterval();
        }
        try {
            Thread.sleep(interval);
        } catch (InterruptedException ignored) {
            Thread.currentThread().interrupt();
            throw e;
        }
        sleptForMillis += interval;
    }

    /**
     * Calculates the time interval to a retry attempt. <br>
     * The interval increases exponentially
     * with each attempt, at a rate of nextInterval *= 1.5 (where 1.5 is the
     * backoff factor), to the
     * maximum interval.
     *
     * @return time in nanoseconds from now until the next attempt.
     */
    long nextMaxInterval() {
        long interval = (long) (period * Math.pow(1.5, attempt - 1));
        return interval > maxPeriod ? maxPeriod : interval;
    }

    @Override
    public Retryer clone() {
        return new MyRetryer(period, maxPeriod, maxAttempts);
    }
}

application.yml

上記2つの実装クラスを指定します。

server:
  port: 8088
  application:
    name: open-feign-client

feign:
  client:
    config:
      weather:
        errorDecoder: com.example.ofc.feign.MyErrorDecoder
        retryer: com.example.ofc.feign.MyRetryer

まとめ

OpenFeignでリトライ処理を実現してみました。
APIクライアント自体には手を入れずに実現できるので、改修もしやすそうですね。

今回のサンプルもこちら
https://github.com/totto357/open-feign-client-example