#はじめに
とある既存システムの改修でDBコネクション切れを契機とするエラーに対し、Spring-Retryを利用したリトライ処理の実装を行った。
その際に得られた知見・対応策を今後の覚書として残そうと思います。
※既存システムに対する改修という性質故に、書き方としてはあまり良くないかもしれないのはご容赦を。。。
#Spring-Retry導入
build.gradleに下記の依存関係を追加でOK
compile('org.springframework.retry:spring-retry')
compile('org.springframework.boot:spring-boot-starter-aop')
#実装
###1. EnableRetry
対象のSpringアプリケーションのエントリークラス(@SpringBootApplication)に@EnableRetryを追記
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クラスに追記
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を追記
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をつけたメソッドを用意
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メソッドを用意する
// any code..
@Recover
public void recover(Exception e, int id) {
throw e.getCause();
}
これでExhaustedRetryExceptionにネストされた既存の例外を既存のエラーハンドリングに戻すように改修しました。
#参考資料
https://tech.atware.co.jp/spring-retry