Rails Runner : Libérer la Puissance Cachée de Vos Projets
Vous maîtrisez les bases de Rails, et vous commencez à travailler sur vos premiers projets professionnels. Mais voilà, vous vous retrouvez face à des tâches répétitives : nettoyer la base de données, envoyer des emails en masse, mettre à jour des statistiques... Et vous vous demandez s'il existe un moyen élégant d'exécuter du code Rails sans passer par le serveur web ou la console interactive.
Bonne nouvelle : Rails Runner est exactement l'outil qu'il vous faut. Souvent méconnu ou sous-utilisé, il représente pourtant une arme redoutable dans l'arsenal de tout développeur Rails. Dans ce guide complet, nous allons explorer ensemble comment cet outil peut transformer votre workflow et vous faire gagner un temps précieux.
Introduction au Rails Runner
Avant de plonger dans les aspects techniques, prenons un moment pour comprendre ce qu'est réellement Rails Runner et pourquoi il mérite votre attention. Contrairement à ce que son nom pourrait suggérer, il ne s'agit pas d'un gem externe ou d'un plugin complexe, mais bien d'une fonctionnalité native de Rails, disponible depuis les premières versions du framework.
Vue d'ensemble du Rails Runner
Rails Runner, accessible via la commande rails runner (ou rails r en version courte), est un exécuteur de code Ruby dans le contexte complet de votre application Rails. Concrètement, cela signifie qu'il charge l'intégralité de votre environnement Rails — vos modèles, vos configurations, vos gems — et vous permet d'exécuter n'importe quel code Ruby avec accès à tous ces éléments.
Imaginez-le comme un pont entre la ligne de commande et votre application. Là où rails console ouvre une session interactive (REPL), Rails Runner exécute un script et se termine immédiatement. C'est cette caractéristique qui le rend particulièrement adapté aux tâches automatisées et aux scripts de maintenance.
Historiquement, Rails Runner est apparu dès Rails 1.0 sous le nom de script/runner. Avec l'évolution du framework vers une architecture plus moderne et l'introduction de la commande unifiée rails, il est devenu rails runner à partir de Rails 3. Mais son essence est restée la même : fournir un moyen simple et direct d'exécuter du code Rails en dehors du cycle requête-réponse HTTP.
Fonctionnalités Clés du Rails Runner
Ce qui rend Rails Runner vraiment unique, c'est sa flexibilité d'utilisation. Vous pouvez l'employer de trois manières différentes :
- Exécution de code inline : Directement dans la ligne de commande
- Exécution de fichiers : En pointant vers un script Ruby
- Lecture depuis l'entrée standard : Pour des pipelines Unix avancés
Cette polyvalence s'accompagne de plusieurs avantages techniques :
- Accès complet à l'environnement Rails : Tous vos modèles, helpers, et configurations sont disponibles
- Gestion des environnements : Possibilité d'exécuter le code en development, production, test, ou tout autre environnement personnalisé
- Intégration avec les outils système : Compatible avec cron, systemd, et autres schedulers
- Pas de surcharge HTTP : Contrairement à une requête web, aucun overhead lié au serveur ou au routing
Prenons un exemple concret. Vous souhaitez vérifier combien d'utilisateurs se sont inscrits aujourd'hui :
rails runner "puts User.where('created_at >= ?', Date.today).count"
Simple, direct, efficace. Pas besoin d'ouvrir une console, de taper le code, puis de quitter. Parfait pour des vérifications rapides ou des scripts automatisés.
Principaux Avantages du Rails Runner
Maintenant que vous comprenez ce qu'est Rails Runner, explorons pourquoi vous devriez l'intégrer dans votre workflow quotidien.
Simplicité d'exécution : Contrairement à l'écriture de tâches Rake complètes, Rails Runner permet d'exécuter rapidement du code sans créer de fichier de tâche. Pour des opérations ponctuelles ou des scripts simples, c'est un gain de temps considérable.
Automatisation facilitée : Rails Runner s'intègre naturellement avec les outils d'automatisation système. Que vous utilisiez cron sur Linux, launchd sur macOS, ou des solutions plus modernes comme systemd timers, Rails Runner devient le pont parfait entre votre système d'exploitation et votre application Rails.
Performance optimisée : En évitant la couche HTTP, vous économisez des ressources. Pas de serveur web à démarrer, pas de middleware à traverser, pas de session à gérer. Juste votre code et votre base de données. Pour des traitements batch ou des migrations de données, cette économie peut représenter une différence significative en termes de temps d'exécution.
Debugging et maintenance : Rails Runner simplifie également les opérations de maintenance. Besoin de corriger des données en production ? Plutôt que de passer par la console interactive (et risquer une erreur de manipulation), vous pouvez écrire un script testé localement, puis l'exécuter en production via Runner avec la certitude qu'il fera exactement ce que vous attendez.
Fonctionnement de Base et Paramétrage
Comprendre comment Rails Runner fonctionne en coulisses vous aidera à l'utiliser de manière plus efficace et à éviter les pièges courants. Plongeons dans les mécanismes techniques et les étapes de configuration.
Installation et Configuration
L'un des grands avantages de Rails Runner, c'est qu'il ne nécessite aucune installation particulière. Si vous avez une application Rails fonctionnelle, vous avez déjà Rails Runner. C'est un outil inclus dans le core de Rails.
Pour vérifier que tout fonctionne, essayez cette commande simple :
rails runner "puts Rails.env"
Vous devriez voir l'environnement actuel s'afficher (probablement development). Si cela fonctionne, vous êtes prêt à utiliser Rails Runner.
Cependant, pour une utilisation optimale, quelques configurations peuvent s'avérer utiles :
Gestion des environnements : Par défaut, Rails Runner s'exécute dans l'environnement spécifié par RAILS_ENV (ou development si non défini). Pour exécuter du code en production :
RAILS_ENV=production rails runner "MonScript.execute"
Ou de manière plus explicite :
rails runner -e production "MonScript.execute"
Organisation des scripts : Créez un répertoire dédié pour vos scripts Runner. Par convention, beaucoup de développeurs utilisent lib/scripts/ ou app/scripts/ :
Puis créez vos scripts dans ce répertoire :
# lib/scripts/cleanup_old_users.rb
class CleanupOldUsers
def self.execute
deleted_count = User.where('last_sign_in_at < ?', 2.years.ago)
.where(active: false)
.destroy_all
.count
puts "Suppression de #{deleted_count} utilisateurs inactifs"
end
end
CleanupOldUsers.execute if __FILE__ == $0
Notez la dernière ligne : if __FILE__ == $0. Cette condition permet d'exécuter le script uniquement s'il est appelé directement, pas s'il est requis par un autre fichier. C'est une bonne pratique qui rend vos scripts plus modulaires.
Pour exécuter ce script :
rails runner lib/scripts/cleanup_old_users.rb
Les Meilleures Pratiques pour le Paramétrage
Maintenant que vous savez configurer Rails Runner, voyons comment l'utiliser de manière professionnelle et éviter les erreurs courantes.
1. Toujours spécifier l'environnement explicitement
Dans un contexte de production ou d'automatisation, ne comptez jamais sur les valeurs par défaut. Spécifiez toujours l'environnement :
rails runner -e production "Script.run"
Cela évite les mauvaises surprises, comme exécuter accidentellement un script de nettoyage sur votre base de données de développement au lieu de la production (ou vice versa, ce qui serait encore pire).
2. Gérer les erreurs proprement
Rails Runner retourne le code de sortie du script exécuté. Utilisez cela à votre avantage dans vos scripts automatisés :
# lib/scripts/safe_cleanup.rb
begin
result = DangerousOperation.perform
puts "Succès : #{result}"
exit 0
rescue StandardError => e
puts "Erreur : #{e.message}"
puts e.backtrace.join("\n")
exit 1
end
Dans vos tâches cron ou vos scripts shell, vous pouvez alors vérifier le succès :
#!/bin/bash
rails runner lib/scripts/safe_cleanup.rb
if [ $? -eq 0 ]; then
echo "Script exécuté avec succès"
else
echo "Échec du script" >&2
# Envoyer une alerte, logger l'erreur, etc.
fi
3. Utiliser le logging Rails
Plutôt que des puts partout dans votre code, utilisez le système de logging de Rails. Cela vous donne plus de contrôle et une meilleure traçabilité :
# lib/scripts/logged_operation.rb
class LoggedOperation
def self.execute
Rails.logger.info "Début de l'opération"
User.find_each do |user|
Rails.logger.debug "Traitement de l'utilisateur #{user.id}"
# Traitement...
end
Rails.logger.info "Opération terminée"
end
end
4. Attention aux fuites mémoire
Un piège fréquent avec Rails Runner : les fuites mémoire lors du traitement de grandes quantités de données. Utilisez find_each ou find_in_batches plutôt que all :
# ❌ Mauvais : charge tous les utilisateurs en mémoire
User.all.each do |user|
user.process_something
end
# ✅ Bon : traite par lots de 1000
User.find_each(batch_size: 1000) do |user|
user.process_something
end
5. Tester vos scripts
Vos scripts Runner sont du code comme un autre. Testez-les ! Créez des specs pour vos scripts dans spec/scripts/ :
# spec/scripts/cleanup_old_users_spec.rb
require 'rails_helper'
require './lib/scripts/cleanup_old_users'
RSpec.describe CleanupOldUsers do
describe '.execute' do
it 'supprime les utilisateurs inactifs depuis plus de 2 ans' do
old_user = create(:user, last_sign_in_at: 3.years.ago, active: false)
recent_user = create(:user, last_sign_in_at: 1.year.ago, active: false)
expect { CleanupOldUsers.execute }
.to change { User.count }.by(-1)
expect(User.exists?(old_user.id)).to be false
expect(User.exists?(recent_user.id)).to be true
end
end
end
Applications Pratiques du Rails Runner
La théorie c'est bien, mais la pratique c'est mieux ! Explorons maintenant des cas d'utilisation concrets qui vont transformer votre manière de travailler avec Rails.
Exécution de Tâches Périodiques
L'une des utilisations les plus fréquentes de Rails Runner est l'exécution de tâches périodiques. Que ce soit pour générer des rapports, nettoyer des données obsolètes, ou envoyer des emails récurrents, Rails Runner combiné à un scheduler système devient un outil redoutable.
Stratégies pour les Tâches Programmées
Pour programmer des tâches avec Rails Runner, vous avez plusieurs options. La plus traditionnelle reste cron, mais des alternatives modernes existent.
Avec Cron (la méthode classique)
Cron est le scheduler de tâches standard sur les systèmes Unix. Pour l'utiliser avec Rails Runner, éditez votre crontab :
Puis ajoutez vos tâches :
# Tous les jours à 2h du matin : nettoyage des sessions expirées
0 2 * * * cd /chemin/vers/mon_app && RAILS_ENV=production bundle exec rails runner "ActiveRecord::SessionStore::Session.where('updated_at < ?', 30.days.ago).delete_all" >> log/cron.log 2>&1
# Toutes les heures : envoi des emails en attente
0 * * * * cd /chemin/vers/mon_app && RAILS_ENV=production bundle exec rails runner lib/scripts/send_pending_emails.rb >> log/cron.log 2>&1
# Tous les lundis à 9h : génération du rapport hebdomadaire
0 9 * * 1 cd /chemin/vers/mon_app && RAILS_ENV=production bundle exec rails runner "WeeklyReport.generate_and_send" >> log/cron.log 2>&1
Quelques points d'attention :
- Utilisez toujours le chemin complet vers votre application
- Spécifiez bundle exec pour garantir l'utilisation des bonnes versions de gems
- Redirigez la sortie vers un fichier de log (>> log/cron.log 2>&1)
- N'oubliez pas de définir RAILS_ENV=production
Avec Whenever (la méthode Rails-friendly)
Si vous préférez une approche plus "Rails", le gem whenever vous permet de définir vos tâches cron en Ruby :
# Gemfile
gem 'whenever', require: false
Puis créez un fichier config/schedule.rb :
# config/schedule.rb
set :output, 'log/cron.log'
set :environment, 'production'
every 1.day, at: '2:00 am' do
runner "ActiveRecord::SessionStore::Session.where('updated_at < ?', 30.days.ago).delete_all"
end
every 1.hour do
runner "lib/scripts/send_pending_emails.rb"
end
every :monday, at: '9:00 am' do
runner "WeeklyReport.generate_and_send"
end
# Syntaxe cron traditionnelle aussi supportée
every '0 */6 * * *' do
runner "DataSyncJob.perform"
end
Pour générer votre crontab :
whenever --update-crontab
L'avantage ? Votre configuration est versionnée avec votre code, plus lisible, et plus facile à maintenir.
Avec des solutions modernes : Sidekiq Scheduler ou Good Job
Si vous utilisez déjà un système de jobs en arrière-plan comme Sidekiq, pourquoi ne pas utiliser son scheduler intégré ?
# Gemfile
gem 'sidekiq-scheduler'
# config/sidekiq.yml
:schedule:
cleanup_sessions:
cron: '0 2 * * *'
class: CleanupSessionsJob
send_pending_emails:
every: '1h'
class: SendPendingEmailsJob
weekly_report:
cron: '0 9 * * 1'
class: WeeklyReportJob
Cette approche a plusieurs avantages :
- Pas de dépendance à cron : tout est géré par Sidekiq
- Monitoring intégré : vous voyez vos jobs schedulés dans l'interface Sidekiq
- Retry automatique : en cas d'échec, le job peut être relancé
- Logs centralisés : tout est tracé au même endroit
Études de Cas : Implémentations Réussies
Voyons maintenant quelques exemples réels et complets qui illustrent la puissance de Rails Runner pour les tâches périodiques.
Cas #1 : Génération et envoi de rapports hebdomadaires
Imaginez que vous devez envoyer chaque lundi matin un rapport des inscriptions de la semaine précédente à votre équipe.
# lib/scripts/weekly_signup_report.rb
class WeeklySignupReport
def self.execute
start_date = 1.week.ago.beginning_of_week
end_date = 1.week.ago.end_of_week
new_users = User.where(created_at: start_date..end_date)
report_data = {
total: new_users.count,
by_day: new_users.group_by_day(:created_at).count,
by_source: new_users.group(:source).count,
revenue: Order.where(user: new_users).sum(:amount)
}
Rails.logger.info "Génération du rapport hebdomadaire"
Rails.logger.info "Période : #{start_date.to_date} - #{end_date.to_date}"
Rails.logger.info "Nouvelles inscriptions : #{report_data[:total]}"
# Envoi par email
AdminMailer.weekly_report(report_data, start_date, end_date).deliver_now
# Sauvegarde dans la base pour historique
WeeklyReport.create!(
start_date: start_date,
end_date: end_date,
data: report_data
)
Rails.logger.info "Rapport envoyé avec succès"
rescue StandardError => e
Rails.logger.error "Erreur lors de la génération du rapport : #{e.message}"
# Alerter l'équipe en cas d'erreur
AdminMailer.error_notification(e).deliver_now
raise
end
end
WeeklySignupReport.execute if __FILE__ == $0
Configuration cron :
0 9 * * 1 cd /var/www/monapp && RAILS_ENV=production bundle exec rails runner lib/scripts/weekly_signup_report.rb
Cas #2 : Nettoyage automatique des données temporaires
Les applications Rails accumulent souvent des données temporaires : sessions expirées, fichiers uploadés temporaires, caches obsolètes...
# lib/scripts/cleanup_temporary_data.rb
class CleanupTemporaryData
def self.execute
Rails.logger.info "Début du nettoyage des données temporaires"
results = {
expired_sessions: cleanup_sessions,
old_temp_files: cleanup_temp_files,
unconfirmed_users: cleanup_unconfirmed_users,
expired_tokens: cleanup_expired_tokens
}
total = results.values.sum
Rails.logger.info "Nettoyage terminé : #{total} éléments supprimés"
Rails.logger.info "Détails : #{results}"
# Notification si beaucoup de données supprimées (possible anomalie)
if total > 10000
AdminMailer.cleanup_alert(results).deliver_now
end
results
end
private
def self.cleanup_sessions
if defined?(ActiveRecord::SessionStore::Session)
deleted = ActiveRecord::SessionStore::Session
.where('updated_at < ?', 30.days.ago)
.delete_all
Rails.logger.info "Sessions expirées supprimées : #{deleted}"
deleted
else
0
end
end
def self.cleanup_temp_files
temp_path = Rails.root.join('tmp', 'uploads')
return 0 unless File.directory?(temp_path)
deleted = 0
Dir.glob(temp_path.join('*')).each do |file|
if File.mtime(file) < 24.hours.ago
File.delete(file)
deleted += 1
end
end
Rails.logger.info "Fichiers temporaires supprimés : #{deleted}"
deleted
end
def self.cleanup_unconfirmed_users
if User.column_names.include?('confirmed_at')
deleted = User.where(confirmed_at: nil)
.where('created_at < ?', 7.days.ago)
.delete_all
Rails.logger.info "Utilisateurs non confirmés supprimés : #{deleted}"
deleted
else
0
end
end
def self.cleanup_expired_tokens
deleted = 0
if defined?(AccessToken)
deleted += AccessToken.where('expires_at < ?', Time.current).delete_all
end
if defined?(ResetPasswordToken)
deleted += ResetPasswordToken.where('created_at < ?', 24.hours.ago).delete_all
end
Rails.logger.info "Tokens expirés supprimés : #{deleted}"
deleted
end
end
CleanupTemporaryData.execute if __FILE__ == $0
Cette approche modulaire permet de facilement activer/désactiver certains nettoyages selon vos besoins.
Cas #3 : Synchronisation avec un service externe
Beaucoup d'applications doivent synchroniser leurs données avec des services tiers (CRM, analytics, etc.).
# lib/scripts/sync_to_analytics.rb
class SyncToAnalytics
BATCH_SIZE = 100
def self.execute
Rails.logger.info "Début de la synchronisation vers Analytics"
last_sync = SyncLog.where(service: 'analytics').order(created_at: :desc).first
since = last_sync&.created_at || 24.hours.ago
events_to_sync = AnalyticsEvent.where('created_at > ?', since)
.where(synced: false)
total = events_to_sync.count
synced = 0
failed = 0
Rails.logger.info "#{total} événements à synchroniser"
events_to_sync.find_in_batches(batch_size: BATCH_SIZE) do |batch|
begin
# Envoi par lots vers le service externe
response = AnalyticsService.send_batch(batch.map(&:to_analytics_format))
if response.success?
batch.each { |event| event.update!(synced: true) }
synced += batch.size
Rails.logger.debug "Lot de #{batch.size} événements synchronisé"
else
failed += batch.size
Rails.logger.error "Échec de synchronisation d'un lot : #{response.error}"
end
rescue StandardError => e
failed += batch.size
Rails.logger.error "Erreur lors de la synchronisation : #{e.message}"
end
sleep 0.5 # Rate limiting
end
# Enregistrement du résultat
SyncLog.create!(
service: 'analytics',
total_events: total,
synced_events: synced,
failed_events: failed
)
Rails.logger.info "Synchronisation terminée : #{synced}/#{total} réussis"
# Alerte si trop d'échecs
if failed > total * 0.1 # Plus de 10% d'échecs
AdminMailer.sync_failure_alert('analytics', failed, total).deliver_now
end
end
end
SyncToAnalytics.execute if __FILE__ == $0
Manipulation Avancée des Bases de Données
Rails Runner excelle dans les opérations sur base de données qui sortent du cadre habituel des requêtes web. Migrations de données, corrections en masse, optimisations... Voyons comment exploiter cette puissance.
Optimisation des Requêtes
L'un des avantages majeurs de Rails Runner pour les opérations sur base de données est la possibilité d'optimiser finement les requêtes sans les contraintes du cycle requête-réponse HTTP.
Traitement par lots pour les grandes volumétries
Quand vous devez traiter des millions d'enregistrements, la mémoire devient votre ennemi principal. Rails Runner permet d'implémenter des stratégies de traitement par lots efficaces :
# lib/scripts/migrate_user_emails.rb
class MigrateUserEmails
BATCH_SIZE = 1000
def self.execute
total = User.where(email_normalized: nil).count
processed = 0
Rails.logger.info "Migration de #{total} emails"
User.where(email_normalized: nil).find_in_batches(batch_size: BATCH_SIZE) do |batch|
# Utilisation d'une transaction pour chaque lot
ActiveRecord::Base.transaction do
batch.each do |user|
user.update_column(:email_normalized, user.email.downcase.strip)
end
end
processed += batch.size
progress = (processed.to_f / total * 100).round(2)
Rails.logger.info "Progression : #{processed}/#{total} (#{progress}%)"
end
Rails.logger.info "Migration terminée"
end
end
Requêtes SQL brutes pour les performances
Parfois, ActiveRecord n'est pas l'outil le plus adapté. Rails Runner vous donne accès direct à la connexion SQL :
# lib/scripts/bulk_update_statistics.rb
class BulkUpdateStatistics
def self.execute
Rails.logger.info "Mise à jour des statistiques utilisateurs"
# Requête SQL optimisée pour éviter les N+1
sql = <<-SQL
UPDATE users
SET
posts_count = (
SELECT COUNT(*)
FROM posts
WHERE posts.user_id = users.id
),
comments_count = (
SELECT COUNT(*)
FROM comments
WHERE comments.user_id = users.id
),
last_activity_at = (
SELECT MAX(created_at)
FROM (
SELECT created_at FROM posts WHERE posts.user_id = users.id
UNION ALL
SELECT created_at FROM comments WHERE comments.user_id = users.id
) AS activities
)
WHERE updated_at < NOW() - INTERVAL '1 day'
SQL
start_time = Time.current
result = ActiveRecord::Base.connection.execute(sql)
duration = Time.current - start_time
Rails.logger.info "#{result.cmd_tuples} utilisateurs mis à jour en #{duration.round(2)}s"
end
end
Utilisation des index et de EXPLAIN
Rails Runner est excellent pour tester et optimiser vos requêtes. Voici un script qui vous aide à identifier les requêtes lentes :
# lib/scripts/analyze_slow_queries.rb
class AnalyzeSlowQueries
def self.execute
queries_to_analyze = [
{ name: 'Recent posts', query: -> { Post.includes(:user).where('created_at > ?', 7.days.ago) } },
{ name: 'Active users', query: -> { User.where('last_sign_in_at > ?', 30.days.ago).order(:email) } },
{ name: 'Popular comments', query: -> { Comment.joins(:post).where('comments.created_at > ?', 1.day.ago).group('posts.id') } }
]
Rails.logger.info "Analyse des requêtes lentes"
queries_to_analyze.each do |query_info|
Rails.logger.info "\n--- #{query_info[:name]} ---"
# Récupération de la requête SQL
relation = query_info[:query].call
sql = relation.to_sql
Rails.logger.info "SQL : #{sql}"
# EXPLAIN de la requête
explain = ActiveRecord::Base.connection.execute("EXPLAIN ANALYZE #{sql}")
Rails.logger.info "EXPLAIN :"
explain.each { |row| Rails.logger.info " #{row.values.join(' ')}" }
# Mesure du temps d'exécution
start_time = Time.current
count = relation.count
duration = ((Time.current - start_time) * 1000).round(2)
Rails.logger.info "Résultats : #{count} enregistrements en #{duration}ms"
if duration > 100
Rails.logger.warn "⚠️ Requête lente détectée !"
end
end
end
end
Gestion des Transactions Complexes
Rails Runner est particulièrement adapté pour gérer des transactions complexes qui impliquent plusieurs modèles ou des opérations conditionnelles.
Migrations de données avec rollback automatique
Quand vous devez migrer des données entre différents modèles, la gestion transactionnelle devient cruciale :
# lib/scripts/migrate_orders_to_new_schema.rb
class MigrateOrdersToNewSchema
def self.execute
Rails.logger.info "Début de la migration des commandes"
total = LegacyOrder.count
migrated = 0
failed = 0
LegacyOrder.find_each do |legacy_order|
ActiveRecord::Base.transaction do
# Création de la nouvelle commande
new_order = Order.create!(
user_id: legacy_order.customer_id,
status: map_status(legacy_order.state),
total: legacy_order.amount,
created_at: legacy_order.order_date
)
# Migration des items
legacy_order.items.each do |legacy_item|
OrderItem.create!(
order: new_order,
product_id: legacy_item.product_ref,
quantity: legacy_item.qty,
price: legacy_item.unit_price
)
end
# Migration des paiements
if legacy_order.payment
Payment.create!(
order: new_order,
amount: legacy_order.payment.amount,
method: legacy_order.payment.type,
processed_at: legacy_order.payment.date
)
end
# Marquer comme migré
legacy_order.update!(migrated: true)
migrated += 1
if migrated % 100 == 0
Rails.logger.info "Progression : #{migrated}/#{total}"
end
end
rescue StandardError => e
failed += 1
Rails.logger.error "Échec migration commande #{legacy_order.id} : #{e.message}"
# La transaction est automatiquement rollback
end
Rails.logger.info "Migration terminée : #{migrated} réussies, #{failed} échecs"
end
private
def self.map_status(legacy_status)
case legacy_status
when 'new' then 'pending'
when 'paid' then 'processing'
when 'shipped' then 'shipped'
when 'completed' then 'delivered'
when 'cancelled' then 'cancelled'
else 'pending'
end
end
end
Opérations conditionnelles complexes
Parfois, vous devez exécuter des opérations qui dépendent de conditions multiples et de vérifications en cascade :
# lib/scripts/process_subscription_renewals.rb
class ProcessSubscriptionRenewals
def self.execute
Rails.logger.info "Traitement des renouvellements d'abonnements"
expiring_subscriptions = Subscription
.where('expires_at <= ?', 3.days.from_now)
.where('expires_at > ?', Time.current)
.where(auto_renew: true)
Rails.logger.info "#{expiring_subscriptions.count} abonnements à renouveler"
success = 0
failed = 0
expiring_subscriptions.find_each do |subscription|
result = renew_subscription(subscription)
if result[:success]
success += 1
Rails.logger.info "✓ Abonnement #{subscription.id} renouvelé"
else
failed += 1
Rails.logger.warn "✗ Échec abonnement #{subscription.id} : #{result[:reason]}"
end
end
Rails.logger.info "Traitement terminé : #{success} réussis, #{failed} échecs"
# Rapport pour l'équipe
SubscriptionMailer.renewal_report(success, failed).deliver_now
end
private
def self.renew_subscription(subscription)
ActiveRecord::Base.transaction do
user = subscription.user
# Vérifier le moyen de paiement
unless user.payment_method&.valid?
raise "Moyen de paiement invalide"
end
# Calculer le montant
plan = subscription.plan
amount = plan.price
# Appliquer une promotion si disponible
if promotion = find_applicable_promotion(user, plan)
amount = amount * (1 - promotion.discount_percentage / 100.0)
Rails.logger.info " Promotion appliquée : #{promotion.code}"
end
# Traiter le paiement
payment = Payment.create!(
user: user,
amount: amount,
payment_method: user.payment_method,
description: "Renouvellement #{plan.name}"
)
result = PaymentProcessor.charge(payment)
unless result.success?
raise "Échec du paiement : #{result.error_message}"
end
# Mettre à jour l'abonnement
subscription.update!(
expires_at: calculate_new_expiration(subscription),
last_renewed_at: Time.current
)
# Créer un enregistrement de facturation
Invoice.create!(
user: user,
subscription: subscription,
payment: payment,
amount: amount
)
# Envoyer l'email de confirmation
SubscriptionMailer.renewal_confirmation(subscription, payment).deliver_later
{ success: true }
end
rescue StandardError => e
# En cas d'erreur, tout est rollback automatiquement
Rails.logger.error " Erreur : #{e.message}"
Rails.logger.error e.backtrace.first(5).join("\n")
# Notifier l'utilisateur
SubscriptionMailer.renewal_failed(subscription, e.message).deliver_later
{ success: false, reason: e.message }
end
def self.find_applicable_promotion(user, plan)
Promotion.active
.where('valid_from <= ? AND valid_until >= ?', Time.current, Time.current)
.where('plan_id = ? OR plan_id IS NULL', plan.id)
.order(discount_percentage: :desc)
.first
end
def self.calculate_new_expiration(subscription)
case subscription.plan.billing_period
when 'monthly' then subscription.expires_at + 1.month
when 'quarterly' then subscription.expires_at + 3.months
when 'yearly' then subscription.expires_at + 1.year
else subscription.expires_at + 1.month
end
end
end
Ce script illustre comment Rails Runner permet de gérer des processus métier complexes avec une gestion d'erreur robuste et des transactions imbriquées.
Cas d'Utilisation Avancés
Maintenant que vous maîtrisez les bases et les applications pratiques, explorons des cas d'utilisation plus avancés qui vont vraiment différencier vos compétences.
Automatisation avec le Rails Runner
L'automatisation va au-delà de la simple exécution périodique. Il s'agit de créer des workflows complets, intelligents, qui s'adaptent aux données et aux situations.
Scripts de Transformation de Données
La transformation de données est une tâche récurrente dans la vie d'une application. Que ce soit pour normaliser des données héritées, enrichir des informations, ou préparer des exports, Rails Runner est l'outil idéal.
ETL (Extract, Transform, Load) simplifié
Voici un exemple complet d'un script ETL qui extrait des données d'une API externe, les transforme, et les charge dans votre base :
# lib/scripts/import_product_catalog.rb
require 'httparty'
class ImportProductCatalog
API_ENDPOINT = ENV['SUPPLIER_API_URL']
API_KEY = ENV['SUPPLIER_API_KEY']
def self.execute
Rails.logger.info "Début de l'import du catalogue produits"
stats = {
fetched: 0,
created: 0,
updated: 0,
skipped: 0,
errors: 0
}
# EXTRACT : Récupération des données de l'API
products_data = fetch_products_from_api
stats[:fetched] = products_data.size
Rails.logger.info "#{stats[:fetched]} produits récupérés de l'API"
# TRANSFORM & LOAD
products_data.each do |raw_product|
begin
result = process_product(raw_product)
stats[result] += 1
rescue StandardError => e
stats[:errors] += 1
Rails.logger.error "Erreur produit #{raw_product['id']} : #{e.message}"
end
end
# Rapport final
Rails.logger.info "Import terminé"
Rails.logger.info "Créations : #{stats[:created]}"
Rails.logger.info "Mises à jour : #{stats[:updated]}"
Rails.logger.info "Ignorés : #{stats[:skipped]}"
Rails.logger.info "Erreurs : #{stats[:errors]}"
# Notification
ImportMailer.catalog_import_report(stats).deliver_now
stats
end
private
def self.fetch_products_from_api
all_products = []
page = 1
loop do
Rails.logger.info "Récupération page #{page}..."
response = HTTParty.get(
"#{API_ENDPOINT}/products",
headers: { 'Authorization' => "Bearer #{API_KEY}" },
query: { page: page, per_page: 100 }
)
break unless response.success?
products = response.parsed_response['products']
break if products.empty?
all_products.concat(products)
page += 1
sleep 0.5 # Rate limiting
end
all_products
end
def self.process_product(raw_product)
# TRANSFORM : Normalisation des données
transformed_data = transform_product_data(raw_product)
# Vérification si le produit existe déjà
existing_product = Product.find_by(external_id: transformed_data[:external_id])
if existing_product
# Mise à jour seulement si les données ont changé
if product_changed?(existing_product, transformed_data)
existing_product.update!(transformed_data)
:updated
else
:skipped
end
else
# LOAD : Création du nouveau produit
Product.create!(transformed_data)
:created
end
end
def self.transform_product_data(raw_product)
{
external_id: raw_product['id'],
name: clean_text(raw_product['name']),
description: clean_text(raw_product['description']),
price: parse_price(raw_product['price']),
category: map_category(raw_product['category_code']),
sku: raw_product['sku']&.upcase,
stock_quantity: raw_product['stock'].to_i,
active: raw_product['status'] == 'active',
metadata: {
supplier_updated_at: raw_product['updated_at'],
supplier_category: raw_product['category_code']
}
}
end
def self.clean_text(text)
return nil if text.blank?
text.strip.gsub(/\s+/, ' ')
end
def self.parse_price(price_string)
price_string.to_s.gsub(/[^0-9.]/, '').to_f
end
def self.map_category(category_code)
mapping = {
'ELEC' => 'Électronique',
'HOME' => 'Maison',
'SPRT' => 'Sport',
'BOOK' => 'Livres'
}
mapping[category_code] || 'Autre'
end
def self.product_changed?(product, new_data)
new_data.any? { |key, value| product.send(key) != value }
end
end
ImportProductCatalog.execute if __FILE__ == $0
Enrichissement de données avec des services externes
Parfois, vous devez enrichir vos données existantes avec des informations provenant de services tiers (géolocalisation, validation, etc.) :
# lib/scripts/enrich_user_locations.rb
class EnrichUserLocations
GEOCODING_SERVICE = 'https://api.geocoding.com/v1'
BATCH_SIZE = 50
def self.execute
Rails.logger.info "Enrichissement des localisations utilisateurs"
users_to_enrich = User.where(latitude: nil)
.where.not(city: nil)
.where.not(country: nil)
total = users_to_enrich.count
processed = 0
enriched = 0
Rails.logger.info "#{total} utilisateurs à enrichir"
users_to_enrich.find_in_batches(batch_size: BATCH_SIZE) do |batch|
# Préparer les requêtes groupées
locations = batch.map do |user|
"#{user.city}, #{user.country}"
end
# Appel groupé au service de géocodage
geocoded_results = geocode_batch(locations)
# Mise à jour des utilisateurs
batch.each_with_index do |user, index|
if result = geocoded_results[index]
user.update!(
latitude: result['lat'],
longitude: result['lng'],
timezone: result['timezone'],
region: result['region']
)
enriched += 1
end
end
processed += batch.size
Rails.logger.info "Progression : #{processed}/#{total} (#{enriched} enrichis)"
sleep 1 # Rate limiting
end
Rails.logger.info "Enrichissement terminé : #{enriched}/#{total} utilisateurs"
end
private
def self.geocode_batch(locations)
response = HTTParty.post(
"#{GEOCODING_SERVICE}/batch",
body: { locations: locations }.to_json,
headers: { 'Content-Type' => 'application/json' }
)
response.success? ? response.parsed_response : []
rescue StandardError => e
Rails.logger.error "Erreur géocodage : #{e.message}"
[]
end
end
Automatisation des Intégrations
Rails Runner est parfait pour automatiser les intégrations avec d'autres systèmes, que ce soit des CRM, des outils analytics, ou des services de monitoring.
Synchronisation bidirectionnelle avec un CRM
# lib/scripts/sync_crm.rb
class SyncCRM
CRM_API = ENV['CRM_API_URL']
def self.execute
Rails.logger.info "Synchronisation CRM bidirectionnelle"
# Phase 1 : Export vers le CRM
export_to_crm
# Phase 2 : Import depuis le CRM
import_from_crm
Rails.logger.info "Synchronisation CRM terminée"
end
private
def self.export_to_crm
Rails.logger.info "Export vers le CRM..."
# Utilisateurs modifiés depuis la dernière sync
last_sync = SyncLog.where(direction: 'export', service: 'crm')
.order(created_at: :desc)
.first
since = last_sync&.created_at || 24.hours.ago
modified_users = User.where('updated_at > ?', since)
Rails.logger.info "#{modified_users.count} utilisateurs à exporter"
exported = 0
modified_users.find_each do |user|
begin
crm_data = user.to_crm_format
if user.crm_id.present?
# Mise à jour
CRMService.update_contact(user.crm_id, crm_data)
else
# Création
crm_id = CRMService.create_contact(crm_data)
user.update_column(:crm_id, crm_id)
end
exported += 1
rescue StandardError => e
Rails.logger.error "Erreur export user #{user.id} : #{e.message}"
end
end
SyncLog.create!(
direction: 'export',
service: 'crm',
records_count: exported
)
Rails.logger.info "#{exported} utilisateurs exportés"
end
def self.import_from_crm
Rails.logger.info "Import depuis le CRM..."
# Récupération des contacts modifiés
modified_contacts = CRMService.get_modified_contacts(since: 1.hour.ago)
Rails.logger.info "#{modified_contacts.count} contacts à importer"
imported = 0
modified_contacts.each do |crm_contact|
begin
user = User.find_by(crm_id: crm_contact['id'])
if user
# Mise à jour sélective (ne pas écraser les données locales)
update_user_from_crm(user, crm_contact)
imported += 1
end
rescue StandardError => e
Rails.logger.error "Erreur import contact #{crm_contact['id']} : #{e.message}"
end
end
SyncLog.create!(
direction: 'import',
service: 'crm',
records_count: imported
)
Rails.logger.info "#{imported} utilisateurs importés"
end
def self.update_user_from_crm(user, crm_contact)
# Mise à jour seulement des champs gérés par le CRM
updates = {}
if crm_contact['company'] && crm_contact['company'] != user.company
updates[:company] = crm_contact['company']
end
if crm_contact['tags'] && crm_contact['tags'] != user.crm_tags
updates[:crm_tags] = crm_contact['tags']
end
if crm_contact['status'] && crm_contact['status'] != user.crm_status
updates[:crm_status] = crm_contact['status']
end
user.update!(updates) if updates.any?
end
end
SyncCRM.execute if __FILE__ == $0
Pipeline de monitoring et alertes
# lib/scripts/health_check.rb
class HealthCheck
def self.execute
Rails.logger.info "Vérification de santé de l'application"
checks = [
check_database,
check_redis,
check_storage,
check_external_services,
check_background_jobs,
check_performance_metrics
]
failed_checks = checks.select { |check| !check[:healthy] }
if failed_checks.any?
Rails.logger.error "#{failed_checks.size} vérifications échouées"
AlertService.send_alert(
severity: 'high',
title: 'Health Check Failed',
details: failed_checks
)
else
Rails.logger.info "Toutes les vérifications sont OK"
end
# Enregistrement des métriques
HealthCheckLog.create!(
checks: checks,
healthy: failed_checks.empty?,
performed_at: Time.current
)
end
private
def self.check_database
start = Time.current
ActiveRecord::Base.connection.execute('SELECT 1')
response_time = ((Time.current - start) * 1000).round(2)
{
name: 'Database',
healthy: response_time < 100,
response_time: response_time,
details: "Response time: #{response_time}ms"
}
rescue StandardError => e
{
name: 'Database',
healthy: false,
error: e.message
}
end
def self.check_redis
start = Time.current
Redis.current.ping
response_time = ((Time.current - start) * 1000).round(2)
{
name: 'Redis',
healthy: response_time < 50,
response_time: response_time
}
rescue StandardError => e
{
name: 'Redis',
healthy: false,
error: e.message
}
end
def self.check_storage
disk_usage = `df -h / | tail -1 | awk '{print $5}'`.strip.to_i
{
name: 'Storage',
healthy: disk_usage < 80,
disk_usage: disk_usage,
details: "Disk usage: #{disk_usage}%"
}
end
def self.check_external_services
services = [
{ name: 'Payment API', url: ENV['PAYMENT_API_URL'] },
{ name: 'Email Service', url: ENV['EMAIL_SERVICE_URL'] }
]
all_healthy = true
services.each do |service|
begin
response = HTTParty.get("#{service[:url]}/health", timeout: 5)
service[:healthy] = response.success?
rescue StandardError
service[:healthy] = false
all_healthy = false
end
end
{
name: 'External Services',
healthy: all_healthy,
services: services
}
end
def self.check_background_jobs
if defined?(Sidekiq)
stats = Sidekiq::Stats.new
failed_count = stats.failed
retry_count = stats.retry_size
{
name: 'Background Jobs',
healthy: failed_count < 100 && retry_count < 50,
failed: failed_count,
retrying: retry_count
}
else
{ name: 'Background Jobs', healthy: true, details: 'Not configured' }
end
end
def self.check_performance_metrics
avg_response_time = calculate_avg_response_time
error_rate = calculate_error_rate
{
name: 'Performance',
healthy: avg_response_time < 500 && error_rate < 1.0,
avg_response_time: avg_response_time,
error_rate: error_rate
}
end
def self.calculate_avg_response_time
# Implémentation dépendante de votre système de logging
200 # Placeholder
end
def self.calculate_error_rate
# Pourcentage d'erreurs sur la dernière heure
0.5 # Placeholder
end
end
Vers une Pratique Avancée
Vous avez maintenant une vue complète de Rails Runner et de ses possibilités. Mais comme tout outil puissant, son efficacité dépend de votre capacité à l'intégrer intelligemment dans votre workflow.
Connecter Théorie et Pratique
Les exemples de ce guide ne sont pas de simples exercices théoriques. Ce sont des patterns éprouvés, utilisés quotidiennement dans des applications Rails en production. La clé pour les maîtriser ? La pratique régulière et l'expérimentation.
Commencez petit. Identifiez une tâche répétitive dans votre projet actuel — peut-être le nettoyage de données obsolètes, ou l'envoi d'un rapport hebdomadaire. Écrivez un script Rails Runner pour cette tâche. Testez-le localement. Déployez-le en production. Observez les résultats.
Puis augmentez progressivement la complexité. Ajoutez de la gestion d'erreur. Implémentez du logging détaillé. Créez des notifications en cas de problème. Optimisez les performances pour gérer de plus grandes volumétries.
Quelques principes à garder en tête :
- Idempotence : Vos scripts doivent pouvoir être exécutés plusieurs fois sans causer de problèmes. Si un script s'arrête en cours de route, il doit pouvoir reprendre proprement.
- Observabilité : Loggez abondamment. En production, vous n'aurez pas accès à la console pour debugger. Vos logs sont votre fenêtre sur ce qui se passe.
- Résilience : Anticipez les échecs. Connexion API qui timeout, base de données temporairement indisponible, données malformées... Votre script doit gérer ces cas.
- Performance : N'oubliez jamais que Rails Runner charge l'intégralité de votre environnement. Pour des tâches très fréquentes, considérez d'autres solutions (background jobs, Redis, etc.).
- Sécurité : Les scripts automatisés sont souvent exécutés avec des privilèges élevés. Soyez particulièrement vigilant sur la validation des données et l'injection SQL.
Ressources et Continuité Appliquée
Rails Runner n'est qu'un outil parmi d'autres dans votre boîte à outils de développeur Rails. Pour aller plus loin, voici quelques directions à explorer :
Documentation officielle
- Rails Guides - Command Line
- Rails API - Rails::Command::RunnerCommand
Outils complémentaires
- Whenever : Gestion de cron en Ruby
- Sidekiq Scheduler : Alternative moderne à cron
- Good Job : Backend de jobs avec scheduler intégré
- Kamal : Pour le déploiement et l'orchestration
Patterns avancés à étudier
- Service Objects : Structurer votre logique métier de manière réutilisable
- Command Pattern : Pour des opérations complexes et transactionnelles
- ETL Pipelines : Pour les transformations de données à grande échelle
- Event Sourcing : Pour l'audit et la traçabilité
Pratique continue
Le meilleur moyen de progresser ? Contribuer à des projets open source ou créer vos propres gems. Vous verrez comment d'autres développeurs structurent leurs scripts, gèrent les erreurs, et optimisent les performances.
Parcourez aussi le code source de Rails lui-même. Les commandes rails runner, rails console, et autres sont écrites en Ruby. Comprendre leur implémentation vous donnera des insights précieux sur les bonnes pratiques.
Communauté
La communauté Rails est active et bienveillante. N'hésitez pas à :
- Poser vos questions sur Stack Overflow
- Participer aux discussions sur Reddit r/rails
- Suivre les développeurs Rails influents sur Twitter/X
- Assister à des meetups Ruby/Rails dans votre région
Conclusion
Rails Runner est bien plus qu'une simple commande pour exécuter du code Ruby. C'est un multiplicateur de productivité, un outil d'automatisation puissant, et un compagnon indispensable pour toute opération qui sort du cadre classique requête-réponse HTTP.
Vous avez maintenant toutes les clés en main :
- Comprendre ce qu'est Rails Runner et comment il fonctionne
- L'utiliser pour des tâches périodiques et du nettoyage de données
- Manipuler efficacement votre base de données
- Créer des scripts complexes d'automatisation et d'intégration
- Appliquer les meilleures pratiques pour du code robuste et maintenable
Mais rappelez-vous : la connaissance sans pratique reste théorique. Ouvrez votre terminal, créez votre premier script, et lancez-vous. Vous ferez des erreurs — c'est normal et même souhaitable. Chaque erreur est une leçon qui vous rapproche de la maîtrise.
Et quand vous aurez automatisé votre première tâche récurrente, quand vous aurez écrit votre premier script de migration de données qui fonctionne parfaitement, vous ressentirez cette satisfaction unique du développeur : celle d'avoir dompté la complexité et fait travailler la machine à votre place.
Bon code, et que Rails Runner libère la puissance cachée de vos projets ! 🚀