22

I'm trying the new HTTP client API from JDK 11, specifically its asynchronous way of executing requests. But there is something that I'm not sure I understand (sort of an implementation aspect). In the documentation, it says:

Asynchronous tasks and dependent actions of returned CompletableFuture instances are executed on the threads supplied by the client's Executor, where practical.

As I understand this, it means that if I set a custom executor when creating the HttpClient object:

ExecutorService executor = Executors.newFixedThreadPool(3);

HttpClient httpClient = HttpClient.newBuilder()
                      .executor(executor)  // custom executor
                      .build();

then if I send a request asynchronously and add dependent actions on the returned CompletableFuture, the dependent action should execute on the specified executor.

httpClient.sendAsync(request, BodyHandlers.ofString())
          .thenAccept(response -> {
      System.out.println("Thread is: " + Thread.currentThread().getName());
      // do something when the response is received
});

However, in the dependent action above (the consumer in thenAccept), I see that the thread doing it is from the common pool and not the custom executor, since it prints Thread is: ForkJoinPool.commonPool-worker-5.

Is this a bug in the implementation? Or something I'm missing? I notice it says "instances are executed on the threads supplied by the client's Executor, where practical", so is this a case where this is not applied?

Note that I also tried thenAcceptAsync as well and it's the same result.

M A
  • 71,713
  • 13
  • 134
  • 174
  • sorry if this is stupid, but help me understand, how did you interpret *it is from the common pool and not the custom executor since it prints Thread is: ForkJoinPool.commonPool-worker-5*?...I also tried `System.out.println(httpClient.executor().get().equals(executor));` within the `thenAccept` consumer and it prints `true`. – Naman Aug 18 '18 at 11:42
  • 2
    @nullpointer I'd assume he printed out `Thread.currentThread().getName()` inside the `thenAccept` `Consumer` and the name indicates the `Thread` is from the common `ForkJoinPool` rather than the custom `Executor`. In other words, OP is not saying the `Executor` of the `HttpClient` has _changed_, OP is wondering why the dependent `CompletableFuture` stage is executed using a different thread pool. – Slaw Aug 18 '18 at 12:08
  • 1
    @nullpointer Exactly what Slaw has said. I also know that the thread is from the common pool because I can give the threads created by the custom executor special names to clearly identify them. As for `httpClient.executor()`, this method just returns the executor I specified upon creation, which is not what the `thenAccept` uses. – M A Aug 18 '18 at 14:53
  • @Slaw @manouti Thanks. I got what you both were pointing to, indeed tried providing a custom named thread to the executor and could see that it's not being used in `thenAccept`. Would look further for details around *where practical* part of it and the bug database as well. – Naman Aug 18 '18 at 18:18
  • 1
    It turns out the documentation was already updated during the progress of this API, so that it describes this behavior. The more recent docs link is https://download.java.net/java/early_access/jdk11/docs/api/java.net.http/java/net/http/package-summary.html – M A Aug 23 '18 at 09:53

2 Answers2

15

I just found an updated documentation (the one I initially linked to seems old) where it explains this implementation behavior:

In general, asynchronous tasks execute in either the thread invoking the operation, e.g. sending an HTTP request, or by the threads supplied by the client's executor. Dependent tasks, those that are triggered by returned CompletionStages or CompletableFutures, that do not explicitly specify an executor, execute in the same default executor as that of CompletableFuture, or the invoking thread if the operation completes before the dependent task is registered.

And the default executor of CompletableFuture is the common pool.

I also found the bug ID that introduces this behavior, in which the API developers fully explains it:

2) Dependent tasks run in the common pool The default execution of dependent tasks has been updated to run in the same executor as that of CompletableFuture's defaultExecutor. This is more familiar to developers that already use CF, and reduces the likelihood of the HTTP Client being starved of threads to execute its tasks. This is just default behaviour, both the HTTP Client and CompletableFuture allow more fine-grain control, if needed.

M A
  • 71,713
  • 13
  • 134
  • 174
  • “_This is just default behaviour, both the HTTP Client and CompletableFuture allow more fine-grain control, if needed._” For `CompletableFuture`, I assume he means using the `*Async(…, Executor)` method variants. However for HTTP Client, I don't see how it helps controlling this behaviour. I don't find using the async method variants everywhere very convenient. – Didier L Aug 23 '18 at 16:22
  • This is what I also thought when I read that point; only the builder allows passing an executor in the HTTP client, which does not even affect dependent tasks in the CF. Furthermore, even the recent documentation does not seem to be consistent as [it still says](https://download.java.net/java/early_access/jdk11/docs/api/java.net.http/java/net/http/HttpClient.Builder.html#executor(java.util.concurrent.Executor)) "Sets the executor to be used for asynchronous and dependent tasks." Maybe they missed updating that part. – M A Aug 24 '18 at 07:01
  • 1
    I upvoted @manouti's answer, which is spot on. Also the following bug has been filed to fix the errant spec that says that dependent tasks are run in the executor, https://bugs.openjdk.java.net/browse/JDK-8209943 – chegar999 Aug 24 '18 at 14:14
  • @chegar999 Cool! And thanks for working to bring this API! FYI I summarized some examples on using the API in a post [here](https://mahmoudanouti.wordpress.com/2018/08/20/new-java-http-client/), during which I came across this behavior. – M A Aug 24 '18 at 15:08
11

Short-version: I think you've identified an implementation detail and that "where practical" is meant to imply that there is no guarantee that the provided executor will be used.

In detail:

I've downloaded the JDK 11 source from here. (jdk11-f729ca27cf9a at the time of this writing).

In src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java, there is the following class:

/**
 * A DelegatingExecutor is an executor that delegates tasks to
 * a wrapped executor when it detects that the current thread
 * is the SelectorManager thread. If the current thread is not
 * the selector manager thread the given task is executed inline.
 */
final static class DelegatingExecutor implements Executor {

This class uses the executor if isInSelectorThread is true, otherwise the task is executed inline. This boils down to:

boolean isSelectorThread() {
    return Thread.currentThread() == selmgr;
}

where selmgr is a SelectorManager. Edit: this class is also contained in HttpClientImpl.java:

// Main loop for this client's selector
private final static class SelectorManager extends Thread {

The upshot: I'm guessing where practical implies that it is implementation dependent and that there is no guarantee that the provided executor will be used.

NOTE: this is different than the default executor, where the builder does not provide an executor. In that case, the code clearly creates a new cached-thread pool. Stated another way, if the builder provides an executor, the identity check for SelectorManager is made.

Michael Easter
  • 23,733
  • 7
  • 76
  • 107
  • 1
    Thanks for your answer. It indeed seems to be an implementation detail. Regarding your last note, however, when I don't specify an executor in the client setup, the dependent actions still use the common pool, and not the default thread cache executor behind the scenes. – M A Aug 23 '18 at 09:37