Servlet 3 and AsyncListener: don’t miss timeouts!
To be asynchronous, Servlet 3 provides AsyncContext#start(Runnable) but it reuses the http pool. So, to have more control over it, you may desire to use a ManagedExecutorService from ee-concurrency-utilities specification.
With that hypothesis, an asynchronous servlet - or a servlet using NIO to be more concrete - will look like:
import javax.annotation.Resource;
import javax.enterprise.concurrent.ManagedExecutorService;
import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(urlPatterns = "/async", asyncSupported = true)
public class AsyncServlet extends HttpServlet {
@Resource
private ManagedExecutorService es;
@Override
protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
final AsyncContext ctx = req.startAsync();
es.submit(() -> {
// something long
try {
ctx.getResponse()
.getWriter().write("{\"done\":true}");
} catch (final IOException e) {
throw new IllegalStateException(e);
}
});
}
}
Some comments on that code:
- asyncSupported needs to be set to true (there is the same flag in web.xml if needed). Otherwise the container will consider the servlet is not asynchronous friendly and will prevent the AsyncContext usage. Note that this applies to any element of the filter chain including filters! So, if you use a filter to wrap the request to initialize your own principal (and this filter is not asyncSupported), then this servlet will fail because of that filter. So take care of the whole chain and not only the leaf (the servlet).
- The handler for asynchronous servlet is the AsyncContext which provides the request/response to use during the asynchronous processing. Note that you can wrap the original one using startAsync(req, resp).
- The thread where the actual request processing is executed is any thread for the Servlet container. So ensure to propagate (by message passing or any other way) the contextual informations you need before starting the asynchronous task.
This is great but you often need more control over the request for simple needs like auditing or more advanced ones like timeout handling. For that purpose, Servlet API offers AsyncListener API. The API is straightforward and looks like that:
@Override
protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
final AsyncContext ctx = req.startAsync();
ctx.addListener(new AsyncListener() {
@Override
public void onStartAsync(final AsyncEvent event) throws IOException {
// something
}
@Override
public void onComplete(final AsyncEvent event) throws IOException {
onDone(event);
}
@Override
public void onTimeout(final AsyncEvent event) throws IOException {
onDone(event);
}
@Override
public void onError(final AsyncEvent event) throws IOException {
onDone(event);
}
private void onDone(final AsyncEvent event) {
// something
}
});
es.submit(() -> {
// like before
});
}
Note: of course, onDone() is a shortcut I used in this sample to handle the termination of the request the same way whatever the cause is. But it needs to be adapted in your case.
Servlet API offers a listener able to notify you when the request starts, when it fails, when it succeeds or when it timeouts.
This looks simple, right? Actually, it doesn’t work. The specification says that listeners are resetted once the request is started. In other words, onDone() will never be called in previous snippet, only onStartAsync() will.
What does that mean? To have the behavior you probably expect (i.e. listen all events), you will need to add back your listener when the request starts. This can be done by modifying our onStartAsync this way:
@Override
public void onStartAsync(final AsyncEvent event) throws IOException {
event.getAsyncContext().addListener(this);
}
This is a small workaround, but not forgetting it will ensure you don’t miss any event and in particular the timeout processing. This can be very important if you want to report an error to an altering system for instance or have statistics about your endpoint.
From the same author:
In the same category:

