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
# 事务在这里提交
# 现在作业被分发到队列

如果事务回滚,作业永远不会入队。

为什么这很重要

这修复了几个常见问题:

  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

如果实际入队稍后失败,作业的状态会更新,但这发生在事务之后。

总结

这是一个明智的默认值,可以防止一类常见的bug。如果你一直在手动使用after_commit回调来入队作业,现在可以简化你的代码了。

对于现有应用,在启用之前要彻底测试,因为时机变化可能会影响对作业顺序的假设。

完整讨论请参阅commitPR #55788