Repeating actions with Spring Retry

Repeating actions with Spring Retry

retry

Some operations in software systems are not reliable. Database operations may deadlock, sending an email may fail, a network request may not reach the destination etc. There is a chance that another attempt of the same action will succeed. It is recommended to wrap such operations with a retrying mechanism. Spring Retry is a library that makes it easy.

 

Operation that may require retrying

Database operation may deadlock and it is absolutely normal in most cases. Updates may collide with each other. Even a select statement is not free from this in SQL Server. Deadlocks are in line with relational database theory and they are handled by database engines by killing one of the processes that participate in the deadlock. It means that you cannot be sure that a valid SQL statement will not fail even if it is absolutely correct. It may happen that other activities in the system will cause your operation to fail.

If you would like to learn more about deadlocks, watch What?! SELECT query caused a deadlock?

To simplify this case, I created a service in my Spring Boot application.

import org.springframework.stereotype.Service;

@Service
public class MyService {
private static final int MAX_FAILURES = 2;
private int numberOfExecutions = 0;

public String doSomething() {
numberOfExecutions++;
// succeed after MAX_FAILURES times in a row
if (numberOfExecutions > MAX_FAILURES) {
return "success";
}
throw new RuntimeException();
}
}

A method doSomething() returns a success message or throws an error. Notice that the first two executions always fail, the third is successful. Imagine that the cause of the error is a deadlock in a database. When executing the third time, some other process has already finished so it does not collide with doSomething().

 

Spring Retry

Spring Retry is a library that is a part of Spring family which is a good recommendation by itself.

My project is built by Gradle so I add spring-retry dependency to build.gradle:

dependencies {
    ...
compile group: 'org.springframework', name: 'spring-aspects', version: '5.0.7.RELEASE'

    compile
group: 'org.springframework.retry', name: 'spring-retry', version: '1.2.2.RELEASE'
   
...

}

and refresh the configuration. Gradle downloads proper libraries.

Once the library is available, I find a main class in my sources - the one that has @SpringBootApplication annotation and add @EnableRetry.

@SpringBootApplication
@EnableRetry
public class Application extends SpringBootServletInitializer {
   
public static void main(String[] args) {
    }
}

Now, Spring Retry is ready to use.

 

Retrying

Telling Spring to retry a method is simplified to adding @Retryable annotation.

import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

@Service
public class MyService {
   
private static final int MAX_FAILURES = 2;
   
private int numberOfExecutions = 0;

   
@Retryable
   
public String doSomething() {
       
numberOfExecutions++;
       
// succeed after MAX_FAILURES times in a row
       
if (numberOfExecutions > MAX_FAILURES) {
           
return "success";
        }
       
throw new RuntimeException();
    }
}

Now, when I execute doSomething() method, it does not fail. Actually it fails twice but in the background. The third execution is successful. The rest of the application is not aware that there were two failures. Those exceptions are not visible to the caller.

By default, maximum three attempts are done before reporting failure. So if doSomething() would fail for the third time in a row, the caller would see the exception. If you would like to modify this parameter, it is called maxAttempts and it can be changed in @Retryable annotation.

A few other useful parameters can be configured like a delay between the attempts. It is called backoff.

Usually retrying is likely to help with some exception and unlikely for some other. You can include or exclude particular exceptions from the retryable policy. These parameters are called include, exclude.

Sample usage:

@Service
public class MyService {
   
private static final int MAX_FAILURES = 2;
   
private int numberOfExecutions = 0;

   
@Retryable (
            maxAttempts =
2,
            backoff =
@Backoff(delay = 200),
            include = SQLException.
class
   
)
   
public String doSomething() {
       
numberOfExecutions++;
       
// succeed after MAX_FAILURES times in a row
       
if (numberOfExecutions > MAX_FAILURES) {
           
return "success";
        }
       
throw new RuntimeException();
    }
}

In the above case, the method will fail as Spring Retry will execute it only twice before giving up. Additionally, only failures caused by SQLException are considered for retrying so actually the first execution causes the final failure.

 

Isn't it simple?