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

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

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


# 仕組み

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

- [ErrorDecoderインターフェース](https://github.com/OpenFeign/feign/blob/master/core/src/main/java/feign/codec/ErrorDecoder.java)
- [Retryerインターフェース](https://github.com/OpenFeign/feign/blob/master/core/src/main/java/feign/Retryer.java)

## ErrorDecoder

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

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

```java
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](https://github.com/OpenFeign/feign/blob/master/core/src/main/java/feign/RetryableException.java)を投げると、以下の`Retryer.continueOrPropagate`が呼ばれます。

## Retryer

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

デフォルトの`Retryer`が[Retryer.Default](https://github.com/OpenFeign/feign/blob/master/core/src/main/java/feign/Retryer.java#L31)は100msで5回試行するものになっています。

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

# 使い方

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

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

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


# サンプル
前回の[お天気サンプル](https://github.com/totto357/open-feign-client-example)にリトライ処理を追加してみます。

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

```java
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](https://github.com/OpenFeign/feign/blob/master/core/src/main/java/feign/Retryer.java#L31)を少し改変しただけです。

```java
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つの実装クラスを指定します。

```yml
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