Wednesday, March 26, 2014

Play Framework Comet / Chunking Support



So my first choice when it comes to full duplex communication with a web server is websockets. Play has amazing support (non-blocking/async) support for websockets and there are plenty of example inside activator templates. But what do we do for those browsers that don't support websockets? In particular Android default browser 4.3 (Jellybeen) and below.

Note: 4.4+ Kitkat will now have chrome as the native browser [websockets, webrtc, webgl, and much more]

In cases like this we need to fallback to some form of "comet" or "long-polling" technique. However in the spirit of play we want to be non-blocking and asynchronous with our approach. With a little bit of digging on google we can find some info on how this was done in play 2.0. This stackoverflow article talks a bit about how comet works in play 2.0

public static void newMessages() {
    List messages = Message.find("date > ?", request.date).fetch();
    if (messages.isEmpty()) {
        suspend("1s");
    }
    renderJSON(messages);
}

The key bit here is suspend("1s") which is what holds the HTTP request open, checking for new data once per second.

However suspend is not going to work for us in play 2.1+ so we need to find another solution. A number of things changed from 2.0 to 2.1 and in particular the "SimpleResult" is now the return type for all actions. They have done away with the other result types and folded them in under simple result. There are some good notes here on migrating to play 2.2. One of the things we can see is the use of a Chunked result

Working with chunked results

Previously the stream method on Status was used to produce chunked results. This has been deprecated, replaced with a chunked method, that makes it clear that the result is going to be chunked. For example:

def cometAction = Action {
  Ok.chunked(Enumerator("a", "b", "c") &> Comet(callback = "parent.cometMessage"))
}

Advanced uses that created or used ChunkedResult directly should be replaced with code that manually sets/checks the TransferEncoding: chunked header, and uses the new Results.chunk and Results.dechunk enumeratees.

So this looks promising. However we still need to make a choice on what we are going to do as a generator for the Enumerator. My first idea was to use to use Enumerator.fromCallback1. This looked like it should work, and even compiles fine, but no chuncked response. After doing some digging I found that my return type looked a little strange and that this may be a bug in "fromCallback1". I found someone else having the same issue in this post here.

Next I played around with a Promise.timeout and a Enumerator.generateM. There is an example of this style of comet usage on the comet-clock. The lines to note here are the following:

Enumerator.generateM {
   Promise.timeout(Some(dateFormat.format(new Date)), 100 milliseconds)
}

...

def liveClock = Action {
   Ok.chunked(clock &> Comet(callback = "parent.clockChanged"))
}

After some more time trying to get this new approach to work, I stumbled into a discussion around Concurrent.broadcast. Which as it turns out... is exactly how I am doing my producer / consumer for my websocket... doh! Should have looked here right at the start :)

Concurrent.broadcast

This article on stackoverflow talks about the Concurrent.broadcast. After a little bit of time playing with this I got a working example. Here are the steps involved.

First you need to get you producer and consumer tuple.

   val (cometOut, cometChanel) = Concurrent.broadcast[JsValue]

Now you can use the cometChannel to push Json data into the cometOut Enumerator.

   cometChanel.push(Json.obj("data" -> "test"))

And finally you can plug your enumerator into the play Comet helper for a chuncked data response.

   Ok.chunked(cometOut &> Comet(callback = "parent.cometMessage"))

Chuncked response and what can go wrong

First off the comet helper in play does some "padding" to get around an issue with chunking. This can be seen with this gist.

 def apply[E](callback: String, initialChunk: Html = Html(Array.fill[Char](5 * 1024)(' ').mkString + ""))(implicit encoder: CometMessage[E]) = new Enumeratee[E, Html] {

Note the Array.fill[Char](5 * 1024)(' ').mkString. So if you are not using the Comet helper in play, you will need to do something similar to have your chunked response work. You can find more information on this here.

Nginx and HTTP 1.1

If you are using nginx to proxy your request like I am, then there are further problems to deal with. First Nginx defaults to HTTP 1.0 which does not have chucking support. You need to explicitly tell your nginx config to use HTTP 1.1.

   location / {
     proxy_http_version 1.1;
     proxy_pass http://localhost:9009/;
     proxy_set_header Host $http_host;
  }

There is another problem

If you view your endpoint now using curl, everything will work fine... but wait. If you try your comet solution in chrome or firefox you will get the infinite loading spinner and you will never see any chunked data until the http request is closed.

If you point your browser directly at play however you will notice that everything is working fine. This again points to a problem within nginx. After using curl to examine the headers for both request, I noticed that nginx response headers contain the line Connection: keep-alive. Browsers seem to wait for the connection to close for a chunked transfer before returning the data.

So we are left trying to get rid of the Connection: keep-alive from the nginx response headers. Unfortunately this is not a configurable option inside nginx and you are left patching it from source. I have posted a comment here.

Update: The solution is to turn GZIP off for nginx
The white space that play pushes into the buffer (described about) obviously compresses very well with gzip... totally defeating the purpose of the buffer to begin with. The solution is to turn off gzip. NOTE: that I don't like this solution and will be looking for alternatives to play http 1.1 chunking.

Update: There is more info surrounding this issue in the above post

Nginx Websocket configuration

https://github.com/dryan/decss-sync/issues/1

Also ran into problems with nginx closing the socket after 1 min. This is related to proxy_read_timeout.. more about this here http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_read_timeout.