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