Rails 8.2が、あなたが気づいていないかもしれないActiveJobの微妙なバグを修正

Rails 8.2はActiveJobがデータベーストランザクションと相互作用する方法を変更し、多くの開発者を悩ませてきた微妙なバグを修正しました。トランザクション内でエンキューされたジョブは、トランザクションがコミットされるまでディスパッチを待つようになりました。

問題

この一般的なパターンを考えてみてください:

class OrdersController < ApplicationController
  def create
    ActiveRecord::Base.transaction do
      @order = Order.create!(order_params)
      OrderConfirmationJob.perform_later(@order)
    end
  end
end

妥当に見えますよね?しかし、競合状態があります。ジョブはトランザクションがコミットされるに実行される可能性があります。ジョブが実行されるとき:

class OrderConfirmationJob < ApplicationJob
  def perform(order)
    # これは失敗するかもしれない!注文がまだデータベースに存在しないかもしれない
    OrderMailer.confirmation(order).deliver_now
  end
end

さらに悪いことに、トランザクションがロールバックされた場合、ジョブはもう存在しないレコードに対して実行されます。

変更内容

Rails 8.2では、config.active_job.enqueue_after_transaction_commitがデフォルトでtrueになりました。トランザクション内でエンキューされたジョブは、トランザクションがコミットされた後まで保持されるようになりました。

ActiveRecord::Base.transaction do
  @order = Order.create!(order_params)
  OrderConfirmationJob.perform_later(@order)  # ジョブは保持され、まだエンキューされていない
end
# トランザクションはここでコミット
# 今、ジョブがキューにディスパッチされる

トランザクションがロールバックされた場合、ジョブはエンキューされません。

なぜこれが重要か

これはいくつかの一般的な問題を修正します:

  1. RecordNotFoundでジョブが失敗: ジョブがレコードが他のデータベース接続から見える前に実行される
  2. ジョブが古いデータを処理: ジョブがロールバックされるコミットされていないデータを読み取る
  3. 重複した作業: トランザクションのリトライが複数のジョブのエンキューを引き起こす

有効化の方法

新しいRails 8.2アプリ

デフォルトで有効です。何もする必要はありません。

Rails 8.2へのアップグレード

config/initializers/new_framework_defaults_8_2.rbに追加:

Rails.application.config.active_job.enqueue_after_transaction_commit = true

またはconfig/application.rbで設定:

config.active_job.enqueue_after_transaction_commit = true

ジョブごとのオーバーライド

特定のジョブが即座にエンキューされるべき場合(おそらくトランザクションに依存しない場合):

class NotificationJob < ApplicationJob
  self.enqueue_after_transaction_commit = false

  def perform(message)
    # このジョブはトランザクション内でも即座にエンキューされる
  end
end

注意点

トランザクション外のジョブ

トランザクション外でエンキューされたジョブは、以前と同様に即座にディスパッチされます:

# トランザクションなし、ジョブは即座にエンキュー
SomeJob.perform_later(data)

ネストされたトランザクション

ジョブは最も外側のトランザクションがコミットされるのを待ちます:

ActiveRecord::Base.transaction do
  User.create!(...)

  ActiveRecord::Base.transaction do
    Order.create!(...)
    OrderJob.perform_later(@order)  # 外側のトランザクションがコミットされるまで保持
  end
end
# ジョブはここでディスパッチ

戻り値のタイミング

perform_laterは実際のエンキューが遅延されていても、ジョブインスタンスを即座に返します:

ActiveRecord::Base.transaction do
  job = MyJob.perform_later(data)
  job.successfully_enqueued?  # true(楽観的に)
end

実際のエンキューが後で失敗した場合、ジョブのステータスは更新されますが、これはトランザクションの後に発生します。

まとめ

これは一般的なバグのクラスを防ぐ賢明なデフォルトです。ジョブをエンキューするために手動でafter_commitコールバックを使用していた場合、コードを簡素化できるようになりました。

既存のアプリの場合、タイミングの変更がジョブの順序に関する仮定に影響を与える可能性があるため、有効にする前に十分にテストしてください。

完全な議論についてはcommitPR #55788を参照してください。