Rails MVC aan je laars lappen met Mongrel handlers
Geplaatst door Remco van 't Veer ma, 24 dec 2007 11:18:00 GMT
Module View Controller is mooi maar soms moet je heel creatief zijn om een oplossing in die mal te proppen. In dergelijke gevallen is een meer lowlevel aanpak veel doeltreffender. In het geval waar ik tegenaan gelopen ben, wil ik audio streams serveren vanuit een Rails applicatie. Om precies te zijn: ik wil MP3 bestanden automatisch omzetten naar een lager bitrate en deze over het netwerk sturen zodat er aan de andere kant naar geluisterd kan worden. Daar is weinig MVC aan, al is het alleen maar dat er niets te view-en valt!
Nog even in het kort, was is Mongrel? Mongrel is een webserver (grotendeels) geschreven in Ruby. Ruby versie 1.8 is standaard uitgerust met een web server genaamd WEBrick maar deze is niet snel en efficient genoeg voor openbare productie web applicaties. Mongrel daarentegen is een kruising tussen een windhond en een poedel, snel en niet stuk te krijgen. Het beestje is multi-threaded en kan daarmee honderden aanvragen tegenlijk afhandelen.
Waarschijnlijk is Mongrel (of een roedel Mongrels) op dit moment de meest gebruikte webserver om Rails te hosten. Als ik dan toch al Mongrel gebruik, en dus verschrikkelijk veel gelijktijdige aanvragen kan afhandelen, waarom zou ik dan niet gewoon toch m’n audio streams MVC modeleren en m’n data met de send_data
methode versturen? Het volgende stukje Mongrel code is het probleem:
@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 }Bron:
mongrel-1.1.2/lib/mongrel/rails.rb
Hier is Dispatcher.dispatch
de Rails code welke een verzoek afhandelt. Het probleem zit hem in dat @guard.synchronize
statement, deze synchronisatie op een Mutex
zorgt er voor dat een enkel Mongrel proces maar één Rails verzoek tegenlijk af kan handelen. Geen situatie waarin je audio streams wilt serveren, vooral omdat veel MP3 spelers maar een paar seconden bufferen en streams dus bijna de hele looptijd een netwerk verbinding open houden.
Simpele handler
Laten we een simpele Mongrel handler maken. We vergeten onze Rails applicatie voorlopig en beginnen met een simpele hello-world variant: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 cruel world!" end end end Mongrel::Configurator.new do listener :port => 3000 do uri "/", :handler => HelloWorldHandler.new end run; join end
Maak van het bovenstaande een Ruby bestand, draai het met ruby hello_word_handler.rb
en wijs je browser naar http://localhost:3000/
.
Mooi maar wat gebeurd er allemaal? In het eerste blok wordt een Mongrel handler gedefinieerd. Deze handler heeft een enkele methode genaamd process
welke een request
en response
object aanneemt. Een HTTP aanvraag wordt afgehandeld door deze methode. Onze methode geeft altijd een 200 OK antwoord. Er wordt aangegeven welk type informatie er terug gegeven gaat worden, "text/plain"
, en het antwoord wordt samengesteld, "Hello cruel world!"
. In het tweede blok wordt Mongrel duidelijk gemaakt dat er geluisterd moet worden op poortnummer 3000 en dat alle aanvragen die beginnen met "/"
moeten worden afgehandeld door een instantie van HelloWorldHandler
.
Heel simpel eigenlijk en voor iemand die wel eens een Java Servlet of WEBrick Servlet gemaakt heeft, niet veel nieuws. Wat nog wel interessant is om op te merken: er wordt maar een enkele instantie van de handler gebruikt. Dat maakt de kosten van een afhandeling erg laag. M’n laptopje kan bijvoorbeeld gemakkelijk 700 verzoeken per seconden afhandelen met de bovenstaande code, tegenover 30 per seconden met een soort gelijke Rails controller. Dat is niet mis!
Streaming
Toch is een handler als de bovenstaande niet bruikbaar om streams te serveren. Het probleem van deze standaard aanpak (met de response.start
methode) is dat het hele antwoord pas over het netwerk wordt gestuurd als het blok dat aan de start
methode meegegeven is, afgerond is. Dat betekent dus dat het gehele antwoord eerst in het geheugen opgebouwd wordt en voor megabytes aan audio stream is dat niet zo’n goed idee. Daarbij komt ook nog eens dat de MP3 eerst omgezet moet worden naar een lager bitrate en we eigenlijk het complete resultaat van die omzetting niet helemaal af wil wachten maar zo snel mogelijk willen streamen.
Laten we eerst het streamen van data eens bekijken. Het volgende voorbeeld laat het antwoord langzaam terug druppelen over het netwerk (gebruik curl of telnet om het effect goed te kunnen zien):
require 'rubygems' require 'mongrel' class KoekoekHandler < 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("Koekoek\r\n") sleep 1 end response.done end end Mongrel::Configurator.new do listener :port => 3000 do uri "/", :handler => KoekoekHandler.new end run; join end
Zoals je ziet moeten we ons wat bewuster zijn van de opbouw van een HTTP response; eerst de status regel sturen (die nil
geeft aan dat we niet weten hoeveel data we gaan sturen), dan de headers en vervolgens de data. Allemaal niet zo ingewikkeld, we gaan meteen verder met audio omzetten en streamen:
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
Nu hebben we een recoding-audio-stream server die alle MP3 van /BigFatDisc
kan om zetten met lame naar een 16kbps stream. Maak van het bovenstaande een Ruby bestand, verander /BigFatDisc
naar een directory waar je MP3’s hebt staan, controleer of je lame geïnstalleerd hebt en open een stream met je favoriete MP3 speler (appeltje-U in iTunes); http://localhost:3000/various/some_track.mp3
.
Samen met Rails
Nu we een stream kunnen leveren is het tijd om onze StreamHandler
in Rails hangen. Ik plaats m’n extra handlers in een Rails applicatie in de lib
directory van m’n applicatie. Alles onder app
is me namelijk veel te MVC.
Rails met een extra handler opstarten is heel simpel. Het mongrel_rails
command heeft een speciale parameter voor extra configuratie scripts; de -S
switch. Scripts die met deze parameter meegegeven worden, worden binnen de context van het listener
blok uitgevoerd. Dit betekent dat we:
Mongrel::Configurator.new do listener :port => 3000 do uri "/", :handler => StreamHandler.new end run; join end
kunnen vervangen door:
uri "/stream", :handler => StreamHandler.new
Merk op dat ik "/"
meteen vervangen heb door "/stream"
om te voorkomen dat m’n Rails applicatie niet meer aanbod komt en alle aanvragen aan de StreamHandler
aangeboden worden. Nu kan je de StreamHandler
meenemen als je je applicatie opstart met:
mongrel_rails start -S lib/stream_handler.rb
Als je je Mongrels in een roedel houdt met Mongrel Cluster, gebruik dan:
config_script: lib/streamer.rb
in je configuratie file.
Vanuit de Rails applicatie kunnen we nu een URL naar een stream maken door simpelweg de root_url
(of wat je dan ook geroute hebt naar ”/”) en er "stream/#{track.filename}"
achter te plakken (gegeven dat de filename
methode in je track model een relative locatie geeft van de bij behorende MP3 in jouw /BigFatDisc
directory). Nu kan je M3U of XSPF playlists opbouwen!
Uiteraard leveren bestandsnamen met tekens daarin die niet im het path onderdeel van een URL passen een probleem. Zie RFC1738 voor de details. Overigens heb je, in je handler, toegang tot je ActiveRecord models en kan je ook met record-identifiers aan de slag. Maar wees een beetje paranoia, de eerder besproken synchronisatie in de Mongrel Rails handler zit er vast niet voor niets..
Klaar?
Dit is nog maar het begin van een volwaardige stream handler; caching van omgezette MP3’s moet nog geïmplementeerd worden, sommige MP3 players willen persé weten hoeveel data er overgezet gaat worden en als je seeking wilt ondersteunen zul je een 206 Partial Content antwoord moeten kunnen geven.
Maar je hebt genoeg voor je public beta! ;)
Mischien is merb een idee, daar kun je je controller een Proc object laten returnen. Ofwel, binnen je controller werk je in een Mutex (want ja, ActiveRecord is niet bepaald thread safe), en vervolgens return je een Proc object dat je mp3 data streamed.
Overigens, mp3 on the fly reencoden naar een lagere bitrate kost een aardige hoeveelheid CPU, gereencode mp3s cachen is aan te raden.
Uiteraard heb ik merb bekeken en overwogen te gebruiken. In het gegeven voorbeeld, had ik echter al een Rails applicatie en gebruikte een externe streaming applicatie. Deze applicatie was aan vervanging toe.
Ah, ok. Ik had eenzelfde soort probleem, alleen moest het ook nog wat op webradio lijken, dus heb ik de libice bindings gebruikt.
Voor dit soort streaming oplossingen met Rails maak ik vaak gebruik van X-Sendfile support in Apache en/of lighttpd. Zie http://blog.lighttpd.net/articles/2006/07/02/x-sendfile voor een korte maar krachtige uitleg. Wat je in wezen doet is dat je Rails controller de file content prepareert i.e. on-the-fly transcoding of de cached versie opzoekt en vervolgens in een HTTP X-Sendfile header lijntje een lokaal disk pad naar het te streamen bestand opgeeft. Vervolgens pikt Apache danwel lighty dit verzoek tot streamen op en klaar. Schaalt erg goed.
Los daarvan zijn Mongrel handlers suuuper cool als je toch nets iets meer controle wilt in de HTTP request/response cycle dan Rails je toelaat. Echt schitterend als je de zoveelste 4K regels Java servlet kan vervangen met 100 regels aan Mongrel handler shait :)
curl werkt alleen met de koekoek alsvolgt:
curl—ignore-content-length http://localhost:3000/