I'm using annotation-style Resilience4j with my SpringBoot app called "demo". When calling an external backend via RestTemplate I want to use TimeLimiter and Retry to achieve the following:
- Limit the REST-call duration to 5 seconds --> if it takes longer, fail with TimeoutException
- Retry on TimeoutException --> do maximum 2 attempts
To see if the configuration of my resilience-setup works as designed I wrote an IntegrationTest. This test runs under profile "test" and is configured using "application-test.yml":
- Uses TestRestTemplate to send a call to my "SimpleRestEndpointController"
- The controller calls my business-service "CallExternalService" which has an annotated method "getPersonById" (Annotations: @TimeLimiter, @Retry)
- From this method a mocked RestTemplate is used to call the external-backend at "FANCY_URL"
- Using Mockito the RestTemplate call to the external-backend is slowed down (using Thread.sleep)
- I expect that the TimeLimiter cancels the call after 5 seconds, and the Retry ensures that the RestTemplate call is tried again (verify RestTemplate to have been called twice)
PROBLEM: TimeLimiter and Retry are registered, but do not do their job (TimeLimiter doesn't limit the call duration). Therefore RestTemplate is only called once, delivering the empty Person
(see code for clarification). The linked example project can be cloned and showcases the problem when running the test.
Code of application-test.yml
(also here: Link to application-test.yml):
resilience4j:
timelimiter:
configs:
default:
timeoutDuration: 5s
cancelRunningFuture: true
instances:
MY_RESILIENCE_KEY:
baseConfig: default
retry:
configs:
default:
maxRetryAttempts: 2
waitDuration: 100ms
retryExceptions:
- java.util.concurrent.TimeoutException
instances:
MY_RESILIENCE_KEY:
baseConfig: default
The code of this Test (also here: Link to IntegrationTest.java):
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {DemoApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@EnableAutoConfiguration
@ActiveProfiles("test")
public class IntegrationTest {
private TestRestTemplate testRestTemplate;
public final String FANCY_URL = "https://my-fancy-url-doesnt-matter.com/person";
private String apiUrl;
private HttpHeaders headers;
@LocalServerPort
private String localServerPort;
@MockBean
RestTemplate restTemplate;
@Autowired
CallExternalService callExternalService;
@Autowired
SimpleRestEndpointController simpleRestEndpointController;
@Before
public void setup() {
this.headers = new HttpHeaders();
this.testRestTemplate = new TestRestTemplate("username", "password");
this.apiUrl = String.format("http://localhost:%s/person", localServerPort);
}
@Test
public void testShouldRetryOnceWhenTimelimitIsReached() {
// Arrange
Person mockPerson = new Person();
mockPerson.setId(1);
mockPerson.setFirstName("First");
mockPerson.setLastName("Last");
ResponseEntity<Person> mockResponse = new ResponseEntity<>(mockPerson, HttpStatus.OK);
Answer customAnswer = new Answer() {
private int invocationCount = 0;
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
invocationCount++;
if (invocationCount == 1) {
Thread.sleep(6000);
return new ResponseEntity<>(new Person(), HttpStatus.OK);
} else {
return mockResponse;
}
}
};
doAnswer(customAnswer)
.when(restTemplate).exchange(
FANCY_URL,
HttpMethod.GET,
new HttpEntity<>(headers),
new ParameterizedTypeReference<Person>() {});
// Act
ResponseEntity<Person> result = null;
try {
result = this.testRestTemplate.exchange(
apiUrl,
HttpMethod.GET,
new HttpEntity<>(headers),
new ParameterizedTypeReference<Person>() {
});
} catch(Exception ex) {
System.out.println(ex);
}
// Assert
verify(restTemplate, times(2)).exchange(
FANCY_URL,
HttpMethod.GET,
new HttpEntity<>(headers),
new ParameterizedTypeReference<Person>() {});
Assert.assertNotNull(result);
Assert.assertEquals(mockPerson, result.getBody());
}
}
The code of my app showcasing the problem: https://github.com/SidekickJohn/demo
I created a swimlane diagram of the "logic" as part of the README.md: https://github.com/SidekickJohn/demo/blob/main/README.md
If you want to mock a real
RestTemplate
bean which is used by yourCallExternalService
, you have to use a Mockito Spy -> https://www.baeldung.com/mockito-spyBut I usually prefer and would recommend to use WireMock instead of Mockito to mock HTTP endpoints.