Resque: manipulación masiva de jobs y workers
Serie Resque — parte 3 de 3
- Infra: iniciando, deteniendo, matando
- Diagnóstico por la consola
- Estás aquí — Manipulación masiva
Correlación con la serie Sidekiq — este post es el espejo de la parte 3 de la serie Sidekiq. La gran diferencia es que Resque no tiene
RetrySet/DeadSet— todo lo que falla va alResque::Failure, indexado por posición (no por ID). Esto cambia la forma de hacer “borrado en masa”: como remover un item desplaza los índices, hay que iterar de atrás hacia adelante. Este cuidado va a aparecer en casi todos los snippets de abajo.
Seleccionando failures por clase
1
2
3
4
class_name = 'MyApp::ImportWorker'
total = Resque::Failure.count
failures = Resque::Failure.all(0, total).each_with_index.select { |f, _| f['payload']['class'] == class_name }
failures.size
A diferencia de Sidekiq (RetrySet#select { |j| j.klass == ... }), la colección de Resque aquí es una lista lineal indexada. La cargo con each_with_index para preservar la posición original — la voy a necesitar para retry/delete.
Seleccionando failures por mensaje de error
1
2
3
4
5
needle = 'Net::OpenTimeout'
failures = Resque::Failure.all(0, Resque::Failure.count).each_with_index.select { |f, _|
f['error'].to_s.include?(needle)
}
failures.size
Combina bien con el conteo por error del post anterior: descubrís que el 90% de las failures son de un único OpenTimeout, aislás solo esas y hacés retry después de que el servicio externo vuelva.
Reencolando una failure individual
1
Resque::Failure.requeue(0) # índice 0 es la falla más antigua
requeue vuelve a poner el job en la cola original sin removerlo del failure set — entonces todavía podés ver el historial después.
Removiendo una failure individual
1
Resque::Failure.remove(0)
Remueve solo esa entrada del failure set. Importante: todas las failures después de ella desplazan su índice en 1.
Retry en masa de una clase — iterando de atrás hacia adelante
1
2
3
4
5
6
7
8
9
10
class_name = 'MyApp::ImportWorker'
indexes = Resque::Failure.all(0, Resque::Failure.count)
.each_with_index
.select { |f, _| f['payload']['class'] == class_name }
.map { |_, i| i }
indexes.reverse_each do |i|
Resque::Failure.requeue(i)
Resque::Failure.remove(i)
end
Este patrón es específico de Resque: como remove desplaza índices, hay que iterar del mayor al menor para que cada operación no invalide las anteriores. Si hacés indexes.each (orden ascendente), vas a borrar/reprogramar el job incorrecto a partir de la segunda iteración.
Existe
Resque::Failure.requeue_allyResque::Failure.clearpara actuar en todas, sin filtro. Usá cuando el filtro es “todo”.
Borrar todas las failures
1
Resque::Failure.clear
Equivalente a DeadSet#clear. Sin vuelta atrás — solo hacelo cuando ya confirmaste via diagnóstico (parte 2) que no hay nada útil ahí.
Moviendo jobs de una clase a una cola dedicada
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
queue_name = 'default'
new_queue_name = 'isolated_import'
class_name = 'MyApp::ImportWorker'
snapshot = Resque.peek(queue_name, 0, Resque.size(queue_name))
moved = 0
snapshot.each_with_index do |payload, _|
next unless payload['class'] == class_name
# remueve la primera ocurrencia exacta de este payload de la cola
removed = Resque.redis.lrem("queue:#{queue_name}", 1, Resque.encode(payload))
if removed > 0
Resque.push(new_queue_name, payload)
moved += 1
end
end
moved
Equivalente directo del snippet “Moviendo 1000 jobs de una clase a otra cola” de la serie Sidekiq. La mecánica es diferente porque Resque guarda los jobs como una LIST en Redis (queue:<nombre>), entonces usar LREM directamente es la forma confiable de remover por payload exacto sin tocar a los vecinos.
Por qué aislar: si una clase está desbordando una cola compartida, en vez de pausar todo, creo una cola dedicada (isolated_import), muevo los jobs problemáticos a ella y levanto un worker separado consumiendo solo esa cola. El resto de la operación no lo nota.
1
QUEUE=isolated_import COUNT=1 bundle exec rake resque:workers
Borrar una cola entera
1
Resque.remove_queue('isolated_import')
Borra tanto el contenido (queue:<nombre>) como el registro de la cola en el índice de colas. Útil para una cola creada temporalmente como la de arriba — sin esto, sigue apareciendo en Resque.queues aunque esté vacía.
Pausar todos los workers (USR2)
1
2
3
4
5
Resque.workers.each do |w|
hostname, pid, _ = w.id.split(':')
next unless hostname == Socket.gethostname # solo workers locales
Process.kill('USR2', pid.to_i) rescue nil
end
Como Resque no tiene API “via Redis” para pausar un worker (a diferencia de Sidekiq::Process#quiet!), la única forma es mandar una señal al proceso. Por eso el filtro de hostname: solo podés señalizar workers que corren en la misma máquina donde estás ejecutando la consola.
Para reanudar:
1
2
3
4
5
Resque.workers.each do |w|
hostname, pid, _ = w.id.split(':')
next unless hostname == Socket.gethostname
Process.kill('CONT', pid.to_i) rescue nil
end
En un cluster multi-host, la alternativa es correr estos snippets via Capistrano/Ansible en cada nodo, u orquestar con systemd. La pausa “por Redis” no existe.
Podar workers fantasma
1
Resque.workers.each(&:prune_dead_workers)
Los workers que murieron sin llamar a unregister_worker (por ejemplo, kill -9 o crash) quedan listados en Redis pero sin proceso correspondiente. prune_dead_workers verifica cada worker en el host actual y desregistra los que no tienen proceso vivo. Correlo en cada host periódicamente — en producción, lo dejo en un cron o en el health check de la unit de systemd.
Limpiar todo el Redis de Resque — BORRA TODO
1
Resque.redis.flushdb
Bomba nuclear, equivalente exacto de Sidekiq.redis { |conn| conn.flushdb } de la serie anterior: borra todo lo que Resque tiene en Redis (colas, workers registrados, failures, stats). Usá solo cuando sabés que podés reprocesar tranquilamente o estás en desarrollo. En producción, esto es un incidente — solo hacelo con un plan de recuperación claro.
Si Redis tiene otros usos en el mismo db (cache, sessions, otros workers),
flushdbborra todo eso también. VerificáResque.redis.client.dbantes — separar Sidekiq/Resque/cache en DBs distintos (/0,/1,/2) es el estándar exactamente para este escenario.
Final de la serie
Esta fue la parte 3 y la última. Volviendo al principio: Infra: iniciando, deteniendo, matando.
Y si administrás los dos al mismo tiempo (caso clásico: servicio legacy en Resque + servicios nuevos en Sidekiq), vale tener las dos series lado a lado. La primera parte de la serie Sidekiq es el punto de entrada equivalente.