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! ;)

Geplaatst in ,  | 1 reactie

Reacties

  1. carnage@lavabit.com zei 21 dagen later:

    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!

(Laat url/e-mail achter »)

   Voorvertoning