Gerenciar Trabalhos Travados

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.)

Compartilhe a saída retornada pelo último nó

Informações sobre sua configuração do n8n

  • Versão do n8n:
  • Banco de dados (padrão: SQLite):
  • Configuração n8n EXECUTIONS_PROCESS (padrão: own, main):
  • Executando n8n via (Docker, npm, n8n cloud, aplicativo desktop):
  • Sistema operacional:

Oi @Mary_Berry, enquanto aguarda uma resposta, aqui estão algumas coisas que podem ajudar:

Recursos sugeridos

Automaticamente correspondido à sua pergunta.

Docs:

Fórum:

@Niffzy, @Emmas - vocês já ajudaram com problemas similares antes, podem dar uma olhada?

Sugerido automaticamente pelo bot da comunidade n8n. É um piloto - compartilhe seu feedback aqui.

@Mary_Berry

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.

Oi @Mary_Berry

Torne a propriedade do trabalho explícita em vez de contar apenas com a idade do status de processamento.

Quando um worker pega um trabalho, atribua a ele um worker_id e um tempo de expiração de concessão:

UPDATE jobs
SET status = 'processing',
    worker_id = $1,
    lease_until = NOW() + INTERVAL '15 minutes'
WHERE id = $2
  AND status = 'pending'
RETURNING id;

Enquanto o trabalho está em execução, o worker pode estender sua concessão:

UPDATE jobs
SET lease_until = NOW() + INTERVAL '15 minutes'
WHERE id = $1
  AND worker_id = $2
  AND status = 'processing';

Um processo de recuperação pode então procurar por concessões expiradas e colocar esses trabalhos de volta na fila.

Reivindicar Trabalho
   ↓
Concessão Ativa
   ↓
Processar + Renovar Concessão
   ↓
Concluído

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.