edge
Más en rubyonrails.org: Más Ruby on Rails

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 hacer que se ejecuten en una variedad de backend de encolamiento. Estos trabajos pueden ser desde limpiezas programadas regularmente, hasta cargos de facturación, hasta envíos de correo. Cualquier cosa que se pueda dividir en pequeñas unidades de trabajo y ejecutar en paralelo, en realidad.

2 El propósito de Active Job

El punto principal es asegurarse de que todas las aplicaciones de Rails tengan una infraestructura de trabajos en su lugar. Luego 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 backend de encolamiento se convierte en más una preocupación operativa, entonces. Y podrás cambiar entre ellos sin tener que reescribir tus trabajos.

NOTA: Rails por defecto viene con una implementación de encolamiento asíncrono que ejecuta trabajos con un grupo de hilos en el proceso. Los trabajos se ejecutarán de forma asíncrona, pero cualquier trabajo en la cola se eliminará al reiniciar.

3 Creando un trabajo

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 ejecutará 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.

Esto es cómo se ve un trabajo:

class GuestsCleanupJob < ApplicationJob
  queue_as :default

  def perform(*guests)
    # Hacer algo más tarde
  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 más tarde
  end
end

3.2 Encolar el trabajo

Encola un trabajo usando perform_later y, opcionalmente, set. Así:

# Encola un trabajo para que se realice tan pronto como el sistema de encolamiento 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 definieron en este último.
GuestsCleanupJob.perform_later(guest1, guest2, filter: 'some_filter')

¡Eso es todo!

4 Ejecución de trabajos

Para encolar y ejecutar trabajos en producción, necesitas configurar un backend de encolamiento, es decir, debes decidir qué biblioteca de encolamiento de terceros debe usar Rails. Rails en sí solo proporciona un sistema de encolamiento en el proceso, que solo mantiene los trabajos en RAM. Si el proceso se bloquea o la máquina se reinicia, entonces todos los trabajos pendientes se pierden con el backend asíncrono predeterminado. Esto puede ser aceptable para aplicaciones más pequeñas o trabajos no críticos, pero la mayoría de las aplicaciones en producción deberán elegir un backend persistente.

4.1 Backends

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

4.2 Configuración del backend

Puedes configurar fácilmente tu backend de encolamiento 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 en función de cada trabajo:

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

# Ahora tu trabajo usará `resque` como su adaptador de cola de backend, anulando lo que
# se configuró 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 bibliotecas de encolamiento requieren que inicies un servicio de encolamiento específico de la biblioteca (además de iniciar tu aplicación Rails) para que el procesamiento de trabajos funcione. Consulta la documentación de la biblioteca para obtener instrucciones sobre cómo iniciar tu backend de encolamiento.

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

5 Colas

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

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

Puedes agregar un prefijo al nombre de la cola para todos tus trabajos utilizando 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 staging

También puedes configurar el prefijo de forma individual para cada trabajo.

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

# Ahora la cola de tu trabajo no tendrá prefijo, anulando lo que
# se configuró en `config.active_job.queue_name_prefix`.

El delimitador predeterminado para el 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 staging

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

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

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)
    # Realizar procesamiento de video
  end
end
ProcessVideoJob.perform_later(Video.last)

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

6 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 en estilo macro para registrarlos como callbacks:

class GuestsCleanupJob < ApplicationJob
  queue_as :default

  around_perform :around_cleanup

  def perform
    # Realizar algo más tarde
  end

  private
    def around_cleanup
      # Realizar algo antes de la ejecución
      yield
      # Realizar algo después de la ejecución
    end
end

Los métodos de clase en 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

6.1 Callbacks Disponibles

7 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, por lo que puedes enviar correos electrónicos de forma asíncrona fácilmente:

# Si quieres enviar el correo electrónico ahora, usa #deliver_now
UserMailer.welcome(@user).deliver_now

# Si quieres enviar el correo electrónico a través de Active Job, usa #deliver_later
UserMailer.welcome(@user).deliver_later

NOTA: Usar la cola asíncrona desde una tarea de Rake (por ejemplo, para enviar un correo electrónico usando .deliver_later) generalmente no funcionará porque es probable que Rake finalice, lo que causará que el grupo de hilos en proceso se elimine, antes de que se procesen todos los correos electrónicos de .deliver_later. Para evitar este problema, usa .deliver_now o ejecuta una cola persistente en desarrollo.

8 Internacionalización

Cada trabajo utiliza la configuración I18n.locale establecida cuando se creó el trabajo. Esto es útil si envías correos electrónicos de forma asíncrona:

I18n.locale = :eo

UserMailer.welcome(@user).deliver_later # El correo electrónico se localizará al esperanto.

9 Tipos admitidos para los argumentos

ActiveJob admite los siguientes tipos de argumentos de forma predeterminada:

  • 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

9.1 GlobalID

Active Job admite GlobalID para los parámetros. Esto permite pasar objetos de Active Record en vivo a su trabajo en lugar de pares de clase/id, que luego debe 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 puede 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 de Active Record.

9.2 Serializadores

Puede ampliar la lista de tipos de argumentos admitidos. Solo necesita definir su propio serializador:

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

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

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

y agregue este serializador a la lista:

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

Tenga en cuenta que no se admite la recarga automática de código recargable durante la inicialización. 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

10 Excepciones

Las excepciones generadas durante la ejecución del trabajo se pueden manejar 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 más tarde
  end
end

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

10.1 Reintentar o Descartar Trabajos Fallidos

Un trabajo fallido no se volverá a intentar, 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 # espera predeterminada de 3s, 5 intentos

  discard_on ActiveJob::DeserializationError

  def perform(*args)
    # Puede generar CustomAppException o ActiveJob::DeserializationError
  end
end

10.2 Deserialización

GlobalID permite serializar objetos completos de Active Record que se pasan a #perform.

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

11 Pruebas de trabajos

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

12 Depuración

Si necesita ayuda para averiguar de dónde vienen los trabajos, puede habilitar registros detallados.

Comentarios

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

Por favor, contribuye si encuentras algún error tipográfico o factual. Para empezar, puedes leer nuestra contribución a la documentación sección.

También puedes encontrar contenido incompleto o desactualizado. Por favor, añade cualquier documentación faltante para main. Asegúrate de revisar Edge Guides primero para verificar si los problemas ya están resueltos o no en la rama principal. Consulta las Directrices de las Guías de Ruby on Rails para el estilo y las convenciones.

Si por alguna razón encuentras algo que corregir pero no puedes solucionarlo tú mismo, por favor abre un problema.

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