Comet with Rails + Mongrel

In my last post I described how to create a mongrel handler. I said you might want to do this for optimization purposes, but my own interest came about in an attempt to solve the server-push problem with Rails.

Comet is the term that seems to be catching on for server-push via XmlHttpRequest. Possible applications include chat clients or a stock ticker. Anything that wants constant updates will be both responsive and less demanding of server resources if it waits for data to be pushed to it, instead of opening a new status query every few seconds.

Since the server can’t initiate a connection to the user’s browser, the only possible solution is to have the browser hold a connection open indefinitely, waiting for an update. Since Rails is single-threaded, however, this means that one whole server instance would be tied up by this connection - clearly infeasible in almost all situations.

You might say, “Why not have another small server listening on a separate port to hold on to these push-status connections?” Good idea - except that XmlHttpRequest won’t let you connect to another port. This is because the port is considered part of the hostname, and connecting to another hostname from within the javascript sandbox would be a big security no-no. (It would be trivially easy, for example, to inject a little javascript into a site which caused all of its visitors’ browsers to start hammering another unrelated site as soon as they visited the homepage.)

Juggernaut gets around this with a little hidden Flash component. This is a nifty idea, but for me it is unappealing because Flash is not readily available for my platform (Ubuntu AMD64). More importantly, I’d prefer to avoid building technology that depends on a proprietary plugin built by a monolithic, old-fashioned (i.e., shrink wrap) software company.

So holding open connections to Rails won’t work due to its controller lock. But as was demonstrated in the previous entry, a mongrel handler won’t have that problem. I’ll extend the auction example shown there to use server-push.


require 'active_record'

class StatusHandler < Mongrel::HttpHandler
   def process(request, response)
      id = request.params['PATH_INFO'].slice(1, 20)
      current = request.params['QUERY_STRING']

      while status(id) == current do
         sleep 0.2
      end

      response.start(200) do |head, out|
         head["Content-Type"] = "text/html"
         out.write status(id)
      end
   end

   def status(id)
      connection.select_value("select status from auctions where id=#{id.to_i}")
   end

   def connection
      ActiveRecord::Base.connection
   end
end

uri "/status", :handler => StatusHandler.new, :in_front => true

This assumes your auctions table has a field named “status,” which I’m using as an integer, but any type should work. http://localhost:3000/status/1 now delivers just one value, the status. Where it gets interesting is something like http://localhost:3000/status/1?100, assuming that the status of auction id=1 is currently set to 100 in the database. Now, the connection will hang and wait for the value to change. (You’ll see the database queries in development.log, but no web hits.) Pop open a sql shell and run “update auctions set status=101″ and the connection will resolve immediately, printing out the new value.

Here’s a simple example of making an ajax call to this url from within a page:


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
   <%= javascript_include_tag :defaults %>
</head>
<body>
   Status is now: <span id="status"></span>

   <script language="javascript">
      function respawn()
      {
         new Ajax.Updater('status', '/status/1?' + $('status').textContent, { onComplete: respawn });
      }

      respawn();
   </script>
</body>
</html>

Experiment with updating the status value in the sql shell and you’ll see that the page always updates instantly. To watch the connections, open the Firebug console, click Options in the upper-right, and make sure “Show XMLHttpRequests” is checked. Reload the page and you’ll now see a POST each time you update the status. There will always be an active one at the bottom, waiting, waiting for the status update.

And there you go. Server-push connections with only Rails and Mongrel.

Update: Mere minutes after I finished writing this article, I came across Shooting Star, a Rails plugin for adding Comet to your apps. So far this looks a little heavy-weight for my purposes, and somewhat platform-dependent so far - not to mention that they push the meteor metaphor a bit far in their method naming. Still, this may be a more robust solution than my little hack, so check it out. If anyone has tried Juggernaut, Shooting Star, and my hack, I’d be curious to hear a comparison.

