Search code examples
springnetty

What for is pendingAcquireTimeout Netty option


I am fine-tuning Netty and not able to figure out what is the purpose of pendingAcquireTimeout option. Using WebClient from Spring Framework 6.1.8 together with Netty 1.1.19

In the documentation there is written:

The maximum time before which a pending acquire must complete, or a TimeoutException is thrown (resolution: ms). If -1 is specified, no such timeout is applied. Default: 45 seconds.

Let's say I have the following configuration:

ConnectionProvider.Builder providerBuilder = ConnectionProvider.builder("custom")
        .maxConnections(20) // pending queue size is 40 (2 x connection size)
        .pendingAcquireTimeout(Duration.ofSeconds(45)); // this should be default
HttpClient httpClient = HttpClient.create(providerBuilder.build());
ReactorClientHttpConnector connector = new ReactorClientHttpConnector(httpClient);
WebClient webClient = WebClient.builder()
        .baseUrl("http://127.0.0.1:8080")
        .clientConnector(connector)
        .build();

Now I call 61 parallel requests to a slow endpoint(Thread.sleep(Duration.ofSeconds(60))).

I expect just one error, see below. There are 20 connections in progress, 40 pending, and 1 is out.

Caused by: org.springframework.web.reactive.function.client.WebClientRequestException: Pending acquire queue has reached its maximum size of 40

So far so good. The question is: Does the pendingAcquireTimeout option affect this use case at all? If so, how?

My hypothesis was, that the requests would stay in the pending queue just only for defined 45 seconds and throw an exception because the endpoint is too slow. But it did not happen, they are gradually being served.


Solution

  • Well, the property pendingAcquireTimeout behaves as expected. Getting this error:

    Error org.springframework.web.reactive.function.client.WebClientRequestException: Pool#acquire(Duration) has been pending for more than the configured timeout of 45000ms
    

    It is not fully clear to me yet, but the root cause is a race condition. When 61 threads start at once, several connection providers are created. Moreover the additional providers do not respect my configuration.

    final Runnable callWebClient = () -> {
        logger.info("Started");
        try {
            final var result = webClient.get()
                    .uri("/blocking")
                    .retrieve()
                    .bodyToMono(String.class)
                    .block();
            logger.info("Finished, {}", result);
        } catch (Exception e) {
            logger.error("Error", e);
        }
    };
    
    final ThreadFactory factory = Thread.ofVirtual().name("routine-", 0).factory();
    try (var executor = Executors.newThreadPerTaskExecutor(factory)) {
        IntStream.range(0, 61)
                .forEach(i -> executor.submit(callWebClient));
    } catch (Exception e) {
        logger.error("Error", e);
    }
    

    The following naive delay fixes that:

    IntStream.range(0, 61)
            .forEach(i -> {
                executor.submit(callWebClient);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException(e);
                }
            });