5

In the example below I create one Java 11 httpClient and then create multiple concurrent HttpRequests.

  1. Is this bad practice?
  2. Should each HttpRequest have its own HttpClient?
  3. Is there an upper limit on the number of HttpRequests a HttpClient can have?

Code

    private static void httpClientExample(){
    
    HttpClient httpClient = HttpClient.newHttpClient();

    System.out.println("TP1");

    var task1 = httpClient.sendAsync(HttpRequest.newBuilder()
            .uri(URI.create("https://www.bing.com/"))
            .build(), HttpResponse.BodyHandlers.ofString())
            .thenApply(HttpResponse::uri).thenAccept(System.out::println);

    var task2 = httpClient.sendAsync(HttpRequest.newBuilder()
            .uri(URI.create("https://openjdk.java.net/"))
            .build(), HttpResponse.BodyHandlers.ofString())
            .thenApply(HttpResponse::uri).thenAccept(System.out::println);
    
    var task3 = httpClient.sendAsync(HttpRequest.newBuilder()
            .uri(URI.create("https://www.google.co.uk/"))
            .build(), HttpResponse.BodyHandlers.ofString())
            .thenApply(HttpResponse::uri).thenAccept(System.out::println);


    System.out.println("Requests Sent");

    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("Main Thread Completed");
    }
Adrian Smith
  • 1,013
  • 1
  • 13
  • 21

2 Answers2

9

This is not explicitly documented in the API docs of HttpClient. But it would be expected that an HttpClient is designed to handle multiple requests. This is in a sense implied on Introduction to the Java HTTP Client:

Once built, an HttpClient can be used to send multiple requests.

Now, your question is likely about managing concurrency on your clients. Rather than with using the same instance of HttpClient, this has much to do with the executor service it uses, which is something you can customize (see here):

ExecutorService executorService = Executors.newFixedThreadPool(10);
HttpClient httpClient  = HttpClient.newBuilder()
                               .executor(executorService)
                               ... //more config
                               .build();

This way, you can manage the thread pool used by the client to run asynchronous requests.

In other words:

Is this bad practice?

No

Should each HttpRequest have its own HttpClient?

No

Is there an upper limit on the number of HttpRequests a HttpClient can have?

You will have to test the optimal concurrency settings for your application and then use an executor service configured accordingly.

ernest_k
  • 44,416
  • 5
  • 53
  • 99
  • That all makes perfect sense @ernest_k. Many thanks. – Adrian Smith Oct 11 '20 at 12:24
  • @ernest_k If we use synchronous API of HttpClient and still want to concurrent requests on the same HttpClient, how are those handled? I am having confusion in understanding that without mentioning anything explicit about connection pooling, how am I able to send concurrent synchronous HttpRequests using a single HttpClient? Talking about HTTP/1.1 here. – Shubham Nov 29 '20 at 12:37
  • @Shubham seems like you're assuming `HttpClient` is not thread-safe. It's quite standard to for http clients to be thread-safe and at least I haven't found it documented that `java.net.HttpClient` is not thread-safe. – ernest_k Nov 29 '20 at 18:31
  • @ernest_k No, not questioning thread safety. So, to send an HTTP/1.1 request, we need a connection and once a request is sent on that connection, we cannot send another request until we receive the response of the last request. If service A concurrently sends a lot of requests to B, we setup a connection pool and use the already established connections to concurrently send requests (other libraries). In Java 11 `HttpClient`, I've not seen anything related to connection pooling or to specify how much connections should we pre-establish. – Shubham Nov 30 '20 at 16:57
  • 1
    @Shubham well, for one, you can use a single-threaded pool for that if you need to push one request at a time. If you want to do that without using a thread pool, then you need to manage your request pipeline per destination, I guess. Maybe the option exists, but I haven't seen the concept of connection reuse with http/1.1 – ernest_k Nov 30 '20 at 18:50
4

I'd say it's all about the number of threads rather than the number of objects as each client can use many threads via an Executor's thread pool, whether explicitly declared or the default one. So the real question boils down to how many threads should we use? That will depend on the use of synchronous or asynchronous requests.

  1. When using send, the more threads we use from the pool, the more requests we will be able to make in parallel until the point where the OS starts threashing, i.e. increasing the number of active threads actually decreases the amount of the work accomplished.
  2. When using sendAsync as in your example, things get interesting as it is a non-blocking method which means the same thread can make multiple requests whilst waiting for responses to process. In this scenario, I'd advise to keep the number of threads in the Executor's pool equal to the number of processors' cores.
dbaltor
  • 2,737
  • 3
  • 24
  • 36
  • 1
    Many thanks for the great advice. I ran a test to compare 10, 20, 30... concurrent tests. 10 concurrent requests gave an average response time of 23ms per url. 20 = 13ms/url. 30 = 10ms/url. 40 = 32ms/url. 50 = 52ms/url. So the sweet-spot is somewhere around 30 concurrent requests per chunk. – Adrian Smith Oct 12 '20 at 12:41
  • 1
    A note on the "sendAsync [...] the same thread can make multiple requests" point: I couldn't find this in the docs, so I just did a quick test sending 10000 async GET requests to a local [httpbin](https://httpbin.org/)'s `/delay/4` path, with different HttpClient executor pool sizes. I saw the requests being processed in batches of ~1000, with little difference between (1) pool size 2, (2) pool size 10 and (3) unconstrained pool. So I can confirm that. – creinig Apr 22 '22 at 09:38