javax.ws.rs.client.Client documentation states that:
The main entry point to the API is a ClientBuilder that is used to bootstrap Client instances - configurable, heavy-weight objects that manage the underlying communication infrastructure and serve as the root objects for accessing any Web resource.
I have doubts if it refers to Client or ClientBuilder instances. Searched in the web, and it appears that it refers to Client, but...
I made a simple load test. It starts 10 threads, each thread makes 50 calls with a random interval (100 to 500 ms) between calls.
Each test call actually do 2 HTTP requests:
- Manual request to get access token with
javax.ws.rs.client.Client; - MicroProfile REST Client managed final request using above access token with
@ClientHeaderParam(name = HttpHeaders.AUTHORIZATION, ....
With this load test I made some implementations tests. The tests are run in a WildFly 26 instance.
First implementation - Each request do a ClientBuilder.newClient()
Obs. Each Response and Client are closed after use.
- Minimum: 31 ms
- Maximum: 196 ms
- Average: ~48 ms
Second Implementation - Shared ClientBuilder instance
Obs. Each Response and Client are closed after use.
- Minimum: 25 ms
- Maximum: 72 ms
- Average: ~32 ms
Here we got some improvements, looks like ClientBuilder really benefits from being shared.
Both implementations run flawless.
Third implementation - Shared Client instance
Obs. Each Response are closed after use.
This test can't run complete, it starts to throw errors about the 7~10 iteration of each thread, so the results are limited:
- Minimum: 14 ms
- Maximum: 53 ms
- Average: ~21 ms
It really looks that a shared instance is a way to go, but the errors made this a no go.
Error: java.net.SocketException: Software caused connection abort: recv failed
Fourth implementation - Shared pooled Client instance with commons-pool2
Obs. Each Response are closed and Client is return to the pool after use.
Created a generic pool with 20 max capacity, Client factory uses a shared ClientBuilder, this pool is filled (by demand) with 10 clients during run, which corresponds to the load test 10 threads. But this test also starts to throw errors (same error above), now about the 9~12 iteration of each thread, so the results are also limited:
- Minimum: 15 ms
- Maximum: 67 ms (with some aberrations to ~5500 ms)
- Average: ~20 ms (ignoring aberrations)
Looks like there is no gain using a pool with shared Client instances.
Observations
It really appears that Client instances should be shared, but for some reasons it appears that this instance starts to broke after some uses.
With a shallow inspection on MicroProfile REST Client, it looks to always use a new ClientBuilder, it is different implementation, but I can't go deeper in my investigation.
So, my question
Should I, or not, to reuse javax.ws.rs.client.Client instances?
References
- https://javaee.github.io/javaee-spec/javadocs/javax/ws/rs/client/package-summary.html
- Is a singleton javax.ws.rs.client.Client thread-safe in case of being used in the JAX-RS Resource?
- Jax rs client pool
- How to correctly share JAX-RS 2.0 client
- Need to create javax.ws.rs.client.Client for each webservcie call
Use case explanation
I have a MicroProfile REST Client that connects to a authenticated endpoint. Authentication is made with OpenID Connect.
@ClientHeaderParam is used to inject the authentication header into MP client requests, and this annotations points to an authentication header generator that do the client_credential authentication with OpenID Server (KeyCloak) to obtain a valid token and return it to the MP method call.
Edit 1
I found how to enable some loggings and I see a lot of this debug output:
[org.apache.http.impl.conn.PoolingHttpClientConnectionManager] (Thread-*) Connection request: [route: {}->http://localhost:80][total available: 0; route allocated: 50 of 50; total allocated: 50 of 50]
This line represents the requests made to REST endpoint, not the OpenID server.
So, now my suspect is that the problem is not directly on Client instance, but in MicroProfile REST Client. For some reason, when the Client instance is shared, MP connection pooling takes to long to release the leased connections, what made it to hang on waiting to available connections. And when Client instance isn't shared, the release occurs more quickly, in this case I never get an allocated: 50 of 50.
With this new info, I found that the 1 thread that do 50 calls caused some contentions, so I isolated the call into a Callable and it resolved the errors, but this also made the average time grow to absurd 2 seconds!
Now I have a new question: How is Client instance sharing is related to MicroProfile REST Client connection pool?
Edit 2
So, it doesn't matter how REST client is being build, all will fall into org.apache.http.impl.conn.PoolingHttpClientConnectionManager.
For some reason, it can't share connections and takes too long to release connections quickly hitting 50 of 50 allocated, then it begins to lock resources, when I use @Inject @RestClient.
When I build proxies manually with RestClientBuilder.newBuilder().baseUri(uri).build(Client.class) it always use a fresh connection manager with 1 of 50 allocated, and connection manager is shut down right after each connection usage.
I think that MicroProfile REST client builder shares the builder: https://github.com/eclipse/microprofile-rest-client/blob/b6eb2f651c0cbb19f0300eba5c6a3242e9e46019/api/src/main/java/org/eclipse/microprofile/rest/client/RestClientBuilder.java#L57
And I think it tries to share the client, but for each request, for the very same endpoint, with the same header (can change when token expires), it just don't reuse the client connection from the pool.