【问题标题】:Test using StepVerifier blocks when using Spring WebClient with retry使用带有重试的 Spring WebClient 时使用 StepVerifier 块进行测试
【发布时间】:2020-04-11 20:22:03
【问题描述】:

编辑:这里的https://github.com/wujek-srujek/reactor-retry-test 是一个包含所有代码的存储库。

我有以下 Spring WebClient 代码要 POST 到远程服务器(为简洁起见,没有导入的 Kotlin 代码):

private val logger = KotlinLogging.logger {}

@Component
class Client(private val webClient: WebClient) {

    companion object {
        const val maxRetries = 2L
        val firstBackOff = Duration.ofSeconds(5L)
        val maxBackOff = Duration.ofSeconds(20L)
    }

    fun send(uri: URI, data: Data): Mono<Void> {
        return webClient
            .post()
            .uri(uri)
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(data)
            .retrieve()
            .toBodilessEntity()
            .doOnSubscribe {
                logger.info { "Calling backend, uri: $uri" }
            }
            .retryExponentialBackoff(maxRetries, firstBackOff, maxBackOff, jitter = false) {
                logger.debug { "Call to $uri failed, will retry (#${it.iteration()} of max $maxRetries)" }
            }
            .doOnError {
                logger.error { "Call to $uri with $maxRetries retries failed with $it" }
            }
            .doOnSuccess {
                logger.info { "Call to $uri succeeded" }
            }
            .then()
    }
}

(它返回一个空的Mono,因为我们不期待答案,也不关心它。)

我想测试 2 个案例,其中一个让我很头疼,即我想测试所有重试都已被解雇的案例。我们正在使用来自 reactor-test 的 MockWebServer (https://github.com/square/okhttp/tree/master/mockwebserver) 和 StepVerifier。 (成功的测试很简单,不需要任何虚拟时间调度魔法,而且工作得很好。)这是失败的代码:

@JsonTest
@ContextConfiguration(classes = [Client::class, ClientConfiguration::class])
class ClientITest @Autowired constructor(
    private val client: Client
) {
    lateinit var server: MockWebServer

    @BeforeEach
    fun `init mock server`() {
        server = MockWebServer()
        server.start()
    }

    @AfterEach
    fun `shutdown server`() {
        server.shutdown()
    }

   @Test
   fun `server call is retried and eventually fails`() {
       val data = Data()
       val uri = server.url("/server").uri()
       val responseStatus = HttpStatus.INTERNAL_SERVER_ERROR

       repeat((0..Client.maxRetries).count()) {
           server.enqueue(MockResponse().setResponseCode(responseStatus.value()))
       }

       StepVerifier.withVirtualTime { client.send(uri, data) }
           .expectSubscription()
           .thenAwait(Duration.ofSeconds(10)) // wait for the first retry
           .expectNextCount(0)
           .thenAwait(Duration.ofSeconds(20)) // wait for the second retry
           .expectNextCount(0)
           .expectErrorMatches {
               val cause = it.cause
               it is RetryExhaustedException &&
                       cause is WebClientResponseException &&
                       cause.statusCode == responseStatus
           }
           .verify()

       // assertions
       }
   }

我正在使用withVirtualTime,因为我不希望测试花费近几秒钟。 问题是测试无限期地阻塞。这是(简化的)日志输出:

okhttp3.mockwebserver.MockWebServer      : MockWebServer[51058] starting to accept connections
Calling backend, uri: http://localhost:51058/server
MockWebServer[51058] received request: POST /server HTTP/1.1 and responded: HTTP/1.1 500 Server Error
Call to http://localhost:51058/server failed, will retry (#1 of max 2)
Calling backend, uri: http://localhost:51058/server
MockWebServer[51058] received request: POST /server HTTP/1.1 and responded: HTTP/1.1 500 Server Error
Call to http://localhost:51058/server failed, will retry (#2 of max 2)

如您所见,第一次重试有效,但第二次阻塞。我不知道如何编写测试,以免它发生。更糟糕的是,客户端实际上会使用抖动,这会使时间难以预测。

以下使用StepVerifier 但不使用WebClient 的测试工作正常,即使重试次数更多:

@Test
fun test() {
    StepVerifier.withVirtualTime {
        Mono
            .error<RuntimeException>(RuntimeException())
            .retryExponentialBackoff(5,
                                     Duration.ofSeconds(5),
                                     Duration.ofMinutes(2),
                                     jitter = true) {
                println("Retrying")
            }
            .then()
    }
        .expectSubscription()
        .thenAwait(Duration.ofDays(1)) // doesn't matter
        .expectNextCount(0)
        .expectError()
        .verify()
}

谁能帮我修复测试,最好解释一下哪里出了问题?

【问题讨论】:

  • 如果你像第二个例子那样做一个大的thenAwait,它的表现如何?
  • 另外,retryExponentialBackoff 是自定义扩展方法吗?核心运算符称为retryBackoff...
  • @SimonBaslé - 它阻止了同样的事情。 retryExponentialBackoff 函数来自这里:github.com/reactor/reactor-kotlin-extensions/blob/master/src/…,由...你编写?
  • 哈哈太专注于核心运算符,以至于我忘记了额外的扩展...您可以尝试使用香草核心 retryBackoff 运算符(自 3.2 以来的核心),看看这是否“仅”一个额外反应堆中的错误?
  • 我会在我到电脑前完成,需要几个小时。

标签: project-reactor spring-webclient


【解决方案1】:

这是对虚拟时间和StepVerifier 中时钟操作方式的限制。 thenAwait 方法与底层调度不同步(例如,作为retryBackoff 操作的一部分发生)。这意味着操作员在时钟已经提前一天的时间点提交重试任务。所以第二次重试安排在+ 1 day and 10 seconds,因为时钟在+1 day。之后,时钟永远不会提前,因此永远不会向MockWebServer 发出额外的请求。

您的情况变得更加复杂,因为涉及到一个额外的组件,MockWebServer,它仍然“实时”工作。 尽管推进虚拟时钟是一个非常快速的操作,但来自MockWebServer 的响应仍然通过套接字,因此对重试调度有一定的延迟,这使得从测试编写的角度来看事情变得更加复杂。

探索一种可能的解决方案是将VirtualTimeScheduler 的创建外部化并将advanceTimeBy 调用绑定到mockServer.takeRequest(),在一个并行线程中。

【讨论】:

  • 在尝试测试需要在retryWhen(Retry.fixedDelay(...)) 真正继续之后继续的流时,我遇到了类似的问题。如果没有虚拟计时器,测试将需要很长时间。您能否详细说明如何提前retry 的计时器?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-01-17
  • 1970-01-01
  • 2019-09-19
  • 2021-12-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多