#344 Queue Classic
- Download:
- source codeProject Files in Zip (86.9 KB)
- mp4Full Size H.264 Video (25.8 MB)
- m4vSmaller H.264 Video (11.3 MB)
- webmFull Size VP8 Video (11 MB)
- ogvFull Size Theora Video (28.5 MB)
En el episodio 342 vimos cómo configurar una aplicación Rails sobre una base de datos PostgreSQL. Una ventaja de Postgres es que puede asumir la responsabilidad de ejecutar cosas que normalmente serían realizadas con un proceso de segundo como por ejemplo gestionar una cola de tareas. A continuación mostramos una pantalla de una aplicación Rails que utiliza Postgres y que envía boletines por correo.
Podemos enviar un boletín haciendo clic en el enlace “Deliver Newsletter” Esto tardará un poco en terminar, tiempo durante el cual la aplicación Rails no procesará ninguna otra petición y el navegador parecerá haberse quedado pegado mientras espera una respuesta. Debido a esto es buena idea mover este tipo de tareas a un proceso de segundo plano siempre que sea posible, y cuando se envíe el boletín veremos un mensaje en la parte superior de la página informando del estado del envío.
Queue Classic
Podemos resolver este problema con la gema queue_classic. Esta gema facilita mover los procesos que tarden mucho a tareas en segundo plano gestionadas por Postgres. Utilizaremos la versión 2 de la gema. El README tiene instrucciones para configurar queue_classic con Rails, y son las que seguiremos. Como siempre, el primer paso es añadir la gema al Gemfile
y ejecutar bundle
. Como instalamos una versión de prelanzamiento tendremos que especificar el número de la versión.
gem 'queue_classic', '2.0.0rc14'
A continuación cargaremos la tareas de Rake de queue_classic creando un fichero queue_classic.rake
bajo /lib/tasks
y pegando las siguientes dos líneas del README.
require "queue_classic" require "queue_classic/tasks"
También tendremos que crear un inicializador para queue_classic
. En él especificaremos la configuración -que se hace mediante variables de entorno, lo que lo hace plenamente compatible con Heroku-. Tan sólo tenemos que añadir la variable DATABASE_URL
indicando la información de la conexión con la base de datos. Como esta base de datos se encuentra en nuestro sistema local no tenemos que especificar un nombre y clave de usuario.
ENV["DATABASE_URL"] = "postgres://localhost/mailer_development"
A continuación necesitamos una migración llamada add_queue_classic
para configurar la base de datos.
$ rails g migration add_queue_classic invoke active_record create db/migrate/20120502000000_add_queue_classic.rb
En esta migración añadiremos código para cargar las funciones de queue_classic
.
class SetupQueueClassic < ActiveRecord::Migration def up QC::Setup.create end def down QC::Setup.drop end end
Tendremos que ejecutar rake db:migrate
para correr la migración. A continuación arrancaremos el proceso de trabajo en segundo plano con la siguiente orden.
$ rake qc:work
Si todo va bien, esta orden no devolverá ninguna salida. Podemos experimentar con queue_classic en una nueva ventana de terminal con la consola de Rails. Para añadir una tarea a la cola tenemos que llamar a QC.enqueue
y pasarle el método que queremos activar, así como los argumentos que le queramos pasar.
1.9.3p125 :001 > QC.enqueue "puts", "hello world" lib=queue_classic action=insert_job elapsed=13 => nil
En la pestaña en la que dejamos corriendo rake qc:work
deberíamos ver “hello world”. Al llamar a enqueue
los argumentos se serializan a JSON y se pasan a la tabla de colas para que sean procesadas. La serialización JSON es bastante delicada por lo que es importante que los argumentos que pasemos sean tan sencillos como sea posible. Ni tan siquiera se admiten símbolos, por lo que si se pasa un hash recibiremos una excepción.
1.9.3p125 :002 > QC.enqueue "puts", msg:"hello world" lib=queue_classic level=error error="QC::OkJson::Error" message="Hash key is not a string: :msg" action=insert_job QC::OkJson::Error: Hash key is not a string: :msg
Si queremos pasar un hash tenemos que utilizar cadenas en lugar de símbolos como claves:
1.9.3p125 :003 > QC.enqueue "puts", "msg" => "hello world"
Esto se ejecutará correctamente.
Envío de boletines con Queue Classic
Modificaremos la aplicación para que cuando hagamos clic en el enlace “deliver newsletter” el proceso de enviar los correos se ejecute en segundo plano. Cuando se hace clic en el enlace se activa la acción deliver
del controlador NewsletterController
. La acción recupera el boletín, espera durante diez segundos para simular que se trata de un proceso que tarda en ejecutarse, y luego actualiza los datos del envío con la fecha.
def deliver newsletter = Newsletter.find(params[:id]) sleep 10 # simulate long newsletter delivery newsletter.update_attribute(:delivered_at, Time.zone.now) redirect_to newsletters_url, notice: "Delivered newsletter." end
Siempre que en un controlador tengamos un proceso que tarda en ejecutarse es buena idea moverlo a un método de clase de un modelo para simplificar al máximo la interfaz. Crearemos un método llamado deliver
en el modelo Newsletter
, que será lo que invocaremos desde el controlador.
def deliver Newsletter.deliver(params[:id]) redirect_to newsletters_url, notice: "Delivered newsletter." end
A continuación escribiremos el método en el modelo.
class Newsletter < ActiveRecord::Base attr_accessible :delivered_at, :subject def self.deliver(id) newsletter = find(id) sleep 10 # simulando un proceso lento newsletter.update_attribute(:delivered_at, Time.zone.now) end end
Ahora es fácil mover el envío a un proceso de segundo plano y cambiar el mensaje flash para que diga que se está enviando el boletín en lugar de que el envío ha acabado.</ActiveRecord::Base>
def deliver QC.enqueue "Newsletter.deliver", params[:id] redirect_to newsletters_url, notice: "Delivering newsletter." end
Podemos hacer ya la prueba. Si hacemos clic en el enlace para enviar el boletín veremos la respuesta al momento, pero el envío propiamente dicho estará todavía pendiente de ser procesado en segundo plano.
Si esperamos diez segundos y recargamos la página veremos que el envío ha finalizado.
Gestión de problemas en las tareas
¿Qué ocurre cuando falla una tarea y se eleva una excepción durante la ejecución? Hagamos la prueba lanzando nosotros la excepción, para ver qué pasa.
def self.deliver(id) newsletter = find(id) raise "Oops" sleep 10 # simulate long newsletter delivery newsletter.update_attribute(:delivered_at, Time.zone.now) end
Cuando ahora hagamos clic en “Deliver newsletter” veremos el mensaje que dice que el envío está en curso pero en el proceso en segundo plano se producirá la excepción, lo que se podrá comprobar en la salida del proceso (en una aplicación de producción esta salida debería enviarse a un log). Por defecto queue_classic no intenta hacer nada como por ejemplo reintentar la tarea. En el código fuente de la clase Worker veremos un método llamado handle_failure
que simplemente imprime la excepción por la salida estándar.
#override this method to do whatever you want def handle_failure(job,e) puts "!" puts "! \t FAIL" puts "! \t \t #{job.inspect}" puts "! \t \t #{e.inspect}" puts "!" end
Podríamos gestionar errores en las tareas de forma diferente si redefiniésemos este método. Un ejemplo sería heredar de la clase Worker
, tal y como se menciona en el README, que también contiene mucha más información útil incluyendo un ejemplo de cómo configurar una tarea que no dependa de Rake sino que sea directamente ejecutable. La ventaja es que podemos controlar exactamente lo que se carga en memoria en caso de que no necesitemos cargar todo el entorno de Rails cada vez que queramos ejecutar el proceso.
Otra funcionalidad útil es la posibilidad de recibir notificaciones de tareas. De esta manera si se añade una tarea a la cola nuestro proceso puede ser notificado instantáneamente y comenzar a procesarla al momento en lugar de esperar el tiempo configurado de sondeo de la base de datos. Esto puede ser importante a la hora de inclinarnos por usar queue_classic en lugar de otras soluciones.
Y esta es la cuestión final: ¿cómo queda queue_classic comparado con otros sistemas de tareas en cola? Al que más se parece es a Delayed Job, que también guarda las tareas en base de datos, pero queue_classic aprovecha mejor las ventajas de Postgres y no requiere ActiveRecord por lo que podemos minimizar bastante el tamaño del proceso que ejecuta las tareas. Otras alternativas como Resque, RabbitMQ y Beanstalk exigen la presencia de un servidor separado que gestione la cola, por o que si queremos que nuestra infraestructura sea lo más sencilla posible queue_classic es una opción interesante, aunque le falta una interfaz web como la que proporciona Resque.