My goal is to support long-polling for multiple web service callers, and to keep track of which callers are currently "parked" on a long poll (i.e., connected). By "long polling," I mean that a caller calls a web service and the server (the web service) does not return immediately, but keeps the caller waiting for some preset period of time (an hour in my application), or returns sooner if the server has a message to send to the caller (in which case the server returns the message by calling asyncResponse.resume("MESSAGE")).
I'll break this into two questions.
First question: is this a reasonable way to "park" the callers who are long-polling?
@GET
@Produces(MediaType.TEXT_PLAIN)
@ManagedAsync
@Path("/poll/{id}")
public Response poller(@Suspended final AsyncResponse asyncResponse, @PathParam("id") String callerId) {
// add this asyncResponse to a HashMap that is persisted across web service calls by Jersey.
// other application components that may have a message to send to a caller will look up the
// caller by callerId in this HashMap and call resume() on its asyncResponse.
callerIdAsyncResponseHashMap.put(callerId, asyncResponse);
asyncResponse.setTimeout(3600, TimeUnit.SECONDS);
asyncResponse.setTimeoutHandler(new TimeoutHandler() {
@Override
public void handleTimeout(AsyncResponse asyncResponse) {
asyncResponse.resume(Response.ok("TIMEOUT").build());
}
});
return Response.ok("COMPLETE").build();
}
This works fine. I'm just not sure if it's following best practices. It seems odd to have the "return Response..." line at the end of the method. This line is executed when the caller first connects, but, as I understand it, the "COMPLETE" result is never actually returned to the caller. The caller either gets "TIMEOUT" response or some other response message sent by the server via asyncResponse.resume(), when the server needs to notify the caller of an event.
Second question: my current challenge is to accurately reflect the population of currently-polling callers in the HashMap. When a caller stops polling, I need to remove its entry from the HashMap. A caller can leave for three reasons: 1) the 3600 seconds elapse and so it times out, 2) another application component looks up the caller in the HashMap and calls asyncResponse.resume("MESSAGE"), and 3) the HTTP connection is broken for some reason, such as somebody turning off the computer running the client application.
So, JAX-RS has two callbacks I can register to be notified of connections ending: CompletionCallback (for my end-poll reasons #1 and #2 above), and ConnectionCallback (for my end-poll reason #3 above).
I can add these to my web service method like this:
@GET
@Produces(MediaType.TEXT_PLAIN)
@ManagedAsync
@Path("/poll/{id}")
public Response poller(@Suspended final AsyncResponse asyncResponse, @PathParam("id") String callerId) {
asyncResponse.register(new CompletionCallback() {
@Override
public void onComplete(Throwable throwable) {
//?
}
});
asyncResponse.register(new ConnectionCallback() {
@Override
public void onDisconnect(AsyncResponse disconnected) {
//?
}
});
// add this asyncResponse to a HashMap that is persisted across web service calls by Jersey.
// other application components that may have a message to send to a caller will look up the
// caller by callerId in this HashMap and call resume() on its asyncResponse.
callerIdAsyncResponseHashMap.put(callerId, asyncResponse);
asyncResponse.setTimeout(3600, TimeUnit.SECONDS);
asyncResponse.setTimeoutHandler(new TimeoutHandler() {
@Override
public void handleTimeout(AsyncResponse asyncResponse) {
asyncResponse.resume(Response.ok("TIMEOUT").build());
}
});
return Response.ok("COMPLETE").build();
}
The challenge, as I said, is to use these two callbacks to remove no-longer-polling callers from the HashMap. The ConnectionCallback is actually the easier of the two. Since it receives an asyncResponse instance as a parameter, I can use that to remove the corresponding entry from the HashMap, like this:
asyncResponse.register(new ConnectionCallback() {
@Override
public void onDisconnect(AsyncResponse disconnected) {
Iterator<Map.Entry<String, AsyncResponse>> iterator = callerIdAsyncResponseHashMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, AsyncResponse> entry = iterator.next();
if (entry.getValue().equals(disconnected)) {
iterator.remove();
break;
}
}
}
});
For the CompletionCallback, though, since the asyncResponse is already done or cancelled at the time the callback is triggered, no asyncResponse parameter is passed in. As a result, it seems the only solution is to run through the HashMap entries checking for done/cancelled ones and removing them, like the following. (Note that I don't need to know whether a caller left because resume() was called or because it timed out, so I don't look at the "throwable" parameter).
asyncResponse.register(new CompletionCallback() {
@Override
public void onComplete(Throwable throwable) {
Iterator<Map.Entry<String, AsyncResponse>> iterator = callerIdAsyncResponseHashMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, AsyncResponse> entry = iterator.next();
if (entry.getValue().isDone() || entry.getValue().isCancelled()) {
iterator.remove();
}
}
}
});
Any feedback would be appreciated. Does this approach seem reasonable? Is there a better or more Jersey/JAX-RS way to do it?