NO LEAS ESTE ARCHIVO EN GITHUB, LAS GUÍAS ESTÁN PUBLICADAS EN https://guides.rubyonrails.org.

Conceptos Básicos de Active Job

Esta guía te proporciona todo lo que necesitas para comenzar a crear, encolar y ejecutar trabajos en segundo plano.

Después de leer esta guía, sabrás:


1 ¿Qué es Active Job?

Active Job es un marco para declarar trabajos y hacerlos funcionar en una variedad de sistemas de cola. Estos trabajos pueden ser desde limpiezas programadas regularmente, hasta cargos de facturación, o envíos de correos. Cualquier cosa que pueda dividirse en pequeñas unidades de trabajo y ejecutarse en paralelo.

2 El Propósito de Active Job

El objetivo principal es asegurar que todas las aplicaciones Rails tengan una infraestructura de trabajos establecida. De esta manera, podemos tener características del marco y otras gemas construidas sobre eso, sin tener que preocuparnos por las diferencias de API entre varios ejecutores de trabajos como Delayed Job y Resque. Elegir tu sistema de cola se convierte más en una preocupación operativa. Y podrás cambiar entre ellos sin tener que reescribir tus trabajos.

NOTA: Rails por defecto viene con una implementación de cola asincrónica que ejecuta trabajos con un grupo de hilos en el mismo proceso. Los trabajos se ejecutarán asincrónicamente, pero cualquier trabajo en la cola se perderá al reiniciar.

3 Crear y Encolar Trabajos

Esta sección proporcionará una guía paso a paso para crear un trabajo y encolarlo.

3.1 Crear el Trabajo

Active Job proporciona un generador de Rails para crear trabajos. Lo siguiente creará un trabajo en app/jobs (con un caso de prueba adjunto en test/jobs):

$ bin/rails generate job guests_cleanup
invoke  test_unit
create    test/jobs/guests_cleanup_job_test.rb
create  app/jobs/guests_cleanup_job.rb

También puedes crear un trabajo que se ejecute en una cola específica:

$ bin/rails generate job guests_cleanup --queue urgent

Si no quieres usar un generador, puedes crear tu propio archivo dentro de app/jobs, solo asegúrate de que herede de ApplicationJob.

Así es como se ve un trabajo:

class GuestsCleanupJob < ApplicationJob
  queue_as :default

  def perform(*guests)
    # Hacer algo después
  end
end

Ten en cuenta que puedes definir perform con tantos argumentos como desees.

Si ya tienes una clase abstracta y su nombre difiere de ApplicationJob, puedes pasar la opción --parent para indicar que deseas una clase abstracta diferente:

$ bin/rails generate job process_payment --parent=payment_job
class ProcessPaymentJob < PaymentJob
  queue_as :default

  def perform(*args)
    # Hacer algo después
  end
end

3.2 Encolar el Trabajo

Encola un trabajo usando perform_later y, opcionalmente, set. De esta manera:

# Encola un trabajo para que se realice tan pronto como el sistema de colas esté
# libre.
GuestsCleanupJob.perform_later guest
# Encola un trabajo para que se realice mañana al mediodía.
GuestsCleanupJob.set(wait_until: Date.tomorrow.noon).perform_later(guest)
# Encola un trabajo para que se realice dentro de 1 semana.
GuestsCleanupJob.set(wait: 1.week).perform_later(guest)
# `perform_now` y `perform_later` llamarán a `perform` internamente, por lo que
# puedes pasar tantos argumentos como se definan en este último.
GuestsCleanupJob.perform_later(guest1, guest2, filter: 'some_filter')

¡Eso es todo!

3.3 Encolar Trabajos en Masa

Puedes encolar múltiples trabajos a la vez usando perform_all_later. Para más detalles, consulta Encolado en Masa.

4 Ejecución de Trabajos

Para encolar y ejecutar trabajos en producción necesitas configurar un sistema de cola, es decir, debes decidir sobre una librería de cola de terceros que Rails debería usar. Rails en sí solo proporciona un sistema de colas en el mismo proceso, que solo mantiene los trabajos en RAM. Si el proceso falla o la máquina se reinicia, entonces todos los trabajos pendientes se pierden con el backend asincrónico por defecto. Esto puede ser suficiente para aplicaciones más pequeñas o trabajos no críticos, pero la mayoría de las aplicaciones en producción necesitarán elegir un backend persistente.

