#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)
Dans cette épisode, nous allons faire une pause dans notre série sur les nouvelles fonctionnalités de Rails 3.1 et jeter un œil sur Resque, un bon outil de gestion des tâches en arrière-plan. Nous avons vu, dans d'autres épisodes, différents moyens de faire cela. Chacun répond à un besoin différent et Resque ne fait pas exception. À la fin de cet épisode, nous vous donnerons quelques astuces pour choisir l'outil correspondant à votre besoin mais, pour le moment, plongeons nous dans Resque et ajoutons le à une application Rails.
L'application que nous allons utiliser est un simple site de partage de bouts de code (snippets), comme Pastie. Avec ce site, nous pouvons saisir des extraits de code et leur donner un nom et un langage.
Lorsque nous soumettons un snippet, il est affiché avec la détection syntaxique appropriée.
La détection syntaxique est gérée par un web service externe et c'est cette partie du code que nous voulons déléguer à une tâche en arrière-plan. Elle est, pour le moment, directement implémentée dans l'action 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
La détection syntaxique est effectuée à la sauvegarde du snippet. Le service http://pygments.appspot.com/ est utilisé. Il a été mis en place par Trevor Turk pour fournir une détection syntaxique sans dépendances locales. Le code effectue une requête POST vers le service en lui envoyant le code et le langage puis, remplit l'attribut highlighted_code
du snippet avec la réponse à cette requête.
Communiquer avec un service externe au travers d'une requête Rails est, en général, une mauvais idée. En effet, le service peut être long à répondre et donc ralentir toute votre application. Il est donc préférable de placer ces requêtes dans un processus externe. Nous allons mettre Resque en place afin de faire exactement cela.
Mettre Resque en route
Resque dépend de Redis, un système de stockage clé-valeur persistant. Redis est génial et mériterait un épisode à lui tout seul mais ici nous allons simplement l'utiliser avec Resque.
Comme nous sommes sur OS X, le plus simple pour installer Redis est d'utiliser Homebrew :
$ brew install redis
Une fois installé, nous pouvons lancer le serveur grâce à
$ redis-server /usr/local/etc/redis.conf
Maintenant que Redis est lancé, nous pouvons ajouter Resque au Gemfile
de notre application et l'installer via bundle.
source 'http://rubygems.org' gem 'rails', '3.0.9' gem 'sqlite3' gem 'nifty-generators' gem 'resque'
Nous devons ensuite ajouter la tâche Rake Resque. Nous allons le faire en créant un fichier resque.task
dans le dossier /lib/tasks
de notre application. Dans ce fichier nous devons faire un require 'resque/tasks'
pour charger les tâches de la gem. Nous allons également charger l'environnement Rails lors du démarrage des workers.
require "resque/tasks" task "resque:setup" => :environment
Cela nous donne accès aux modèles de notre application depuis les workers. Cependant, si nous voulons garder nos workers légers, il peut être intéressant d'implémenter une solution personnalisée de façon à ne pas charger tout l'environnement Rails.
Nous avons maintenant une tâche Rake que nous pouvons utiliser pour lancer les workers Resque. Pour ce faire, nous devons passer un argument QUEUE. Nous pouvons passer soit le nom d'une queue que nous souhaitons utiliser, soit '*' pour travailler avec n'importe quelle queue.
$ rake resque:work QUEUE='*'
Ce script ne fait aucun affichage mais il fonctionne.
Déplacer l'appel au Web Service
Maintenant que nous avons mis Resque en place, nous pouvons nous concentrer sur le déplacement du code qui appel le web service dans un processus en arrière-plan et le gérer au travers d'un worker. Nous devons ajouter une tâche dans la queue de façon à ce que le travail soit géré par un worker Resque. Voici, de nouveau, l'action 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
Nous ajoutons une tâche dans la queue en appelant Resque.enqueue
. Cette méthode accepte un certain nombre d'arguments. Le premier est la classe du worker à utiliser. Nous n'avons pas encore créé de workers mais nous allons en ajouter un rapidement et l'appeler SnippetHighlighter
. Nous devons également passer tous les paramètres supplémentaires que nous souhaitons donner au worker. Dans notre cas, nous pourrions passer le snippet mais tout ce que nous passons à enqueue
est converti en JSON pour être stocké dans Redis. Cela signifie que nous ne devrions pas passer d'objet complexe comme un modèle ActiveRecord. Nous allons donc récupérer le snippet depuis le worker, à partir de son id.
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
Nous allons ensuite créer le worker et y placer le code pris du contrôleur. Nous allons placer le worker dans un nouveau dossier workers dans /app
. Nos pourrions le placer dans /lib
mais en choisissant /app
le fichier est automatiquement chargé et déjà présent dans le chemin de chargement (loadpath) de Rails.
Un worker est juste une classe avec deux fonctionnalités. Tout d'abord, elle nécessite une variable d'instance nommée @queue
qui contient le nom de la queue. Cela limite la liste des queues gérées par le worker. Ensuite, elle a besoin d'une méthode de classe, appelée perform
, qui prend en paramètre les arguments passés à enqueue
, dans notre cas, l'id
du snippet. Dans cette méthode, nous pouvons mettre le code extrait de l'action create
qui appel le serveur distant et retourne le code interprété en remplaçant les appels à @snippet_id
par la variable locale du même nom.
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
Essayons et voyons si cela fonctionne. Nous allons créer un nouveau snippet et le soumettre. Lorsque nous le faisons, nous voyons le snippet sans détection syntaxique.
Nous n'allons pas voir la détection tout de suite puisqu'elle est maintenant effectuée en arrière-plan. Si nous attendons quelques secondes et rechargeons la page, elle n'est toujours pas effectuée. Tentons donc de comprendre ce qui ne fonctionne pas. Resque fournit une interface web, écrite avec Sinatra. Cela permet de surveiller et de gérer facilement ses tâches. Nous pouvons le lancer en appelant
$ resque-web
Cela fait, l'interface d'administration s'ouvre et nous pouvons voir que nous avons une tâche en échec.
Si nous cliquons sur la tâche échouée, nous pouvons voir le détail de l'erreur : uninitialized constant SnippetHighlighter
.
La classe SnippetHighlighter
n'est pas trouvée. Cela est dû au fait que nous avons lancé la tâche Rake avant de l'écrire. Relançons la tâche Rake et voyons si cela corrige le problème.
Une fois la tâche Rake relancée, nous pouvons cliquer sur le lien “Retry” pour relancer la tâche Resque. Lorsque nous le faisons et retournons sur la page “Overview”, il n'y a qu'une tâche en échec listée. Cela signifie que, cette fois, la tâche a fonctionné. Nous pouvons le confirmer en rechargeant la page du snippet. La syntaxe est détectée, notre tâche a donc bien marché.
Si nous saisissons un nouveau snippet, sa syntaxe sera détectée. Il se peut toutefois qu'un délai de quelques secondes soit nécessaire avant que le résultat n'apparaisse.
Intégration de l'interface de Resque
Resque est maintenant configuré et gère les tâches que nous lui donnons. Il serait pratique cependant d'intégrer l'interface web de Resque dans notre application de façon à pouvoir y accéder sans avoir besoin de la démarrer.
Rails 3 interagit très bien avec les applications Rack et Sinatra est justement une application Rack. Nous pouvons donc facilement le faire en montant l'application dans le fichier de routage de Rails.
Coderbits::Application.routes.draw do resources :snippets root :to => "snippets#new" mount Resque::Server, :at => "/resque" end
L'interface web de Resque sera maintenant montée sur http://localhost:3000/resque
. Nous devons nous assurer que le serveur Resque est lancé pour que cela fonctionne. Nous ajoutons donc une option require
sur la gem resque
dans notre Gemfile
.
source 'http://rubygems.org' gem 'rails', '3.0.9' gem 'sqlite3' gem 'nifty-generators' gem 'resque', :require => 'resque/server'
Si nous relançons le serveur de notre application et visitons http://localhost:3000/resque
, nous allons voir l'interface web de Resque.
Nous ne voulons pas que cette page soit publique. Comment pouvons nous ajouter un peu de limitation des accès pour la garder privée ? Si nous utilisons, par exemple, Devise dans notre application, il est facile de sécuriser cette page en plaçant notre route dans un appel à authenticate
.
Coderbits::Application.routes.draw do resources :snippets root :to => "snippets#new" authenticate :admin do mount Resque::Server, :at => "/resque" end end
Nous n'utilisons ni Devise ni aucun autre système d'authentification dans notre application. Nous allons donc utiliser HTTP Basic Authentication. Pour ce faire, nous allons créer un initializer dans le dossier config/initializers
et le nommer resque_auth.rb
.
Resque::Server.use(Rack::Auth::Basic) do |user, password| password == "secret" end
Dans ce fichier, nous appelons la méthode use
sur Resque::Server
, qui est une application Rack, et ajoutons l'authentification Basic. Dans le bloc, nous vérifions que le mot de passe correspond. Évidemment, nous pouvons personnaliser ce code pour configurer le nom d'utilisateur et le mot de passe ou ajouter la logique voulue. Lorsque nous redémarrons le serveur et rechargeons la page, nous voyons l'invite de connexion et nous devons saisir le mot de passe que nous avons spécifié dans l'initializer pour y accéder.
Resque et ses alternatives
Ce sera tout pour Resque. Comment choisir entre ce dernier et les autres systèmes de gestion des tâches en arrière-plan disponibles ? L'un des avantages de Resque est l'interface d'administration qui nous permet de surveiller les queues, relancer les tâches échouées, etc. Un autre raison de considérer Resque est que Github s'en sert. Github répond à une lourde charge, Resque devrait donc gérer tout ce que nous lui passons.
Si la dépendance à Redis pose problème, Delayed Job est une très bonne alternative. Nous l'avons vu dans l'épisode 171 [regarder, lire] et c'est une solution plus simple puisqu'elle utilise la base de données de votre application pour gérer les queues de tâches. Elle charge cependant l'environnement de l'application Rails pour tous les workers. Si nous voulons garder nos workers légers, ce n'est peut être pas la meilleure solution.
Resque et Delayed Job utilisent tous deux un système de sélection sur les queues. Il est donc possible qu'un délai existe entre l'ajout d'une tâche et le moment où elle est traitée. Si la queue est souvent vide et que nous voulons que nos tâches soient traitées tout de suite, Beanstalkd, vu dans l'épisode 243 [regarder, lire], est une meilleure option. Beanstalkd gère les tâches au travers d'un événement push. Il peut donc commencer de traiter une tâche immédiatement, lors de son ajout dans la queue.