Spring Retry

Spring Retry provides a feature that automatically calls a specified method again when an exception occurs while calling a method.

Overview

This article creates a simple project that uses Spring Retry with Kotlin.

Spring Retry provides a feature that automatically calls a specified method again when an exception occurs while calling a method.
It is useful for failover when a temporary network connection failure occurs.

Failover: alternative system operation.
This means building a non-stop system by keeping the server normally used and a cloned server, then letting the cloned server take over the work when the active server becomes difficult to use due to a failure.

Create the Project

Create the initial Spring Boot project with the following curl command.

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 -

Running this command creates a web project with Java 11 and Spring Boot 2.7.2.

Retry Configuration

Build Script

Add the libraries required to run Retry to the build script as follows.

/build.gradle.kts

dependencies {
    // ... omitted ...

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

    // ... omitted ...
}
  • spring-retry: Retry library
  • spring-aspects: AOP-related library
  • kotlin-logging: logging library for checking behavior

Add the Retry Configuration Class

Create a new RetryConfig configuration class and write it as follows.

/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

Add the Service Interface and Class with Retry Applied

/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"
    }
}
  • The retry function declared with the @Retryable annotation retries when an SQLException occurs. It tries up to 2 retries with an interval of 1000 milliseconds. If the second attempt also fails, the recover function configured with the @Recover annotation is executed.
  • The retry function raises an error when the argument variable error is true, and does not raise an error when error is false.

Check with Retry Test Code

Add the Retry Test Code Class

Write the following test code to check the behavior.

/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)
    }
}

Check Retry Test Code Execution

When you run the test code, it behaves as follows.

When a Retry Error Occurs

The result when the retry error=true function is called is as follows.

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

When a Retry Error Does Not Occur

The result when the retry error=false function is called is as follows.

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

RetryTemplate Configuration

RetryOperations Interface

Spring Retry provides the RetryOperations interface, which offers a series of excute() methods.

package org.springframework.retry;

import org.springframework.retry.support.DefaultRetryState;

public interface RetryOperations {
    // ... omitted ...

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

    // ... omitted ...
}

The RetryCallback parameter of execute() is an interface for inserting business logic that should be retried when it fails.

package org.springframework.retry;

public interface RetryCallback<T, E extends Throwable> {

	T doWithRetry(RetryContext context) throws E;

}

If all retries fail, RetryOperations calls RecoveryCallback. To use this feature, the client must pass a RecoveryCallback object when calling the execute() method.

package org.springframework.retry;

public interface RecoveryCallback<T> {

	T recover(RetryContext context) throws Exception;

}

RetryOperations Implementation

RetryTemplate is an implementation of RetryOperations. Configure a RetryTemplate Bean in the @Configuration class.

/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 configures the number of retries for an operation. SimpleRetryPolicy is used to retry a fixed number of times.
  • BackOffPolicy is used to control the backoff for retries. FixedBackOffPolicy pauses for a fixed amount of time before continuing.

Check with RetryTemplate Test Code

Create the RetryTemplate Test Class

Only templateRetry was added to the existing test code.

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

package com.devkuma.retry.service

// ... omitted ...

class RetryServiceTest {

    @Autowired
    private lateinit var retryService: RetryService

    @Autowired
    private lateinit var retryTemplate: RetryTemplate

    // ... omitted ...

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

Check RetryTemplate Test Execution

When TemplateRetry Does Not Produce an Error

The result when the templateRetry function is called is as follows.

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

References

The example code above is available on GitHub.