Spring Retryを試す
Spring Retryを試してみたメモです。
Spring Retryはリトライ機能を提供するspring projectです。
springの公式に独立したprojectのドキュメントは無いようでした。
GitHubのREADMEと、spring-batchの公式を参考にしてみます。
https://github.com/spring-projects/spring-retry
https://docs.spring.io/spring-batch/trunk/reference/html/retry.html
下記のバージョンで試してみます。
依存関係
dependencyにspring-retryを追加します。
後ほどAOPも試すので、spring-boot-starter-aopも追加しました。
個別のversionは指定せずにparentでversionを指定します。
pom.xml
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.4</version> <scope>provided</scope> </dependency> </dependencies>
RetryTemplate
RetryTemplateはリトライ操作のテンプレートクラスです。
RetryTemplate (Spring Retry 1.2.2.RELEASE API)
色々なメソッドがありますが、単純にリトライ操作を指定する下記メソッドを使用してみます。
T execute(RetryCallback<T,E> retryCallback)
RetryCallbackを実装します。
RetryCallbackは単純にリトライする操作を定義するだけのインターフェースです。
public interface RetryCallback<T> { T doWithRetry(RetryContext context) throws Throwable; }
下記のような必ず失敗するメソッドを用意します。
public User getUserUnsuccessfully() throws UserNotFoundException { throw new UserNotFoundException(); }
RetryCallbackを実装して、上記のメソッドを呼び出してリトライしてみます。
デフォルトではすべてのExceptionでリトライされます。
デフォルトでは初回の実行も含めて3回リトライします。(1回初期実行 + 2回リトライ)
public void execute() throws Throwable { RetryTemplate template = new RetryTemplate(); User user = template.execute((RetryCallback<User, Throwable>) retryContext -> { System.out.println(LocalDateTime.now() + " retry=" + retryContext.getRetryCount()); return getUserUnsuccessfully(); }); System.out.println(user); }
実行。
計3回実行されているのがわかります。(1回初期実行 + 2回リトライ)
2019-01-20T23:34:22.137 retry=0 2019-01-20T23:34:22.138 retry=1 2019-01-20T23:34:22.138 retry=2 com.example.retry.spring.springretryexample.service.MyService$UserNotFoundException at com.example.retry.spring.springretryexample.service.MyServiceWithRetryTemplate.getUserUnsuccessfully(MyServiceWithRetryTemplate.java:215) : :
RetryPolicy
RetryPolicyでリトライを行うかどうかの条件を定義することが出来ます。
https://github.com/spring-projects/spring-retry#retry-policies
SimpleRetryPolicyではリトライ回数を指定できます。
SimpleRetryPolicy (Spring Retry 1.2.2.RELEASE API)
public void executeWithRetryPolicy() throws Throwable { RetryTemplate template = new RetryTemplate(); SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(5); template.setRetryPolicy(retryPolicy); User user = template.execute((RetryCallback<User, Throwable>) retryContext -> { System.out.println(LocalDateTime.now() + " retry=" + retryContext.getRetryCount()); return getUserUnsuccessfully(); }); System.out.println(user); }
実行。
5回リトライされていることが分かります。
2019-01-20T23:44:47.369 retry=0 2019-01-20T23:44:47.370 retry=1 2019-01-20T23:44:47.370 retry=2 2019-01-20T23:44:47.370 retry=3 2019-01-20T23:44:47.370 retry=4 com.example.retry.spring.springretryexample.service.MyService$UserNotFoundException at com.example.retry.spring.springretryexample.service.MyServiceWithRetryTemplate.getUserUnsuccessfully(MyServiceWithRetryTemplate.java:215) : :
BackOffPolicy
BackOffPolicyでリトライ時の待機時間(backoff)を指定出来ます。
ExponentialBackOffPolicyは指数関数的にbackoffが変化していくクラスです。
ExponentialBackOffPolicy (Spring Retry 1.2.2.RELEASE API)
setInitialInterval()で初回のリトライのsleep時間を設定します。
デフォルトではリトライするたびに2倍ずつsleep時間が長くなります。
setInitialInterval()で1秒を指定してみます。
public void executeWithInitialInterval() throws Throwable { RetryTemplate template = new RetryTemplate(); SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(5); ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy(); backOffPolicy.setInitialInterval(1_000L); template.setRetryPolicy(retryPolicy); template.setBackOffPolicy(backOffPolicy); User user = template.execute((RetryCallback<User, Throwable>) retryContext -> { System.out.println(LocalDateTime.now() + " retry=" + retryContext.getRetryCount()); return getUserUnsuccessfully(); }); System.out.println(user); }
実行。
リトライする度に、sleep時間が1秒、2秒、4秒、8秒と倍になっているのがわかります。
2019-01-20T23:45:31.303 retry=0 2019-01-20T23:45:32.304 retry=1 2019-01-20T23:45:34.304 retry=2 2019-01-20T23:45:38.305 retry=3 2019-01-20T23:45:46.306 retry=4 com.example.retry.spring.springretryexample.service.MyService$UserNotFoundException at com.example.retry.spring.springretryexample.service.MyServiceWithRetryTemplate.getUserUnsuccessfully(MyServiceWithRetryTemplate.java:215) : :
setMultiplier()を設定するとoffsetを何倍にしていくかを設定出来ます。
setMultiplier(1.0)で1倍に指定してみます。
public void executeWithMultiplier() throws Throwable { RetryTemplate template = new RetryTemplate(); SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(5); ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy(); backOffPolicy.setInitialInterval(1_000L); backOffPolicy.setMultiplier(1.0); template.setRetryPolicy(retryPolicy); template.setBackOffPolicy(backOffPolicy); User user = template.execute((RetryCallback<User, Throwable>) retryContext -> { System.out.println(LocalDateTime.now() + " retry=" + retryContext.getRetryCount()); return getUserUnsuccessfully(); }); System.out.println(user); }
実行。
リトライの度に1秒待機しているのが分かります。
2019-01-20T23:46:17.109 retry=0 2019-01-20T23:46:18.111 retry=1 2019-01-20T23:46:19.113 retry=2 2019-01-20T23:46:20.113 retry=3 2019-01-20T23:46:21.114 retry=4 com.example.retry.spring.springretryexample.service.MyService$UserNotFoundException at com.example.retry.spring.springretryexample.service.MyServiceWithRetryTemplate.getUserUnsuccessfully(MyServiceWithRetryTemplate.java:215) : :
setMaxInterval()で最大の待機時間を指定します。この時間以上は待機しません。
setMaxInterval(4_000L)で4秒を指定してみます。
public void executeWithMaxInternal() throws Throwable { RetryTemplate template = new RetryTemplate(); SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(5); ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy(); backOffPolicy.setInitialInterval(2_000L); backOffPolicy.setMaxInterval(4_000L); template.setRetryPolicy(retryPolicy); template.setBackOffPolicy(backOffPolicy); User user = template.execute((RetryCallback<User, Throwable>) retryContext -> { System.out.println(LocalDateTime.now() + " retry=" + retryContext.getRetryCount()); return getUserUnsuccessfully(); }); System.out.println(user); }
実行。
最大4秒までリトライ時間が倍増しているのがわかります。
2019-01-20T23:47:16.781 retry=0 2019-01-20T23:47:18.782 retry=1 2019-01-20T23:47:22.782 retry=2 2019-01-20T23:47:26.783 retry=3 2019-01-20T23:47:30.783 retry=4 com.example.retry.spring.springretryexample.service.MyService$UserNotFoundException at com.example.retry.spring.springretryexample.service.MyServiceWithRetryTemplate.getUserUnsuccessfully(MyServiceWithRetryTemplate.java:215) : :
RecoveryCallback
executeでリトライが失敗した場合のリカバリ用のcallbackを指定出来ます。
T execute(RetryCallback<T,E> retryCallback, RecoveryCallback<T> recoveryCallback)
RecoveryCallbackは単純にリカバリする操作を定義するだけのインターフェースです。
public Interface RecoveryCallback {
T recover(RetryContext context)
}
リトライに失敗した場合にリカバリメソッドとして下記のメソッドを呼んでみます。
public User getUserSuccessfully() { return new User("Bobby", 20); }
RecoveryCallbackを実装して、リトライが失敗した場合に上記のリカバリメソッドを呼ぶようにしてみます
public void executeWithRecovery() throws Throwable { RetryTemplate template = new RetryTemplate(); SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(5); ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy(); backOffPolicy.setInitialInterval(1_000L); backOffPolicy.setMultiplier(1.0); template.setRetryPolicy(retryPolicy); template.setBackOffPolicy(backOffPolicy); User user = template.execute((RetryCallback<User, Throwable>) retryContext -> { System.out.println(LocalDateTime.now() + " retry=" + retryContext.getRetryCount()); return getUserUnsuccessfully(); }, retryContext -> { System.out.println("recovered!"); return getUserSuccessfully(); }); System.out.println(user); }
実行。
リトライが5回失敗したあと、RecoveryCallbackが呼ばれているのが分かります。
2019-01-20T23:48:02.287 retry=0 2019-01-20T23:48:03.288 retry=1 2019-01-20T23:48:04.289 retry=2 2019-01-20T23:48:05.289 retry=3 2019-01-20T23:48:06.289 retry=4 recovered! MyService.User(name=Bobby, age=20)
RetryableExceptions
リトライ対象のexceptionを指定することが出来ます。
UserNotFoundExceptionが発生した場合だけリトライをするようにしてみます。
Mapでリトライ対象のexceptionを定義し、SimpleRetryPolicy生成時に指定します。
public void executeWithRetryableExceptions() throws Throwable { RetryTemplate template = new RetryTemplate(); // specify exceptions that are retryable Map<Class<? extends Throwable>, Boolean> retryableExceptions = Collections.singletonMap(UserNotFoundException.class, true); SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(5, retryableExceptions); ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy(); backOffPolicy.setInitialInterval(1_000L); backOffPolicy.setMultiplier(1.0); template.setRetryPolicy(retryPolicy); template.setBackOffPolicy(backOffPolicy); // will retry System.out.println("will retry"); User user = template.execute((RetryCallback<User, Throwable>) retryContext -> { System.out.println(LocalDateTime.now() + " retry=" + retryContext.getRetryCount()); throw new UserNotFoundException(); }, retryContext -> { System.out.println("recovered!"); return getUserSuccessfully(); }); System.out.println(user); // will not retry System.out.println("will not retry"); template.execute((RetryCallback<User, Throwable>) retryContext -> { System.out.println(LocalDateTime.now() + " retry=" + retryContext.getRetryCount()); throw new UnexpectedException(); }, retryContext -> { System.out.println("recovered!"); return getUserSuccessfully(); }); }
実行。
UserNotFoundExceptionがthrowされた場合はリトライが実行されてます。
will retry 2019-01-21T02:53:33.735 retry=0 2019-01-21T02:53:34.736 retry=1 2019-01-21T02:53:35.737 retry=2 2019-01-21T02:53:36.737 retry=3 2019-01-21T02:53:37.737 retry=4 recovered! MyService.User(name=Bobby, age=20)
UnexpectedExceptionがthrowされた場合はリトライが実行されないことが分かります。
will not retry 2019-01-21T02:53:37.738 retry=0 recovered!
AOP
Spring RetryではAOP interceptorも提供しているので、試してみます。
Configuration
configurationクラスに@EnableRetryを付与して有効にします。
@EnableRetry @SpringBootApplication public class SpringRetryExampleApplication implements CommandLineRunner { : : }
@Retryable
@Retryableをメソッドに付与すると、Exceptionが発生した際にリトライが実行されます。
private int retryCount = 0; @Retryable public void retryable() { System.out.println(LocalDateTime.now() + " retry=" + retryCount); retryCount++; throw new RuntimeException(); }
実行。
デフォルトでは3回実行されているのが分かります。
2019-01-21T03:10:31.479 retry=0 2019-01-21T03:10:32.481 retry=1 2019-01-21T03:10:33.481 retry=2 org.springframework.retry.ExhaustedRetryException: Cannot locate recovery method; nested exception is java.lang.RuntimeException at org.springframework.retry.annotation.RecoverAnnotationRecoveryHandler.recover(RecoverAnnotationRecoveryHandler.java:60) : : ... 13 more
maxAttemptsを設定すると最大のリトライ回数を指定できます。
private int retryCount = 0; @Retryable(maxAttempts = 5) public void retryableWithRetryPolicy() { System.out.println(LocalDateTime.now() + " retry=" + retryCount); retryCount++; throw new RuntimeException(); }
実行。
5回リトライされていることが分かります。
2019-01-21T03:11:55.973 retry=0 2019-01-21T03:11:56.974 retry=1 2019-01-21T03:11:57.975 retry=2 2019-01-21T03:11:58.975 retry=3 2019-01-21T03:11:59.976 retry=4 org.springframework.retry.ExhaustedRetryException: Cannot locate recovery method; nested exception is java.lang.RuntimeException at org.springframework.retry.annotation.RecoverAnnotationRecoveryHandler.recover(RecoverAnnotationRecoveryHandler.java:60) : : ... 13 more
@Backoff
@Backoffでbackoffを指定できます。
delayで待機時間を2秒に指定してみます。
private int retryCount = 0; @Retryable(backoff = @Backoff(delay = 2_000)) public void retryableWithBackoffPolicy() { System.out.println(LocalDateTime.now() + " retry=" + retryCount); retryCount++; throw new RuntimeException(); }
実行。
2秒間隔でリトライされていることが分かります。
2019-01-21T03:12:26.382 retry=0 2019-01-21T03:12:28.383 retry=1 2019-01-21T03:12:30.384 retry=2 org.springframework.retry.ExhaustedRetryException: Cannot locate recovery method; nested exception is java.lang.RuntimeException at org.springframework.retry.annotation.RecoverAnnotationRecoveryHandler.recover(RecoverAnnotationRecoveryHandler.java:60) : : ... 13 more
delayとmaxDelayを指定すると、delay~maxDelayの時間でランダムで待機します。
private int retryCount = 0; @Retryable(backoff = @Backoff(delay = 2_000, maxDelay = 4_000)) public void retryableWithMaxDelay() { System.out.println(LocalDateTime.now() + " retry=" + retryCount); retryCount++; throw new RuntimeException(); }
実行。
2秒から4秒でランダムに待機されています。
2019-01-21T03:13:00.958 retry=0 2019-01-21T03:13:04.188 retry=1 2019-01-21T03:13:06.315 retry=2 org.springframework.retry.ExhaustedRetryException: Cannot locate recovery method; nested exception is java.lang.RuntimeException at org.springframework.retry.annotation.RecoverAnnotationRecoveryHandler.recover(RecoverAnnotationRecoveryHandler.java:60) : : ... 13 more
@Recover
@Recoverでリトライが失敗した場合のリカバリメソッドを指定できます。
UserNotFoundExceptionが発生した場合のリカバリメソッドに@Recoverを付与します。
リカバリメソッドの引数にはリトライメソッドの引数を受け取ることができます。
retryableWithException("test")として引数を指定して呼んでみます。
private int retryCount = 0; @Retryable(UserNotFoundException.class) public void retryableWithException(String name) throws UserNotFoundException { System.out.println(LocalDateTime.now() + " retry=" + retryCount + ", name=" + name); retryCount++; throw new UserNotFoundException(); } @Recover public void recover(UserNotFoundException e, String name) { e.printStackTrace(); System.out.println("recovered! name=" + name); }
実行。
リトライが失敗したあとにリカバリメソッドが実行されているのが分かります。
リトライメソッドの引数をリカバリメソッドで取得出来ているのが分かります。
2019-01-21T03:13:32.855 retry=0, name=test 2019-01-21T03:13:33.856 retry=1, name=test 2019-01-21T03:13:34.857 retry=2, name=test recovered! name=test com.example.retry.spring.springretryexample.service.MyService$UserNotFoundException at com.example.retry.spring.springretryexample.service.MyService.retryableWithException(MyService.java:52) : :
こんなとこです。
サンプルコードは下記にあげました。