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 :

  1. 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
  2. Job traite des données obsolètes : Le job lit des données non validées qui sont annulées
  3. 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.