← voltar aos artigos
05/08/2026Ruby on Rails · Active Job · Sidekiq · Redis · Deduplicação

Deduplicação de jobs com Sidekiq e Redis, sem complicação

Um guia prático de SET NX EX, deduplicação, debounce, retries e idempotência em Rails.

Uma fila agendada passou de mil jobs, e quase metade era repetida. Em outro momento, o mesmo tenant gerou cerca de 1,4 mil recomputações equivalentes. O resultado foi CPU e banco ocupados com o mesmo trabalho, enquanto e-mails e integrações esperavam.

A primeira tentativa foi procurar um job igual antes de cada enqueue. Isso exigia varrer filas, jobs agendados e retries. Quanto maior a fila, mais cara ficava a busca. E havia uma corrida: dois processos podiam procurar ao mesmo tempo, não encontrar nada e publicar a mesma tarefa.

A solução em quatro passos

  • 1. Monte um fingerprint: um identificador curto calculado com SHA-256 a partir da classe do job e de seus argumentos.
  • 2. Tente reservar uma chave no Redis antes de publicar o job.
  • 3. Se a reserva já existir, não publique a duplicata.
  • 4. Se o job terminar com sucesso, libere a chave; se falhar, preserve-a para o retry ou até o TTL expirar.
AntesDepois
Varrer todas as filasConsultar uma chave direta no Redis
Custo cresce com a filaUma operação curta por tentativa
Dois processos podem publicarSó a primeira reserva vence
Regra espalhadaRegra no job base

SET NX EX, palavra por palavra

redis
SET jobs:dedup:<digest> 1 NX EX 5400
  • SET grava um valor no Redis.
  • key é a chave jobs:dedup:<digest>, como uma ficha de reserva para aquele trabalho.
  • value é 1 neste exemplo didático: apenas indica que a reserva existe.
  • NX significa “somente se não existir”. Só o primeiro produtor recebe OK.
  • EX define a expiração em segundos.
  • ttl é o tempo de vida da chave. Ele funciona como a validade de uma senha temporária.

O Redis executa essa decisão de uma vez. Não existe intervalo entre “verificar” e “reservar”. TTL significa tempo de vida: quando ele acaba, o Redis remove a chave e evita um bloqueio permanente.

Deduplicação genérica com Active Job

Fingerprint é a assinatura curta que representa “mesma classe e mesmos argumentos”. O exemplo usa callbacks do Active Job e a conexão redis-client entregue por Sidekiq.redis.

ruby
require "digest"
require "json"

class DeduplicatedJob < ApplicationJob
  DEDUP_TTL = 90.minutes.to_i

  before_enqueue :reserve_once, unless: :retrying_existing_job?
  after_perform :release_reservation

  private

  def dedup_key
    payload = JSON.generate([self.class.name, arguments])
    digest = Digest::SHA256.hexdigest(payload)
    "jobs:dedup:#{digest}"
  end

  def retrying_existing_job?
    executions.positive?
  end

  def reserve_once
    acquired = Sidekiq.redis do |connection|
      connection.call(
        "SET", dedup_key, "1", "NX", "EX", DEDUP_TTL
      ) == "OK"
    end

    throw(:abort) unless acquired
  rescue RedisClient::Error => error
    Rails.logger.warn("dedup unavailable: #{error.class}")
    # Fail-open: Redis failed, so enqueue the job anyway.
  end

  def release_reservation
    Sidekiq.redis { |connection| connection.call("DEL", dedup_key) }
  rescue RedisClient::Error => error
    Rails.logger.warn("dedup release failed: #{error.class}")
  end
end

class RefreshProjectionJob < DeduplicatedJob
  queue_as :default
  sidekiq_options retry: 4

  def perform(tenant_id, period)
    ProjectionRefresher.call(tenant_id:, period:)
  end
end

RefreshProjectionJob.perform_later(42, "2026-08")

before_enqueue tenta reservar a chave. Uma duplicata recebe false do Redis e o callback cancela o enqueue. O after_perform só roda após sucesso e libera a reserva. Se perform falhar, a chave permanece durante os retries ou até o TTL. Um retry do próprio Active Job reutiliza o job existente e, por executions, não tenta adquirir a chave outra vez.

