Gérer les tâches bloquées

SALUT Je fais fonctionner plusieurs workers n8n qui traitent des jobs depuis PostgreSQL. Les workers revendiquent les jobs en changeant leur statut de pending à processing.
Le problème, c’est qu’occasionnellement un worker plante ou redémarre après avoir revendiqué un job. Le job reste bloqué en processing indéfiniment et n’est jamais repris.
Je pense utiliser un timestamp locked_at pour qu’un autre worker puisse identifier les jobs obsolètes :
SELECT id, payload
FROM jobs
WHERE status = ‘processing’
AND locked_at < NOW() - INTERVAL ‘15 minutes’;
Ensuite je pourrais réinitialiser ces jobs à pending.
Je m’intéresse principalement à un pattern qui gère les plantages de workers de manière sécurisée sans créer de traitements dupliqués.

Décrivez le problème/erreur/question

Quel est le message d’erreur (le cas échéant) ?

Veuillez partager votre workflow

(Sélectionnez les nœuds sur votre canevas et utilisez les raccourcis clavier CMD+C/CTRL+C et CMD+V/CTRL+V pour copier et coller le workflow.)

Partagez la sortie renvoyée par le dernier nœud

Informations sur votre configuration n8n

  • Version n8n :
  • Base de données (par défaut : SQLite) :
  • Paramètre n8n EXECUTIONS_PROCESS (par défaut : own, main) :
  • Exécution n8n via (Docker, npm, n8n cloud, application de bureau) :
  • Système d’exploitation :

Salut @Mary_Berry, en attendant une réponse, voici quelques ressources qui pourraient t’aider :

Ressources suggérées

Automatiquement associées à ta question.

Documentation :

Forum :

@Niffzy, @Emmas - vous avez déjà aidé sur des problèmes similaires, vous pouvez y jeter un œil ?

Automatiquement suggéré par le bot communautaire de n8n. C’est un projet pilote - veuillez partager vos commentaires ici.

@Mary_Berry

utilise un modèle de bail/battement de cœur plutôt que de simplement supposer que chaque travail de traitement datant de plus de 15 minutes est mort.

Je donnerais à chaque travail revendiqué un horodatage locked_at et de préférence un worker_id. Les workers peuvent mettre à jour périodiquement le battement de cœur pendant le traitement.

Pour la récupération, j’utiliserais une mise à jour atomique pour que deux workers ne puissent pas récupérer le même travail :

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;

Si la requête retourne une ligne, ce worker l’a repris avec succès. Si elle ne retourne rien, un autre worker l’a déjà traité ou le travail n’est plus éligible.

Pour les travaux de longue durée, je préférerais vraiment un battement de cœur à un délai d’expiration fixe :

Revendiquer travail
↓
status = processing
↓
Mise à jour du battement de cœur
↓
Traiter le travail
↓
Marquer comme terminé

Ensuite, j’aurais un petit workflow de récupération qui rechercherait régulièrement les travaux dont le battement de cœur a expiré.

Bonjour @Mary_Berry

Rendez la propriété du travail explicite au lieu de ne vous fier qu’à l’ancienneté du statut de traitement.

Quand un worker récupère un travail, donnez-lui un worker_id et un délai d’expiration du bail :

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

Tandis que le travail s’exécute, le worker peut prolonger son bail :

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

Un processus de récupération peut alors rechercher les baux expirés et remettre ces travaux dans la file d’attente.

Claim Job
   ↓
Lease Active
   ↓
Process + Renew Lease
   ↓
Complete

Si le worker plante, le bail expire finalement et un autre worker peut prendre le relais.

Je rendrais aussi le traitement réel du travail idempotent et garderais un compteur de tentatives, afin qu’un travail défaillant de façon répétée puisse finalement passer à une dead-letter queue au lieu de réessayer indéfiniment.

Salut @Mary_Berry
Un bail dans votre tableau ne peut pas se renouveler de la manière que le fait n8n. n8n détient un bail de 60 secondes sur l’exécution et l’augmente toutes les 10 secondes sur une minuterie, mais une exécution est un passage direct à travers les nœuds sans fil d’arrière-plan, donc rien ne touche locked_at tandis qu’un nœud HTTP Request est bloqué. Le renouvellement ne se produit que là où un nœud s’exécute, ce qui signifie une Loop Over Items avec un nœud de renouvellement à l’intérieur. Tout le reste doit être dimensionné plutôt que de battement cardiaque.
Le dimensionner signifie concilier deux horloges. Celle qui décide que votre worker est mort est celle de n8n, pas la vôtre, et un worker trop occupé pour se renouveler en moins de 60 secondes voit son exécution déclarée bloquée alors que la ligne est toujours marquée comme vôtre. Sur 2.x rien ne réexpédie cette exécution, donc elle est perdue et la ligne attend vos 15 minutes complètes. Placez le bail de n8n au-dessus de votre travail le plus lent, puis votre bail de travail au-dessus de celui-ci :

QUEUE_WORKER_LOCK_DURATION=900000

Avec l’ordre inverse, le moissonneur remet le travail à un deuxième worker tandis que le premier l’exécute encore.