1 O que é Active Job?
O Active Job é um framework para declarar jobs e fazê-los executar em uma variedade de backends de fila. Estes jobs podem ser qualquer coisa, de limpezas programadas regularmente, a cobranças de despesas, a envio de emails. Qualquer coisa que possa ser cortada em pequenas unidades de trabalho e executadas paralelamente, sério.
2 O Propósito do Active Job
O ponto principal é garantir que todas as aplicações Rails terão uma infraestrutura de jobs no lugar. Nós podemos então ter features de frameworks e outras gems construídas em cima dela, sem ter que nos preocupar com diferenças de API entre vários executadores de job como Delayed Job e Resque. Dessa forma, escolher o seu backend de enfileiramento se torna mais uma preocupação operacional. E você poderá alternar entre eles sem ter que reescrever os seus jobs.
Rails por padrão vem com uma implementação de fila assíncrona que executa jobs com uma pool de threads no processo. Jobs serão executados da maneira assíncrona, mas quaisquer jobs na fila serão derrubados ao reinicializar.
3 Criando um Job
Esta seção fornecerá um guia passo a passo para criar um job e enfileirá-lo.
3.1 Crie o Job
O Active Job fornece um gerador Rails para criar jobs. O seguinte criará um
job em app/jobs
(com um teste anexado em 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
Você também pode criar um job que será executado em uma fila específica:
$ bin/rails generate job guests_cleanup --queue urgent
Se você não quiser usar um gerador, você pode criar seu próprio arquivo dentro de
app/jobs
, apenas certifique-se de que herda de ApplicationJob
.
Aqui está a aparência de um job:
class GuestsCleanupJob < ApplicationJob
queue_as :default
def perform(*guests)
# Do something later
end
end
Observe que você pode definir perform
com quantos argumentos quiser.
3.2 Enfileirar o Job
Enfileire um job usando perform_later
e, opcionalmente, set
. Assim:
# Enfileirar um _job_ para ser executado assim que o sistema de enfileiramento estiver
# livre.
GuestsCleanupJob.perform_later guest
# Enfileirar um _job_ para ser executado amanhã ao meio-dia.
GuestsCleanupJob.set(wait_until: Date.tomorrow.noon).perform_later(guest)
# Enfileirar um _job_ para ser executado em 1 semana a partir de agora.
GuestsCleanupJob.set(wait: 1.week).perform_later(guest)
# `perform_now` e `perform_later` irão chamar `perform` por baixo dos panos, então
# você pode passar quantos argumentos forem definidos no último.
GuestsCleanupJob.perform_later(guest1, guest2, filter: 'some_filter')
É isso aí!
4 Execução de Job
Para enfileirar e executar jobs em produção você precisa configurar um backend de filas, ou seja, você precisa decidir por uma lib de enfileiramento de terceiros que o Rails deve usar, o Rails em si fornece apenas um sistema de filas em processo, que só mantém os jobs em memória(RAM). Se o processo quebra ou a máquina é reiniciada, todos os jobs pendentes serão perdidos com o backend assíncrono padrão. Isso pode ser bom para aplicações menores ou jobs não críticos, mas a maioria das aplicações em produção precisará escolher um backend de persistência.
4.1 Backends
O Active Job tem adaptadores built-in para múltiplos backends de fila (Sidekiq
Resque, Delayed Job e outros). Para obter uma lista atualizada dos adaptadores,
consulte a documentação da API para ActiveJob::QueueAdapters
.
4.2 Configurando o Backend
Você pode definir facilmente o backend de fila usando config.active_job.queue_adapter
:
# config/application.rb
module YourApp
class Application < Rails::Application
# Be sure to have the adapter's gem in your Gemfile
# and follow the adapter's specific installation
# and deployment instructions.
config.active_job.queue_adapter = :sidekiq
end
end
Você também pode configurar seu backend por um job base:
class GuestsCleanupJob < ApplicationJob
self.queue_adapter = :resque
# ...
end
# Agora o seu job vai usar `resque` como gerenciados de fials sobrescrevendo o
# que estava configurado em `config.active_job.queue_adapter`.
4.3 Iniciando o Backend
Uma vez que os jobs são executados em paralelo à sua aplicação Rails, a maioria das bibliotecas de filas exigem que você inicie um serviço de enfileiramento específico (além de iniciar sua aplicação Rails) para que o processamento do job funcione. Consulte a documentação da biblioteca para obter instruções sobre como iniciar o backend da fila.
Aqui está uma lista não abrangente de documentação:
5 Filas
A maioria dos adapters suportam múltiplas filas. Com o Active Job você pode agendar
o job para executar em uma fila específica usando queue_as
:
class GuestsCleanupJob < ApplicationJob
queue_as :low_priority
# ...
end
Você pode prefixar o nome da fila para todos os jobs usando
config.active_job.queue_name_prefix
in 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
# Agora seu job irá executar na fila production_low_priority no seu
# ambiente de produção e na staging_low_priority
# no seu ambiente de desenvolvimento
Você também pode configurar o prefixo para cada job.
class GuestsCleanupJob < ApplicationJob
queue_as :low_priority
self.queue_name_prefix = nil
# ...
end
# Agora a fila do seu job não terá um prefixo, sobrescrevendo o que
# foi configurado em `config.active_job.queue_name_prefix`.
O prefixo delimitador padrão de nome de fila é '_'. Isso pode ser alterado configurando o
config.active_job.queue_name_delimiter
no 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
# Agora seu job irá executar na fila production.low_priority no seu
# ambiente de produção e na staging.low_priority
# no seu ambiente de staging
Se você quiser mais controle em qual fila um job será executado, você pode passar
uma opção :queue
ao set
:
MyJob.set(queue: :another_queue).perform_later(record)
Para controlar a fila a partir do nível do job, você pode passar um bloco para queue_as
.
O bloco será executado no contexto do job (o que te permite acessar self.arguments
), e ele
deve retornar o nome da fila:
class ProcessVideoJob < ApplicationJob
queue_as do
video = self.arguments.first
if video.owner.premium?
:premium_videojobs
else
:videojobs
end
end
def perform(video)
# Processa o vídeo
end
end
ProcessVideoJob.perform_later(Video.last)
Tenha certeza de que o seu backend de fila "escuta" o nome da fila. Para alguns backends você precisará especificar as filas a serem "ouvidas".
6 Callbacks
O Active Job fornece Hooks para disparar lógica durante o ciclo de vida de um Job. Assim como em outros callbacks no Rails, você pode implementar callbacks como métodos comuns e usar um método macro de classe para registrá-los como callbacks:
class GuestsCleanupJob < ApplicationJob
queue_as :default
around_perform :around_cleanup
def perform
# Do something later
end
private
def around_cleanup
# Do something before perform
yield
# Do something after perform
end
end
Os métodos macro de classe podem também receber um bloco. Considere usar esse estilo, se o código dentro do bloco for tão pequeno que cabe numa única linha. Por exemplo, você pode enviar métricas para cada job enfileirado:
class ApplicationJob < ActiveJob::Base
before_enqueue { |job| $statsd.increment "#{job.class.name.underscore}.enqueue" }
end
6.1 Callbacks Disponíveis
7 Action Mailer
Um dos jobs mais comuns em uma aplicação web moderna é enviar e-mails fora do ciclo de request-response, para que o usuário não tenha que esperar por ele. O Active Job está integrado com o Action Mailer, o que te permite enviar e-mails assincronamente:
# If you want to send the email now use #deliver_now
UserMailer.welcome(@user).deliver_now
# If you want to send the email through Active Job use #deliver_later
UserMailer.welcome(@user).deliver_later
Se você usar uma fila assíncrona de uma Rake task (por exemplo, para enviar um e-mail
usando .deliver_later
), geralmente não irá funcionar porque a Rake irá provavelmente
terminar, fazendo com que o processo interno do thread pool seja removido, antes de
qualquer ou todos os e-mails serem processados. Para evitar esse problema, use o
.deliver_now
ou execute uma fila persistente no ambiente de desenvolvimento.
8 Internacionalização
Cada job usa o I18n.locale
configurado quando o job é criado. Isso é útil se você
enviar e-mails assincronamente:
I18n.locale = :eo
UserMailer.welcome(@user).deliver_later # O e-mail será localizado para Esperanto.
9 Tipos Suportados por Argumentos
O ActiveJob suporta os seguintes tipos de argumentos por padrão:
- Basic types (
NilClass
,String
,Integer
,Float
,BigDecimal
,TrueClass
,FalseClass
) Symbol
Date
Time
DateTime
ActiveSupport::TimeWithZone
ActiveSupport::Duration
-
Hash
(Keys should be ofString
orSymbol
type) ActiveSupport::HashWithIndifferentAccess
Array
Range
Module
Class
9.1 GlobalID
O Active Job suporta o GlobalID como parâmetros. Isso possibilita passar objetos ativos do Active Record para o job ao invés do par classe/id, que você deve desserializar manualmente. Anteriormente, os jobs eram feitos dessa forma:
class TrashableCleanupJob < ApplicationJob
def perform(trashable_class, trashable_id, depth)
trashable = trashable_class.constantize.find(trashable_id)
trashable.cleanup(depth)
end
end
Agora são feitos dessa forma:
class TrashableCleanupJob < ApplicationJob
def perform(trashable, depth)
trashable.cleanup(depth)
end
end
Isso funciona com qualquer classe que está mesclada em GlobalID::Identification
que, por padrão, já está mesclada dentro das classes Active Record.
9.2 Serializers
Você pode ampliar a lista de tipos de argumentos suportados. Só é preciso definir seu próprio serializer:
# app/serializers/money_serializer.rb
class MoneySerializer < ActiveJob::Serializers::ObjectSerializer
# Checa se um argumento pode ser serializado a partir desse serializer.
def serialize?(argument)
argument.is_a? Money
end
# Converte um objeto em um representante mais simples usando tipos de objetos suportados.
# O representante recomendado é uma Hash com uma key específica. As Keys podem ser somente de tipos básicos.
# Você deve invocar o `super` para adicionar o serializer customizado na hash.
def serialize(money)
super(
"amount" => money.amount,
"currency" => money.currency
)
end
# Converte o valor serializado em um objeto apropriado.
def deserialize(hash)
Money.new(hash["amount"], hash["currency"])
end
end
e adicionar o serializer na lista:
# config/initializers/custom_serializers.rb
Rails.application.config.active_job.custom_serializers << MoneySerializer
Observe que o carregamento automático de código durante a inicialização não é suportado. Assim é recomendado
configurar os serializadores para serem carregados apenas uma vez, por exemplo alterando config/application.rb
assim:
# config/application.rb
module YourApp
class Application < Rails::Application
config.autoload_once_paths << Rails.root.join('app', 'serializers')
end
end
10 Exceções
Exceções criadas durante a execução do job podem ser manipuladas com rescue_from
:
class GuestsCleanupJob < ApplicationJob
queue_as :default
rescue_from(ActiveRecord::RecordNotFound) do |exception|
# Do something with the exception
end
def perform
# Do something later
end
end
Se a exceção não for tratada dentro do job, então o job é reconhecido como "failed (falhado)"
10.1 Reexecução ou Descarte de jobs Falhos
Um job que falhou não será executado novamente, exceto se configurado para fazer isso.
É possível reexecutar ou descartar um job que falhou usando retry_on
ou discard_on
, respectivamente. Por exemplo:
class RemoteServiceJob < ApplicationJob
retry_on CustomAppException # defaults to 3s wait, 5 attempts
discard_on ActiveJob::DeserializationError
def perform(*args)
# Might raise CustomAppException or ActiveJob::DeserializationError
end
end
10.2 Desserialização
GlobalID permite serializar todos os objetos do Active Record passado para o #perform
.
Se um registro for deletado após o job ser enfileirado mas antes do método #perform
ser chamado, o Active Job vai lançar a exceção ActiveJob::DeserializationError
.
11 Testando os Jobs
Você pode encontrar instruções mais detalhadas sobre como testar seus jobs no guia de teste.
Feedback
Você é incentivado a ajudar a melhorar a qualidade deste guia.
Por favor, contribua caso veja quaisquer erros, inclusive erros de digitação. Para começar, você pode ler nossa sessão de contribuindo com a documentação.
Você também pode encontrar conteúdo incompleto ou coisas que não estão atualizadas. Por favor, adicione qualquer documentação em falta na main do Rails. Certifique-se de checar o Edge Guides (en-US) primeiro para verificar se o problema já foi resolvido ou não no branch main. Verifique as Diretrizes do Guia Ruby on Rails para estilo e convenções.
Se, por qualquer motivo, você encontrar algo para consertar, mas não conseguir consertá-lo, por favor abra uma issue no nosso Guia.
E por último, mas não menos importante, qualquer tipo de discussão sobre a documentação do Ruby on Rails é muito bem vinda na forum oficial do Ruby on Rails e nas issues do Guia em português.