Spring Retry

Spring Retryは、メソッド呼び出しで例外が発生したとき、指定したメソッドを自動的に再呼び出しする機能を提供する。

概要

Kotlin言語を使って、Spring Retryを活用する簡単なプロジェクトを作成してみる。

Spring Retryは、メソッドを呼び出して例外が発生したとき、指定したメソッドを自動的に再呼び出しする機能を提供する。
一時的なネットワーク接続障害が発生したときのFailoverに有用である。

Failover: システムの代替動作。
普段使用するサーバーとそのクローンサーバーを用意しておき、使用中のサーバーが障害により利用しづらくなった場合、クローンサーバーにその処理を代行させ、無停止システムを構築することを意味する。

プロジェクト作成

次のようにcurlコマンドを使って、Spring Bootの初期プロジェクトを作成する。

curl https://start.spring.io/starter.tgz \
-d bootVersion=2.7.2 \
-d baseDir=spring-retry \
-d groupId=com.devkuma \
-d artifactId=spring-retry \
-d packageName=com.devkuma.retry \
-d applicationName=RetryApplication \
-d packaging=jar \
-d language=kotlin \
-d javaVersion=11 \
-d type=gradle-project | tar -xzvf -

このコマンドを実行すると、Java 11、Spring Boot 2.7.2のWebプロジェクトが作成される。

Retry設定

ビルドスクリプト

Retryを動作させるためのライブラリを、ビルドスクリプトに次のように追加する。

/build.gradle.kts

dependencies {
    // ... 中略 ...

	implementation("org.springframework.retry:spring-retry:1.3.3")
	implementation("org.springframework:spring-aspects:5.3.22")
	implementation("io.github.microutils:kotlin-logging:2.1.23")

    // ... 中略 ...
}
  • spring-retry: Retryライブラリ
  • spring-aspects: AOP関連ライブラリ
  • kotlin-logging: 動作確認用のログライブラリ

Retry設定クラスの追加

新規にRetryConfig設定クラスを作成し、次のように記述する。

/src/main/kotlin/com/devkuma/retry/config/RetryConfig.kt

package com.devkuma.retry.config

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

@EnableRetry
@Configuration
class RetryConfig

Retry適用関連クラスの作成

Retryを適用したServiceインターフェースとクラスの追加

/src/main/kotlin/com/devkuma/retry/service/RetryService.kt

package com.devkuma.retry.service

import org.springframework.retry.annotation.Backoff
import org.springframework.retry.annotation.Retryable
import java.sql.SQLException

interface RetryService {

    @Retryable(value = [SQLException::class], maxAttempts = 2, backoff =  Backoff(delay = 1000))
    fun retry(error: Boolean) : String

    fun templateRetry(error: Boolean) : String
}

/src/main/kotlin/com/devkuma/retry/service/impl/RetryServiceImpl.kt

package com.devkuma.retry.service.impl

import com.devkuma.retry.service.RetryService
import mu.KotlinLogging
import org.springframework.retry.annotation.Recover
import java.sql.SQLException

private val log = KotlinLogging.logger {}

class RetryServiceImpl : RetryService {

    override fun retry(error: Boolean) : String {
        log.info { "Retry called. error=$error" }
        if (error)
            throw SQLException("retry SQLException")
        return "Success"
    }

    @Recover
    fun recover(exception: SQLException, error: Boolean) : String {
        log.info { "Recover called: message=${exception.message}"}
        return "Success"
    }
}
  • @Retryableアノテーションを宣言したretry関数は、SQLExceptionが発生したときに再試行する。最大2回の再試行を1000ミリ秒間隔で実行する。 2回試しても成功しない場合は、@Recoverアノテーションを設定したrecover関数が実行される。
  • retry関数は、引数変数errortrueならエラーが発生し、errorfalseならエラーが発生しない。

Retryテストコードで確認

Retryテストコードクラスの追加

動作確認のため、次のようにテストコードを作成する。

/src/test/kotlin/com/devkuma/retry/service/RetryServiceTest.kt

package com.devkuma.retry.service

import com.devkuma.retry.config.RetryConfig
import com.devkuma.retry.service.impl.RetryServiceImpl
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest(
    classes = [
        RetryServiceImpl::class,
        RetryConfig::class
    ]
)
class RetryServiceTest {

    @Autowired
    private lateinit var retryService: RetryService

    @Test
    fun `retry error=true`(){
        // Given

        // When
        var result = retryService.retry(true)

        // Then
        Assertions.assertEquals("Success", result)
    }

    @Test
    fun `retry error=false`(){
        var result = retryService.retry(false)

        Assertions.assertEquals("Success", result)
    }
}

Retryテストコード実行確認

作成したテストコードを実行すると、次のように動作する。

Retryエラーが発生した場合

retry error=true関数が呼び出されたときの結果は次のとおりである。

