使用StepVerifier虚拟时间测试带退避策略的WebClient请求失败
问题:WebClient退避重试在虚拟时间测试中仅触发两次重试
场景复现
业务方法使用WebClient实现带退避策略的重试:
protected Mono<String> executeRequest(String url) { return webClient.method(HttpMethod.GET) .uri(url) .retrieve() .bodyToMono(String.class) .doOnError(t -> log.info("Damn, error: {}", t.getMessage())) .retryWhen(Retry.backoff(5, Duration.ofSeconds(5))); }
测试用MockWebServer模拟错误响应,搭配StepVerifier虚拟时间验证重试逻辑:
@Test void test() { String mockWebServerUrl = mockWebServer.url("/path") .uri() .toString(); mockWebServer.enqueue(MockWebServerUtil.createMockResponse(HttpStatus.NOT_FOUND)); mockWebServer.enqueue(MockWebServerUtil.createMockResponse(HttpStatus.NOT_FOUND)); mockWebServer.enqueue(MockWebServerUtil.createMockResponse(HttpStatus.NOT_FOUND)); mockWebServer.enqueue(MockWebServerUtil.createMockResponse(HttpStatus.NOT_FOUND)); mockWebServer.enqueue(MockWebServerUtil.createMockResponse(HttpStatus.NOT_FOUND)); mockWebServer.enqueue(MockWebServerUtil.createMockResponse(HttpStatus.NOT_FOUND)); StepVerifier.withVirtualTime(() -> dumbClass.executeRequest(mockWebServerUrl)) .expectSubscription() .thenAwait(Duration.ofDays(100)) .expectError() .verify(); }
问题现象:
- 使用
Retry.max(5)时,测试正常触发5次重试后失败 - 使用
Retry.backoff或Retry.fixedDelay时,仅触发前两次重试,后续重试不执行
简单解决思路
核心原因
Retry.backoff默认使用全局并行调度器(Schedulers.parallel()),而StepVerifier的虚拟时间仅能管控订阅链内使用的调度器,无法接管Retry的延迟调度线程,导致虚拟时间推进时,Retry的后续延迟任务不会被触发。
解决方案(无需修改业务代码)
在测试中临时将默认调度器替换为虚拟时间调度器,让Retry使用虚拟时间来管控延迟:
@Test void test() { VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.getOrSet(); Scheduler originalDefault = Schedulers.getDefault(); try { // 临时替换默认调度器为虚拟时间调度器 Schedulers.setDefault(virtualTimeScheduler); String mockWebServerUrl = mockWebServer.url("/path") .uri() .toString(); // 批量入队6次错误响应(初始请求+5次重试) for (int i = 0; i < 6; i++) { mockWebServer.enqueue(MockWebServerUtil.createMockResponse(HttpStatus.NOT_FOUND)); } StepVerifier.withVirtualTime(() -> dumbClass.executeRequest(mockWebServerUrl)) .expectSubscription() // 等待足够覆盖所有退避延迟的时间(5*(1+2+4+8+16)=155秒,这里设200秒足够) .thenAwait(Duration.ofSeconds(200)) .expectError() .verify(); } finally { // 恢复原默认调度器,避免影响其他测试 Schedulers.setDefault(originalDefault); } }
另一种方案(修改业务代码,更优雅)
如果允许调整业务代码,可以直接让Retry绑定自适应调度器(生产环境下会自动使用真实调度器,测试时适配虚拟时间):
protected Mono<String> executeRequest(String url) { return webClient.method(HttpMethod.GET) .uri(url) .retrieve() .bodyToMono(String.class) .doOnError(t -> log.info("Damn, error: {}", t.getMessage())) .retryWhen(Retry.backoff(5, Duration.ofSeconds(5)) .withScheduler(Schedulers.fromExecutorService(Runnable::run))); }
内容的提问来源于stack exchange,提问作者Aleksandar Obreshkov
相关产品推荐
相关产品推荐

