1 O que é o Active Job?
O Active Job é um framework para declarar jobs e executá-los em vários backends de enfileiramento. Esses jobs podem ser desde limpezas programadas regularmente, até cobranças de faturamento, até envios de emails. Qualquer coisa que possa ser dividida em unidades de trabalho menores e executadas em paralelo, na verdade.
2 O objetivo do Active Job
O principal objetivo é garantir que todos os aplicativos Rails tenham uma infraestrutura de jobs em funcionamento. Assim, podemos ter recursos do framework e outras gems construídas em cima disso, sem nos preocuparmos com diferenças de API entre vários executores de jobs, como Delayed Job e Resque. A escolha do backend de enfileiramento se torna mais uma preocupação operacional. E você poderá alternar entre eles sem precisar reescrever seus jobs.
NOTA: O Rails vem por padrão com uma implementação de enfileiramento assíncrono que executa jobs com uma pool de threads em processo. Os jobs serão executados de forma assíncrona, mas qualquer job na fila será descartado ao reiniciar.
3 Criando um Job
Esta seção fornecerá um guia passo a passo para criar um job e enfileirá-lo.
3.1 Criar o Job
O Active Job fornece um gerador do Rails para criar jobs. O seguinte comando criará um job em app/jobs
(com um caso de teste associado 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, pode criar seu próprio arquivo dentro de app/jobs
, apenas certifique-se de que ele herde de ApplicationJob
.
Veja como um job se parece:
class GuestsCleanupJob < ApplicationJob
queue_as :default
def perform(*guests)
# Faça algo mais tarde
end
end
Observe que você pode definir perform
com quantos argumentos quiser.
Se você já tem uma classe abstrata e o nome dela é diferente de ApplicationJob
, você pode passar a opção --parent
para indicar que deseja uma classe abstrata diferente:
$ bin/rails generate job process_payment --parent=payment_job
class ProcessPaymentJob < PaymentJob
queue_as :default
def perform(*args)
# Faça algo mais tarde
end
end
3.2 Enfileirar o Job
Enfileire um job usando perform_later
e, opcionalmente, set
. Assim:
# Enfileire um job para ser executado assim que o sistema de enfileiramento estiver livre.
GuestsCleanupJob.perform_later guest
# Enfileire um job para ser executado amanhã ao meio-dia.
GuestsCleanupJob.set(wait_until: Date.tomorrow.noon).perform_later(guest)
# Enfileire um job para ser executado daqui a 1 semana.
GuestsCleanupJob.set(wait: 1.week).perform_later(guest)
# `perform_now` e `perform_later` chamarão `perform` internamente, então
# você pode passar quantos argumentos forem definidos no último.
GuestsCleanupJob.perform_later(guest1, guest2, filter: 'algum_filtro')
É isso!
4 Execução do Job
Para enfileirar e executar jobs em produção, você precisa configurar um backend de enfileiramento, ou seja, precisa escolher uma biblioteca de enfileiramento de terceiros que o Rails deve usar. O Rails em si fornece apenas um sistema de enfileiramento em processo, que mantém os jobs apenas na memória. Se o processo falhar ou a máquina for reiniciada, todos os jobs pendentes serão perdidos com o backend assíncrono padrão. Isso pode ser aceitável para aplicativos menores ou jobs não críticos, mas a maioria dos aplicativos em produção precisará escolher um backend persistente.
4.1 Backends
O Active Job possui adaptadores integrados para vários backends de enfileiramento (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 facilmente configurar o seu backend de enfileiramento com config.active_job.queue_adapter
:
# config/application.rb
module YourApp
class Application < Rails::Application
# Certifique-se de ter a gem do adaptador no seu Gemfile
# e siga as instruções específicas de instalação
# e implantação do adaptador.
config.active_job.queue_adapter = :sidekiq
end
end
Você também pode configurar o seu backend em uma base por job:
class GuestsCleanupJob < ApplicationJob
self.queue_adapter = :resque
# ...
end
# Agora o seu job usará `resque` como adaptador de fila de backend, substituindo o que
# foi configurado em `config.active_job.queue_adapter`.
4.3 Iniciando o Backend
Como os jobs são executados em paralelo com a sua aplicação Rails, a maioria das bibliotecas de filas de espera requer que você inicie um serviço de fila específico da biblioteca (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 adaptadores suporta várias filas. Com o Active Job, você pode agendar o job para ser executado em uma fila específica usando queue_as
:
class GuestsCleanupJob < ApplicationJob
queue_as :low_priority
# ...
end
Você pode adicionar um prefixo ao nome da fila para todos os seus jobs usando config.active_job.queue_name_prefix
em 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 o seu job será executado na fila production_low_priority no ambiente de produção e na fila staging_low_priority no ambiente de staging
Você também pode configurar o prefixo em um job específico.
class GuestsCleanupJob < ApplicationJob
queue_as :low_priority
self.queue_name_prefix = nil
# ...
end
# Agora a fila do seu job não terá prefixo, substituindo o que foi configurado em `config.active_job.queue_name_prefix`.
O delimitador padrão do prefixo do nome da fila é '_'. Isso pode ser alterado definindo config.active_job.queue_name_delimiter
em 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 o seu job será executado na fila production.low_priority no ambiente de produção e na fila staging.low_priority no ambiente de staging
Se você deseja ter mais controle sobre em qual fila um job será executado, pode passar a opção :queue
para 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 (para que possa acessar self.arguments
) e 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)
# Fazer o processamento do vídeo
end
end
ProcessVideoJob.perform_later(Video.last)
NOTA: Certifique-se de que o backend da fila esteja "ouvindo" o nome da sua fila. Para alguns backends, você precisa especificar as filas a serem ouvidas.
6 Callbacks
O Active Job fornece ganchos para acionar lógica durante o ciclo de vida de um job. Assim como outros callbacks no Rails, você pode implementar os callbacks como métodos comuns e usar um método de classe no estilo macro para registrá-los como callbacks:
class GuestsCleanupJob < ApplicationJob
queue_as :default
around_perform :around_cleanup
def perform
# Fazer algo mais tarde
end
private
def around_cleanup
# Fazer algo antes da execução
yield
# Fazer algo depois da execução
end
end
Os métodos de classe no estilo macro também podem receber um bloco. Considere usar esse estilo se o código dentro do seu bloco for tão curto que caiba em uma ú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 é o envio de emails fora do ciclo de solicitação-resposta, para que o usuário não precise esperar por ele. O Active Job está integrado ao Action Mailer, para que você possa enviar emails de forma assíncrona facilmente:
# Se você quiser enviar o email agora, use #deliver_now
UserMailer.welcome(@user).deliver_now
# Se você quiser enviar o email através do Active Job, use #deliver_later
UserMailer.welcome(@user).deliver_later
NOTA: Usar a fila assíncrona a partir de uma tarefa Rake (por exemplo, para enviar um email usando .deliver_later
) geralmente não funcionará porque o Rake provavelmente será encerrado, fazendo com que o pool de threads em execução seja excluído, antes que todos os emails .deliver_later
sejam processados. Para evitar esse problema, use .deliver_now
ou execute uma fila persistente no ambiente de desenvolvimento.
8 Internacionalização
Cada job usa o I18n.locale
definido quando o job foi criado. Isso é útil se você enviar emails de forma assíncrona:
I18n.locale = :eo
UserMailer.welcome(@user).deliver_later # O email será localizado para Esperanto.
9 Tipos Suportados para Argumentos
O ActiveJob suporta os seguintes tipos de argumentos por padrão:
- Tipos básicos (
NilClass
,String
,Integer
,Float
,BigDecimal
,TrueClass
,FalseClass
) Symbol
Date
Time
DateTime
ActiveSupport::TimeWithZone
ActiveSupport::Duration
Hash
(As chaves devem ser do tipoString
ouSymbol
)ActiveSupport::HashWithIndifferentAccess
Array
Range
Module
Class
9.1 GlobalID
O Active Job suporta o GlobalID para parâmetros. Isso torna possível passar objetos ativos do Active Record para o seu job em vez de pares de classe/id, que você então precisa desserializar manualmente. Antes, os jobs ficavam assim:
class TrashableCleanupJob < ApplicationJob
def perform(trashable_class, trashable_id, depth)
trashable = trashable_class.constantize.find(trashable_id)
trashable.cleanup(depth)
end
end
Agora você pode simplesmente fazer:
class TrashableCleanupJob < ApplicationJob
def perform(trashable, depth)
trashable.cleanup(depth)
end
end
Isso funciona com qualquer classe que mistura GlobalID::Identification
, que
por padrão foi misturado nas classes do Active Record.
9.2 Serializadores
Você pode estender a lista de tipos de argumentos suportados. Você só precisa definir seu próprio serializador:
# app/serializers/money_serializer.rb
class MoneySerializer < ActiveJob::Serializers::ObjectSerializer
# Verifica se um argumento deve ser serializado por este serializador.
def serialize?(argument)
argument.is_a? Money
end
# Converte um objeto para uma representação mais simples usando tipos de objeto suportados.
# A representação recomendada é um Hash com uma chave específica. As chaves podem ser apenas de tipos básicos.
# Você deve chamar `super` para adicionar o tipo de serializador personalizado ao hash.
def serialize(money)
super(
"amount" => money.amount,
"currency" => money.currency
)
end
# Converte o valor serializado em um objeto adequado.
def deserialize(hash)
Money.new(hash["amount"], hash["currency"])
end
end
e adicione este serializador à lista:
# config/initializers/custom_serializers.rb
Rails.application.config.active_job.custom_serializers << MoneySerializer
Observe que o carregamento automático de código recarregável durante a inicialização não é suportado. Portanto, é 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
As exceções lançadas durante a execução do job podem ser tratadas com
rescue_from
:
class GuestsCleanupJob < ApplicationJob
queue_as :default
rescue_from(ActiveRecord::RecordNotFound) do |exception|
# Faça algo com a exceção
end
def perform
# Faça algo depois
end
end
Se uma exceção de um job não for resgatada, então o job é considerado "falhado".
10.1 Retentativa ou Descarte de Jobs Falhados
Um job falhado não será retentado, a menos que configurado de outra forma.
É possível retentar ou descartar um job falhado usando retry_on
ou
discard_on
, respectivamente. Por exemplo:
class RemoteServiceJob < ApplicationJob
retry_on CustomAppException # espera padrão de 3s, 5 tentativas
discard_on ActiveJob::DeserializationError
def perform(*args)
# Pode lançar CustomAppException ou ActiveJob::DeserializationError
end
end
10.2 Desserialização
O GlobalID permite serializar objetos completos do Active Record passados para #perform
.
Se um registro passado for excluído após o job ser enfileirado, mas antes do método #perform
ser chamado, o Active Job lançará uma exceção ActiveJob::DeserializationError
.
11 Testando Jobs
Você pode encontrar instruções detalhadas sobre como testar seus jobs no guia de testes.
12 Depuração
Se você precisar de ajuda para descobrir de onde vêm os jobs, você pode ativar o registro detalhado.
Feedback
Você é incentivado a ajudar a melhorar a qualidade deste guia.
Por favor, contribua se encontrar algum erro de digitação ou factual. Para começar, você pode ler nossa contribuição à documentação seção.
Você também pode encontrar conteúdo incompleto ou desatualizado. Por favor, adicione qualquer documentação ausente para o principal. Certifique-se de verificar Guias Edge primeiro para verificar se os problemas já foram corrigidos ou não no branch principal. Verifique as Diretrizes dos Guias do Ruby on Rails para estilo e convenções.
Se por algum motivo você encontrar algo para corrigir, mas não puder corrigi-lo você mesmo, por favor abra uma issue.
E por último, mas não menos importante, qualquer tipo de discussão sobre a documentação do Ruby on Rails é muito bem-vinda no Fórum oficial do Ruby on Rails.