Juggernaut: "Server Push" - works in 95% of all browsers!

Posted by Dan Grigsby
on Wednesday, June 27

Juggernaut is a clever “server push” solution for Rails that’ll work in nearly all browsers.

With Juggernaut, your server can push updates into an already rendered page. Push a useful alternative to Ajax style polling, especially when your application is event-based with an update cycle that is irregularly/unpredictably timed.

Chat is the most common example of using push, though it’s easy to imagine other collaborative applications where this would work.

I’ve spent the evening pouring over the code; the Juggernaut site and README contain useful documentation to get you started; I’ll use this post to describe how Juggernaut actually works behind the scene.

Juggernaut Internals

Juggernaut’s implementation is clever, works in better than ninety five percent of all browsers, and is both small-enough and straightforward-enough to be approachable/understandable to most Rails programmers.

Juggernaut is made up of three main elements: (1) in-browser/client side elements, (2) a stand alone push server and (3) Rails elements to tie everything together.

In-Browser/Client Side Elements (media/juggernaut.as ; media/juggernaut.js)

Juggernaut works by embedding a Flash object into pages that receive push messages. It accomplishes this by establishing an long-lived outbound TCP connection to the oush server, which lives on a separate port than the web server.

Unlike a browser initiated HTTP connection that is closed after the page renders, the Flash initiated TCP connection remains open, waiting for messages. (Long-lived HTTP connections don’t work well with Rails because of Rails’ oft-discussed single-threaded implementation; this approach side-steps that potential problem.)

The push server sends message down the already established connection. Messages are strings, often a rendered RJS partial, containing JavaScript that is eval’d in the page. So, borrowing an example from the README, you could use Prototype’s Insertion object you could update the “chat_data” div with the following message string:


new Insertion.Top('chat_data', '<li>#{input_data}</li>');

The Flash component is tiny – the ActionScript is only 38 lines!—and really straight forward: it simply opens the TCP connection (using ActionScript’s XMLSocket object to connect to host/port that are passed in as arguments to in the the with host and port params passed in in the enclosing HTML) and connects an event handler to hand off messages that it receives to a JavaScript method in the browser that runs eval on the message string.

Modern browsers, designed for HTTP/1.1, limit the number of connections to a host. IE, for example, won’t allow more than 2 simultaneous connections. By using keep-alive to allow a single connection to handle multiple requests browsers are able to avoid the time/overhead of connection setup and takedown. Using the Flash VM to handle the connections is handy in this regard because it doesn’t use one of your previous few connections.

Stand Alone Push Server (media/push_server)

Conceptually, the push server is straightforward: it receives messages (usually from the web server) and forwards it to the clients.

Messages are generated based on events that happen “elsewhere” (from the perspective of the client receiving the message). For example: we’re both in a chat room; you enter a message that is delivered to the web server using Ajax. The web server receives the message from you, which should ultimately be displayed in my browser, and forwards it to the push server for delivery to me in my browser.

The push server receives messages serialized to JSON over a standard TCP socket (that it opens for listening at startup) and not via DRB. Presumably this is to more easily allow non-Ruby processes to send messages, though there are implications with session that I’ll discuss below in the Broadcast/Channels section.

Rails Components (lib/juggernaut.rb ; lib/juggernaut_helper.rb)

Juggernaut’s Rails components tie everything together.

Form helpers instantiate the Flash object with the appropriate host/port parameters and include the necessary JavaScripts to evaluate events. Using the helper is a one-line non-event:


  <%= listen_to_juggernaut_channels :chat_channel %>

A couple of Juggernaut class methods make relaying messages easy. For example this code…


Juggernaut.send_data(data, channels])

...opens a new socket to the push server, passes the message on, and closes/cleans-up.

Broadcast/Channels

When creating messages, you choose whether to send them to a single user (via the Juggernaut.send_to method) or to broadcast them (via the Juggernaut.send_data method).

