Spring-Retry 攻略記

#はじめに
とある既存システムの改修でDBコネクション切れを契機とするエラーに対し、Spring-Retryを利用したリトライ処理の実装を行った。
その際に得られた知見・対応策を今後の覚書として残そうと思います。

※既存システムに対する改修という性質故に、書き方としてはあまり良くないかもしれないのはご容赦を。。。

#Spring-Retry導入
build.gradleに下記の依存関係を追加でOK

```build.gradle
compile('org.springframework.retry:spring-retry')
compile('org.springframework.boot:spring-boot-starter-aop')
```

#実装
###1. EnableRetry
対象のSpringアプリケーションのエントリークラス(@SpringBootApplication)に@EnableRetryを追記

```RetrySampleApplication.java
package retrysample.app

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.retry.annotation.EnableRetry;

@SpringBootApplication
@EnableRetry
public class RetrySampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}
```

もしくは@Configurationクラスに追記

```RetrySampleConfig.java
package retrysample.app.config

import org.springframework.context.annotation.Configuration;
import org.springframework.retry.annotation.EnableRetry;

@Configuration
@EnableRetry
public class RetrySampleConfig {
    // any code...
}
```

###2. Retryable
特定の例外発生時にリトライさせたい@Controllerもしくは@Serviceのメソッドに@Retryableを追記

```RetrySampleService.java
package retrysample.app.service

import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

@Service
public class RetrySampleService {

    @Retryable(value = {CannotCreateTransactionException.class, TransactionSystemException.class}, maxAttempts = 3, backoff = @Backoff(delay = 500))
    public void retrySampleService() {
        // any code...
    }
}
```

この時@Retryableに下記のような設定が出来ます。

* value(include)：リトライ対象とする例外クラス、コンマ区切りで複数指定可能
* exclude：上記のvalue(include)で指定した例外のうちリトライ対象外とする例外クラス
* maxAttempts：最大リトライ回数
* backoff：リトライ間隔、単位はミリ秒

###3. Recover
指定した回数リトライ後に@Retryableで指定した例外が起きた時の独自処理を行う場合は、@Recoverをつけたメソッドを用意

```RetrySampleService.java
package retrysample.app.service

import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

@Service
public class RetrySampleService {

    @Retryable(value = {CannotCreateTransactionException.class, TransactionSystemException.class}, maxAttempts = 3, backoff = @Backoff(delay = 500))
    public void retrySampleService(int id) {
        // any code...
    }

    // any code..

    @Recover
    public void recover(CannotCreateTransactionException e, int id) {
        // any code...
    }

    @Recover
    public void recover(TransactionSystemException e, int id) {
        // any code...
    }
}
```

この@Recoverメソッドの注意点として

* @Retryableメソッドと同一クラスもしくはスーパークラスに書く必要がある
* @Retryableメソッドと戻り値の型を一致させる必要がある

また、@Retryableメソッドの引数と@Recoverメソッドの引数に利用することも出来ます。(エラーログ出すときに便利)

#個人的な気付き・躓き
これは私自身の思い込み・勘違いに拠るところが大きいのですが。。。

実装後に単体試験を実施したところ、過去に実施した異常系で失敗するようになってしまいました。
「想定してた例外と違う例外吐いてるぞ！」と怒られていたようなのでspring-Retry関連の資料をよく読んでみたところ

**@Recoverで用意していない例外が@Retryableメソッド内で発生した場合は当該の例外がネストされたExhaustedRetryExceptionが発生する**

ことを単体試験の段階で知りました。
@Retryableのvalueで設定していない例外が発生した場合はこれまで通りのエラーハンドリングをしてくれるとばかり思い込んでいたこともあり、前調査の不足を痛感することとなりました。。。

```
Account accountInfo = accountRepository.findById(int id);
if(Objects.isNull(accountInfo)) {
    throw new AppUniqueException("アカウントが存在しません", e);
}
```

このような対象アカウント存在チェック時の例外で最終的にスローされるのは、AppUniqueExceptionではなくAppUniqueExceptionをネストしたExhaustedRetryExceptionだったのです。

```
org.springframework.retry.ExhaustedRetryException: Cannot locate recovery method; nested exception is retrysample.app.exception.AppUniqueException: アカウントが存在しません
```

####対応策
既存のエラーハンドラーにスローする用の@Recoverメソッドを用意する

```RetrySampleService.java

// any code..

@Recover
public void recover(Exception e, int id) {
    throw e.getCause();
}
```

これでExhaustedRetryExceptionにネストされた既存の例外を既存のエラーハンドリングに戻すように改修しました。

#参考資料
https://tech.atware.co.jp/spring-retry

https://github.com/spring-projects/spring-retry

https://docs.spring.io/spring-retry/docs/api/current/overview-summary.html