volontariapp-async-remediation
Playbook opérationnel pas-à-pas pour diagnostiquer, débloquer et réconcilier les pannes asynchrones (Poison pills DLQ, Redis Streams XCLAIM/XACK, réconciliation SQL outbox).
- License 许可
- MIT
- 最近更新
- 2026-11-10
- Stars
- ★ 0
- Forks
- 0
01 前言 Front Matter
- name
- volontariapp-async-remediation
- tags
- [ "async", "remediation", "dlq", "redis-streams", "xclaim", "xack", "outbox", "playbook" ]
- type
- Playbook
- paths
- [ "post-processors-runner/**", "workers-runners/**", "npm-packages/packages/post-processors/**" ]
- title
- Remédiation et Dépilement des Files Asynchrones (DLQ & Streams)
- status
- stable
- sources
- [ { "id": "retry", "kind": "local_file", "title": "RetryHelper (DLQ <stream>-dlq)", "resource": "npm-packages/packages/post-processors/src/core/helpers/retry.helper.ts" }, { "id": "outbox-status", "kind": "local_file", "title": "OutboxStatus", "resource": "npm-packages/packages/database/src/outbox/types/outbox.status.ts" }, { "id": "panne", "kind": "local_file", "title": "Scénarios de panne, problèmes P2 et P4", "resource": "docs/stockage-fichiers/11-scenarios-de-panne.md" } ]
- verified
- [ { "at": "2026-11-04T21:34:45Z", "by": "claude-code/agent", "digest": "33b9ef4b5b3df65a" }, { "at": "2026-11-04T21:36:01Z", "by": "claude-code/agent", "digest": "036777c37dd4f448" }, { "at": "2026-11-07T23:14:09Z", "by": "claude-code/agent", "digest": "2360205de2f7abcd" }, { "at": "2026-11-07T23:14:26Z", "by": "claude-code/agent", "digest": "2360205de2f7abcd" }, { "at": "2026-11-08T12:02:22Z", "by": "claude-code/agent", "digest": "9c93894442a0fd6f" }, { "at": "2026-11-08T12:07:09Z", "by": "claude-code/agent", "digest": "2a04ea25b2d069e3" } ]
- generated
- { "at": "2026-10-12T11:58:00Z", "by": "claude-code/agent" }
- mesh_keys
- [ "dlq-remediation", "xclaim-recovery", "stream-remediation" ]
- description
- Playbook opérationnel pas-à-pas pour diagnostiquer, débloquer et réconcilier les pannes asynchrones (Poison pills DLQ, Redis Streams XCLAIM/XACK, réconciliation SQL outbox).
02 资源文件 3
- 参考文档index.md602 B
- 参考文档log.md9.1 KB
- 参考文档references/lessons.md1.7 KB
03 SKILL.md 正文
Remédiation et Dépilement des Files Asynchrones (DLQ & Streams)
Quand utiliser ce playbook
Utiliser ce playbook pour intervenir en production ou en environnement de test en cas d'incident sur un flux asynchrone :
- Messages arrivés en fin de tentatives et routés vers la Dead Letter Queue (
<stream>-dlq)^retry. - Blocage d'un groupe de consommateurs Redis Streams (consommateur crashé, PEL saturée).
- Erreur récurrente sur un Job BullMQ sans reprise automatique.
- Désynchronisation ou blocage persistant de statut dans
jobs_outbox,event_queueoujob_audit^outbox-status.
(Pour une phase d'audit passif préalable sans action corrective, se référer à la skill d'investigation volontariapp-trace-async-flow).
Règles d'or d'intervention
- Aucune suppression aveugle : Ne jamais exécuter un
XDELou purger une DLQ sans avoir copié et sauvegardé le payload JSON incriminé. - Idempotence des consommateurs : Tout rejeu de message (
replay) suppose que le consommateur cible supporte d'être appelé deux fois pour la même action (gestion du « déjà traité »). - Consistance transactionnelle bilatérale : Débloquer un message dans Redis nécessite systématiquement de vérifier et réconcilier l'état de la ligne correspondante dans la base PostgreSQL du microservice émetteur (
event_queueoujobs_outbox)^panne.
Procédure pas-à-pas
Étape 1 : Inspection de la Poison Pill dans la DLQ
Lorsqu'un post-processor échoue après maxRetries (par défaut 5 tentatives), RetryHelper bascule le message dans <stream>-dlq^retry.
Seules les erreurs métier suivent ce chemin. Une erreur d'infrastructure (connexion base ou Redis refusée ou perdue, timeout, détectée par isInfrastructureError de @volontariapp/errors) ne compte aucune tentative et n'envoie rien en DLQ : le message reste dans la PEL et le consumer le relit claimIntervalMs plus tard (un message absent de la DLQ pendant une panne de base est donc normal). Chaque occurrence est loguée en warn (Infrastructure error, message kept pending) avec le stream et l'id du message. Le circuit breaker, lui, compte toujours l'échec.
- Se connecter à l'instance Redis :
redis-cli - Inspecter les derniers messages en DLQ :
XRANGE <stream>-dlq - + COUNT 10 - Chaque entrée contient :
messageId: ID d'origine du message dans le flux initial.error: Message et stacktrace de l'exception ayant provoqué l'échec.payload: Contenu JSON complet de l'événement d'origine.timestamp: Horodatage du basculement en DLQ.
- Sauvegarder localement le
payloadet analyser la racine de l'échec (ex. entité introuvable, schéma désynchronisé, contrainte de clé étrangère violée).
Étape 2 : Déblocage des messages orphelins (Pending Entries List)
Si un worker ou post-processor a crashé pendant le traitement, le message reste bloqué dans la Pending Entries List (PEL) sans être ni acquitté ni retenté.
Le claim (claimPendingMessages) ignore les messages du même consumer : un message resté en PEL chez un consumer vivant n'est relu que lorsque readPending repasse à true (démarrage, claim, file de retry, ou relecture planifiée après une erreur d'infrastructure). Piège connu : le verrou d'idempotence d'un message (SET NX, TTL idempotencyTtlSeconds, 24 h) non libéré parce que Redis était indisponible fait traiter la relecture comme « déjà traité » : le message est alors acquitté sans effet. Dans ce cas, supprimer la clé de verrou (RedisStreamHelper.getIdempotencyKey) avant de redémarrer le pod, car le démarrage relit la PEL.
- Vérifier les messages en souffrance pour le groupe consommateur :
XPENDING <stream> <nom-du-groupe> - Lister les identifiants et les consommateurs assignés :
XPENDING <stream> <nom-du-groupe> - + 10 - Si un consommateur est inactif depuis plus de 5 minutes (300 000 ms), réclamer le message au profit d'un consommateur sain via
XCLAIM:XCLAIM <stream> <nom-du-groupe> <nouveau-consommateur> 300000 <messageId> - Si le message a été traité avec succès de manière externe, l'acquitter manuellement :
XACK <stream> <nom-du-groupe> <messageId>
Étape 3 : Correction de la cause racine
Avant toute réinjection :
- Si bug de logique applicative : Livrer le correctif dans le microservice ou le post-processor (
post-processors-runner), redémarrer le pod. - Si anomalie de données en base : Corriger la donnée orpheline ou invalide directement dans la base PostgreSQL du service concerné.
Étape 4 : Réinjection sécurisée (Replay) depuis la DLQ
Une fois le correctif en place :
- Rejouer l'événement dans le flux nominal :
XADD <stream> * payload '<json-payload-corrigé>' - Confirmer la prise en charge et le traitement réussi par le post-processor via les logs applicatifs.
- Purger le message résolu de la DLQ pour maintenir la file propre :
XDEL <stream>-dlq <id-du-message-dlq> - Nettoyer les clés de métadonnées de retry résiduelles créées par
RetryHelper^retry :DEL retry:post-processor:<groupe>:<messageId> DEL retry:post-processor:<groupe>:<messageId>:attempt DEL retry:post-processor:<groupe>:<messageId>:lastError ZREM retry-queue:post-processor:<groupe> <messageId>
Étape 5 : Réconciliation transactionnelle SQL
Pour garantir la cohérence des compteurs et des sagas métier :
- Identifier la ligne dans la base du microservice émetteur (
event_queueoujobs_outbox)^outbox-status :SELECT id, event_name, status, retry_count, last_error FROM event_queue WHERE id = '<id-evenement>'; - Si le message a été réinjecté et traité, synchroniser le statut :
UPDATE event_queue SET status = 'COMPLETED', updated_at = NOW() WHERE id = '<id-evenement>' AND status = 'FAILED'; - Vérifier la table
job_auditpour confirmer la clôture propre du cycle d'exécution.
Étape 6 : Enregistrement du piège (Boucle de progrès)
Si la panne a révélé une vulnérabilité d'architecture ou un comportement inattendu, consigner immédiatement la leçon apprise :
python3 .agents/skills/volontariapp-skill-evolution/scripts/evolve.py learn \
--skill volontariapp-async-remediation \
--kind pitfall \
--rule "Description de la cause racine et de la règle pour éviter la récidive."
Checklist de remédiation
- [ ] Le payload du message en échec a été exporté et analysé depuis
<stream>-dlq^retry. - [ ] La PEL Redis a été auditée (
XPENDING) et les messages orphelins réclamés (XCLAIM) ou acquittés (XACK). - [ ] La cause racine (applicative ou modèle de données) a été corrigée et vérifiée.
- [ ] Le message a été réinjecté (
XADD) et traité avec succès par les consommateurs. - [ ] Le message traité a été supprimé de la DLQ (
XDEL) et les clés de retry nettoyées. - [ ] Les statuts de l'outbox relationnelle (
event_queue/jobs_outbox) ont été réconciliés (COMPLETED)^outbox-status. - [ ] La leçon a été consignée via
evolve.py learnsi un piège structurel a été mis en évidence.
Références
04 同仓库更多 Skills
volontariapp-user-account-deletion
Suppression de compte en deux temps (PENDING_DELETION): requestDeletionWithUserDeleted, finalizeDeletion, fan-out de user.deleted vers les domaines et gather ws. A utiliser pour tout changement de domain-user touchant la suppression, le statut ou le cycle de vie d'un user.
volontariapp-proto-contract-evolution
Modifier un contrat Protobuf de proto-registry (champ, message, enum, RPC) sans casser la compatibilité wire, puis suivre la cascade proto-registry, PR automatique dans npm-packages, publication, consommateurs. À utiliser avant toute édition d'un .proto.
volontariapp-meta-scripts
Utiliser et faire evoluer la CLI Node de meta (setup, doctor, migrate, certs, repos, bump) et le manifeste repos.json, source unique des repos et unites du workspace.
volontariapp-implement-async-event-flow
Guide pas-à-pas pour concevoir et implémenter un flux asynchrone complet (messaging -> outbox -> post-processors -> ws-service -> client).
research
Investigate a question against high-trust primary sources and capture the findings as a Markdown file in the repo. Use when the user wants a topic researched, docs or API facts gathered, or reading legwork delegated to a background agent.
prototype
Build a throwaway prototype to answer a design question. Use when the user wants to sanity-check whether a state model or logic feels right, or explore what a UI should look like.