Basic error handling in spring-data-aerospike
Working with external resources may be tricky. There are lots of components that can fail during your call to the resource: network, hardware, operating system, load balancer etc. Some of these errors are transient, meaning there are random failures for a very short period of time. Such type of errors can be easily mitigated by using a proven technique called Retries.
Implementing simple retries is quite easy, but there is already a nice library for that with all the building blocks for creating reliable retry mechanism — Spring Retry.
Note: Before continuing it is expected that your project has spring-data-aerospike already setup. Please check this guide to find out how to do it.
We are going to use movies domain to demonstrate retries configuration. Here is a basic document and repository:
@Value
@Document(collection = "demo-service-movies")
@Builder(toBuilder = true)
@AllArgsConstructor
public class MovieDocument {
@Id
String id;
@Field
String name;
@Field("desc")
String description;
@Field
double rating;
@Version
@NonFinal
long version;
}public interface MovieRepository extends CrudRepository<MovieDocument, String> {
}
To add retries into your application add the following dependencies into your pom.xml (spring-boot-starter-aop is required as spring-retry uses Spring Aop internally):
<dependencies>
<!-- Other dependencies omitted-->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
Next you will need to specify which operations to retry and when. This can be done via @Retryable
annotation. You can check all the available options here. In this demo we will cover only the most commonly used:
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;@Retryable( // (1)
include = { // (2)
QueryTimeoutException.class,
TransientDataAccessResourceException.class,
OptimisticLockingFailureException.class
},
maxAttempts = 5, // (3)
backoff = @Backoff( // (4)
delay = 3,
multiplier = 2,
random = true // (5)
)
)
@RequiredArgsConstructor
public class MovieOperations {
private final MovieRepository repository;
private final AerospikeTemplate template;
public void createMovie(Movie movie) {
try {
template.insert(MovieDocument.builder()
.id(movie.getName())
.name(movie.getName())
.description(movie.getDescription())
.rating(movie.getRating())
.version(0L)
.build());
} catch (DuplicateKeyException e) {
throw new IllegalArgumentException("Movie with name: " + movie.getName() + " already exists!");
}
}
public void deleteMovie(String name) {
repository.deleteById(name);
}
public Optional<Movie> findMovie(String name) {
return repository.findById(name)
.map(this::toMovie);
}
public Movie updateMovieRating(String name, double newRating) {
return update(name, existingMovie ->
repository.save(existingMovie.toBuilder().rating(newRating).build()));
}
public Movie updateMovieDescription(String name, String newDescription) {
return update(name, existingMovie ->
repository.save(existingMovie.toBuilder().description(newDescription).build()));
}
private Movie update(String name, Function<MovieDocument, MovieDocument> updateFunction) {
return repository.findById(name)
.map(updateFunction)
.map(this::toMovie)
.orElseThrow(() -> new IllegalArgumentException("Movie with name: " + name + " not found"));
}
private Movie toMovie(MovieDocument doc) {
return new Movie(doc.getName(), doc.getDescription(), doc.getRating());
}
}
@Retryable
explained:
@Retryable
specifies that all the methods inMovieOperations
class will be retried when exceptions specified ininclude
field occur. Note that if you place@Retryable
annotation over one method — only this method will be retried.include
specifies exception types that should be retried.maxAttempts
specifies number of max attempts of retries.backoff
specifies backoff configuration for the retries.random = true
enables jitter in backoff timeouts.
To enable spring-retry you need to add @EnableRetry
either into existing configuration class or create a separate class e.g.AerospikeRetryConfiguration
:
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.annotation.EnableRetry;@EnableRetry
@Configuration
public class AerospikeRetryConfiguration {}
Demo project is located on GitHub — https://github.com/aerospike-community/spring-data-aerospike-demo.