4.1 Backends

Active Job tiene adaptadores integrados para múltiples backends de cola (Sidekiq, Resque, Delayed Job, y otros). Para obtener una lista actualizada de los adaptadores, consulta la Documentación de la API para ActiveJob::QueueAdapters.

4.2 Configurando el Backend

Puedes configurar fácilmente tu backend de cola con config.active_job.queue_adapter:

# config/application.rb
module YourApp
  class Application < Rails::Application
    # Asegúrate de tener la gema del adaptador en tu Gemfile
    # y sigue las instrucciones específicas de instalación
    # y despliegue del adaptador.
    config.active_job.queue_adapter = :sidekiq
  end
end

También puedes configurar tu backend por trabajo:

class GuestsCleanupJob < ApplicationJob
  self.queue_adapter = :resque
  # ...
end

# Ahora tu trabajo usará `resque` como su adaptador de cola backend, sobrescribiendo lo que
# fue configurado en `config.active_job.queue_adapter`.

4.3 Iniciando el Backend

Dado que los trabajos se ejecutan en paralelo a tu aplicación Rails, la mayoría de las librerías de colas requieren que inicies un servicio de cola específico de la librería (además de iniciar tu aplicación Rails) para que el procesamiento de trabajos funcione. Consulta la documentación de la librería para instrucciones sobre cómo iniciar tu backend de cola.

Aquí hay una lista no exhaustiva de documentación:

5 Colas

La mayoría de los adaptadores soportan múltiples colas. Con Active Job puedes programar el trabajo para que se ejecute en una cola específica usando queue_as:

class GuestsCleanupJob < ApplicationJob
  queue_as :low_priority
  # ...
end

Puedes prefijar el nombre de la cola para todos tus trabajos usando config.active_job.queue_name_prefix en application.rb:

# config/application.rb
module YourApp
  class Application < Rails::Application
    config.active_job.queue_name_prefix = Rails.env
  end
end
# app/jobs/guests_cleanup_job.rb
class GuestsCleanupJob < ApplicationJob
  queue_as :low_priority
  # ...
end

# Ahora tu trabajo se ejecutará en la cola production_low_priority en tu
# entorno de producción y en staging_low_priority
# en tu entorno de pruebas

También puedes configurar el prefijo por trabajo.

class GuestsCleanupJob < ApplicationJob
  queue_as :low_priority
  self.queue_name_prefix = nil
  # ...
end

# Ahora la cola de tu trabajo no tendrá prefijo, sobrescribiendo lo que
# fue configurado en `config.active_job.queue_name_prefix`.

El delimitador por defecto del prefijo del nombre de la cola es '_'. Esto se puede cambiar configurando config.active_job.queue_name_delimiter en application.rb:

# config/application.rb
module YourApp
  class Application < Rails::Application
    config.active_job.queue_name_prefix = Rails.env
    config.active_job.queue_name_delimiter = '.'
  end
end
# app/jobs/guests_cleanup_job.rb
class GuestsCleanupJob < ApplicationJob
  queue_as :low_priority
  # ...
end

# Ahora tu trabajo se ejecutará en la cola production.low_priority en tu
# entorno de producción y en staging.low_priority
# en tu entorno de pruebas

Para controlar la cola desde el nivel del trabajo, puedes pasar un bloque a queue_as. El bloque se ejecutará en el contexto del trabajo (por lo que puede acceder a self.arguments), y debe devolver el nombre de la cola:

class ProcessVideoJob < ApplicationJob
  queue_as do
    video = self.arguments.first
    if video.owner.premium?
      :premium_videojobs
    else
      :videojobs
    end
  end

  def perform(video)
    # Procesar video
  end
end
ProcessVideoJob.perform_later(Video.last)

Si deseas tener más control sobre en qué cola se ejecutará un trabajo, puedes pasar una opción :queue a set:

