0

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:

  1. Manual request to get access token with javax.ws.rs.client.Client;
  2. 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


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.

Claudio Weiler
  • 589
  • 2
  • 15
  • Are you want to use the Jakarta REST Client or the MicroProfile REST Client? The MP REST client is generally done via injection where as with the Jakarta REST Client the builder is used. Note in WildFly 27+ you can @Inject a client. – James R. Perkins Jun 13 '23 at 20:19
  • Hi @JamesR.Perkins! Both are used, problem is with Jakarta REST Client (actually Java EE 8), or, at least, I think it is. – Claudio Weiler Jun 13 '23 at 20:53
  • The lifecycle of an MP REST Client is bound to CDI. I can't give an exact answer to how the pooling is done. It should be the same as a Jakarta REST Client though. Currently by default RESTEasy uses the Apache HTTP Client, though I'd like to make changes there. – James R. Perkins Jun 14 '23 at 22:43
  • @JamesR.Perkins *"It should be the same as a Jakarta REST Client though"* From my tests: Yes, it is. But route pool for barely uses "20 of 50". – Claudio Weiler Jun 15 '23 at 15:36

1 Answers1

1

You should be able to share a Client instance. If not, I would probably consider that a bug. Given you are using WildFly, you could file an issue with RESTEasy. One thing to note is WildFly 29+ would be the only releases to see the fix.

With regard to should you share or not, IMO it depends. It should be fine to share in most cases. However, there could be scenarios you don't want to share an instance and those should be thought about.

If the overhead is a concern because you make a lot of REST calls, then sharing makes sense there as well. However, if they are not common then the overhead likely doesn't matter.

One option would be to use your own CDI producer to inject the Client being used. For the MicroProfile REST Client it would be a bit different because it is already managed via CLI.

James R. Perkins
  • 16,800
  • 44
  • 60
  • Thanks! The manual `Client` handling is made out of CDI. I can file an issue, but for now I have no clue if this is a bug or expected behavior. But I found some more infos, added to main question. – Claudio Weiler Jun 14 '23 at 19:22