Diagnóstico de Resque pelo console: info, contagens e workers
Série Resque — parte 2 de 3
- Infra: iniciando, parando, matando
- Você está aqui — Diagnóstico pelo console
- Manipulação em massa de jobs e workers
Correlação com a série Sidekiq — esse post é o espelho da parte 2 da série Sidekiq. Mesma filosofia: antes de sair deletando ou movendo job, fazer uma rodada read-only pra entender o estado. As APIs são diferentes (Resque expõe muita coisa via
Resque.infoe classes auxiliares em vez deSidekiq::Stats/Sidekiq::Queue), mas as perguntas que você quer responder são as mesmas: o que está pendente, o que está falhando, e quem está trabalhando agora.
Status geral
1
2
3
4
5
6
7
8
9
10
11
Resque.info
# => {
# :pending => 1234,
# :processed => 98765,
# :queues => 5,
# :workers => 8,
# :working => 3,
# :failed => 42,
# :servers => ["redis://localhost:6379/0"],
# :environment => "production"
# }
Em uma chamada você tem o panorama: pendente (soma de todas as filas), total processado historicamente, número de filas, workers vivos, workers ativos agora e total de failures acumuladas. É o equivalente do Sidekiq::Stats.new da série anterior.
Listando filas e tamanhos
1
2
3
4
5
Resque.queues
# => ["default", "critical", "mailers", "low"]
Resque.queues.map { |q| [q, Resque.size(q)] }.to_h
# => {"default"=>120, "critical"=>0, "mailers"=>3, "low"=>15000}
Resque.size('queue_name') é O(1) — usa LLEN no Redis. Pode chamar à vontade.
Total de jobs em uma fila por classe
1
2
3
4
5
queue_name = 'default'
Resque.peek(queue_name, 0, Resque.size(queue_name))
.group_by { |job| job['class'] }
.map { |k, v| [k, v.length] }
.to_h
Resque.peek(queue, start, count) devolve os payloads sem remover (LRANGE no Redis). Como o payload é um Hash já parseado, basta agrupar por 'class'.
Útil pra responder a mesma pergunta da série Sidekiq: “qual worker está dominando essa fila?”. Em incidente pós-deploy, normalmente é uma classe específica produzindo job mais rápido do que o cluster consome.
Cuidado: se a fila tem milhões de itens, evite carregar tudo. Pegue uma amostra (
Resque.peek(queue, 0, 5000)) — geralmente já dá pra inferir a distribuição.
Failures por classe
Resque não separa “retry” e “dead” como o Sidekiq — toda falha vai pro failure backend (geralmente o Redis). A leitura é parecida:
1
2
3
4
5
total = Resque::Failure.count
Resque::Failure.all(0, total)
.group_by { |f| f['payload']['class'] }
.map { |k, v| [k, v.length] }
.to_h
Mostra qual classe está dominando o failure set. Depois de um deploy ruim, se uma classe nova aparece dominando, é forte indício de regressão — mesma heurística da parte 2 do Sidekiq.
Failures por mensagem de erro
Uma vantagem do Resque: o failure guarda a exceção e a mensagem direto no payload, então dá pra agrupar por erro também:
1
2
3
4
5
Resque::Failure.all(0, Resque::Failure.count)
.group_by { |f| "#{f['exception']}: #{f['error']}" }
.map { |k, v| [k, v.length] }
.sort_by { |_, v| -v }
.first(10)
Top 10 erros mais frequentes. Em incidente, isso responde “todas as 5000 falhas são do mesmo Net::OpenTimeout ou tem coisa nova no meio?” em segundos.
Listando workers (todos os processos vivos)
1
2
3
Resque.workers.each do |w|
puts "#{w.to_s} | host=#{w.hostname} pid=#{w.pid} queues=#{w.queues.join(',')}"
end
Cada worker se registra no Redis quando sobe e desregistra quando sai limpo. Se você vê um worker listado mas o processo não existe mais (caiu sem QUIT), é um worker fantasma — abordo isso na próxima parte.
O que cada worker está executando agora
1
2
3
4
Resque.working.each do |w|
job = w.job
puts "#{w.to_s} | class=#{job['payload'] && job['payload']['class']} queue=#{job['queue']} run_at=#{job['run_at']}"
end
Resque.working filtra só os que têm job em mãos. Equivalente ao Sidekiq::Workers.new da série anterior — mostra threads/processos em execução neste exato momento, qual classe e em qual fila.
Quando o Resque.info[:processed] parou de subir mas os workers ainda estão “working”, é aqui que você descobre quem travou.
Tempo que cada worker está em um job
1
2
3
4
5
6
7
8
require 'time'
Resque.working.each do |w|
job = w.job
next if job.empty?
run_at = Time.parse(job['run_at'])
puts "#{w.to_s} -> #{job['payload']['class']} rodando há #{(Time.now - run_at).to_i}s"
end
Acha worker travado: se o rodando há está em milhares de segundos pra um job que deveria levar 200ms, alguém ficou pendurado em I/O.
Workers fantasma
1
2
3
4
Resque.workers.reject { |w|
hostname, pid, _ = w.id.split(':')
hostname == `hostname`.strip && system("ps -p #{pid} > /dev/null 2>&1")
}
Lista workers registrados no Redis cujo processo não existe mais na máquina onde rodam. Em cluster com vários hosts, esse check só vale pros workers locais — pra cluster real, melhor confiar no prune_dead_workers (que vou usar na parte 3).
Próximo da série
Manipulação em massa de jobs e workers — selecionar failures por classe, requeue/remove em massa, mover entre filas, pausar workers via sinal e a bomba nuclear Resque.redis.flushdb.