OI, estou executando vários workers do n8n que processam jobs do PostgreSQL. Os workers reivindicam jobs alterando seu status de pending para processing.
O problema é que ocasionalmente um worker falha ou é reiniciado após reivindicar um job. O job fica travado em processing para sempre e nunca é selecionado novamente.
Estou pensando em usar um timestamp locked_at para que outro worker possa identificar jobs obsoletos:
SELECT id, payload
FROM jobs
WHERE status = ‘processing’
AND locked_at < NOW() - INTERVAL ‘15 minutes’;
Então eu poderia resetar esses jobs de volta para pending.
Estou principalmente interessado em um padrão que lide com travamentos de worker com segurança sem criar processamento duplicado.
Descreva o problema/erro/pergunta
Qual é a mensagem de erro (se houver)?
Por favor, compartilhe seu workflow
(Selecione os nós em sua tela e use os atalhos de teclado CMD+C/CTRL+C e CMD+V/CTRL+V para copiar e colar o workflow.)
use a lease/heartbeat model rather than simply assuming every processing job older than 15 minutes is dead.
Eu daria a cada job reclamado um timestamp locked_at e preferencialmente um worker_id. Os workers podem atualizar periodicamente o heartbeat enquanto processam.
Para recuperação, eu usaria uma atualização atômica para que dois workers não recuperem o mesmo job:
UPDATE jobs
SET status = ‘pending’,
locked_at = NULL,
worker_id = NULL
WHERE id = $1
AND status = ‘processing’
AND locked_at < NOW() - INTERVAL ‘15 minutes’
RETURNING id;
Se a consulta retornar uma linha, esse worker reclamou com sucesso. Se não retornar nada, outro worker já lidou com isso ou o job não está mais elegível.
Para jobs de longa duração, definitivamente preferiria um heartbeat em vez de um timeout fixo:
Claim Job
↓
status = processing
↓
Update Heartbeat
↓
Process Job
↓
Mark Completed
Então teria um pequeno fluxo de recuperação que periodicamente procuraria jobs cujo heartbeat expirou.
Se o worker falhar, a concessão eventualmente expira e outro worker pode assumir.
Eu também tornaria o processamento real do trabalho idempotente e manteria um contador de tentativas, para que um trabalho que falha repetidamente possa eventualmente passar para uma fila de letras mortas em vez de tentar novamente para sempre.
Oi @Mary_Berry
Um lease na sua tabela não consegue renovar a si mesmo do jeito que o n8n faz. O n8n mantém um lease de 60 segundos na execução e o estende a cada 10 segundos por um timer, mas uma execução é uma passagem direta pelos nós sem thread de fundo, então nada toca locked_at enquanto um nó HTTP Request está bloqueado. A renovação só acontece onde um nó é executado, o que significa um Loop Over Items com um nó de renovação dentro dele. Qualquer outra coisa tem que ser dimensionada em vez de ter heartbeat.
Dimensioná-la significa reconciliar dois relógios. O que decide se seu worker está morto é do n8n, não seu, e um worker ocupado demais para renovar dentro de 60 segundos tem sua execução declarada como travada enquanto a linha ainda está marcada como sua. No 2.x nada entrega novamente essa execução, então ela se foi e a linha aguarda seus 15 minutos completos. Coloque o lease do n8n acima do seu trabalho mais lento e depois seu lease de trabalho acima disso:
QUEUE_WORKER_LOCK_DURATION=900000
Com a ordem invertida o reaper entrega o trabalho a um segundo worker enquanto o primeiro ainda está executando-o.