Rails 8.2修复了一个你可能不知道存在的ActiveJob微妙bug
Rails 8.2改变了ActiveJob与数据库事务交互的方式,修复了一个困扰许多开发者的微妙bug。在事务内入队的作业现在会等到事务提交后才被分发。
问题
考虑这个常见模式:
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
如果实际入队稍后失败,作业的状态会更新,但这发生在事务之后。
总结
这是一个明智的默认值,可以防止一类常见的bug。如果你一直在手动使用after_commit回调来入队作业,现在可以简化你的代码了。
对于现有应用,在启用之前要彻底测试,因为时机变化可能会影响对作业顺序的假设。