28 Responses to “Comet with Rails + Mongrel”

  1. John Nunemaker Says:

    Will the AJAX request timeout if the status doesn’t change for a long time?

  2. adam Says:

    Nope. I experimented with it and Firefox doesn’t ever seem to timeout on ajax requests, and this handler does not.

    A production site would probably want to wrap the sleep loop in something like:

    require 'timeout'
    timeout do
       begin
          ...
       rescue
          response.start(200) { |h,o| out.write "timed out" }
       end
    end
    

    That’s just off the top of my head, there’s probably a better HTTP response code for this situation.

  3. Dr Nic Says:

    Seeing more and more mongrel handlers being developed is fascinating. This one especially!

  4. weepy Says:

    how about IE and safari ?

  5. weepy Says:

    Reading the FAQ on Juggernaut it states :

    Juggernaut > Comet because :
    # It’s much less of a hack
    # It doesn’t crash your browser (Comet can do this after a while)

    Is there any substance to these claims ?

  6. weepy Says:

    i also found this :

    http://cyll.org/blog/tech/2006-08-09-themongrelcomet.html

    doesn’t look like its been worked on for a while

  7. Nic Says:

    I’m wondering a few things about this. One being the speed and effectiveness of using mongrel to keep the connection open. Juggernaut is quite fast, especially if you use some javascript hackery to make client initiated actions seem instantaneously instead of having it sent to the server, then back via Jug. I will be looking into how to cluster Jug very soon as well and then begin testing it’s scalability. In theory, since this comet is in mongrel, clustering mongrel also would cluster the push technology. That’s not so for Juggernaut. But then again, keeping the app and push servers separate, less stress is put on each individual server.

    Like weepy said, it doesn’t look like anything has been written recently on this other than this post. Is there a proof of concept demo anywhere?

  8. weepy Says:

    a current benefit of Jug over this method currently is that there’s no polling in Jug. — this method has essentially moved the polling to the server-side.

    I guess this could be fixed if we could get it to accept local connections from Rails ?

  9. Alex MacCaw Says:

    I’m not sure I agree with your views on Adobe. They have, after all, just open sourced Flex. As to comet it has some drawbacks. As far as I can remember IE can only have 2 connection streams open at the same time and has been known to crash with long running comet connections. Firefox isn’t perfect either.
    However, Alex from Dojo has solved the crossdomain issue using some iframe hackery. You might want to check out his comet framework.

  10. Eribium » Blog Archive » More Juggernaut Features (Server Push for Rails) Says:

    […] Also, this chap has been experimenting with comet and mongrel. The problem being that Comet can only connect to the same host (and port) the original request was from. However, I hear Alex Russell from dojo has cracked that with some sort of iframe hackery (but there are some caveats) . Anyways, I still retain the opinion that using a flash socket is the best method available at the moment (and frankly, the 2% of people who don’t have flash turned on probably don’t have JavaScript turned on either). […]

  11. Pratik Says:

    This surely looks good. Just concerned about one thing. Won’t this approach kill the database if used in a live system ? Because we need to constantly poll the database for any kind of updates in order to be fast. What could be the possible work arounds ? I guess it’d help to use memcached or it might help to build some kind of bridge/middleman between mongrel comet handlers and the database. Suggestions ?

    Thanks,
    Pratik

  12. Pratik Says:

    Also, this works fine when I update the polled value from console. But lets say, on the same page where you have the polling ajax request and you try to update the polled value using another ajax call, both the requests waits indefinitely. I tried this with a single mongrel server and also with 3 mongrel servers behind a pen cluster.

    Thanks,
    Pratik

  13. adam Says:

    Regarding killing the database on a live system: a simple one-row read accessed by the primary key index should be very, very, very fast on any kind of modern database. I don’t think this will be a bottleneck for most cases. On the very high end, yes you might want to look some sort of memory cache.

    Regarding doing an update in one ajax call and a write on another: I just tried this and had no difficulties. I added this code to the view:

    <%= link_to_function "Update", "new Ajax.Updater('update_result', '/comet/update')" %>
    <div id="update_result"></div>
    

    And in the controller:

    def update
       ActiveRecord::Base.connection.execute("update auctions set status=status+1")
       render :text => "done"
    end
    

    Works like a charm.

  14. links for 2007-05-11 « Caiwangqin’s delicious bog Says:

    […] Adam @ Bitscribe » Comet with Rails + Mongrel (tags: RubyonRails mongrel comet ajax web) […]

  15. weepy Says:

    >> Just concerned about one thing. Won’t this approach kill the database if used in a live system ?

    The only scalable way to deal with this is to allow Rails to inform the Comet client that an update has occured. This is the way Jug works and it seems to work well.

    It would require some server threading system - perhaps event machine would be a good way to go ?

    weepy

  16. reality » Blog Archive » Ruby on Rails + AJAX + Mongrel + JMS/MQ/MOM = 即時通訊 Says:

    […] 答案是 No and Yes。用普通的方法 Rails 不能做到以上效果。Rails 是一個單線程的環境 ,也就是說全部 Request 也由同一支 Thread 控制。在以上情境中「叫 Client 在 Request 中等待直至有信息」等於叫整個 server 停下來。幸好,Rails 單線程不等於 Server 也必須單線程。Adamçš„ Comet with Rails + Mongrel 中示範了怎樣用自訂 HttpHandler 讓 Rails 可以持續地跟 Client 連接。我們可以在 Client Side 呼叫 AJAX,在 Mongrel çš„ HttpHandler 中把這些 Request 轉成向 MOM 的呼叫,如下圖。 […]

  17. siuying Says:

    Based on your hacks, I tried to implement a HttpHandler that communicate with JMS server using STOMP and response user with AJAX. (http://www.reality.hk/articles/2007/07/03/740/)

    However, it will deadlock during bidirectional communication. I suspects this could related to green thread system of Ruby, where any blocking syscall like IO blocks all threads. If this is the case, it takes more works to making it work like shooting star or juggernaut

  18. adam Says:

    Sluying -

    Interesting work. I don’t know anything about Stomp, but looking at what is done in your mongrel handler code I can see that this is fundamentally the same thing that is happening when you connect to a database, which is also a server being connected to via socket. There are two differences in the implementation, though: for one, ActiveRecord maintains a persistent connection throughout the life of the Mongrel process, rather than opening a new one on each request; and the database connectors are probably all written in C.

    Assuming you’ve ruled out everything else, I’d probably start by trying to hold open a persistent connection to port 61613 through a wrapper class as part of your Rails app. If that’s still producing the same results, you could rewrite the key functions in that class in C, thereby avoiding Ruby threading/socketing issues.

  19. 美丽书签博客 : 前文研究过利用 Flash XMLSocket 作即时 Says:

    […] 答案是 No and Yes。用普通的方法 Rails 不能做到以上效果。Rails 是一个单线程的环境 ,也就是说全部 Request 也由同一支 Thread 控制。在以上情境中“叫 Client 在 Request 中等待直至有信息”等于叫整个 server 停下来。幸好,Rails 单线程不等于 Server 也必须单线程。Adamçš„ Comet with Rails + Mongrel http://adam.blogs.bitscribe.net/2007/05/08/comet-with-rails-mongrel/ 中示范了怎样用自订 HttpHandler 让 Rails 可以持续地跟 Client 连接。我们可以在 Browser 呼叫 AJAX,在 Mongrel çš„ HttpHandler 中把这些 Request 转成向 MOM 的呼叫,把消息化为 HTTP Response 送回 browser,如下图。 […]

  20. siuying Says:

    Adam,

    I believe I found the difference. In your example you update the auction status externally, where I try to update the auction status in Rails.

    Try the example: http://www.reality.hk/~siuying/tmp/poll.tgz

    1. Uncompress the file and start mongrel by: mongrel_rails start -C config/mongrel.yml

    2. Enter the poller URL http://127.0.0.1:3000/

    3. Press “Updater”, updater page opened. If we update status there, the status in poller page is updated immediately.

    4. Open another poller, it loads correctly

    5. Update status, you will find no page getting update

    Any idea?

  21. adam Says:

    Yeah, this has nothing to do with your Rails app: browsers typically limit the number of XmlHttpRequest connections open at once. In Mozilla browsers, the most you can have is two. So once you’ve opened two updater pages, a third one will just hang as it waits for one or the other requests to complete. I don’t think there’s any real solution to this - if your app expects users to keep many pages with updaters on them open at once, this approach to Comet just won’t work - you’ll need something like Juggernaut instead. I think for most apps, though, the user will only ever have one window open, so this shouldn’t be a problem. For development purposes, you may just want to use two different browser processes (i.e. on different users, or perhaps use Firefox for one and Opera for the other).

  22. siuying Says:

    Oh thanks! I dont realized this is browser issue.When I open another browser, it works.

    But the problem is, if people open two page with same browser, it blocks your server! Not a very secure web application…

  23. siuying Says:

    After careful experiment, it is really my firefox hangs not rails. Thanks again!

  24. Michael Carter Says:

    I think you should take a look at orbited, an open source distributed comet server. It has a very simple api that allows you to send events from ruby to any browser connected to the comet server. While Most of the material is python oriented, there is a ruby client.

    Orbited solves many problems, least of all scaling the app. (It uses an architecture not unlike memcached.) Take a look and see what you think.

  25. adam Says:

    Michael -

    Interesting, I hadn’t seen this before. (It should be noted that the term “Comet” actually originates in the Python world, as part of Dojo.) Here’s the Orbited homepage, but unfortunately the documentation is extremely sparse. (The “docs” link is a 404 error!) This appears to be the best bit of info: CherryChat

  26. Pete Says:

    > Comet can only connect to the same host (and port) the original request was from.

    So how does juggernaut solve this?

    Port 1: Webserver
    Port 2: Juggernaut

    Port 1 Port 2

    ??

  27. Pete Says:

    Port 1 not equal Port 2

    (my last post was somehow crippled)

  28. saracen Says:

    I think juggernaut solves this by using flash (an embedded .swf file that gets sent to the browser) and not javascript to do the connection. Flash doesn’t have the same port restrictions as javascript and so it gets around that problem.