homeASCIIcasts

271: Resque 

(view original Railscast)

Other translations: En Es Ja

Written by Simon Courtois

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.

Le site de partage de snippets.

Lorsque nous soumettons un snippet, il est affiché avec la détection syntaxique appropriée.

Un nouveau snippet montrant la detection syntaxique.

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.

/app/controller/snippets_controller.rb

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.

/Gemfile

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.

/lib/tasks/resque.rake

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.

/app/controller/snippets_controller.rb

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.

/app/controllers/snippets_controller.rb

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.

/app/workers/snippet_highlighter.rb

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.

Le snippet est cree sans detection 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.

Liste des taches Resque en echec.

Si nous cliquons sur la tâche échouée, nous pouvons voir le détail de l'erreur : uninitialized constant SnippetHighlighter.

Details de la tache en echec.

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é.

La syntaxe du snippet est maintenant detectee.

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.

/config/routes.rb

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.

/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.

La page integree de Resque est visible par tout le monde.

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.

/config/routes.rb

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.

/config/initializers/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.

La page demande maintenant une authentification.

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.