#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)
Dans l'épisode 342, nous avons vu comment initialiser une application Rails qui utilise PostgreSQL comme base de données. Une bonne chose avec Postgres est qu'il peut être responsable de choses qui requièrent des tâches de fond comme la gestion de queue. Ci-dessous, un écran d'une application Rails qui utilise Postgres et qui envoie des newsletter.
Nous pouvons envoyer une newsletter en cliquant sur son lien "Deliver Newsletter". Ceci prend un certain temps à se faire et pendant ce temps, l'instance Rails sera incapable de traiter une autre requête et le navigateur paraîtra "figé" pendant qu'il attend une réponse. À cause de ceci, c'est une bonne idée de déplacer les tâches longues à exécuter dans un processus de fond quand c'est possible. Quand une newsletter est finalement envoyée, nous verrons un message en haut de la page et son statut sera affiché dans la liste.
Introduction aux queues classiques
Nous résoudrons ce problème en utilisant la gemme queue_classic. Cette gemme facilite la prise d'une tâche longue à exécuter et la déplacer en tâche de fond gérée par Postgres. Nous utiliserons la version 2 de cette gemme qui est actuellement à l'état de Release Candidate. Le README a certaines instructions pour initialiser queue_classic dans
une application Rails et nous allons les suivre. Comme toujours la première étape est d'ajouter la gemme au Gemfile et exécuter la commande bundle
. Comme nous installons une version pre-release, nous devons spécifier un numéro de version.
gem 'queue_classic', '2.0.0rc14'
Ensuite, nous chargerons les tâches Rake de queue_clqssic en créant un fichier queue_classic.rake
dans le répertoire /lib/tasks
et en y insérant les 2 lignes du README.
require "queue_classic" require "queue_classic/tasks"
Nous aurons également besoin de créer un initializer queue_classic
. Dans celui-ci, nous spécifions la configuration, ce qui se fait via les variables d'environnement. Cela le rend complètement compatible avec Heroku. Nous avons juste besoin d'ajouter une variable DATABASE_URL
qui contient les informations de connexion pour la base de données. Comme la base de données est sur notre système local, nous n'avons pas besoin de spécifier de nom d'utilisateur
et de mot de passe.
ENV["DATABASE_URL"] = "postgres://localhost/mailer_development"
Ensuite, nous avons besoin d'une migration add_queue_classic
pour initialiser la base de données.
$ rails g migration add_queue_classic invoke active_record create db/migrate/20120502000000_add_queue_classic.rb
Dans cette migration, nous ajouterons du code pour charger les fonctions queue_classic
.
class SetupQueueClassic < ActiveRecord::Migration def up QC::Setup.create end def down QC::Setup.drop end end
Nous aurons besoin d'exécuter la commande rake db:migrate
pour lancer la migration. Ensuite, nous démarrerons le processus de fond du worker en lançant cette commande :
$ rake qc:work
Si tout se passe bien, vous ne verrez aucune sortie de cette commande. Nous pouvons expérimenter queue_classic dans la console Rails dans une nouvelle fenêtre. Pour ajouter un job à la queue, nous appelons QC.enqueue
et lui passons la méthode que l'on veut déclencher et les arguments que nous voulons transmettre à cette méthode.
1.9.3p125 :001 > QC.enqueue "puts", "hello world" lib=queue_classic action=insert_job elapsed=13 => nil
Dans l'onglet exécutant rake qc:working
, nous devrions maintenant voir "hello world". Quand on appelle enqueue
, les arguments sont sérialisés en JSON et transmis à la table de queue à traiter par le worker. La sérialisation JSON est un peu pointilleuse donc il est important de garder les arguments qu'on transmet aussi simple que possible. Ça n'apprécie même pas les symboles donc si on transmet un hash comme ci-dessous, une exception sera jetée.
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 on vue transmettre un hash, nous aurons besoin d'utiliser des strins, comme là :
1.9.3p125 :003 > QC.enqueue "puts", "msg" => "hello world"
Ceci s'exécutera correctement et affichera le hash.
Envoyer des newsletter avec une queue classique
Nous allons modifier notre application afin que quand on clique sur un lien "deliver newsletter", le long processus d'envoi d'une newsletter se passe en arrière-plan. Quand un lien est cliqué, il déclenche une action deliver
dans NewsletterController
. Cette action récupère une newsletter, patiente pendant 10 secondes pour simuler un processus long puis met à jour l'heure d'envoi de la newsletter.
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
Quand nous avons un processus long dans un contrôleur, il est bon de le déplacer dans une méthode de classe d'un modèle pour simplifier l'interface autant que possible. Nous allons créer une méthode deliver
dans le modèle Newsletter
et l'appeler depuis le contrôleur.
def deliver Newsletter.deliver(params[:id]) redirect_to newsletters_url, notice: "Delivered newsletter." end
Ensuite, nous allons écrire cette méthode dans le modèle
class Newsletter < ActiveRecord::Base attr_accessible :delivered_at, :subject def self.deliver(id) newsletter = find(id) sleep 10 # simulate long newsletter delivery newsletter.update_attribute(:delivered_at, Time.zone.now) end end
Il est maintenant aisé de déplacer la livraison dans un processus de fond et modifier la notification flash pour dire "Livraison de la newsletter en cours." au lieu de "Newsletter envoyée."
def deliver QC.enqueue "Newsletter.deliver", params[:id] redirect_to newsletters_url, notice: "Delivering newsletter." end
Nous pouvons déjà essayer. Si on clique sur un lien "Deliver newsletter", nous verrons la réponse directement mais la newsletter ne sera pas envoyer pour le moment puisque c'est traité en tâche de fond donc elle ne sera pas montrée comme livrée dans la liste.
Si on attend 10 secondes et qu'on recharge la page, nous verrons que la newsletter a été envoyée.
Traiter les jobs érronés
Que se passe-t-il quand un job plante et qu'une exception est soulevée pendant le traitement ? Nous allons jeter une exception pour voir quel est le résultat.
def self.deliver(id) newsletter = find(id) raise "Oops" sleep 10 # simulate long newsletter delivery newsletter.update_attribute(:delivered_at, Time.zone.now) end
Maintenant, quand on clique sur "Deliver Newsletter", nous verrons un message disant que la newsletter est en train d'être livrée mais l'exception sera touchée quand le job est traité en tâche de fond. Si on regarde la sortir du processus du worker, nous voyons que l'erreur y est mentionnée. (Nous devrions logger la sortie de ce worker dans une application de production.) Par défaut, queue_classic n'essaie pas de faire quoique ce soit tel que de réessayer automatiquement le job. Si on
regarde le code source de la classe Worker
, nous trouverons une méthode handle_failure
qui affiche l'erreur.
#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
Si on veut traiter les jobs échoués d'une manière différente, nous avons besoin de surcharger cette méthode. Un exemple de comment nous pouvons faire ceci en héritant de la classe Worker
est montrée dans le README. Il y a beaucoup d'informations utiles dans ce README tel qu'un exemple de comment définir un exécutable worker
au lieu d'utiliser une tâche Rake. L'avantage de ceci est que nous pouvons contrôler exactement ce qui sera chargé d'où si l'on n'a pas
besoin que toute notre application soit chargée pour ce worker, nous pouvons la sauter et épargner de la mémoire.
Une autre fonctionnalité intéressante est l'habilité d'écouter des notifications des jobs. De cette façon, is un job est ajouté à la queue, notre worker peut être notifié instantanément au lieu d'attendre un billet de la base de donnée. Ceci devrait jouer un rôle dans le choix de queue_classic plutôt qu'une autre solution de queue.
La question finale est : comment queue_classic se compare aux autres solutions de queueing ? C'est proche de Delayed Job, qui est aussi soutenu par une base de données, mais queue_classic prend un meilleur avantage des fonctionnalités de Postgres et ne requiert par ActiveRecord donc avec ça, nous pouvons minimiser le processus du worker. Comme pour les autres options, comme Resque, RabbitMQ et Beanstealk, cela requiert un démon séparé pour gérer la queue. Si vous voulez conserver une initialisation serveur simple, queue_classic est une excellente option, bien qu'une chose soit manquante, à savoir une interface web comme celle que Resque fourni.