Basic error handling in spring-data-aerospike

Anastasiia Smirnova
Aerospike Developer Blog
3 min readMay 14, 2020
Everything fails all the time. © Werner Vogels

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:

  1. @Retryable specifies that all the methods in MovieOperations class will be retried when exceptions specified in include field occur. Note that if you place @Retryable annotation over one method — only this method will be retried.
  2. include specifies exception types that should be retried.
  3. maxAttempts specifies number of max attempts of retries.
  4. backoff specifies backoff configuration for the retries.
  5. 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 {
}

--

--