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

下記のバージョンで試してみます。

  • Java 1.8.0_181
  • spring-retry 1.2.2.RELEASE
  • spring-aop 5.1.3.RELEASE

依存関係

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)
	                                         :
	                                         :


こんなとこです。


サンプルコードは下記にあげました。

github.com