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

Spring Reactive WebClient重试逻辑用StepVerifier虚拟时间测试卡住问题

问题根因
  • Retry.fixedDelay默认使用Reactor内置的Schedulers.parallel()调度器执行延迟逻辑,而StepVerifier.withVirtualTime只能接管虚拟时间调度器的时序,默认调度器的延迟不受虚拟时间控制,会按照真实系统时间等待2秒/次,导致测试看起来卡在重试环节。
  • 如果你使用OkHttp的MockWebServer做接口模拟,会产生真实的TCP网络IO请求,这部分逻辑运行在独立IO线程,不受Reactor虚拟时间管控,也会引发时序错乱导致测试卡住。
解决方案

方案1:修改重试逻辑支持调度器注入(推荐,兼顾逻辑正确性和测试能力)

业务代码中给Retry配置显式指定可替换的调度器,测试时替换为虚拟时间调度器即可:

// 业务代码新增可配置的调度器
private Scheduler retryScheduler = Schedulers.parallel();

// 提供setter方法供测试注入自定义调度器
public void setRetryScheduler(Scheduler retryScheduler) {
    this.retryScheduler = retryScheduler;
}

public Mono<Todo> getTodoWithSimpleRetry(String id) {
    RetryBackoffSpec retrySpec = Retry.fixedDelay(3, Duration.ofSeconds(2))
            .scheduler(retryScheduler) // 新增这行指定调度器
            .doBeforeRetry(beforeRetry -> {
                log.info("Attempt to retry: " + (beforeRetry.totalRetries() + 1));
            }).doAfterRetry(afterRetry -> {
                log.error("After retry {}", afterRetry.failure().getMessage());
            })
            .onRetryExhaustedThrow(((retrySpec1, retrySignal) -> {
                throw new TodoServiceException("Max retry exceeded. Todo Service is not available", HttpStatus.SERVICE_UNAVAILABLE.value());
            }));

    return webClient.get()
            .uri("/todo/{id}", id)
            .retrieve()
            .bodyToMono(Todo.class)
            .retryWhen(retrySpec);
}

测试代码中注入虚拟时间调度器:

private VirtualTimeScheduler virtualTimeScheduler;

@BeforeEach
void setUp() {
    virtualTimeScheduler = VirtualTimeScheduler.create();
    todoWebClient.setRetryScheduler(virtualTimeScheduler);
}

@Test
void testRetry() {
    mockBackEnd.enqueue(new MockResponse().setResponseCode(500));
    mockBackEnd.enqueue(new MockResponse().setResponseCode(500));
    mockBackEnd.enqueue(new MockResponse().setResponseCode(500));
    mockBackEnd.enqueue(new MockResponse().setResponseCode(500));
    
    StepVerifier.withVirtualTime(() -> todoWebClient.getTodoWithSimpleRetry("1"))
            .expectSubscription()
            .thenAwait(Duration.ofSeconds(6)) // 直接快进总等待时间即可
            .expectError(TodoServiceException.class)
            .verify();
}

方案2:替换MockWebServer为Spring MockWebClient

使用Spring自带的MockWebClient做接口模拟,不需要真实网络IO,完全适配Reactor的调度器体系,避免外部线程干扰虚拟时间控制:

@Test
void testRetry() {
    MockWebClient mockWebClient = MockWebClient.create();
    TodoWebClient todoWebClient = new TodoWebClient(mockWebClient);
    
    // 配置4次500响应,对应1次初始请求+3次重试
    mockWebClient.get().uri("/todo/1").times(4)
            .respond(response -> response.statusCode(500));

    StepVerifier.withVirtualTime(() -> todoWebClient.getTodoWithSimpleRetry("1"))
            .expectSubscription()
            .thenAwait(Duration.ofSeconds(6))
            .expectError(TodoServiceException.class)
            .verify();
}

方案3:快速验证方案(仅验证异常和重试次数)

如果不需要校验重试间隔的正确性,可以直接将测试环境的重试间隔改为0,跳过等待逻辑:

// 测试时用无延迟的重试规则代替固定延迟重试
RetryBackoffSpec testRetrySpec = Retry.max(3)
        .doBeforeRetry(beforeRetry -> log.info("Attempt to retry: " + (beforeRetry.totalRetries() + 1)))
        .onRetryExhaustedThrow(((retrySpec1, retrySignal) -> {
            throw new TodoServiceException("Max retry exceeded. Todo Service is not available", HttpStatus.SERVICE_UNAVAILABLE.value());
        }));

内容的提问来源于stack exchange,提问作者ExcepOra

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.12 03:57:00