JeuWeb - Crée ton jeu par navigateur
[Ruby on Rails] Créer des tâches automatisées avec Rake - Version imprimable

+- JeuWeb - Crée ton jeu par navigateur (https://jeuweb.org)
+-- Forum : Discussions, Aide, Ressources... (https://jeuweb.org/forumdisplay.php?fid=38)
+--- Forum : Programmation, infrastructure (https://jeuweb.org/forumdisplay.php?fid=51)
+--- Sujet : [Ruby on Rails] Créer des tâches automatisées avec Rake (/showthread.php?tid=4420)



[Ruby on Rails] Créer des tâches automatisées avec Rake - Sephi-Chan - 21-10-2009

Rake est un outil plutôt génial écrit en Ruby qui permet d'écrire des tâches qui pourront être utilisées plus tard, dans un CRON par exemple. Il est un peu à Ruby ce que Make est au C.

Rake est l'une des raisons (et une parmi tant d'autres) qui fait que développer avec Ruby on Rails sans avoir un accès à un terminal (typiquement le cas d'un serveur mutualisé), c'est moins drôle.

Rails utilise déjà abondamment Rake, pour connaître les tâches disponible, il faut lancer la commande rake -T. Comme il y en a pas mal, je vous colle le rendu de cette commande : tâche disponible par défaut dans une application Ruby on Rails.

Celles que j'utilise le plus souvent :
  • rake db:migrate (et rake db:rollback), dès que je souhaite modifier la structure de ma base de données ;
  • rake routes, que l'on a vu rapidement sur le tutorial précédent ;
  • rake gems:install, qui installe les gem dont l'application a besoin. C'est très utile quand on récupère un projet : je récupère les sources d'un projet puis je lance ça pour mettre à jour ou installer les gems (dans leur bonne version) nécessaires ;

Vous l'aurez compris, dans nos jeux par navigateur, ce genre de tâches sont très pratique : les maintenances nocturnes, les opérations asynchrones (par exemple recharger les points d'actions des personnages toutes les 20 minutes, etc.).

Tout d'abord, comment créer une tâche Rake dans une application Ruby on Rails ? C'est simple, c'est juste un fichier qui contient du Ruby. Les tâches sont placées dans le répertoire lib/tasks/. On leur donne l'extension .rake.


task :hello_world do
puts "Hello World!"
end

task :introduce => :hello_world do
puts "My name is Bond. James Bond."
end

Voilà deux tâches Rake.

Pour lancer une tâche, il suffit de lancer la commande rake nom_de_la_tâche. Par exemple rake nom_de_la_tâche.

Comme on peut le voir, la seconde tâche diffère un peu dans sa déclaration. Elle implique en effet une dépendance : quand on lance la tâche "introduce", la tâche "hello_world" est d'abord exécutée. Notez qu'on peut définir plusieurs dépendances :


task :open_the_door do
puts "I open the door of the car."
end

task Confusedtart_engine do
puts "I start the engine."
end

task :go_on_baby => [ :open_the_door, Confusedtart_engine ] do
puts "Vroooom!"
end


Maintenant qu'on sait définir une tâche, voyons un exemple concret.

Dans mon travail, je vais être amené à écrire une tâche qui permettra d'automatiser la recherche de bande-annonces de films sur YouTube.

Pour cela, la tâche va récupérer les titres des films qui passent dans la journée, envoyer des requêtes à l'API YouTube, lire les flux XML renvoyés et stocker les URLs en base. Le matin, quand l'un des administrateur du site arrivera, il va voir une page avec les vidéos que le système aura trouvé, les visionnera et les approuvera ou non. Et selon son approbation, la vidéo sera ajoutée au programme télé.

Voilà ce à quoi pourrait ressembler la tâche (qui sera stockée dans un fichier nommé trailers.rake) :


require 'open-uri' # Lib to simplify HTTP request.
require 'hpricot' # Amazing HTML (and XML) parser.

# We put the task in a namespace, to be clean.
# To call a namespaced task, simply use the following notation :
# rake namespace_name:task_name
namespace :trailers do

# Perl syntax to declare array of strings.
CHANELS_TO_SEARCH = %w( tf-1 france-2 france-3 m6 canal canal-cinema w9 nt-1 tmc )

# In this case, it's sexier than pure litteral notation...
CHANELS_TO_SEARCH = [ 'tf-1', 'france-2', 'france-3', 'm6', 'canal', 'canal-cinema', 'w9', 'nt-1', 'tmc' ]

# The desc is only a human readable comment displayed in the tasks list.
desc "Search for trailers for films of the day"
task Confusedearch => :environment do
# Confusedearch is the name of the task.
# We will call it with : rake trailersConfusedearch
# The "=> :environment" mean that when the task is fired,
# the task :environment (default defined in Rails) is fired before
# to initialize ActiveRecord and other Rails cool stuff.

# We want the trailers of today movies.
date_from = Date.today
date_to = date_from + 24.hours

# Find the channel for which we want trailers.
channels = Channel.find_all_by_pretty_name(CHANELS_TO_SEARCH)

# We want only trailers for movies subsets (thriller, horror, porn ?).
# Don't care about "Plus belle la vie"...
tv_program_kinds = TvProgramKind.find_by_name('Cinéma').children

# We have few ways to build the query. I decided to use dynamic scopes.
# The dynamics scopes are just a cool way to build the WHERE condition.
tv_programs = TvProgram.scoped_by_air_time(date_from..date_to).
scoped_by_tv_program_kind_id(tv_program_kinds.map(&:id)).
scoped_by_channel_id(channels.map(&:id)).
all

# Iterate through the TV programs found.
tv_programs.each do |tv_program|

# Database is encoded in latin1 and we need UTF-8 to send to YouTube API.
# Iconv is designed to receive many string after the two first arguments
# and return an array. The first string we passed (tv_program.title) is
# the first of the result. So we call "first" on this array.
utf8_title = Iconv.iconv('UTF-8', 'LATIN1', tv_program.title).first

# We add quotes to the "q" param to search the exact terms.
url = build_url_for_youtube_api({ 'q' => '"' + utf8_title + '"' })

# We send the HTTP request passing a URL to open, thanks to open-uri.
# The we parse the XML stream returned by YouTube with the amazing Hpricot.
xml_stream = open(url) do |stream|
Hpricot.XML(stream)
end

# Explore the XML feed as documented in :
# code.google.com/intl/fr-FR/apis/youtube/2.0/developers_guide_protocol_understanding_video_feeds.html
xml_stream.search('/feed/entry').each do |entry|

# TODO: Try to do it with pure XPath instead of Ruby condition.
# It should be something like :
# //media:content[@yt:format='5']
entry.search("//media:content").each do |e|
if e.attributes['yt:format'] == '5'
# It's the link we want to embed the video player.
# No we can save it into the database.
trailer_url = e.attributes['url']
Trailer.create! :title => utf8_title,
:url => trailer_url
end
end
end

end

end


# Return the URL to query the YouTube API.
def build_url_for_youtube_api(options = {})
# Default options.
# format 5 say that only embeddable videos should be selected.
options.reverse_merge! 'lr' => 'fr',
'max-results' => 1,
'strict' => true,
'format' => 5

service_url = 'http://gdata.youtube.com/feeds/api/videos'
query_array = [ ] # [ "foo=bar", "foo2=bar2" ]
options.each do |key, value|
query_array << "#{key}=#{ERB::Util::url_encode(value)}"
end
query_string = query_array.join("&")

"#{service_url}?#{query_string}"
end


end

La tâche en elle-même, c'est juste du Ruby (avec tous les apports sympas de Ruby on Rails, comme ActiveRecord, ActiveSupport, etc.), on peut donc inclure des librairies (open-uri et hrpicot dans notre cas) comme dans n'importe quel script.

Notons que maintenant, quand on demande à rake de nous lister ses tâches, il a ajouté la nôtre !


rake trailersConfusedearch # Search for trailers for films of the day


Voilà donc, plus qu'à appeler cette tâche ! Vous savez comment faire ! Smile

Bien sûr, tout l'intérêt des tâches est de pouvoir être lancé automatiquement.

Ici, nous allons utiliser un CRON, nous allons donc ajouter une ligne à l'un des fichiers contenant des crons (car il y en a moult sur un système Unix !), en l'occurrence /etc/crontab.



0 2 * * * cd /path/to/application && /path/to/ruby rake trailersConfusedearch RAILS_ENV=production

Grâce à cette ligne, le script se lancera quand il sera 2h00, tous les jours du mois, tous les mois et tous les jours de la semaine. Autrement dit, tous les jours à 2h du matin.

On lancera la commande en lui transmettant la variable RAILS_ENV, qui représente l'environnement à utiliser : en l'occurrence l'environnement de production (il y a aussi development et test).

Une telle variable sera ajoutée temporairement aux variables d'environnement du système et sera disponible dans le tableau associatif ENV[:RAILS_ENV]. Pour y accéder, on pourra faire ENV['RAIL_ENV'].

Il existe un autre moyen de passer des arguments, mais je rencontre quelques problèmes quand je l'utilise avec mon shell Zsh (avec Bash, ça fonctionne). Je l'expliquerai quand j'en saurais plus. Notez toutefois que Rake est surtout intéressant pour les tâches automatisées, et donc il est rarement nécessaire de passer des arguments (puisque si ces arguments sont automatisables, ils peuvent être générée par la tâche)



Voilà, j'espère que ça vous aura plu et si vous avez des questions, des critiques, des commentaires, etc. n'hésitez pas.

Ça me prend du temps d'écrire tout ça, ça me fait plaisir d'avoir des feedbacks. :p



Sephi-Chan


RE: [Ruby on Rails] Créer des tâches automatisées avec Rake - Sephi-Chan - 23-10-2009

J'ai rajouté quelques notes à propos de passage d'arguments.

En écrivant cette tâche pour le boulot, je me suis rendu compte que la version de Rails que j'utilisais n'incluait pas encore les dynamic scopes.

J'ai donc eu plusieurs options :

Les finders dynamiques :


tv_programs = TvProgram.find_all_by_air_time_and_tv_program_kind_id_and_channel_id(from..to, tv_program_kinds, channel_ids)


La requête à base d'une chaîne de caractère avec des placeholders anonymes :


condition = "air_time IN(?) AND tv_program_kind_id IN (?) AND channel_id IN (?)"
tv_programs = TvProgram.all :conditions => [
condition,
from..to,
tv_program_kind_ids,
channel_ids
]


La requête à base d'une chaîne de caractère avec des placeholders nommés :


condition = "air_time IN(:time) AND tv_program_kind_id IN (:tv_program_kind_ids) AND channel_id IN (:channel_ids)"
tv_programs = TvProgram.all :conditions => [
condition,
{
:time => from..to,
:tv_program_kind_ids => tv_program_kind_ids,
:channel_ids => channel_ids
}
]



La requête avec une condition sous forme de hash :


tv_programs = TvProgram.find :all,
:conditions => {
:air_time => date_from..date_to,
:tv_program_kind_id => tv_program_kind_ids,
:channel_id => channel_ids
}