Single user message recipients are identified by a unique id. Unless otherwise specified, this is set to session.session_id. If you plan to create messages outside of the Rails framework, it’s probably handier to use another unique ID can be easily associated with the user; using Rails internal session identifier outside of Rails seems more complicated than necessary. The ID column from the user table seems like a natural choice.

For broadcast, Juggernaut uses a “channel” metaphor. The HTML helpers pass the channels that the page/user subscribe to the Flash object, which in turn sends them to the push server when it establishes its connection.

Sessions and Security

The push server uses a “shared secret” string to restrict unauthorized users from pushing messages out to end users.

Client access control is optional. If turned on, it works at a connection level (i.e., you can either connect to the push server or not). It’s implemented in a clever way: when the Flash player connects to the push server it includes its Rails session.session_id when it sets up the connection. The push server then makes an HTTP request to an authenticated page within the Rails app using this session id. If it succeeds (and gets a HTTP response code of 200) then the user is considered authenticated.

Some TODOs

From looking at the code, it appears that if the client’s connection to the push server is lost that it won’t automatically reconnect. There is a “num_tries” configuration directive for setting up the number of times to try to reconnect after a failure, but it’s unused in the Flash. Similarly, the “num_secs” directive, which sets the number of seconds to try between delivery attemts, is unused.

Conclusion

All in all, it’s a very elegant solution to an interesting problem.

Comments

Leave a response

  1. AkitaOnRailsJune 27, 2007 @ 12:20 PM

    I wonder how far this architecture scales. Say its used to broadcast Steve Jobs’ keynote, that probably knocks every Apple related blogs down :-)

    Can anyone compare a regular Ajax approach to this one?

  2. Dan GrigsbyJune 27, 2007 @ 01:16 PM

    Hi Akita:

    This should, in theory, scale better than Ajax, particularly when you need frequent, near real-time updates. Ajax has to poll periodically, with all the overhead of setup/takedown of the connection. If you’re doing this every second that’s a lot of load. This is even worse if you’re updating infrequently (but need low latency) because most of the polling connections would result in no update. Also, there’s the whole issue around Rails being single-threaded. If you have 10 users that poll 1x a second and only 1 mongrel child that child will have to handle 10 requests a second to avoid having requests queued.

    -Dan

  3. Jon DahlJune 27, 2007 @ 02:25 PM

    Akita: Juggernaut should scale far better than Ajax when the alternative is frequent polling. Let’s say 5000 people are watching Steve Jobs’ keynote on your site, and a new post happens every minute or so, but you want new posts to show up within 5 seconds. With Ajax (polling), the server will need to handle 5000 * 60 / 5 (60,000) requests per minute. With Juggernaut, the server will only need to communicate 5,000 times in that same minute.

    It looks like Juggernaut is useful when polling would produce a high number of “empty” requests – checking for updates, but not finding any changes. It might actually be worse than polling if the average poll request found multiple updates. So use the right tool for the job.

    As for a direct 1-1 comparison of a Juggernaut request vs. an Ajax request: I’m not sure. Anyone know?

  4. Jon DahlJune 27, 2007 @ 02:26 PM

    Eh, missed Dan’s reply. :)

  5. Alex MacCawJune 27, 2007 @ 04:04 PM

    Also, if I might add to the scaling discussion, Juggernaut uses eventmachine which has a non-blocking io and can manage loads of simultaneous connections. Indeed work is underway for a linux version of EM that supports many more connections. The push_server isn’t stateful either, so you can expand and expand by just starting up more push servers and using a round robin approach where clients get siphoned off to random push servers, so the possibilities for scaling are huge. Also, just to reiterate, there are other ways of doing server push, like comet. However they have big drawbacks. As well as the two connection limit for IE (and the crashing), comet requires you to connect back to exactly the same host you got the original page from (even the same port). So you end up having some sort of hacky routing to the push server – Flash is definitely the best way forward. Thanks for the great article.