#271 Resque
- Download:
- source codeProject Files in Zip (206 KB)
- mp4Full Size H.264 Video (18.8 MB)
- m4vSmaller H.264 Video (12.1 MB)
- webmFull Size VP8 Video (13.8 MB)
- ogvFull Size Theora Video (28 MB)
Hagamos un alto en nuestra serie dedicada a las novedades de Rails 3.1 y vamos a echarle un vistazo a Resque, que es una forma muy interesante de gestionar tareas en segundo plano en las aplicaciones Rails. Ya hemos visto varias formas de hacer este procesamiento en segundo plano; cada una tiene sus particularidades y Resque no es ninguna excepción. Al final de este episodio veremos algunos consejos que pueden resultar de ayuda a la hora de escoger una implementación de tareas en segundo plano. Pero por ahora, vamos a empezar con Resque añadiéndolo a una aplicación Rails.
La aplicación que vamos a usar es un sitio que permite compartir fragmentos de código al estilo de Pastie. Con este sitio podemos introducir un código de ejemplo y asignarle un nombre y un lenguaje.
Cuando enviemos un fragmento, éste aparecerá con el coloreado sintáctico apropiado según el lenguaje escogido.
El coloreado sintáctico se gestiona con un servicio web externo, y esta es la parte del código que queremos que se haga con una tarea en segundo plano. Por ahora se ejecuta en línea como parte de la acción create
de SnippetController
.
def create @snippet = Snippet.new(params[:snippet]) if @snippet.save uri = URI.parse('http://pygments.appspot.com/') request = Net::HTTP.post_form(uri, {'lang' => @snippet.language, 'code' => @snippet.plain_code}) @snippet.update_attribute(:highlighted_code, request.body) redirect_to @snippet, :notice => "Successfully created snippet." else render 'new' end end
El resaltado sintáctico se efectua cuando se guarda el fragmento de código. Utiliza el servicio http://pygments.appspot.com/, creado por Trevor Turk, que proporciona resaltado sintáctico sin requerir dependencias locales. El código realiza una petición POST a este servicio, enviando el código junto con el lenguaje, y coge la respuesta del servicio para rellenar el atributo highlighted_code
del modelo Snippet.
Por lo general no es muy buena idea comunicarse con servicios externos dentro de una petición Rails porque estos servicios podrían demorar su respuesta y bloquear totalmente el proceso Rails así como todas las peticiones que intentan conectarse con él. Configuremos Resque para poder mover esta petición a una tarea de Resque.
Cómo poner en marcha Resque
Resque utiliza Redis que es un almacén de claves y valores persistente. Redis ya es bastante sorprendente por sí mismo, y merecería la pena un capítulo dedicado en exclusiva, pero por ahora sólo lo vamos a usar con Resque.
Como estamos ejecutando OS X la forma más fácil de instalar Redis es con Homebrew lo que podemos hacer con la siguiente orden:
$ brew install redis
Una vez que se haya instalado podemos arrancar el servidor de la siguiente manera:
$ redis-server /usr/local/etc/redis.conf
Con Redis en marcha podemos poner Resque en el Gemfile
de nuestra aplicación y luego instalarlo con bundle
.
source 'http://rubygems.org' gem 'rails', '3.0.9' gem 'sqlite3' gem 'nifty-generators' gem 'resque'
A continuación tenemos que crear las tareas Rake de Resque. Lo haremos creando un fichero resque.rake
en el directorio lib/tasks
de nuestra aplicación. En este fichero tenemos que hacer un require "resque/tasks"
de forma que se carguen las tareas de Rake que incorpora la propia gema. También cargaremos el entorno de Rails cuando se arranquen los trabajos.
require "resque/tasks" task "resque:setup" => :environment
De esta forma las tareas de Resque tendrán acceso a los modelos de la aplicación pero si queremos que nuestros procesos de ejecución sean más ligeros nos interesará implementar nuestra propia solución de forma que no se cargue todo el entorno de Rails.
Ya tenemos una tarea de Rake que podemos usar para lanzar los procesos de ejecución de Resque. Para ello tenemos que pasar un argumento QUEUE
. Podemos bien pasar el nombre de una cola específica o '*' para trabajar con cualquier cola.
$ rake resque:work QUEUE='*'
Este script no genera ninguna salida pero está funcionando.
Mover el procesamiento del código del servicio web
Con Resque configurado ya podemos centrarnos en mover el código que llama al servicio web a un proceso en segundo plano y gestionarlo con un proceso de ejecución de Resque. Tenemos que añadir el trabajo a la cola para que Resque lo pueda procesar. Veamos como quedaría la acción create
.
def create @snippet = Snippet.new(params[:snippet]) if @snippet.save uri = URI.parse('http://pygments.appspot.com/') request = Net::HTTP.post_form(uri, {'lang' => @snippet.language, 'code' => @snippet.plain_code}) @snippet.update_attribute(:highlighted_code, request.body) redirect_to @snippet, :notice => "Successfully created snippet." else render 'new' end end
Los trabajos se añaden a la cola invocando Resque.enqueue
. Este método recibe varios argumentos, el primero es la clase que queremos utilizar para gestionar la tarea. Todavía no hemos creado ninguna, pero la que crearemos en breve se llamará SnippetHighlighter
También tenemos que poner todos los argumentos adicinoales que le queramos pasar a la tarea. En este caso podríamos pasar la propia instancia del modelo pero todo lo que le pasemos a enqueue
será convertido a JSON y almacenado en Redis, por lo que no es recomendable pasar objetos complejos (como modelos de ActiveRecord) así que en vez de pasar toda la instancia sólo le pasaremos el id
, y recuperaremos la instancia dentro de la tarea.
def create @snippet = Snippet.new(params[:snippet]) if @snippet.save Resque.enqueue(SnippetHighlighter, @snippet.id) redirect_to @snippet, :notice => "Successfully created snippet." else render 'new' end end
A continuación crearemos la clase que implementa la tarea, y moveremos el código que hemos quitado del controlador a ella. Pondremos la clase en el directorio /app/workers
. También podríamos poner la clase en /lib
pero si hacemos que cuelgue de app/
Rails cargará el código automáticamente.
Una tarea es tan sólo una clase con dos funcionalidades. Primero tiene que tener una variable de instancia llamada @queue
, que tiene el nombre de la cola. Esto limita las colas que puede gestionar la clase. Segundo necesita un método de clase llamado perform
que recibe los argumentos que se pasaron enqueue
, en este caso el id
del objeto ActiveRecord del fragmento de código. En este método podemos poner el código que sacamos de la acción create
y que invoca al servidor remoto y devuelve el código resaltado.
class SnippetHighlighter @queue = :snippets_queue def self.perform(snippet_id) snippet = Snippet.find(snippet_id) uri = URI.parse('http://pygments.appspot.com/') request = Net::HTTP.post_form(uri, {'lang' => snippet.language, 'code' => snippet.plain_code}) snippet.update_attribute(:highlighted_code, request.body) end end
Veamos si esto funciona. Creemos un nuevo fragmento de código y subámoslo. Si lo hacemos veremos que el fragmento no muestra ningún tipo de coloreado.
No podemos ver de inmediato el resaltado sintáctico porque se efectúa en segundo plano. Si esperamos unos segundos y recargamos la página veremos que sigue sin verse los colores así que vamos a intentar depurar el código para ver qué es lo que no está funcionando. Resque viene con una interfaz web, escrita en Sinatra. De esta manera es muy fácil monitorizar y gestionar las tareas. Podemos arrancar esta interfaz con:
$ resque-web
Cuando ejecutemos esta orden aparecerá la interfaz administrativa y podremos ver que tenemos una tarea fallida.
Si hacemos clic en la tarea que ha dado error veremos los detalles: uninitialized constant SnippetHighlighter
.
La razón por la que no se encuentra la clase SnippetHighlighter
es porque hemos empezado la tarea Rake que ejecuta el servidor antes de escribir dicha clase. Reiniciemos el servidor y veamos si esto corrige el error.
Una vez que hayamos reiniciado el servidor con la tarea Rake podemos hacer clic en el enlace ‘Retry’ para volver a ejecutar la tarea que falló. Si lo hacemos y volvemos a la vista fe ‘Overview’ veremos que sólo hay una ejecución fallida por lo que parece que esta vez el trabajo ha concluido correctamente. Podemos confirmarlo recargando la página con el último fragmento de código que cargamos. Esta vez veremos que el código se encuentra coloreado, lo que confirma que la tarea de segundo plano se ha ejecutado correctamente.
Si volvemos a subir un nuevo fragmento de código veremos que aparece resaltado, aunque pueden pasar varios segundos de retraso hasta que los colores aparezcan.
Cómo embeber la interfaz web de Resque
Ya tenemos Resque configurado y gestionando los trabajos que le damos. Sería muy útil poder embeber la interfaz administrativa de Resque en nuestra aplicación Rails para no tener que arrancarla y gestionar otro proceso separado.
Rails 3 se integra perfectamente con aplicaciones Rack y Sinatra no es sino otra aplicación Rack por lo que es fácil hacer lo que queremos simplemente montando la aplicación en el fichero de rutas:
Coderbits::Application.routes.draw do resources :snippets root :to => "snippets#new" mount Resque::Server, :at => "/resque" end
La interfaz web de Resque quedará montada en nuestra aplicación Rails en http://localhost:3000/resque
. Tenemos que asegurarnos de que el servidor Resque está cargado para que esto funcione por lo que en el Gemfile
tenemos que usar la opción require
con la gema resque
.
source 'http://rubygems.org' gem 'rails', '3.0.9' gem 'sqlite3' gem 'nifty-generators' gem 'resque', :require => 'resque/server'
Si ahora reiniciamos el servidor de nuestra aplicación y visitamos
http://localhost:3000/resque
podremos ver la interfaz administrativa de Resque.
No queremos que esta página sea accesible públicamente, pero podemos añadir algo de autorización para evitar que la gente curiosee. Si usásemos algo como Devise en nuestra aplicación sería fácil asegurar esto rodeando la ruta por una llamada a authenticate
.
Coderbits::Application.routes.draw do resources :snippets root :to => "snippets#new" authenticate :admin do mount Resque::Server, :at => "/resque" end end
Como no estamos usando Devise u otro sistema de autenticación vamos a usar la autenticación básica de HTTP. Crearemos un nuevo inicializador en config/initializers
llamado resque_auth.rb
.
Resque::Server.use(Rack::Auth::Basic) do |user, password| password == "secret" end
En este fichero llamaremos a use
sobre Resque::Server
, que es una aplicación Rack, y añadiremos la autenticación básica de HTTP. En el bloque comprobaremos que la clave coincide. Obviamente deberíamos personalizar este código para establecer la clave y el usuario o para añadir otra lógica que necesitemos. Si reiniciamos el servidor y recargamos la página veremos el diálogo de contraseña, que tendremos que introducir para ver la página.
Las alternativas a Resque
Con esto terminamos nuestro repaso a Resque. ¿Cómo escoger entre esta u otras herramientas de procesamiento en segundo plano? Una de las ventajas de Resque es la interfaz administrativa que nos permite inspeccionar la cola, reintentar las tareas que hayan fallado, etc. Otra razón por la que considerar Resque es que GitHub la utiliza, y eso debería bastar para estar seguros de que puede soportar cualquier nivel de carga a que lo sometamos.
Si la dependencia de Redis es un problema podemos usar como alternativa Delayed Job. Ya lo vimos en el episodio 171 [verlo, leerlo] y es una solución más sencilla porque utiliza la base de datos de nuestra aplicación para gestionar la cola de tareas. Sin embargo, carga el entorno de Rails en todas las tareas, por lo que puede que no sea la mejor solución si queremos que las tareas sean tan ligeras que sea posible.
Tanto Resque como Delayed Job cogen sus tareas consultando periódicamente las colas por lo que puede haber cierto retraso entre el momento en que se añade una tarea a una cola y el momento en que empieza el procesamiento de dicha tarea. Si la cola por lo general está vacía queremos que los procesos se ejecuten de inmediato, y en ese caso Beanstalkd, que vimos en el episodio 243 [verlo, leerlo], será una opción mejor. Beanstalkd gestiona las tareas mediante eventos por lo que puede empezar a procesar un trabajo tan pronto como éste se añada a la cola.