Alexandr
Alexandr

Reputation: 9515

Reusing JAX RS Client in multi-threaded environment (with resteasy)

According to the documentation,

"Clients are heavy-weight objects that manage the client-side communication infrastructure. Initialization as well as disposal of a Client instance may be a rather expensive operation. It is therefore advised to construct only a small number of Client instances in the application. "

Ok, I'm trying to cache Client itself and WebTarget instances in a static variable, the someMethod() is invoked in multi-threaded environment:

private static Client client = ClientBuilder.newClient();
private static WebTarget webTarget = client.target("someBaseUrl");
...
public static String someMethod(String arg1, String arg2)
{
    WebTarget target = entrTarget.queryParam("arg1", arg1).queryParam("arg2", arg2);
    Response response = target.request().get();
    final String result = response.readEntity(String.class);
    response.close();
    return result;
}

But sometimes (not always) I'm get an exception:

Invalid use of BasicClientConnManager: connection still allocated. Make sure to release the connection before allocating another one.

How can Client/WebTarget be reused/cached correctly? Is it possible with JAX RS Client API? Or I have to use some framework-specific features (resteasy/jersey) Could you provide some example or documentation?

Upvotes: 24

Views: 17273

Answers (4)

TastyWheat
TastyWheat

Reputation: 2337

The documentation is unfortunately not very clear about what can and can't be reused safely. When in doubt reuse nothing. However, if you're determined to minimize overhead you can safely reuse most objects depending on the methods called.

Starting with your code, here's some comments about what's happening:

// (1) Store an instance of Client with its own configuration
private static Client client = ClientBuilder.newClient();
// (2) Store an instance of WebTarget with its own configuration (inherited from client)
private static WebTarget webTarget = client.target("someBaseUrl");
...
public static String someMethod(String arg1, String arg2)
{
    // (3) New instance of WebTarget (copy entrTarget config) with "arg1" param
    // (4) New instance of WebTarget (copy anonymous config) with "arg2" param
    WebTarget target = entrTarget.queryParam("arg1", arg1).queryParam("arg2", arg2);
    // (5) New instance of Invocation.Builder (copy target config)
    // (6) Invoke GET request with stored configuration
    Response response = target.request().get();
    final String result = response.readEntity(String.class);
    response.close();
    return result;
}

I commented on the code as-is but I'm guessing (3) should've referenced the static webTarget field.

A lot of objects are getting created here. Every time an object is created there's a new instance with its own copy of the configuration (so it won't affect its predecessors). In this particular case there should be no race conditions, but there are definitely ways in which this could go wrong.

If on or before (3) you had done something like this (assuming these are legitimate properties):

WebTarget target = webTarget.property("foo", fooProperty).queryParam("arg1", arg1);

Then you would be altering the configuration of the static webTarget field and that could cause a race condition. There are many ways to alter the configuration from the static fields so you either need to guard them carefully or just not have them at all.

Also, be aware that pretty much every object that spawns from the original client will have a reference to it for the purpose of determining if the httpEngine has been closed. So unless you're trying to gracefully shutdown your application it's likely never a good idea to close the client.

And I found out all of this by digging through the source code because there's really no good reference.

Upvotes: 0

Patrick
Patrick

Reputation: 799

Since this issue is still open at the time of writing (version 3.0.X) RESTEASY: deprecated Apache classes cleanup

You can go deeper to use the newer, non-deprecated classes instead to create you resteasy client. You will also have more control over how you want the pool to be etc.

Here is what I did:

// This will create a threadsafe JAX-RS client using pooled connections.
// Per default this implementation will create no more than than 2
// concurrent connections per given route and no more 20 connections in
// total. (see javadoc of PoolingHttpClientConnectionManager)
PoolingHttpClientConnectionManager cm =
        new PoolingHttpClientConnectionManager();

CloseableHttpClient closeableHttpClient =
        HttpClientBuilder.create().setConnectionManager(cm).build();
ApacheHttpClient4Engine engine =
        new ApacheHttpClient4Engine(closeableHttpClient);
return new ResteasyClientBuilder().httpEngine(engine).build();

Also make sure you release the connection after making a call. Calling response.close() will do that for you so probably put that in a finally block.

Upvotes: 8

Pavel Pscheidl
Pavel Pscheidl

Reputation: 344

First, do not reuse WebTarget. For simplicity, you can always create new WebTarget.

Second, if you're using Resteasy, you can add provided dependency for Resteasy client to your project. Example in Gradle:

    provided 'org.jboss.resteasy:resteasy-client:3.0.14.Final'

Then, you can create your connection like this:

        ResteasyClientBuilder builder = new ResteasyClientBuilder();
        builder.connectionPoolSize(200);

There is no need to set maxPooledPerRoute, this is set automatically by RestEasy (can be found in RestEasyClientBuilder class source code).

When you set connectionPoolSize, you will no longer get error when Client is reused and you can happily re-use them all across the application. I've tried this solution on many projects and it actually works well. But when you deploy your application to a non-resteasy container (like Glassfish), your code won't work and you will have to use ClientBuilder class again.

Upvotes: 4

lefloh
lefloh

Reputation: 10981

Your implementation is not thread-safe. When two threads access someMethod at the same time they are sharing the same Client and one will try to make a second request while the first one is not finished.

You have two choices:

  • Synchronize the access to the Client and WebTarget manually.
  • Let the container manage concurrency by annotating the enclosing type with @javax.ejb.Singleton which guarantees thread safety. (see chapter 4.8.5 of the EJB specification)

If someMethod in a container managed environment I would use the second approach.

Upvotes: 9

Related Questions