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]