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?