2022-08-11 18:20:55.787  INFO 13102 --- [    Test worker] c.d.retry.service.impl.RetryServiceImpl  : Retry called. param error=true
2022-08-11 18:20:56.792  INFO 13102 --- [    Test worker] c.d.retry.service.impl.RetryServiceImpl  : Retry called. param error=true
2022-08-11 18:20:56.792  INFO 13102 --- [    Test worker] c.d.retry.service.impl.RetryServiceImpl  : Recover called: message=retry SQLException, param error=true

Retryエラーが発生しなかった場合

retry error=false関数が呼び出されたときの結果は次のとおりである。

2022-08-11 18:20:56.801  INFO 13102 --- [    Test worker] c.d.retry.service.impl.RetryServiceImpl  : Retry called. param error=false

RetryTemplate設定

RetryOperationsインターフェース

Spring Retryは、一連のexcute()メソッドを提供するRetryOperationsインターフェースを提供する。

package org.springframework.retry;

import org.springframework.retry.support.DefaultRetryState;

public interface RetryOperations {
    // ... 中略 ...

	<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback)
			throws E;

    // ... 中略 ...
}

execute()のパラメータであるRetryCallbackは、失敗時に再試行すべきビジネスロジックを挿入するためのインターフェースである。

package org.springframework.retry;

public interface RetryCallback<T, E extends Throwable> {

	T doWithRetry(RetryContext context) throws E;

}

再試行がすべて失敗すると、RetryOperationsRecoveryCallbackを呼び出す。この機能を使うには、クライアントはexecute()メソッドを呼び出すときにRecoveryCallbackオブジェクトを渡す必要がある。

package org.springframework.retry;

public interface RecoveryCallback<T> {

	T recover(RetryContext context) throws Exception;

}

RetryOperations実装

RetryTemplateはRetryOperationsの実装である。@ConfigurationクラスでRetryTemplate Beanを構成してみる。

/src/main/kotlin/com/devkuma/retry/config/RetryConfig.kt

package com.devkuma.retry.config

import com.devkuma.retry.support.DefaultListenerSupport
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.retry.annotation.EnableRetry
import org.springframework.retry.backoff.FixedBackOffPolicy
import org.springframework.retry.policy.SimpleRetryPolicy
import org.springframework.retry.support.RetryTemplate

@EnableRetry
@Configuration
class RetryConfig {

    @Bean
    fun retryTemplate(): RetryTemplate {
        val fixedBackOffPolicy = FixedBackOffPolicy()
        fixedBackOffPolicy.backOffPeriod = 2000L

        val retryPolicy = SimpleRetryPolicy()
        retryPolicy.maxAttempts = 3

        val retryTemplate = RetryTemplate()
        retryTemplate.setBackOffPolicy(fixedBackOffPolicy)
        retryTemplate.setRetryPolicy(retryPolicy)
        retryTemplate.registerListener(DefaultListenerSupport())
        return retryTemplate
    }
}
  • RetryPolicyは処理の再試行回数を設定する。SimpleRetryPolicyは固定回数だけ再試行するために使われる。
  • BackOffPolicyは再試行時のバックオフを制御するために使われる。FixedBackOffPolicyは、続行前に一定時間停止する。

RetryTemplateテストコードで確認

RetryTemplateテストクラスの作成

既存のテストコードにtemplateRetryだけを追加した。

/src/test/kotlin/com/devkuma/retry/service/RetryServiceTest.kt

package com.devkuma.retry.service

// ... 中略 ...

class RetryServiceTest {

    @Autowired
    private lateinit var retryService: RetryService

    @Autowired
    private lateinit var retryTemplate: RetryTemplate

    // ... 中略 ...

    @Test
    fun templateRetry() {
        retryTemplate.execute<Any, SQLException>(
            { retryService.templateRetry(true) },
            { retryService.templateRetry(false) }
        )
    }
}

RetryTemplateテスト実行確認

TemplateRetryでエラーが発生しなかった場合

templateRetry関数が呼び出されたときの結果は次のとおりである。

2022-08-11 18:21:38.192  INFO 13197 --- [    Test worker] c.d.r.support.DefaultListenerSupport     : onOpen
2022-08-11 18:21:41.384  INFO 13197 --- [    Test worker] c.d.retry.service.impl.RetryServiceImpl  : Retry called. param error=true
2022-08-11 18:21:42.389  INFO 13197 --- [    Test worker] c.d.retry.service.impl.RetryServiceImpl  : Retry called. param error=true
2022-08-11 18:21:42.398  INFO 13197 --- [    Test worker] c.d.retry.service.impl.RetryServiceImpl  : Recover called: message=retry SQLException, param error=true
2022-08-11 18:21:44.146  INFO 13197 --- [    Test worker] c.d.r.support.DefaultListenerSupport     : onClose

参考

上記のサンプルコードはGitHubで確認できる。