我在名为“demo”的SpringBoot 应用程序中使用注解式 Resilience4j。通过 RestTemplate 调用外部后端时,我想使用 TimeLimiter 和 Retry 来实现以下目标:
- 将 REST 调用持续时间限制为 5 秒 --> 如果需要更长时间,则失败并显示 TimeoutException
- 重试 TimeoutException --> 最多尝试 2 次
为了查看我的弹性设置的配置是否按设计工作,我编写了一个集成测试。此测试在配置文件“test”下运行,并使用“application-test.yml”进行配置:
- 使用 TestRestTemplate 向我的“SimpleRestEndpointController”发送呼叫
- 控制器调用我的业务服务“CallExternalService”,它有一个带注释的方法“getPersonById”(注释:@TimeLimiter,@Retry)
- 通过这个方法,一个模拟的 RestTemplate 用于在“FANCY_URL”调用外部后端
- 使用 Mockito 对外部后端的 RestTemplate 调用减慢(使用 Thread.sleep)
- 我希望 TimeLimiter 在 5 秒后取消调用,并且 Retry 确保再次尝试 RestTemplate 调用(验证 RestTemplate 是否已被调用两次)
问题: TimeLimiter 和 Retry 已注册,但没有完成它们的工作(TimeLimiter 不限制调用持续时间)。因此 RestTemplate 只被调用一次,提供空的Person
(见代码澄清)。可以克隆链接的示例项目并在运行测试时展示问题。
代码application-test.yml
(也在这里:链接到 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
此测试的代码(也在这里:链接到 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());
}
}
我的应用程序代码展示了这个问题: https ://github.com/SidekickJohn/demo
我创建了一个“逻辑”的泳道图作为 README.md 的一部分:https ://github.com/SidekickJohn/demo/blob/main/README.md