MyJob.set(queue: :another_queue).perform_later(record)

NOTA: Asegúrate de que tu backend de cola "escuche" en el nombre de tu cola. Para algunos backends necesitas especificar las colas a las que escuchar.

6 Prioridad

Algunos adaptadores soportan prioridades a nivel de trabajo, donde los trabajos pueden ser priorizados en relación con otros en la cola o a través de todas las colas.

Puedes programar un trabajo para que se ejecute con una prioridad específica usando queue_with_priority:

class GuestsCleanupJob < ApplicationJob
  queue_with_priority 10
  # ...
end

Ten en cuenta que esto no tendrá ningún efecto con adaptadores que no soporten prioridades.

Similar a queue_as, también puedes pasar un bloque a queue_with_priority para que sea evaluado en el contexto del trabajo:

class ProcessVideoJob < ApplicationJob
  queue_with_priority do
    video = self.arguments.first
    if video.owner.premium?
      0
    else
      10
    end
  end

  def perform(video)
    # Procesar video
  end
end
ProcessVideoJob.perform_later(Video.last)

También puedes pasar una opción :priority a set:

MyJob.set(priority: 50).perform_later(record)

NOTA: Si un número de prioridad más bajo se ejecuta antes o después de un número de prioridad más alto depende de la implementación del adaptador. Consulta la documentación de tu backend para obtener más información. Se anima a los autores de adaptadores a tratar un número más bajo como más importante.

7 Callbacks

Active Job proporciona ganchos para activar lógica durante el ciclo de vida de un trabajo. Al igual que otros callbacks en Rails, puedes implementar los callbacks como métodos ordinarios y usar un método de clase estilo macro para registrarlos como callbacks:

class GuestsCleanupJob < ApplicationJob
  queue_as :default

  around_perform :around_cleanup

  def perform
    # Hacer algo después
  end

  private
    def around_cleanup
      # Hacer algo antes de perform
      yield
      # Hacer algo después de perform
    end
end

Los métodos de clase estilo macro también pueden recibir un bloque. Considera usar este estilo si el código dentro de tu bloque es tan corto que cabe en una sola línea. Por ejemplo, podrías enviar métricas para cada trabajo encolado:

class ApplicationJob < ActiveJob::Base
  before_enqueue { |job| $statsd.increment "#{job.class.name.underscore}.enqueue" }
end

7.1 Callbacks Disponibles

Ten en cuenta que cuando encolas trabajos en masa usando perform_all_later, callbacks como around_enqueue no se activarán en los trabajos individuales. Consulta Callbacks de Encolado en Masa.

8 Encolado en Masa

Puedes encolar múltiples trabajos a la vez usando perform_all_later. El encolado en masa reduce el número de viajes de ida y vuelta al almacén de datos de la cola (como Redis o una base de datos), haciéndolo una operación más eficiente que encolar los mismos trabajos individualmente.

perform_all_later es una API de nivel superior en Active Job. Acepta instancias de trabajos como argumentos (ten en cuenta que esto es diferente de perform_later). perform_all_later llama a perform internamente. Los argumentos pasados a new se pasarán a perform cuando finalmente se llame.

Aquí tienes un ejemplo llamando a perform_all_later con instancias de GuestCleanupJob:

# Crear trabajos para pasar a `perform_all_later`.
# Los argumentos para `new` se pasan a `perform`
guest_cleanup_jobs = Guest.all.map { |guest| GuestsCleanupJob.new(guest) }

# Encolará un trabajo separado para cada instancia de `GuestCleanupJob`
ActiveJob.perform_all_later(guest_cleanup_jobs)

# También puedes usar el método `set` para configurar opciones antes de encolar trabajos en masa.
guest_cleanup_jobs = Guest.all.map { |guest| GuestsCleanupJob.new(guest).set(wait: 1.day) }

ActiveJob.perform_all_later(guest_cleanup_jobs)

perform_all_later registra el número de trabajos encolados con éxito, por ejemplo, si Guest.all.map arriba resultó en 3 guest_cleanup_jobs, registraría Enqueued 3 jobs to Async (3 GuestsCleanupJob) (asumiendo que todos fueron encolados).