Fail-open significa continuar o enqueue quando o Redis está indisponível. Pode haver duplicata, mas o trabalho não é perdido. Isso é adequado quando repetir é menos grave que deixar de executar.

Quando endurecer a solução

O valor constante 1 simplifica o exemplo, mas tem um limite: se o TTL vencer durante um job lento, outro produtor pode criar uma nova reserva e o primeiro job pode apagá-la. Em operações críticas, grave um token aleatório e remova a chave somente se o token ainda for o seu, com uma operação condicional (por exemplo, Lua). Se perder um comando for inaceitável, uma outbox no banco é uma evolução possível.

Deduplicação não é debounce

Deduplicação remove cópias com a mesma classe e os mesmos argumentos. Debounce agrupa vários eventos próximos que pedem o mesmo resultado final, mesmo que os eventos não sejam idênticos.

ruby
class RecomputeFinancialsJob < ApplicationJob
  WINDOW = 30.seconds
  KEY_TTL = 90.seconds.to_i

  def self.schedule(tenant_id)
    key = "finance:recompute:tenant:#{tenant_id}"
    acquired = Sidekiq.redis do |connection|
      connection.call("SET", key, "1", "NX", "EX", KEY_TTL) == "OK"
    end

    return false unless acquired

    set(wait: WINDOW).perform_later(tenant_id)
  rescue RedisClient::Error => error
    Rails.logger.warn("debounce unavailable: #{error.class}")
    set(wait: WINDOW).perform_later(tenant_id) # Fail-open
  end

  def perform(tenant_id)
    release_debounce_key(tenant_id)
    FinancialsRecalculator.call(tenant_id:)
  end

  private

  def release_debounce_key(tenant_id)
    key = "finance:recompute:tenant:#{tenant_id}"
    Sidekiq.redis { |connection| connection.call("DEL", key) }
  rescue RedisClient::Error => error
    Rails.logger.warn("debounce release failed: #{error.class}")
  end
end

A primeira chamada cria uma chave por tenant e agenda o job para 30 segundos depois. Chamadas seguintes nessa janela são agrupadas. O job apaga a chave no início; assim, eventos que chegarem durante a execução podem agendar uma próxima rodada.

Retries e idempotência ainda importam

Deduplicar o enqueue não garante execução única. Um worker pode concluir o efeito e falhar antes de confirmar o job. Por isso, limite retries conforme o custo e o tipo de erro. Idempotência significa que repetir a operação produz o mesmo resultado correto, sem cobrar, enviar ou gravar duas vezes.

Use proteções no banco e chaves de idempotência em serviços externos quando necessário. O job também deve tolerar uma execução parcialmente concluída.

Limitações para observar

  • TTL curto demais deixa duplicatas passarem; longo demais bloqueia trabalho legítimo.
  • Existe uma pequena janela entre reservar no Redis e publicar no Sidekiq. O TTL limita o problema, mas não cria uma transação entre os dois.
  • Fail-open troca unicidade por disponibilidade quando o Redis falha.
  • Argumentos equivalentes precisam gerar o mesmo fingerprint; mantenha seu formato estável.
  • A deduplicação reduz publicações repetidas, mas não substitui idempotência nem um mutex.

E a fila que já estava cheia?

Depois de proteger os produtores, faça uma limpeza administrativa única dos jobs antigos: percorra as filas de forma controlada, compare fingerprints e remova duplicatas confirmadas. Não transforme essa varredura em parte permanente do enqueue e não apague jobs em execução.

Checklist curto

  • Defina exatamente o que conta como duplicata.
  • Escolha e monitore o TTL.
  • Decida conscientemente entre fail-open e fail-closed.
  • Limite retries e torne o efeito idempotente.
  • Meça reservas, duplicatas evitadas e falhas do Redis.

Conclusão

SET key value NX EX ttl troca uma varredura cara por uma ficha de reserva simples e atômica. Use deduplicação para jobs iguais e debounce para agrupar uma onda de eventos. Depois complete a proteção com TTL adequado, retries limitados, idempotência e atenção às janelas de falha.

Referências

EOF, Israel Santos

← voltar aos artigos