Hi folks,

TL;DR: here is -- as promised -- a possible solution how to pool HTTP sessions with a single HttpClient:

First, I had to create the custom object for holding that session information:

public class RawSession {

  private CookieStore cookieStore;
  private MutableInt requestId;
  private String clientId;
  private String serverId;

public RawSession(CookieStore cookieStore, MutableInt requestId, String clientId, String serverId) {
    this.cookieStore = cookieStore;
    this.requestId = requestId;
    this.clientId = clientId;
    this.serverId = serverId;
  }
...
  // hashCode and equals generated by Eclipse with the unique id
  ...
}

Followed by a shim to the underlying pool:
public class RawSessionPool implements DisposableBean {

  private GenericObjectPool<RawSession> objectPool;

public RawSessionPool(RawSessionPooledObjectFactory factory, GenericObjectPoolConfig config) { Validate.notNull(factory, "RawSessionPooledObjectFactory cannot be null");
    Validate.notNull(config, "GenericObjectPoolConfig cannot be null");
    this.objectPool = new GenericObjectPool<RawSession>(factory, config);
  }

  public RawSession acquire() throws Exception {
    return objectPool.borrowObject();
  }

  public void release(RawSession session) throws Exception {
    objectPool.returnObject(session);
  }

  public void invalidate(RawSession session) throws Exception {
    objectPool.invalidateObject(session);
  }

  @Override
  public void destroy() throws Exception {
    objectPool.close();
  }

}

and now the factory for the sessions:
public class RawSessionPooledObjectFactory extends BasePooledObjectFactory<RawSession> {

  @Autowired
  @Qualifier("httpClient")
  CloseableHttpClient httpClient;

  @Override
  public RawSession create() throws Exception {
    CookieStore cookieStore = new BasicCookieStore();

    MutableInt requestId = new MutableInt();
ResponseHandler<CustomResponse> responseHandler = new CustomResponseHandler();

    String clientId = requestUtils.generateClientId();

    // Login
    requestId.increment();
    HttpPost login = new HttpPost(<login-url>);
    String loginBody = ...;
    HttpEntity entity = requestUtils.mergeAndCreate(loginBody, <vars>);
    login.setEntity(entity);

    HttpClientContext httpContext = HttpClientContext.create();
    httpContext.setCookieStore(cookieStore);
CustomResponse response = httpClient.execute(login, responseHandler, httpContext);
    ...

RawSession session = new RawSession(cookieStore, requestId, clientId, serverId);

    return session;
  }

  @Override
  public PooledObject<RawSession> wrap(RawSession session) {
    return new DefaultPooledObject<RawSession>(session);
  }

  @Override
  public void destroyObject(PooledObject<RawSession> p) throws Exception {
    RawSession session = p.getObject();
    MutableInt requestId = session.getRequestId();
    String clientId = session.getClientId();
    String serverId = session.getServerId();

    CookieStore cookieStore = session.getCookieStore();

ResponseHandler<CustomResponse> responseHandler = new CustomResponseHandler();

    requestId.increment();
    // Logout
    HttpPost logout = new HttpPost(<logout-url>);
    String logoutBody = ...;

    HttpEntity entity = requestUtils.mergeAndCreate(logoutBody, <vars>);

    logout.setEntity(entity);

    HttpClientContext httpContext = HttpClientContext.create();
    httpContext.setCookieStore(cookieStore);
CustomResponse response = httpClient.execute(logout, responseHandler, httpContext);
    ...
  }

}

After all this has been done, we need to wire this now with Spring:
<beans:bean id="rawSessionPool"
    class="RawSessionPool">
    <beans:constructor-arg>
      <beans:bean
        class="RawSessionPooledObjectFactory" />
    </beans:constructor-arg>
    <beans:constructor-arg>
<beans:bean class="org.apache.commons.pool2.impl.GenericObjectPoolConfig"
        p:maxTotal="15" p:timeBetweenEvictionRunsMillis="3600000"
        p:minEvictableIdleTimeMillis="1800000" p:maxWaitMillis="15000"
        p:minIdle="0" p:maxIdle="10" />
    </beans:constructor-arg>
  </beans:bean>

  <beans:bean id="httpClientBuilder"
class="org.apache.http.impl.client.HttpClientBuilder" factory-method="create">
    <beans:property name="connectionManager">
      <beans:bean

class="org.apache.http.impl.conn.PoolingHttpClientConnectionManager"
        c:timeToLive="10" c:tunit="SECONDS" p:maxTotal="30"
        p:defaultMaxPerRoute="10" p:validateAfterInactivity="5000" />
    </beans:property>
  </beans:bean>

<beans:bean id="httpClient" factory-bean="httpClientBuilder" factory-method="build" />

All of the above will guarentee you that you will sessions pooled and freed in time and virtually
no stale HTTP connections will arise.

Lets wire up our beans in the repo impl now:
public class DefaultRepository implements Repository {

  @Autowired
  private RawSessionPool sessionPool;

  @Autowired
  @Qualifier("httpClient")
  CloseableHttpClient httpClient;

  @Override
  public CustomObject createObject(@Valid CustomInput input)
      throws IOException {
    RawSession session = sessionPool.acquire();

    MutableInt requestId = session.getRequestId();
    String clientId = session.getClientId();
    String serverId = session.getServerId();
    HttpClientContext httpContext = HttpClientContext.create();
    httpContext.setCookieStore(session.getCookieStore());

ResponseHandler<CustomResponse> responseHandler = new CustomResponseHandler();

    // Create Object
    requestId.increment();
    HttpPost createObject = new HttpPost(<create-object-url>);
    String createObjectBody = ...;
    createObject.setEntity(entity);

CustomResponse response = httpClient.execute(createObject, responseHandler,
        httpContext);
    ...

    sessionPool.release(session);

    return createdObject;
  }

That's it. Exception handling with finally blocks have been left out for brevity.

After a few tests with curl and GNU parallel, this scales very well. Session creation is about 10 s (that is why the pool was necessary), requests on their own in general a few hundred milliseconds. Please bear in mind that the code isn't perfect, especially the request preparation with the session
information.

All of this runs on a Tomcat 6 which serves requests as an intermediate between a backend server and several SOAP and REST clients. Needless to say that I have developed the workflow in JMeter first with the HttpClient4 interface and used this as template for the Java implementation.

Interesting to say what I have achieved in particular with this:
There was a requirement to create a lightweight interface to a backend file store. To put a file on that store requires several requests to server A first, then put to server B (file store) and finalize back in server A. I have reduced this for us in the frontend to one request. In a way that the client streams his file, may be gigabytes big, to the REST endpoint, I take that input stream and pass it on to HttpClient. The other way around is trickier. The input stream from the CloseableHttpResponse cannot simply be returned to the client without the response going out of scope and causing a
resource leak. The idea was to wrap that with a HttpResponseInputStream:

public class HttpResponseInputStream extends InputStream {

  CloseableHttpResponse response;

  public HttpResponseInputStream(CloseableHttpResponse response) {
    super();
    this.response = response;
  }

  @Override
  public int read() throws IOException {
    return this.response.getEntity().getContent().read();
  }

  @Override
  public int available() throws IOException {
    return this.response.getEntity().getContent().available();
  }

  @Override
  public void close() throws IOException {
    try {
      this.response.getEntity().getContent().close();
    } finally {
      response.close();
    }
  }

}

This approach makes it possible to downstream gigabytes without loading the data off to local disk at all.

Thanks to all who made this great piece of software possible, especially Oleg with his knowledge and
guidance.

Improvement advises and comments are welcome!

Michael

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to