Aprimorando o Envio de E-mails no Rails com Sidekiq - Parte 2

Aprimorando o Envio de E-mails no Rails com Sidekiq - Parte 2
Photo by Hermes Rivera / Unsplash

O Sidekiq é uma ferramenta poderosa para processamento de tarefas em background no Ruby on Rails. Neste post, vamos explorar como usar o Sidekiq para enviar e-mails de forma assíncrona, aumentando a eficiência e a escalabilidade da aplicação.

Jobs e Workers são conceitos centrais em sistemas de processamento de tarefas em background, como o Sidekiq, que é amplamente utilizado em aplicações Ruby on Rails. Vamos entender o que são e como o Sidekiq funciona por baixo dos panos.

O que são Jobs e Workers?

  1. Jobs:
    • Um "Job" é uma unidade de trabalho que precisa ser executada. Em termos de programação, é uma tarefa que você quer executar, mas não necessariamente de forma imediata ou no mesmo processo que seu aplicativo web principal.
    • Jobs podem incluir tarefas como enviar e-mails, processar arquivos, realizar cálculos pesados, etc.
    • Em Rails, um job é geralmente definido como uma classe com um método perform que contém a lógica para a tarefa que você deseja executar.
  2. Workers:
    • Um "Worker" é um processo que executa os jobs. Ele fica ouvindo uma fila de jobs e, quando um job é disponibilizado, o worker o pega e executa o código definido no job.
    • Em sistemas como o Sidekiq, você pode ter vários workers executando simultaneamente, permitindo o processamento paralelo de múltiplos jobs.

Processos do Sistema Operacional e Threads no Contexto do Sidekiq

  1. Processos do Sistema Operacional:
    • Um processo é uma instância de um programa em execução. No contexto do Sidekiq, cada worker é executado como um processo separado.
    • Isso significa que, para o sistema operacional, cada worker do Sidekiq é um programa independente com seu próprio espaço de memória e conjunto de recursos.
    • Por exemplo, se você iniciar três workers do Sidekiq, o sistema operacional verá três processos distintos, cada um representando um desses workers.
  2. Threads Dentro de um Processo:
    • Uma thread é uma sequência de instruções que pode ser executada de forma independente dentro de um processo.
    • Dentro de cada processo do worker do Sidekiq, podem existir várias threads. Cada uma dessas threads pode executar um job por vez.
    • As threads compartilham o mesmo espaço de memória do processo ao qual pertencem, o que as torna mais leves e rápidas de criar e gerenciar do que processos separados.
  3. Exemplo Prático:
    • Imagine que você configurou o Sidekiq para ter 2 workers, e cada worker pode ter até 5 threads.
    • Isso resulta em dois processos separados no sistema operacional (dois workers do Sidekiq).
    • Dentro de cada um desses processos, podem existir até 5 threads trabalhando simultaneamente.
    • Se você enfileirar 10 jobs, cada worker (processo) pode pegar até 5 desses jobs e processá-los em paralelo, usando suas threads.
  4. Aproveitando a Concorrência:
    • A capacidade de cada worker processar vários jobs simultaneamente através de múltiplas threads é o que permite ao Sidekiq ser eficiente e rápido.
    • Por exemplo, se um job está aguardando uma resposta de um serviço externo (uma operação I/O bound), a thread que está executando esse job pode ficar ociosa. Enquanto isso, outras threads no mesmo worker podem continuar processando outros jobs.
  5. Visualizando o Processo:
    • Você pode visualizar isso como uma fábrica com duas linhas de montagem (dois workers). Cada linha de montagem tem cinco trabalhadores (cinco threads). Cada trabalhador pode montar um produto (processar um job) por vez. Mesmo que um trabalhador esteja esperando por uma peça (I/O bound), os outros podem continuar montando outros produtos.

Vou repetir, pois você precisará desses conhecimentos acima para entender ele melhor. :)

a view of a city at night from the top of a hill
Photo by ZHENYU LUO / Unsplash

Como o Sidekiq Funciona?

O Sidekiq utiliza Redis, um armazenamento de estrutura de dados em memória, para gerenciar filas de jobs. Aqui está um resumo de como ele funciona:

  1. Enfileiramento de Jobs:
    • Quando um job é criado (por exemplo, MyJob.perform_async), ele é serializado e enviado para uma fila no Redis. O Sidekiq suporta várias filas com diferentes prioridades.
  2. Processamento de Jobs:

Claro, vamos detalhar mais sobre o processamento de jobs no Sidekiq, focando em como os workers operam e como a concorrência é gerenciada.

red and yellow thread in needle
Photo by amirali mirhashemian / Unsplash

