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.