Rails 8.2 corrige un bug subtil d'ActiveJob que vous ignorez peut-être
Rails 8.2 change la façon dont ActiveJob interagit avec les transactions de base de données, corrigeant un bug subtil qui a mordu de nombreux développeurs. Les jobs mis en file d’attente dans une transaction attendent maintenant que la transaction soit validée avant d’être dispatchés.
Le Problème
Considérez ce pattern commun :
class OrdersController < ApplicationController
def create
ActiveRecord::Base.transaction do
@order = Order.create!(order_params)
OrderConfirmationJob.perform_later(@order)
end
end
end
Ça semble raisonnable, non ? Mais il y a une condition de concurrence. Le job pourrait s’exécuter avant que la transaction ne soit validée. Quand le job s’exécute :
class OrderConfirmationJob < ApplicationJob
def perform(order)
# Ça pourrait échouer ! La commande pourrait ne pas encore exister dans la base de données
OrderMailer.confirmation(order).deliver_now
end
end
Pire encore, si la transaction est annulée, le job s’exécute quand même sur un enregistrement qui n’existe plus.
Ce Qui a Changé
Dans Rails 8.2, config.active_job.enqueue_after_transaction_commit est true par défaut. Les jobs mis en file d’attente dans une transaction sont maintenant retenus jusqu’après la validation de la transaction.
ActiveRecord::Base.transaction do
@order = Order.create!(order_params)
OrderConfirmationJob.perform_later(@order) # Job retenu, pas encore mis en file d'attente
end
# La transaction est validée ici
# MAINTENANT le job est dispatché vers la file d'attente
Si la transaction est annulée, le job n’est jamais mis en file d’attente.
Pourquoi C’est Important
Cela corrige plusieurs problèmes courants :
- Job échoue avec RecordNotFound : Le job s’exécute avant que l’enregistrement ne soit visible pour d’autres connexions à la base de données
- Job traite des données obsolètes : Le job lit des données non validées qui sont annulées
- Travail en double : Les retries de transaction causent la mise en file d’attente de plusieurs jobs
Comment l’Activer
Nouvelles apps Rails 8.2
C’est activé par défaut. Rien à faire.
Mise à niveau vers Rails 8.2
Ajoutez à config/initializers/new_framework_defaults_8_2.rb :
Rails.application.config.active_job.enqueue_after_transaction_commit = true
Ou définissez-le dans config/application.rb :
config.active_job.enqueue_after_transaction_commit = true
Override par job
Si un job spécifique doit être mis en file d’attente immédiatement (peut-être qu’il ne dépend pas de la transaction) :
class NotificationJob < ApplicationJob
self.enqueue_after_transaction_commit = false
def perform(message)
# Ce job sera mis en file d'attente immédiatement, même dans une transaction
end
end
Points d’Attention
Jobs hors transactions
Les jobs mis en file d’attente hors d’une transaction sont dispatchés immédiatement, comme avant :
# Pas de transaction, le job est mis en file d'attente immédiatement
SomeJob.perform_later(data)
Transactions imbriquées
Le job attend que la transaction la plus externe soit validée :
ActiveRecord::Base.transaction do
User.create!(...)
ActiveRecord::Base.transaction do
Order.create!(...)
OrderJob.perform_later(@order) # Retenu jusqu'à ce que la transaction externe soit validée
end
end
# Job dispatché ici
Timing de la valeur de retour
perform_later retourne toujours l’instance du job immédiatement, même si la mise en file d’attente réelle est différée :
ActiveRecord::Base.transaction do
job = MyJob.perform_later(data)
job.successfully_enqueued? # true (optimistement)
end
Si la mise en file d’attente réelle échoue plus tard, le statut du job est mis à jour, mais cela arrive après la transaction.
Conclusion
C’est une valeur par défaut sensée qui prévient une classe commune de bugs. Si vous avez utilisé manuellement des callbacks after_commit pour mettre des jobs en file d’attente, vous pouvez maintenant simplifier votre code.
Pour les apps existantes, testez soigneusement avant d’activer, car le changement de timing pourrait affecter les suppositions sur l’ordre des jobs.
Consultez le commit et PR #55788 pour la discussion complète.