Everything fails all the time.
Add spring-retry
dependency together with spring-boot-starter-aop
:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
You can either specify retry configuration directly via @Retryable
properties or create RetryInterceptor
bean and set it’s name in @Retryable
interceptor
property.
For this demo we’ll use simple configuration via @Retryable
.
package com.example.demo.service;
import com.example.demo.persistence.simplecrud.MovieDocument;
import com.example.demo.persistence.simplecrud.MovieRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.dao.QueryTimeoutException;
import org.springframework.dao.TransientDataAccessResourceException;
import org.springframework.data.aerospike.core.AerospikeTemplate;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import java.util.Optional;
import java.util.function.Function;
@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
specifies that the method(s) will be retied. -
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.
Add @EnableRetry
either into existing AerospikeConfiguration
or create separate class AerospikeRetryConfiguration
:
package com.example.demo.persistence.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.annotation.EnableRetry;
@EnableRetry
@Configuration
public class AerospikeRetryConfiguration {
}
In the context of retries there are two types of errors: retryable and non retryable. For example, retrieving value by key may result in DataRetrievalFailureException
(key not found), which in most cases should not be retried; whereas in case there are connectivity issues to Aerospike or any network congestion issues you on the contrary would retry. Consider this when configuring your retry policy.
When retrying errors there are two basic backoff policies: fixed and exponential. Fixed backoff policy does exactly how it’s named, each retry occurs within fixed time interval. This backoff policy is simple and easy for understanding, but is not recommended for production, because in case external resource is overloaded and all clients experience issues, constantly increasing number of requests at the same time may lead to the total resource outage. Instead to overcome the issues resource should be given some time to heal, this can be achieved by exponential backoff policy, which increases each retry time interval by specific multiplier. This backoff polic usually is used together with jitter — added randomized time interval into the backoff, which removes retry waves at specific time from multiple clients. With spring-retry
you can use org.springframework.retry.backoff.ExponentialRandomBackOffPolicy
.