Jobs Travados¶
Como resolver jobs que ficam travados em PROCESSING.
Problema¶
Jobs marcados como PROCESSING mas que não avançam há muito tempo.
Identificar Jobs Travados¶
Query Básica¶
SELECT
id,
"eventKey",
"processingAt",
NOW() - "processingAt" as stuck_for,
attempts,
"lockedBy"
FROM "OutboxJob"
WHERE status = 'PROCESSING'
AND "processingAt" < NOW() - INTERVAL '5 minutes'
ORDER BY "processingAt";
Query Detalhada¶
SELECT
j.id,
j."eventKey",
j."processingAt",
NOW() - j."processingAt" as stuck_for,
j.attempts,
j."lockedBy",
e."storeCode",
e."totalAmount",
r.name as restaurant
FROM "OutboxJob" j
JOIN "NayaxEvent" e ON j."eventKey" = e."transactionKey"
LEFT JOIN "Restaurant" r ON e."restaurantId" = r.id
WHERE j.status = 'PROCESSING'
AND j."processingAt" < NOW() - INTERVAL '5 minutes'
ORDER BY j."processingAt";
Solução 1: Aguardar o Reaper (Automático)¶
O sistema tem um Reaper que roda a cada 20 segundos e reseta automaticamente jobs travados.
Como Funciona¶
// Roda a cada 20 segundos
@Cron('*/20 * * * * *')
async reaper() {
const thresholdMs = 120000; // 2 minutos
const cutoff = new Date(Date.now() - thresholdMs);
const stuck = await this.prisma.outboxJob.findMany({
where: {
status: 'PROCESSING',
processingAt: { lt: cutoff }
}
});
// Reseta para FAILED com backoff
for (const job of stuck) {
await this.prisma.outboxJob.update({
where: { id: job.id },
data: {
status: 'FAILED',
nextRunAt: new Date(Date.now() + backoff),
processingAt: null,
lockedBy: null
}
});
}
}
Verificar se Reaper está Rodando¶
# Ver logs do reaper
tail -f logs/app.log | grep -i reaper
# Deve aparecer a cada 20 segundos:
# [JobsProcessor] Reaper: resetting 3 stuck PROCESSING jobs
# ou
# [JobsProcessor] Reaper: no stuck jobs
Solução 2: Reset Manual¶
Se precisar resetar imediatamente:
Resetar Job Específico¶
UPDATE "OutboxJob"
SET
status = 'FAILED',
"nextRunAt" = NOW(),
"processingAt" = NULL,
"lockedBy" = NULL,
"lastError" = 'Manual reset - job stuck',
"updatedAt" = NOW()
WHERE id = 'job_abc123';
Resetar Todos Jobs Travados¶
UPDATE "OutboxJob"
SET
status = 'FAILED',
"nextRunAt" = NOW(),
"processingAt" = NULL,
"lockedBy" = NULL,
"lastError" = 'Manual reset - stuck > 5 minutes',
"updatedAt" = NOW()
WHERE status = 'PROCESSING'
AND "processingAt" < NOW() - INTERVAL '5 minutes';
Forçar Reprocessamento Imediato¶
UPDATE "OutboxJob"
SET
status = 'PENDING',
"nextRunAt" = NOW(),
"processingAt" = NULL,
"lockedBy" = NULL,
attempts = 0,
"lastError" = NULL,
"updatedAt" = NOW()
WHERE id = 'job_abc123';
Solução 3: Verificar Processo¶
Verificar se Aplicação Está Rodando¶
# Verificar processos Node
ps aux | grep node
# Verificar com PM2
pm2 list
# Verificar logs
tail -f logs/app.log
Verificar se JobsProcessor Está Ativo¶
# Deve aparecer logs a cada 20 segundos
tail -f logs/app.log | grep -i "tick\|processor"
# Logs esperados:
# [JobsProcessor] tick: start
# [JobsProcessor] tick: fetched 5 job(s)
# [JobsProcessor] tick: end in 1234ms
Restart da Aplicação¶
# Development
npm run start:dev
# Production com PM2
pm2 restart nayax-saipos
# Production sem PM2
killall node
npm run start:prod
Causas Comuns¶
1. Aplicação Caiu¶
Sintoma: Nenhum log novo, processo não está rodando
Solução: Restart da aplicação
2. Lock de Banco¶
Sintoma: Jobs não avançam mas aplicação está rodando
Verificar:
Solução: Kill transações travadas ou restart PostgreSQL
3. Timeout na API Saipos¶
Sintoma: Job trava sempre no mesmo ponto
Verificar logs:
Solução: Aumentar timeout ou verificar rede
4. Memory Leak¶
Sintoma: Aplicação lenta, jobs acumulando
Verificar:
Solução: Restart aplicação, investigar leak
5. Dead Lock no Banco¶
Sintoma: Jobs não avançam, queries lentas
Verificar:
Solução: Restart PostgreSQL
Prevenção¶
1. Monitoramento¶
Configure alertas para jobs travados:
-- Crie uma view
CREATE VIEW stuck_jobs_alert AS
SELECT COUNT(*) as stuck_count
FROM "OutboxJob"
WHERE status = 'PROCESSING'
AND "processingAt" < NOW() - INTERVAL '10 minutes';
-- Use em script de monitoramento
2. Healthcheck¶
Adicione healthcheck que verifica jobs:
@Get('health')
async health() {
const stuckJobs = await this.prisma.outboxJob.count({
where: {
status: 'PROCESSING',
processingAt: { lt: new Date(Date.now() - 600000) } // 10 min
}
});
if (stuckJobs > 0) {
throw new ServiceUnavailableException(`${stuckJobs} stuck jobs`);
}
return { status: 'ok' };
}
3. Ajustar TTL¶
Se jobs precisam de mais tempo:
4. Timeout na API Saipos¶
Configure timeout adequado:
Debug de Job Específico¶
Passo a Passo¶
-
Identificar job:
-
Ver histórico:
-
Ver logs do período:
-
Verificar processo que travou:
Quando Escalar¶
Se jobs continuam travando frequentemente:
- Aumentar workers: Escalar horizontalmente
- Otimizar queries: Adicionar índices
- Aumentar recursos: CPU/RAM
- Separar workers: Jobs em serviço separado
Métricas Importantes¶
-- Taxa de jobs travados
SELECT
COUNT(*) FILTER (WHERE status = 'PROCESSING' AND "processingAt" < NOW() - INTERVAL '5 minutes') as stuck,
COUNT(*) FILTER (WHERE status = 'PROCESSING') as processing,
ROUND(
COUNT(*) FILTER (WHERE status = 'PROCESSING' AND "processingAt" < NOW() - INTERVAL '5 minutes')::numeric /
NULLIF(COUNT(*) FILTER (WHERE status = 'PROCESSING'), 0) * 100,
2
) as stuck_percentage
FROM "OutboxJob";
-- Tempo médio em PROCESSING
SELECT
AVG(EXTRACT(EPOCH FROM (COALESCE("updatedAt", NOW()) - "processingAt"))) as avg_seconds,
MAX(EXTRACT(EPOCH FROM (COALESCE("updatedAt", NOW()) - "processingAt"))) as max_seconds
FROM "OutboxJob"
WHERE status = 'PROCESSING';
Próximos Passos¶
- Debug - Debug detalhado
- Problemas Comuns - Outros problemas
- FAQ - Perguntas frequentes