Spring Retry Aspect Oriented Programming
Introduction
Let’s say you have a microservices architecture, where the failure of an unknown system can cause the failure of your system, paraphrased from Leslie Lamport. This means that errors, or bad responses from other systems are a common occurrence, relatively speaking.
This means each microservice must be fault tolerant, and able to handle bad states or fail gracefully. Spring framework can facilitate this nearly effortlessly, and the point of this post is an introduction to automating retry logic. This is useful for distributed systems whose failures may be successes on subsequent requests, whether it be HTTP or some other protocol.
This assumes some familiarity with Spring, especially @Service
.
The Problem
Your system depends on some other brittle system, who fails at some
unknown likelihood per request (hopefully low!). You know that if you retry
a request, it could succeed on a second, or even third attempt.
Spring framework handles this easily. Enter spring-retry
.
Spring Retry
Spring retry is exposed through
Aspect Oriented Programming.
Aspect Oriented Programming is essentially a natural
extension of inversion of control concepts, where logic can be
applied to some service, independent of the actual business logic.
More specifically, retry logic can be applied to a Spring @Service
.
This means that adding an annotation to a Spring @Service
is almost
all that is needed to have graceful retries.
Dependencies
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
spring-aspects
is needed because spring-retry
is made available
through proxying facilitated by spring-aspects
.
Service Level Implementation
@Service
public class ReliableResource {
public String getResource(){
return "Resource";
}
}
The above is an example of your typical Spring @Service
that retrieves
some resource.
@Service
public class BrittleResource {
public String getBrittleResource() throws RetryableException{
int result = new Random().nextInt() % 2;
if(result == 0){
System.out.println("Failed to retrieve data!");
throw new RetryableException("Brittle service failure");
}
return "Brittle resource";
}
}
The above is a brittle service that fails exactly half the time.
It would be nice to have retries for this service. Note that RetryableException
is a class I defined, indicating that the specific situation that arises
at the throw
of this exception is able to be retried.
There may
be other cases that are not retryable. It is an anti-pattern to retry
on all exception
, because there may be exceptions caused by developer
error in our system, and we should not add unnecessary load to
other systems. Retryable situations should be identified,
and the throw new RetryableException(...)
is a case
that should be retried, such as a a timeout error, or a 500
response
back from a server. An example of a situation that should not be retried
would be 400
, malformed request, or specific database SQLException
,
possibly a malformed query.
@Retryable(include = RetryableException.class)
public String getBrittleResource() throws RetryableException{
int result = new Random().nextInt() % 2;
if(result == 0){
System.out.println("Failed to retrieve data!");
throw new RetryableException("Brittle service failure");
}
return "Brittle resource";
}
include = RetryableException.class
indicates that only thrown
RetryableException
exceptions should be retried. The default retry
behavior is a maximum of 3 attempts with 1 second between attempts.
There are various backoff schemes, including exponential. That configuration is
achieved in our @Configuration
class.
Configuration
@Configuration
@ComponentScan(basePackages = "com.cmorterud.examples.spring.retryTemplateExample")
@EnableRetry
public class AppConfig {
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
fixedBackOffPolicy.setBackOffPeriod(500);
retryTemplate.setBackOffPolicy(fixedBackOffPolicy);
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(3);
retryTemplate.setRetryPolicy(retryPolicy);
return retryTemplate;
}
}
In our Spring @Configuration
class, we need to @EnableRetry
so that the Spring-level class proxy provided
by spring-aspects
should handle retry logic.
@ComponentScan
is basic Spring configuration, more at
Baeldung.
The @Bean
is to override the definition for a RetryTemplate
object
that governs retry logic within the Spring-level class proxy.
Controller
The @Controller
is nearly abstract with respect to the service level
implementation for the brittle @Service
dependency. The controller
only needs to handle the exception thrown by the brittle service,
and return the appropriate response to the client.
@Controller
@EnableAutoConfiguration
@RequestMapping("/resource")
public class ExampleController {
@Autowired
private ReliableResource reliableResource;
@Autowired
private BrittleResource brittleResource;
@RequestMapping("/getResource")
ResponseEntity<String> getResource(){
return new ResponseEntity<String>(reliableResource.getResource(), HttpStatus.OK);
}
@RequestMapping("/getBrittleResource")
ResponseEntity<String> getBrittle() {
String brittleResourceData;
try {
brittleResourceData = brittleResource.getBrittleResource();
} catch (RetryableException e) {
brittleResourceData = null;
}
if(brittleResourceData == null) {
return new ResponseEntity<>("failed to retrieve data!", HttpStatus.INTERNAL_SERVER_ERROR);
}
return new ResponseEntity<>(brittleResourceData, HttpStatus.OK);
}
}
Testing
I wrote some tests using MockMvc
that validate the retries occurring.
@SpringBootTest
@AutoConfigureMockMvc
class RetryTemplateExampleApplicationTests {
@Autowired
private MockMvc mockMvc;
@Test
void reliableResourceSucceeds() throws Exception {
this.mockMvc.perform(get("/resource/getResource")).andDo(print()).andExpect(status().isOk())
.andExpect(content().string("Resource"));
}
@Test
void brittleResourceSucceeds() throws Exception {
this.mockMvc.perform(get("/resource/getBrittleResource")).andDo(print()).andExpect(status().isOk())
.andExpect(content().string("Brittle resource"));
}
}
I can see two failures (and success on the third) when running the tests.
Failed to retrieve data!
Failed to retrieve data!
Repository
Caveat
Spring-level class proxying *only* works if the Retryable
method
is called from *outside* the class. Meaning, if I had a definition
of the brittle resource like this,
public String getBrittleResource() throws RetryableException {
return getActualBrittleResource();
}
@Retryable(include = RetryableException.class)
private String getActualBrittleResource() throws RetryableException {
int result = new Random().nextInt() % 2;
if(result == 0){
System.out.println("Failed to retrieve data!");
throw new RetryableException("Brittle service failure");
}
return "Brittle resource";
}
when RetryableException
is thrown, no retries will occur!
Thanks!
I learned most of this through trial and error as well as
Baeldung’s article.
You can find the working maven
project at
my github repository and
please feel free to email me with any questions or concerns.
Thanks for reading!