Asynchronous programming with HTTP
This section explains how to deal with asynchonism in a Play application to achieve typical long-polling, streaming and other Comet-style applications that can scale to thousands of concurrent connections.
Suspending HTTP requests
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 number of processors + 1
to set the default pool size.
That means that if a request’s processing time is very long (for example waiting for a long computation) it will block the thread pool and penalise your application’s responsiveness. Of course you could add more threads to the pool, but that would result in wasted resources, and anyway the pool size will never be infinite.
Consider a chat application where browsers send a blocking HTTP request that waits for a new message to display. These requests can be very very long (typically several seconds) and will block the thread pool. If you plan to allow 100 users to connect simultaneously to your chat application you will need to provision at least 100 threads. OK it’s feasible. But what about 1,000 users? Or 10,000?
To resolve these use cases, Play allows you to temporarily suspend a request. The HTTP request will stay connected, but the request execution will be popped out of the thread pool and tried again later. You can either tell Play to try the request later after a fixed delay, or wait for a Promise
value to be available.
Tip. You can see a real example in samples-and-tests/chat
.
For example, this action will launch a very long job and wait for its completion before returning the result to the HTTP response:
public static void generatePDF(Long reportId) {
Promise<InputStream> pdf = new ReportAsPDFJob(report).now();
InputStream pdfStream = await(pdf);
renderBinary(pdfStream);
}
Here we use await(…)
to ask Play to suspend the request until the Promise<InputStream>
value is redeemed.
Continuations
Because the framework needs to recover the thread you were using in order to use it to serve other requests, it has to suspend your code. In the previous Play version the await(…)
equivalent was waitFor(…)
, which suspended your action, and then called it again later from the beginning.
To make it easier to deal with asynchronous code we have introduced continuations. Continuations allow your code to be suspended and resumed transparently. So you write your code in a very imperative way, as:
public static void computeSomething() {
Promise<String> delayedResult = veryLongComputation(…);
String result = await(delayedResult);
render(result);
}
In fact here, your code will be executed in two steps, in two different threads. But as you see, it is transparent to your application code.
Using await(…)
and continuations, you could write a loop:
public static void loopWithoutBlocking() {
for(int i=0; i<=10; i++) {
Logger.info(i);
await("1s");
}
renderText("Loop finished");
}
While using only one thread to process requests, the default in development mode, Play is able to run concurrently these loops for several requests at the same time.
A more realistic example is asynchronously fetching content from remote URLs. The following example, performs three remote HTTP requests in parallel: each call to the play.libs.WS.WSRequest.getAsync()
method executes a GET request asynchronously and returns a play.libs.F.Promise
. The action method suspends the incoming HTTP request by calling await(…)
on the combination of the three Promise
instances. When all three remote calls have a response, a thread will resume processing and render a response.
public class AsyncTest extends Controller {
public static void remoteData() {
F.Promise<WS.HttpResponse> r1 = WS.url("http://example.org/1").getAsync();
F.Promise<WS.HttpResponse> r2 = WS.url("http://example.org/2").getAsync();
F.Promise<WS.HttpResponse> r3 = WS.url("http://example.org/3").getAsync();
F.Promise<List<WS.HttpResponse>> promises = F.Promise.waitAll(r1, r2, r3);
// Suspend processing here, until all three remote calls are complete.
List<WS.HttpResponse> httpResponses = await(promises);
render(httpResponses);
}
}
Callbacks
A different way to implement the previous example of three asynchronous remote requests is to use a callback. This time, the call to await(…)
includes a play.libs.F.Action
implementation, which is a callback that is executed when the promises
are done.
public class AsyncTest extends Controller {
public static void remoteData() {
F.Promise<WS.HttpResponse> r1 = WS.url("http://example.org/1").getAsync();
F.Promise<WS.HttpResponse> r2 = WS.url("http://example.org/2").getAsync();
F.Promise<WS.HttpResponse> r3 = WS.url("http://example.org/3").getAsync();
F.Promise<List<WS.HttpResponse>> promises = F.Promise.waitAll(r1, r2, r3);
// Suspend processing here, until all three remote calls are complete.
await(promises, new F.Action<List<WS.HttpResponse>>() {
public void invoke(List<WS.HttpResponse> httpResponses) {
render(httpResponses);
}
});
}
}
HTTP response streaming
Now that you can loop without blocking the request, you may want to send data to the browser as soon you have part of the result available. That is the point of the Content-Type:Chunked
HTTP response type. It allows to send your HTTP response several times using multiples chunks. The browser will receive these chunks as soon as they are published.
Using await(…)
and continuations, you can now achieve that using:
public static void generateLargeCSV() {
CSVGenerator generator = new CSVGenerator();
response.contentType = "text/csv";
while(generator.hasMoreData()) {
String someCsvData = await(generator.nextDataChunk());
response.writeChunk(someCsvData);
}
}
Even if the CSV generation takes one hour, Play is able to simultaneously process several requests using a single thread, sending back the generated data to the client as soon as they are available.
Using WebSockets
WebSockets are a way to open a two-way communication channel between a browser and your application. On the browser side, you open a socket using a “ws://” url:
new Socket("ws://localhost:9000/helloSocket?name=Guillaume")
On the Play side you declare a WS route:
WS /helloSocket MyWebSocket.hello
MyWebSocket
is a WebSocketController
. A WebSocket controller is like a standard HTTP controller but handles different concepts.
- It has a request object, but no response object.
- It has access to the session, but read-only.
- It doesn’t have
renderArgs
,routeArgs
or flash scope. - It can read params only from the route pattern or from the QueryString.
- It has two communication channels: inbound and outbound.
When the client connects to the ws://localhost:9000/helloSocket
socket, Play will invoke the MyWebSocket.hello
action method. Once the MyWebSocket.hello
action method exits, the socket is closed.
So a very basic socket example would be:
public class MyWebSocket extends WebSocketController {
public static void hello(String name) {
outbound.send("Hello %s!", name);
}
}
Here when the client connects to the socket, it receive the ‘Hello Guillaume’ message, and then Play closes the socket.
Of course usually you don’t want to close the socket immediately. But it is easy to achieve using await(…)
and continuations.
For example a basic Echo server:
public class MyWebSocket extends WebSocketController {
public static void echo() {
while(inbound.isOpen()) {
WebSocketEvent e = await(inbound.nextEvent());
if(e instanceof WebSocketFrame) {
WebSocketFrame frame = (WebSocketFrame)e;
if(!e.isBinary) {
if(frame.textData.equals("quit")) {
outbound.send("Bye!");
disconnect();
} else {
outbound.send("Echo: %s", frame.textData);
}
}
}
if(e instanceof WebSocketClose) {
Logger.info("Socket closed!");
}
}
}
}
In the previous example, the nested ‘if’ and ‘cast’ soup was tedious to write and error prone. And here Java sucks. Even for this simple case it is not easy to handle. And for more complicated cases where you combine several streams, and have even more event types, it becomes a nightmare.
That’s why we have introduced a basic pattern matching in Java in the play.libs.F library.
So we can rewrite our previous echo sample as:
public static void echo() {
while(inbound.isOpen()) {
WebSocketEvent e = await(inbound.nextEvent());
for(String quit: TextFrame.and(Equals("quit")).match(e)) {
outbound.send("Bye!");
disconnect();
}
for(String message: TextFrame.match(e)) {
outbound.send("Echo: %s", message);
}
for(WebSocketClose closed: SocketClosed.match(e)) {
Logger.info("Socket closed!");
}
}
}
Continuing the discussion
Next, doing Ajax request.