Breaking out of Rails MVC with Mongrel handlers
Geplaatst door Remco van 't Veer za, 23 feb 2008 11:21:00 GMT
Module View Controller is a nice concept but sometimes you need te be very creative to fit a problem in that mold. In such cases a more lowlevel approach can be much more effective. The case in ran into recently involves streaming audio from a Rails application. I wanted to recode and stream MP3 files over HTTP. There’s not much MVC about that, there’s nothing to view!
A memory refresher; what is Mongrel? Mongrel is a web server (mostly) written in Ruby. Ruby version 1.8 comes with a web server named WEBrick but its performance is very limited and not useable for public production web applications. Mongrel, on the other hand, is more like a hybrid between a greyhound and a poodle, fast and tough. The little beast is multi-threaded and can easily handle hundreds of concurrent requests.
Mongrel (or a pack of Mongrels) is, at this moment, the most popular deployment platform for Rails applications. So when we are already running with this animal, why can’t we just force MVC on our problem and stream data using the send_data
method? Well.. look at the following piece of Mongrel code:
@guard.synchronize { @active_request_path = request.params[Mongrel::Const::PATH_INFO] Dispatcher.dispatch(cgi, ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, response.body) @active_request_path = nil }Source:
mongrel-1.1.2/lib/mongrel/rails.rb
In this sample, take from the Mongrel source code, Dispatcher.dispatch
is the Rails code which handles a request. The problem is the @guard.synchronize
statement, this locks the Mongrel process using a Mutex
and makes sure a Mongrel process only handles one Rails request at the time. This is not a situation in which you would want to serve audio streams, especially since MP3 players usually only buffer a couple of seconds hogging the Mongrel process for the full playing time of the audio clip.
Simple handler
Let’s make a simple Mongrel handler and forget our Rails application for now. Here’s a simple hello-world implementation:require 'rubygems' require 'mongrel' class HelloWorldHandler < Mongrel::HttpHandler def process(request, response) response.start(200) do |head,out| head['Content-Type'] = "text/plain" out << "Hello world!" end end end Mongrel::Configurator.new do listener :port => 3000 do uri "/", :handler => HelloWorldHandler.new end run; join end
Copy this code into a file named hello_word_handler.rb
, run it with ruby hello_word_handler.rb
and point your browser to http://localhost:3000/
.
That was easy! But how does it work? The first block defines a Mongrel handler, HelloWorldHandler
. This handler has a single method called process
which accepts a request
and response
object. A HTTP request is processed by this handler. Our handler always returns a 200 OK status, it tells what type of output it will produce, "text/plain"
, and the output is assembled, "Hello world!"
. The second block configures Mongrel to listen on portnumber 3000 and process all incoming request starting with "/"
using an instance of HelloWorldHandler
. There’s nothing to it, it’s actually very similar to making a Java or WEBRick Servlet.
Note only one instance of the handler is used to handle requests, which keeps the processing costs very low. Your handler does need to be thread safe! My laptop can easily handle 700 request per second using this example compared to 30 per second using a Rails controller rendering text.
Streaming
The handle we just wrote is not usable to serve streams. The way a response is build, using the response.start
method, is not suitable because the entire response is constructed before being send back to the client. This means the entire response body resides in system memory which isn’t always a good idea when we are preparing a couple of megabytes of audio to send.
There’s another reason we want to send our data as soon as possible. A recode may take several seconds or even minutes so we want to respond to a client request even before the recoding is complete.
Let’s stream some data! The following example slowly trickles data across the network (use curl or telnet so see the effect):
require 'rubygems' require 'mongrel' class CuckooHandler < Mongrel::HttpHandler def process(request, response) response.status = 200 response.send_status(nil) response.header['Content-Type] = "text/plain" response.send_header 5.times do response.write("Cuckoo\r\n") sleep 1 end response.done end end Mongrel::Configurator.new do listener :port => 3000 do uri "/", :handler => CuckooHandler.new end run; join end
As you can see we need to be more aware of what we are doing when building a HTTP response; first send the status line (which we provide with nil
since we don’t know how many bytes we’re sending), than the header and finally the data. Not that complicated, so let’s immediately dive into recoding and streaming audio:
require 'rubygems' require 'mongrel' class StreamHandler < Mongrel::HttpHandler def process(request, response) response.status = 200 response.send_status(nil) response.header['Content-Type'] = "audio/mpeg" response.send_header fn = File.join('/BigFatDisc', request.params['PATH_INFO']) IO.popen("lame --quiet --preset 16 #{fn} -") do |trans| while data = trans.read(1024) response.write(data) end end response.done end end Mongrel::Configurator.new do listener :port => 3000 do uri "/", :handler => StreamHandler.new end run; join end
Now we have a recoding-audio-stream server which serves all MP3s from /BigFatDisc
recoded to 16kbs using lame. Put the above code in a Ruby file, change /BigFatDisc
to the directory name where you keep your MP3s, verify you installed lame and open a stream using your favorite MP3 player to (apple-key-U in iTunes); http://localhost:3000/various/some_track.mp3
.
With Rails
The streaming part is take care of by StreamHandler
, let’s put it on Rails. I prefer to put my extra handler in the lib
directory of my Rails application. But you could add something like a handlers
directory in app
if you like.
Starting Rails with an extra handler is really easy. The mongrel_rails
command has a switch for providing extra configuration scripts; the -S
switch. Scripts passed with this switch are executed within the listener
block context. Which means we can replace:
Mongrel::Configurator.new do listener :port => 3000 do uri "/", :handler => StreamHandler.new end run; join end
by:
uri "/stream", :handler => StreamHandler.new
Please note I replaced "/"
by "/stream"
to make sure ordinary requests get directed to Rails. Otherwise all requests endup in the StreamHandler
. Now you can start your applicatie including StreamHandler
using:
mongrel_rails start -S lib/stream_handler.rb
If your using a pack of mongrels using Mongrel Cluster, add:
config_script: lib/streamer.rb
to your configuration file.
An URL from our Rails application to a stream can be contructed by simply appending "stream/#{track.filename}"
to root_url
(or whatever route you have for ”/”). Where track
is a model object which has a filename
attribute containing the file location relative to your /BigFatDisc
directory. Now you van make M3U or XSPF playlists!
Be careful with filenames containing characters which are not allowed to be in the path part of an URL. See RFC1738 for details. You do have access to your ActiveRecord models inside the handler so you can also use records identifiers instead of filenames. But be warned; the synchronized block in the Mongrel Rails handler is there for a reason!
Finished?
This is just the beginnen of a stream handler; caching the recoded MP3s still needs to be implemented, some MP3 players need to know how many bytes they will receive before playing and if you want seeking to work you’ll need to be able to give a 206 Partial Content response.
But this should be enough for your public beta! ;)
Can you please give me some link or documentation on how i can make full streaming app in Rails, the most important part being seeking! I’d love to be able to seek an streaming audio from a flex app!