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:
- Job falla con RecordNotFound: El job se ejecuta antes de que el registro sea visible para otras conexiones de base de datos
- Job procesa datos obsoletos: El job lee datos no confirmados que luego se revierten
- 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.