Processamento de Jobs no Sidekiq

  1. Workers Ouvindo as Filas:
    • O Sidekiq utiliza o Redis para armazenar as filas de jobs. Cada fila pode ser configurada com diferentes prioridades e características.
    • Os workers do Sidekiq ficam constantemente verificando (ou "ouvindo") essas filas no Redis para ver se há novos jobs disponíveis para processamento.
  2. Pegando Jobs da Fila:
    • Quando um novo job é enfileirado (por exemplo, através de um comando como MyJob.perform_async), ele é adicionado à fila correspondente no Redis.
    • Um worker que está ouvindo essa fila detecta o novo job e o "pega" para processamento. Isso significa que o worker retira o job da fila e começa a executar o código definido no método perform do job.
  3. Execução Concorrente com Threads:
    • Cada worker do Sidekiq roda em seu próprio processo do sistema operacional. Dentro de cada processo de worker, o Sidekiq pode executar várias threads.
    • Cada thread pode processar um job por vez. Isso permite que um único worker processe múltiplos jobs simultaneamente, aproveitando a concorrência.
    • Por exemplo, se um worker do Sidekiq é configurado para usar 10 threads, ele pode processar até 10 jobs ao mesmo tempo.
  4. Exemplo Prático:
    • Imagine que você tem um aplicativo Rails que envia e-mails em massa. Cada envio de e-mail é um job.
    • Quando um usuário dispara uma ação que envia e-mails, vários jobs de envio de e-mail são enfileirados no Redis.
    • Os workers do Sidekiq, cada um possivelmente com várias threads, começam a processar esses jobs. Enquanto uma thread está esperando a resposta do servidor de e-mail (uma operação I/O bound), outras threads no mesmo worker podem continuar processando outros jobs.
  5. Vantagens da Concorrência com Threads:
    • Usar threads em vez de processos separados para cada job é mais eficiente em termos de uso de memória e tempo de inicialização.
    • Isso é particularmente vantajoso em operações I/O bound, onde a thread pode ficar ociosa esperando por uma resposta externa, permitindo que outras threads no mesmo processo continuem trabalhando.
  6. Considerações de Thread-Safety:
    • Como o Sidekiq usa threads, é crucial que os jobs sejam "thread-safe". Isso significa que eles devem ser escritos de forma a evitar condições de corrida e outros problemas relacionados à concorrência.
    • Por exemplo, se um job modifica uma variável global ou um recurso compartilhado, ele precisa garantir que essa modificação não cause conflitos com outros jobs que podem estar executando simultaneamente em outras threads.
  7. Concorrência e Threads:
    • O Sidekiq é eficiente porque usa threads em vez de processos para o paralelismo. Isso significa que ele pode executar muitos jobs em paralelo dentro de um único processo, economizando memória e tempo de inicialização.
    • No entanto, isso também significa que os jobs devem ser "thread-safe" para evitar condições de corrida e outros problemas relacionados à concorrência.
  8. Re-tentativas e Monitoramento:
    • Se um job falha (por exemplo, devido a um erro temporário), o Sidekiq pode automaticamente tentar executá-lo novamente após um intervalo de tempo.
    • O Sidekiq também fornece um painel de controle para monitorar e gerenciar jobs, incluindo filas, histórico de jobs, e jobs falhados.
  9. Escalabilidade:
    • Você pode escalar o processamento de jobs horizontalmente adicionando mais workers. Isso pode ser feito aumentando o número de threads por worker ou adicionando mais máquinas (cada uma com seu próprio conjunto de workers do Sidekiq).

Vamos para a prática

man and woman doing karate on road at daytime
Photo by Thao LEE / Unsplash

Configuração do Ambiente com Docker

  1. Adicione o Sidekiq ao seu Gemfile:
gem 'sidekiq'
  1. Crie um arquivo docker-compose.yml:
version: '3'
services:
 redis:
   image: redis
   ports:
     - "6379:6379"
 web:
   build: .
   command: bundle exec puma -C config/puma.rb
   volumes:
     - .:/myapp
   ports:
     - "3000:3000"
   depends_on:
     - redis
 worker:
   build: .
   command: bundle exec sidekiq
   volumes:
     - .:/myapp
   depends_on:
     - redis

Configurando o Sidekiq

  1. Configure o Sidekiq como o adaptador de fila:

Em config/application.rb, adicione:

config.active_job.queue_adapter = :sidekiq
  1. Crie um arquivo de configuração para o Sidekiq:
    Em config/sidekiq.yml, defina as filas e outras configurações.

Implementando o Envio de E-mails com Sidekiq

  1. Modifique o método de envio no Mailer:.

Em app/mailers/order_mailer.rb, use deliver_later:

OrderMailer.order_confirmation(user, order).deliver_later
  1. Crie um Job para o envio de e-mails:
class EmailJob < ApplicationJob
 queue_as :default

 def perform(user, order)
   OrderMailer.order_confirmation(user, order).deliver_now
 end
end

Escrevendo Testes

  1. Adicione a configuração do Sidekiq para testes:

Em spec/rails_helper.rb:

require 'sidekiq/testing'
Sidekiq::Testing.fake!
  1. Escreva testes para o Job:
# spec/jobs/email_job_spec.rb
require 'rails_helper'

RSpec.describe EmailJob, type: :job do
  include ActiveJob::TestHelper

  let(:user) { create(:user) }
  let(:order) { create(:order, user: user) }

  it 'enqueues email job' do
    expect {
      EmailJob.perform_later(user, order)
    }.to have_enqueued_job(EmailJob)
      .with(user, order)
      .on_queue("default")
  end

  it 'executes perform' do
    perform_enqueued_jobs do
      EmailJob.perform_later(user, order)
    end

    expect(ActionMailer::Base.deliveries.size).to eq 1
  end

  after do
    clear_enqueued_jobs
    clear_performed_jobs
  end
end

Executando e Depurando

  1. Inicie os serviços com Docker:
docker-compose up
  1. Para depurar, acesse o terminal do container do Sidekiq:
docker-compose exec worker /bin/bash
  1. Use ferramentas de depuração como byebug ou pry dentro do contexto do Sidekiq.

Por que usar Sidekiq para E-mails?

  • Performance: O Sidekiq processa tarefas em background, liberando o servidor web para atender outras requisições mais rapidamente.
  • Escalabilidade: Facilita o escalonamento horizontal da aplicação.
  • Confiabilidade: Possui um sistema robusto de re-tentativas e monitoramento.

O uso do Sidekiq para o envio de e-mails em uma aplicação Rails oferece vantagens significativas em termos de performance e escalabilidade. Com a configuração em Docker, você obtém um ambiente isolado e replicável, facilitando o desenvolvimento e a depuração.

Read more