El valor de retorno de perform_all_later es nil. Ten en cuenta que esto es diferente de perform_later, que devuelve la instancia de la clase de trabajo encolada.

8.1 Encolar Múltiples Clases de Active Job

Con perform_all_later, también es posible encolar diferentes instancias de clases de Active Job en la misma llamada. Por ejemplo:

class ExportDataJob < ApplicationJob
  def perform(*args)
    # Exportar datos
  end
end

class NotifyGuestsJob < ApplicationJob
  def perform(*guests)
    # Enviar correos a los invitados
  end
end

# Instanciar instancias de trabajos
cleanup_job = GuestsCleanupJob.new(guest)
export_job = ExportDataJob.new(data)
notify_job = NotifyGuestsJob.new(guest)

# Encola instancias de trabajos de múltiples clases a la vez
ActiveJob.perform_all_later(cleanup_job, export_job, notify_job)

8.2 Callbacks de Encolado en Masa

Cuando encolas trabajos en masa usando perform_all_later, callbacks como around_enqueue no se activarán en los trabajos individuales. Este comportamiento está en línea con otros métodos en masa de Active Record. Dado que los callbacks se ejecutan en trabajos individuales, no pueden aprovechar la naturaleza en masa de este método.

Sin embargo, el método successfully_enqueued? puede usarse para averiguar si un trabajo dado fue encolado con éxito.

8.3 Soporte de Backend de Cola

Para perform_all_later, el encolado en masa necesita ser respaldado por el backend de cola.

Por ejemplo, Sidekiq tiene un método push_bulk, que puede enviar una gran cantidad de trabajos a Redis y prevenir la latencia de red de ida y vuelta. GoodJob también soporta el encolado en masa con el método GoodJob::Bulk.enqueue. El nuevo backend de cola Solid Queue ha añadido soporte para el encolado en masa también.

Si el backend de cola no soporta el encolado en masa, perform_all_later encolará trabajos uno por uno.

9 Action Mailer

Uno de los trabajos más comunes en una aplicación web moderna es enviar correos electrónicos fuera del ciclo de solicitud-respuesta, para que el usuario no tenga que esperar. Active Job está integrado con Action Mailer para que puedas enviar correos electrónicos fácilmente de manera asincrónica:

# Si deseas enviar el correo ahora usa #deliver_now
UserMailer.welcome(@user).deliver_now

# Si deseas enviar el correo a través de Active Job usa #deliver_later
UserMailer.welcome(@user).deliver_later

NOTA: Usar la cola asincrónica desde una tarea Rake (por ejemplo, para enviar un correo usando .deliver_later) generalmente no funcionará porque Rake probablemente terminará, causando que el grupo de hilos en el mismo proceso sea eliminado, antes de que se procesen todos/alguno de los correos .deliver_later. Para evitar este problema, usa .deliver_now o ejecuta una cola persistente en desarrollo.

10 Internacionalización

Cada trabajo usa el I18n.locale establecido cuando se creó el trabajo. Esto es útil si envías correos electrónicos de manera asincrónica:

I18n.locale = :eo

UserMailer.welcome(@user).deliver_later # El correo será localizado a esperanto.

11 Tipos Soportados para Argumentos

ActiveJob soporta los siguientes tipos de argumentos por defecto:

  • Tipos básicos (NilClass, String, Integer, Float, BigDecimal, TrueClass, FalseClass)
  • Symbol
  • Date
  • Time
  • DateTime
  • ActiveSupport::TimeWithZone
  • ActiveSupport::Duration
  • Hash (Las claves deben ser de tipo String o Symbol)
  • ActiveSupport::HashWithIndifferentAccess
  • Array
  • Range
  • Module
  • Class

11.1 GlobalID

Active Job soporta GlobalID para parámetros. Esto hace posible pasar objetos Active Record en vivo a tu trabajo en lugar de pares clase/id, que luego tienes que deserializar manualmente. Antes, los trabajos se verían así:

class TrashableCleanupJob < ApplicationJob
  def perform(trashable_class, trashable_id, depth)
    trashable = trashable_class.constantize.find(trashable_id)
    trashable.cleanup(depth)
  end
