Rails 8.2 corrige un bug sutil de ActiveJob que quizás no sabías que tenías

Rails 8.2 cambia cómo ActiveJob interactúa con las transacciones de base de datos, corrigiendo un bug sutil que ha afectado a muchos desarrolladores. Los jobs encolados dentro de una transacción ahora esperan hasta que la transacción se confirme antes de ser despachados.

El Problema

Considera este patrón común:

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

Parece razonable, ¿verdad? Pero hay una condición de carrera. El job podría ejecutarse antes de que la transacción se confirme. Cuando el job se ejecuta:

class OrderConfirmationJob < ApplicationJob
  def perform(order)
    # ¡Esto podría fallar! El pedido podría no existir aún en la base de datos
    OrderMailer.confirmation(order).deliver_now
  end
end

Peor aún, si la transacción se revierte, el job todavía se ejecuta contra un registro que ya no existe.

Qué Cambió

En Rails 8.2, config.active_job.enqueue_after_transaction_commit es true por defecto. Los jobs encolados dentro de una transacción ahora se retienen hasta después de que la transacción se confirme.

ActiveRecord::Base.transaction do
  @order = Order.create!(order_params)
  OrderConfirmationJob.perform_later(@order)  # Job retenido, aún no encolado
end
# La transacción se confirma aquí
# AHORA el job se despacha a la cola

Si la transacción se revierte, el job nunca se encola.

Por Qué Esto Importa

Esto corrige varios problemas comunes:

  1. Job falla con RecordNotFound: El job se ejecuta antes de que el registro sea visible para otras conexiones de base de datos
  2. Job procesa datos obsoletos: El job lee datos no confirmados que luego se revierten
  3. Trabajo duplicado: Los reintentos de transacción causan que se encolen múltiples jobs

Cómo Habilitarlo

Nuevas apps Rails 8.2

Está habilitado por defecto. Nada que hacer.

Actualizando a Rails 8.2

Agrega a config/initializers/new_framework_defaults_8_2.rb:

Rails.application.config.active_job.enqueue_after_transaction_commit = true

O configúralo en config/application.rb:

config.active_job.enqueue_after_transaction_commit = true

Anulación por job

Si un job específico debe encolarse inmediatamente (quizás no depende de la transacción):

class NotificationJob < ApplicationJob
  self.enqueue_after_transaction_commit = false

  def perform(message)
    # Este job se encolará inmediatamente, incluso dentro de una transacción
  end
end

Cosas a Tener en Cuenta

Jobs fuera de transacciones

Los jobs encolados fuera de una transacción se despachan inmediatamente, como antes:

# Sin transacción, el job se encola inmediatamente
SomeJob.perform_later(data)

Transacciones anidadas

El job espera a que la transacción más externa se confirme:

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

  ActiveRecord::Base.transaction do
    Order.create!(...)
    OrderJob.perform_later(@order)  # Retenido hasta que la transacción externa se confirme
  end
end
# Job despachado aquí

Timing del valor de retorno

perform_later todavía devuelve la instancia del job inmediatamente, aunque el encolado real está diferido:

ActiveRecord::Base.transaction do
  job = MyJob.perform_later(data)
  job.successfully_enqueued?  # true (optimistamente)
end

Si el encolado real falla después, el estado del job se actualiza, pero esto sucede después de la transacción.

Conclusión

Este es un valor por defecto sensato que previene una clase común de bugs. Si has estado usando manualmente callbacks after_commit para encolar jobs, ahora puedes simplificar tu código.

Para apps existentes, prueba exhaustivamente antes de habilitar, ya que el cambio de timing podría afectar suposiciones sobre el orden de los jobs.

Consulta el commit y PR #55788 para la discusión completa.