This belongs to the net-dev mailing list, which I CC'ed.

On Wed, Oct 1, 2025 at 10:56 AM Michał G. <[email protected]> wrote:
>
> Hi all,
>
> I recently ran into an issue with HttpClientImpl where I wanted to handle the 
> reply right after calling sendAsync. What surprised me is that the returned 
> CompletableFuture already runs on the commonPool, instead of using the 
> executor I provided to the HttpClient.
>
> Looking into the implementation, I noticed this piece of code:
>
> // makes sure that any dependent actions happen in the CF default
> // executor. This is only needed for sendAsync(...), when
> // exchangeExecutor is non-null.
> if (exchangeExecutor != null) {
>     res = res.whenCompleteAsync((r, t) -> { /* do nothing */}, ASYNC_POOL);
> }
>
> I understand that this exchangeExecutor is meant to cover the 
> request/response exchange itself, not necessarily the CompletableFuture 
> chaining. But the fact that we always force the returned future back onto the 
> commonPool, without any way to change this, feels quite limiting.
>
> In environments where the commonPool is already heavily loaded, this can 
> easily introduce performance issues or unpredictable behavior. And since
>
> private static final Executor ASYNC_POOL = new 
> CompletableFuture<Void>().defaultExecutor();
>
> is fixed and cannot be replaced, users don’t have any way around it. For 
> comparison, the default executor for CompletableFuture.supplyAsync or for 
> parallelStream() is also the commonPool, but in those cases it’s easy to 
> override it with a custom executor. It would be nice if HttpClientImpl 
> offered the same flexibility.
>
> This is why I’d like to propose a change: when creating an HttpClientImpl, it 
> should be possible to specify not only the exchange executor, but also the 
> executor used for the returned CompletableFuture
>
> This would be backwards compatible (just an additional optional builder 
> parameter), and it could bring several benefits:
>
> reduced context switching for clients that care about which thread executes 
> response handling,
>
> more predictable performance in environments where commonPool is shared with 
> other workloads,
>
> easier integration with frameworks that already manage their own executors,
>
> clearer control and observability over where callbacks are executed.
>
> Would such a change make sense, or is there a strong reason why we must 
> always fallback to the commonPool?

Reply via email to