You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 11:16:09