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
# トランザクションはここでコミット
# 今、ジョブがキューにディスパッチされる
トランザクションがロールバックされた場合、ジョブはエンキューされません。
なぜこれが重要か
これはいくつかの一般的な問題を修正します:
- RecordNotFoundでジョブが失敗: ジョブがレコードが他のデータベース接続から見える前に実行される
- ジョブが古いデータを処理: ジョブがロールバックされるコミットされていないデータを読み取る
- 重複した作業: トランザクションのリトライが複数のジョブのエンキューを引き起こす
有効化の方法
新しい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コールバックを使用していた場合、コードを簡素化できるようになりました。
既存のアプリの場合、タイミングの変更がジョブの順序に関する仮定に影響を与える可能性があるため、有効にする前に十分にテストしてください。