Documentation

You are viewing the documentation for Play 1. The documentation for Play 2 is here.

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 thousand 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 nb of processors + 1 to set the default pool size.

That means that if a request is very long (for example waiting for a long computation) it will block the thread pool and penalize your application responsiveness. Of course you could add more threads to the pool, but it will result in wasted resources, and anyway the pool size will never be infinite.

Think for example about 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 redeem.

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(…). And waitFor was suspending your action, and then recalling it 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 2 steps, in 2 different threads. But as you see it, it’s very transparent for 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");
}

And using only 1 thread (which is the default in development mode) to process requests, Play is able to run concurrently these loops for several requests at the same time.

HTTP response streaming

Now, because you are able to loop without blocking the request, perhaps you want to send data to the browser as soon you have part of the result available. It is the point of the “Content-Type:Chunked” HTTP response type. It allows to send your HTTP response is several times using multiples chunks. The browser will receive these chunk 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 take 1 hour, Play is able to process simultaneously 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 2 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 handle different concepts:

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 exit, 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 imbricated ‘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 kind of 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 msg: TextFrame.match(e)) {
             outbound.send("Echo: %s", frame.textData);
         }
         
         for(WebSocketClose closed: SocketClosed.match(e)) {
             Logger.info("Socket closed!");
         }
    }
}

Continuing the discussion

Next, doing Ajax request.