#366 Sidekiq
- Download:
- source codeProject Files in Zip (59.6 KB)
- mp4Full Size H.264 Video (30.6 MB)
- m4vSmaller H.264 Video (13.4 MB)
- webmFull Size VP8 Video (14.5 MB)
- ogvFull Size Theora Video (29.4 MB)
En Rails existen muchas soluciones para implementar los procesos que tarden demasiado como tareas de segundo plano. Cada una una tiene sus ventajas, y Sidekiq no es la excepción. Se trata de una gema similar a Resque, que ya vimos en el episodio 271 pero la diferencia principal es que gestiona múltiples tareas de forma concurrente mediante el uso de threads en lugar de procesos lo que resulta en un menor uso de la memoria.
Nuestra aplicación de fragmentos de código
La interfaz de Sidekiq es similar a la de otras soluciones, por lo que la veremos rápidamente y luego nos centraremos en la funcionalidad que lo hace único. La aplicación de ejemplo con la que vamos a trabajar se muestra a continuación; es bastante sencilla y tiene un formulario con un menú desplegable que incluye una lista de lenguajes y una caja de texto en la que podemos pegar un fragmento de código. Al enviar el formulario se mostrará el fragmento con resaltado sintático.
En la acción create
de SnippetsController
veremos cómo se hace el resaltado sintáctico.
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 else render :new end end
Tras guardar el fragmento de código, se hace una petición a un servicio web externo que utiliza Pygments para obtener el resaltado sintáctico. Si hacemos una petición POST e incluimos el lenguaje y el código en plano, la respuesta contendrá el código con las etiquetas de color. Siempre es buena idea ejecutar este tipo de llamadas a servicios externos como procesos en segundo plano, de forma que si el servicio se encuentra caído o tarda en responder el usuario no se vea afectado. Podemos utilizar Sidekiq para hacerlo.
Cómo añadir Sidekiq a nuestra aplicación
Al igual que Resque, Sidekiq utiliza Redis para gestionar la cola de trabajos, por lo que primero tenemos que instalar Redis. En OS X la forma más fácil es con Homebrew:
$ brew install redis
Tras la instalación podemos arrancar el servidor Redis con esta orden:
$ redis-server /usr/local/etc/redis.conf
A continuación podemos añadir la gema Sidekiq al Gemfile
y ejecutar bundle
para instalarlo.
gem 'sidekiq'
Sidekiq soporta varias interfaces. La forma más normal de usarlo es creando una clase separada para la tarea, así que la crearemos en el directorio app/workers
, lo que garantiza que será cargada automáticamente por la aplicación.
class PygmentsWorker include Sidekiq::Worker def perform end end
La clase debe incluir el módulo Sidekiq::Worker
, que veremos más adelante, así como un método llamado perform
que contiene el código que queremos que se ejecute en segundo plano. Moveremos el código referente al coloreado sintáctico del controlador a este método, y para ejecutarlo invocaremos PygmentsWorker.perform_async
que añadirá el trabajo a Redis y luego invocará a perform
de manera asíncrona. El método perform
debe tener acceso al registro del fragmento de código, y si bien lo podríamos pasar directamente a perform_async
no sería la mejor idea porque tendríamos que serializar el objeto entero en Redis. Es mejor serializar objetos sencillos como cadenas o enteros que modelos de ActiveRecord, por lo que simplemente pasaremos el id
y luego recuperaremos el registro de la base de datos en nuestra tarea de segundo plano.
def create @snippet = Snippet.new(params[:snippet]) if @snippet.save PygmentsWorker.perform_async(@snippet.id) redirect_to @snippet else render :new end end
Podemos pegar ahora en la clase de la tarea el código que hemos eliminado del controlador, modificándolo para que recupere el registro de la base de datos por su id
.
class PygmentsWorker include Sidekiq::Worker def 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
El último paso es arrancar el proceso en segundo plano ejecutando la orden sidekiq
en el directorio de nuestra aplicación. Nótese que es posible que tengamos que usar bundle exec
para que funcione correctamente.
$ bundle exec sidekiq
Ahora que Sidekiq se encuentra a la espera de nuevas tareas, tras reiniciar nuestro servidor web (para que se cargue la nueva clase) podemos hacer la siguiente prueba.
Veremos que el fragmento de código no sale de inmediato con resaltado sintáctico porque el proceso en segundo plano está todavía en ejecución. Para verlo aplicado tenemos que esperar un par de segundos y recargar la página.
Cuestiones a considerar
Cuando usamos Sidekiq debemos tener en cuenta varias cosas. Si un trabajo falla porque ha ocurido un error Sidekiq volverá a intentar ejecutarlo, lo que quiere decir que si se eleva una excepción en cualquier punto del método perform
tenemos que asegurarnos de que no habrá efectos laterales no deseados si el código se vuelve a ejecutar. Esto es especialmente importante cuando trabajamos con correos electrónicos, porque no queremos enviarle a un usuario el mismo correo varias veces. Para desactivar esta funcionalidad podemos utilizar el método sidekiq_options
así:
class PygmentsWorker include Sidekiq::Worker sidekiq_options retry: false # Rest of class omitted. end
De momento vamos a dejar activados los reintentos porque no hay motivo para no reintentar este tipo de tareas si se produce un fallo.
Otro posible problema a tener en cuenta es que el código de la tarea debe ser seguro cuando se ejecuta en un hilo (thread-safe). Ya lo vimos en el episodio 365, pero por lo general debemos evitar compartir datos mutables entre instancias. Esto quiere decir en Ruby que son datos a nivel de clase, y deberíamos evitar usarlos. Además, también deben ser seguras las librerías que utiliza el código de nuestra tarea.
También debemos tener en cuenta el límite en el tamaño de conexiones en el fichero de configuración de la base de datos. Por defecto es 5, lo que quiere decir que sólo pueden conectarse a la base de datos 5 hilos cada vez. Es buena idea incrementar este límite. Por defecto Sidekiq ejecuta hasta 25 tareas a la vez por lo que es buena idea incrementar el tamaño del juego de conexiones a este valor, si bien el valor ideal dependerá de la configuración de cada aplicación en concreto.
Funcionalidades de Sidekiq
Veamos ahora algunas de las funcionalidades de Sidekiq, muchas de las cuales están documentadas en el wiki de Sidekiq. Una de las cosas que Sidekiq facilita es hacer que una tarea se ejecute en un momento futuro. En lugar de invocar perform_async
en la tarea podemos invocar perform_in
con el número de segundos que queremos que pasen hasta que se procese el trabajo.
PygmentsWorker.perform_in(1.hour, @snippet.id)
Esto no tiene mucho sentido en nuestra aplicación pero puede ser muy útil para expirar cachés, por ejemplo. Otra funcionalidad atractiva es la posibilidad de priorizar las colas. Supongamos que nuestra aplicación tiene varias tareas y queremos que algunas se procesen antes. Para hacerlo tendremos que asignar un proceso de ejecución a cada cola, lo que podemos hacer con la opción queue
:
class PygmentsWorker include Sidekiq::Worker sidekiq_options queue: "high" # Rest of class omitted. end
Si no se especifica el nombre de una cola la tarea terminará por defecto en una cola llamada default
. Si lanzamos la orden sidekiq
podemos especificar las colas que queremos procesar, así como su peso relativo, con la opción -q
.
$ bundle exec sidekiq -q height,5 default,1
Con esta orden la cola high
tendrá más prioridad.
Existe una receta de Capistrano que podemos usar para el despliegue. Si hay opciones concretas que queramos pasar a la orden sidekiq
(como la opción -q
que aabamos de ver) podemos ponerlas en un fichero sidekiq.yml
en el fichero config
. El fichero tiene este aspecto:
# Sample configuration file for Sidekiq. # Options here can still be overridden by cmd line args. # sidekiq -C config.yml --- :verbose: false :concurrency: 25 :queues: - [often, 7] - [default, 5] - [seldom, 3]
Cómo monitorizar Sidekiq
Sidekiq incluye una interfaz web, para monitorizar el funcionamiento de las tareas al estilo de Resque, que está basada en Sinatra y que podemos montar en el fichero de rutas de nuestra aplicación Rails.
require 'sidekiq/web' Example::Application.routes.draw do resources :snippets root to: "snippets#new" mount Sidekiq::Web, at: "/sidekiq" end
Nótese que tenemos que requerir sidekiq/web
porque por se incluye por defecto. Si lo hacemos tenemos que incluir también otras gemas en el Gemfile
, tras lo cual tenemos que ejecutar bundle
y reiniciar el servidor para que coja los cambios.
gem 'sinatra', require: false gem 'slim'
Si ahora visitamos la ruta /sidekiq
veremos la interfaz web que dice cuántos procesos hay en ejecución, el número de fallos, nuestras colas y qué procesos están activos.
Esta ruta debe estar protegida por una clave cuando la aplicación esté en producción, en el wiki de Sidekiq se explica cómo hacerlo.
El código fuente de Sidekiq
Vamos a terminar el episodio echándole un vistazo al código fuente de Sidekiq, porque siempre se aprende algo al hacerlo. Empezaremos viendo el módulo Sidekiq::Worker
que hemos incluido en la clase PygmentsWorker
. El módulo es bastante sencillo y tiene varios métodos de clase, incluyendo los métodos perform_async
y perform_in
que ya hemos utilizado. Estos métodos insertan un hash con los detalles en Redis.
def perform_async(*args) client_push('class' => self, 'args' => args) end def perform_in(interval, *args) int = interval.to_f ts = (int < 1_000_000_000 ? Time.now.to_f + int : int) client_push('class' => self, 'args' => args, 'at' => ts) end alias_method :perform_at, :perform_in def get_sidekiq_options # :nodoc: self.sidekiq_options_hash ||= DEFAULT_OPTIONS end
El módulo también incluye el método sidekiq_options
que usábamos antes, y hay documentación acerca de las opciones que recibe.
## # Allows customization for this type of Worker. # Legal options: # # :queue - use a named queue for this Worker, default 'default' # :retry - enable the RetryJobs middleware for this Worker, default *true* # :timeout - timeout the perform method after N seconds, default *nil* # :backtrace - whether to save any error backtrace in the retry payload to display in web UI, # can be true, false or an integer number of lines to save, default *false* def sidekiq_options(opts={}) self.sidekiq_options_hash = get_sidekiq_options.merge(stringify_keys(opts || {})) end DEFAULT_OPTIONS = { 'retry' => true, 'queue' => 'default' } def get_sidekiq_options # :nodoc: self.sidekiq_options_hash ||= DEFAULT_OPTIONS end
Otra parte interesante de Sidekiq es su middleware. No debe confundirse con los middlewares de Rack; en este caso se trata de definir comportamientos que ocurren alrededor del procesamiento de una tarea. Sidekiq dispone de middlewares en el cliente que se ejecutan antes de insertar la tarea en Redis y otros en el lado del servidor que se ejecutan antes de procesar dicha tarea. Estos middlewares son los que se encargan de reintentar tareas, trazar su funcionamiento y gestionar las excepcones. El último de ellos puede conectar con varios gestores de excepciones como Airbrake, Exception o ExceptionNotifier. En el wiki se puede encontrar información acerca de cómo configurar estos servicios, que son un buen ejemplo de lo sencillo que es escribir middlewares en Sidekiq, por lo que podemos escribir los nuestros para extender el comportamiento de Sidekiq.
También merece la pena mirar la clase processor
, que gestiona el procesamiento de las tareas desde que se extraen de Redis. La clase lista los middlewares incluidos por defecto. Cuando se procesa una tarea se invoca cada uno de estos middlewares, esta clase también incluye Celluloid, que es clave para el funcionamiento multihilo de Sidekiq. Celluloid es un proyecto muy importante que puede ser de ayuda para gestionar la concurrencia con Ruby. No lo veremos aquí en detalle pero merece la pena echarle un vistazo.