edge
Mais em rubyonrails.org: Mais Ruby on Rails

Noções básicas do Active Job

Este guia fornece tudo o que você precisa para começar a criar, enfileirar e executar jobs em segundo plano.

Após ler este guia, você saberá:

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 tipo String ou Symbol)
  • 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.