v7.0.0
Veja mais em rubyonrails.org: Mais Ruby on Rails

Básico de Active Jobs

Este guia oferece a você tudo o que você precisa para começar a criar, enfileirar e executar jobs em background

Depois de ler este guia, você saberá:

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:

# 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 of String or Symbol 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 lista de discussão rubyonrails-docs e nas issues do Guia em português.


dark theme icon