end

Ahora simplemente puedes hacer:

class TrashableCleanupJob < ApplicationJob
  def perform(trashable, depth)
    trashable.cleanup(depth)
  end
end

Esto funciona con cualquier clase que mezcle GlobalID::Identification, que por defecto se ha mezclado en las clases Active Record.

11.2 Serializadores

Puedes extender la lista de tipos de argumentos soportados. Solo necesitas definir tu propio serializador:

# app/serializers/money_serializer.rb
class MoneySerializer < ActiveJob::Serializers::ObjectSerializer
  # Verifica si un argumento debe ser serializado por este serializador.
  def serialize?(argument)
    argument.is_a? Money
  end

  # Convierte un objeto a una representación más simple usando tipos de objetos soportados.
  # La representación recomendada es un Hash con una clave específica. Las claves solo pueden ser de tipos básicos.
  # Deberías llamar a `super` para añadir el tipo de serializador personalizado al hash.
  def serialize(money)
    super(
      "amount" => money.amount,
      "currency" => money.currency
    )
  end

  # Convierte un valor serializado en un objeto adecuado.
  def deserialize(hash)
    Money.new(hash["amount"], hash["currency"])
  end
end

y añade este serializador a la lista:

# config/initializers/custom_serializers.rb
Rails.application.config.active_job.custom_serializers << MoneySerializer

Ten en cuenta que la recarga de código cargado automáticamente durante la inicialización no está soportada. Por lo tanto, se recomienda configurar los serializadores para que se carguen solo una vez, por ejemplo, modificando config/application.rb de esta manera:

# config/application.rb
module YourApp
  class Application < Rails::Application
    config.autoload_once_paths << Rails.root.join('app', 'serializers')
  end
end

12 Excepciones

Las excepciones levantadas durante la ejecución del trabajo pueden manejarse con rescue_from:

class GuestsCleanupJob < ApplicationJob
  queue_as :default

  rescue_from(ActiveRecord::RecordNotFound) do |exception|
    # Hacer algo con la excepción
  end

  def perform
    # Hacer algo después
  end
end

Si una excepción de un trabajo no se rescata, entonces el trabajo se considera "fallido".

12.1 Reintentando o Descartando Trabajos Fallidos

Un trabajo fallido no se reintentará, a menos que se configure de otra manera.

Es posible reintentar o descartar un trabajo fallido usando retry_on o discard_on, respectivamente. Por ejemplo:

class RemoteServiceJob < ApplicationJob
  retry_on CustomAppException # por defecto espera 3s, 5 intentos

  discard_on ActiveJob::DeserializationError

  def perform(*args)
    # Podría lanzar CustomAppException o ActiveJob::DeserializationError
  end
end

12.2 Deserialización

GlobalID permite serializar objetos Active Record completos pasados a #perform.

Si un registro pasado se elimina después de que el trabajo sea encolado pero antes de que se llame al método #perform, Active Job lanzará una excepción ActiveJob::DeserializationError.

13 Pruebas de Trabajos

Puedes encontrar instrucciones detalladas sobre cómo probar tus trabajos en la guía de pruebas.

14 Depuración

Si necesitas ayuda para averiguar de dónde vienen los trabajos, puedes habilitar registro detallado.


Comentarios

Se te anima a ayudar a mejorar la calidad de esta guía.

Por favor contribuye si ves algún error tipográfico o errores fácticos. Para comenzar, puedes leer nuestra sección de contribuciones a la documentación.

También puedes encontrar contenido incompleto o cosas que no están actualizadas. Por favor agrega cualquier documentación faltante para main. Asegúrate de revisar Guías Edge primero para verificar si los problemas ya están resueltos o no en la rama principal. Revisa las Guías de Ruby on Rails para estilo y convenciones.

Si por alguna razón detectas algo que corregir pero no puedes hacerlo tú mismo, por favor abre un issue.

Y por último, pero no menos importante, cualquier tipo de discusión sobre la documentación de Ruby on Rails es muy bienvenida en el Foro oficial de Ruby on Rails.