So, you’re probably aware that ruby 1.9 adds Fibers, a lightweight concurrency library.
There’s some cool stuff using this, and I’m sure that you could use that to implement the following, but it felt better rolling it from scratch.
So, the use case:
Assume that I am a user at a web frontend watching the status of my application. I don’t want to poll the app every N seconds to get the data from it, but I still want as-it-happens updates to the data on my screen. Say, every time a ‘Widget’ model is created I want to update a certain div with the new Widget.count.
To do that, we’d need to send an AJAX call to the server, which is going to purposefully hang the response and then wake it up again whenever our event occurs.
The following code should do just that:
require 'socket' server = TCPServer.new('127.0.0.1', '8080') #fork do module FiberPool class << self def fire event $queued_fibers ||= {} $queued_fibers[event] ||= [] while(!$queued_fibers[event].empty?) do x = $queued_fibers[event].shift puts "\tFiring #{x.inspect}" x.resume end $queued_fibers[event] = [] end end end $events = [] $queued_fibers = {} f = Thread.fork do loop do begin session = server.accept_nonblock $queued_fibers['respond'] ||= [] $queued_fibers['respond'] << Fiber.new { puts "Firing a fiber: #{session.gets}" session.puts "Hello Fiber!" session.close } rescue Errno::EAGAIN, Errno::ECONNABORTED, Errno::EPROTO, Errno::EINTR ensure FiberPool.fire $events.shift unless $events.empty? end end end at_exit {Thread.kill(f)}
I’ve used some global variables here as a way of passing messages across threads, but in reality you’d use a Redis database or something instead.
So, every time we get a request on 8080, we hang the request into its own fiber. The server continually checks the $events variable (or just as easily, a redis event queue) for events. Then, as soon as we see the ‘respond’ event, we wake up all of those threads and fire them off.
For demonstration purposes, append the following code, giving the terminal control over the ‘event sending.’
begin loop do puts "Register an event:" event = gets.chomp puts "got event #{event}" if event == 'show' puts "Queued Fibers = #{$queued_fibers.inspect}" puts "Events = #{$events.inspect}" else $events << event end end rescue Interrupt puts "\nCaught interrupt, exiting" end
Save all that to a file, run it, and point your web browser to localhost:8080. It should hang, until you go to your command line and type ‘respond’ as the event to raise, at which point the web browser should immediately see ‘Hello Fiber!’
Now, imagine that you’ve made the request through a new Ajax.updater and the response is a JSON map of DOMID => Value pairs computed by the server, and you see why this is exciting. You can set up your Widget.after_create to throw a new event to redis, at which point all of your clients will receive a response to this AJAX call, updating their page. You can set up the ‘onsuccess’ callback to make another request, and you’ve bound a dom object to a server-side map.
If you’re used to programming GUIs in the desktop, or IPhone, you’ll notice this is pretty much the way that those work. A change in the state of the bound object triggers an update in the view, and the view can send events to the controller to change the bound objet.
The gap between the real-time GUIs of application development and request-based transactions of web development just got a hell of a lot smaller.
References:
http://www.double.co.nz/pdf/continuations.pdf
2 Comments
Soo…wouldn’t this make a DoS/DDoS attack trivially easy to pull off?
If all you did was spin-wait all of those connections until something happened, then sure! There might be some way to do some hacking to ’serialize’ the TCP session (The packet headers) so that when the event goes off and you want to continue, you pull the session information from Redis or Memory and respond in kind.
Of course, that would set up the possibility of an attacker ‘bombing’ the server, trying to get it to respond to a bunch of requests all at once, so you could do some metering there…
It might be more trouble than its worth, but I thought the ‘hanging response’ would be a cool way to send the client some updates. What are some other ideas that don’t involve constant polling of the webapp?