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 tipoString
oSymbol
)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.