Play framework and async I/O
A few weeks ago, I had the chance to evaluate the Play framework, a high-productivity Java web framework. At LinkedIn, we already use Grails and JRuby/Sinatra, so I was curious how Play would compare. The thing that really caught my eye was Play's support for asynchronous programming, which would be immensely useful for fetching data from our many backend services in parallel and supporting comet-style interactions. Unfortunately, the Play documentation seemed a bit sparse and scattered when it came to async programming, so in this post, I'll capture a quick experiment I did and what I found.
Play: threaded or evented?
The documentation for Play recommends the following configuration:
Play is intended to work with very short requests. It uses a fixed thread pool to process requests queued by the HTTP connector. To get optimum results, the thread pool should be as small as possible. We typically use the optimum value of nb of processors + 1 to set the default pool size.
With so few threads to spare, you can't afford to have any of them blocked by I/O. If all your threads are tied up waiting for the DB, the file system, or a remote service, they can't service other requests; those requests end up waiting in a queue and your users get a slow user experience. To avoid this, Play supports asynchronous programming. The typical usage pattern is as follows:
- Use Play or Java NIO libraries to perform I/O asynchronously.
- Each NIO call should return a Promise object, which implements the Future interface
- Suspend the current request - which frees up the current thread - by calling one of the Promise wait methods.
- When the I/O completes, a different thread can pickup the request and continue processing it. To do this, you can use continuations or callbacks.
The ability to suspend a request and devote little-to-no resources to it while it "waits" is what allows Play to handle many concurrent connections with only a tiny thread pool. For each processor on the server, the Play framework effectively represents an evented model, with a single "event loop" to handle all requests. This is very different than the one-thread-per-request model traditionally used by Java web frameworks based on servlets. Depending on the load profile of the server, the evented approach may allow a far greater degree of concurrency than the threaded model; for persistent connections as in comet, the evented model is the only way to go.
Async I/O via continuations
Let's look at an example: as is common with dynamic web applications, processing the current request requires calls to several remote services to gather data before returning a response. In this example, we'll do these remote calls in parallel and make sure not to block the request thread while waiting for a response:
We are using Play's WS library, a handy HTTP client with a clean API and support for making requests asynchronously. The getAsync method fires off each HTTP request asynchronously - and consequently, in parallel - and returns a Promise. We wrap all of the Promise objects into a single one using F.Promise.waitAll. When we call await, the Play framework suspends the request and frees up the current thread to process other requests. When all (since we called waitAll) the asynchronous calls are completed, another thread will pickup this request and continue processing at the following line: in this case, the call to render.
Async I/O via callbacks
This example is identical to the previous, except we use an explicit callback instead of a continuation.
While Play's support for continuations and callbacks is fairly powerful and largely transparent to the programmer, I did stumble upon a couple of gotchas:
Key things to note:
- Values put into renderArgs before the call to await are no longer available when the request is resumed. This is true both for continuations and callbacks. This may be a bug.
- As is typical with Java anonymous inner classes, any variable from the "outer" scope must be declared final to be visible in the inner scope. This is another area where the lack of closures in Java can hurt and where Scala Play may come in handy.
I enjoyed my time with Play: the community seems very active, hot reload works as advertised, it's reasonably DRY, and the support for async I/O is as good as I've seen in the JVM world. There are a few gotchas and the documentation/JavaDoc needs some improvement, but we will be evaluating Play further to see if it fits in the LinkedIn ecosystem.
Update: the feedback from this post has led to a couple bugs being filed (renderArgs lost when using await() with callback and Improve ‘Asynchronous programming with HTTP’ documentation). Ah, the benefits of an active community on an open source project. Kudos!