Rendre le traitement résistant aux échecs
Objectif : Éviter les factures en double et récupérer les tâches interrompues ou échouées.
Une tâche peut être exécutée plusieurs fois : le worker peut s’arrêter après avoir créé une facture, sans avoir confirmé son succès à la file. À la reprise, le même message revient ; si le traitement n’est pas protégé, la commande reçoit une deuxième facture. Vous allez rendre cette répétition sûre et distinguer les erreurs temporaires des échecs à examiner.
La leçon précédente a séparé le worker du serveur web et lui a permis de consommer plusieurs tâches en parallèle. Ici, vous complétez ce dispositif : une tâche reprise doit produire le même résultat métier, les erreurs transitoires peuvent être retentées et les échecs persistants doivent rester accessibles pour diagnostic.
Rendre la création de facture idempotente
L’idempotence signifie qu’exécuter plusieurs fois le même traitement pour une commande ne crée pas plusieurs factures. La protection doit se trouver au niveau du service métier et du stockage, pas uniquement dans la file : une file peut livrer un message plus d’une fois, et deux workers peuvent parfois tenter la même opération en parallèle. Utilisez l’identifiant de commande comme clé métier stable, ainsi qu’une contrainte d’unicité correspondante dans le stockage des factures.
Le service de facturation reçoit un dépôt injecté. Celui-ci expose une opération atomique qui crée la facture si aucune facture n’existe pour la commande ; en cas de doublon, il retourne la facture existante. La contrainte d’unicité en base reste indispensable : une vérification suivie d’une insertion séparée laisserait une fenêtre de concurrence.
// src/services/factures.js
export function creerServiceFactures({ depotFactures }) {
if (!depotFactures || typeof depotFactures.creerSiAbsente !== 'function') {
throw new TypeError('Le dépôt de factures doit fournir creerSiAbsente.');
}
return {
async genererPourCommande(commandeId) {
if (typeof commandeId !== 'string' || commandeId.length === 0) {
throw new TypeError('Un identifiant de commande est requis.');
}
return depotFactures.creerSiAbsente({ commandeId });
}
};
}Retenter avec une temporisation progressive
Une indisponibilité temporaire de la base ou d’un service externe peut justifier une nouvelle tentative. Évitez de relancer immédiatement et sans limite : cela augmente la charge au moment où une dépendance est déjà en difficulté. Configurez un nombre maximal de tentatives et un délai croissant, plafonné à une durée raisonnable ; votre infrastructure de file doit appliquer ces paramètres et confirmer le message uniquement après le succès du traitement.
Le code ci-dessous exprime une politique de reprise que votre adaptateur de file peut traduire dans les options de la bibliothèque effectivement déployée. Il ne choisit aucune bibliothèque, car le contrat du projet ne fixe pas de technologie de file. La temporisation double à chaque tentative, sans dépasser le plafond défini par configuration.
// Politique à transmettre à l’adaptateur de file depuis la composition existante.
export function creerPolitiqueReprise({ tentativesMax, delaiInitialMs, delaiMaxMs }) {
for (const [nom, valeur] of Object.entries({ tentativesMax, delaiInitialMs, delaiMaxMs })) {
if (!Number.isInteger(valeur) || valeur < 1) {
throw new TypeError(`${nom} doit être un entier positif.`);
}
}
return {
tentativesMax,
delaiAvantTentative(numeroTentative) {
if (!Number.isInteger(numeroTentative) || numeroTentative < 1) {
throw new TypeError('Le numéro de tentative doit commencer à 1.');
}
return Math.min(delaiInitialMs * (2 ** (numeroTentative - 1)), delaiMaxMs);
}
};
}
const politique = creerPolitiqueReprise({
tentativesMax: 5,
delaiInitialMs: 1000,
delaiMaxMs: 30000
});
console.log(politique.delaiAvantTentative(1));
console.log(politique.delaiAvantTentative(4));1000
8000Dans le déploiement réel, fournissez ces nombres par la configuration existante et validez-les au démarrage, comme vu dans la leçon de configuration. La valeur maximale des tentatives doit être limitée afin qu’une tâche défectueuse ne monopolise pas indéfiniment la file. Les délais peuvent aussi intégrer une légère variation aléatoire si l’adaptateur le permet, afin d’éviter que de nombreuses tâches échouées repartent simultanément.
Isoler les échecs définitifs et organiser la reprise
Après épuisement des tentatives, ne supprimez pas silencieusement la tâche et ne la remettez pas en circulation sans fin. Configurez l’adaptateur pour la déplacer dans une file d’échec, en conservant le message, son identifiant de corrélation, le nombre de tentatives, l’horodatage et une erreur résumée qui ne contient pas de secret. Cette file permet de séparer le trafic normal des cas nécessitant une analyse.
La reprise est une action contrôlée : examinez la cause, vérifiez que le traitement est corrigé, puis republiez la tâche d’origine ou un message équivalent conservant l’identifiant de commande. Grâce à la création idempotente, relancer une tâche dont la facture a déjà été créée retourne le résultat existant au lieu d’en produire une seconde. Journalisez l’échec et la reprise avec les champs structurés établis précédemment, sans écrire le contenu sensible de la commande.
À retenir
- Faites de l’identifiant de commande la clé d’idempotence et imposez son unicité au stockage.
- Limitez les tentatives et appliquez une temporisation progressive plafonnée pour les erreurs temporaires.
- Déplacez les échecs persistants dans une file dédiée avec les informations utiles au diagnostic.
- Reprenez une tâche après analyse ; l’idempotence protège contre la création d’une facture en double.
La prochaine leçon applique une autre stratégie de maîtrise des ressources : lire les commandes en lots depuis la base pour préparer les exports volumineux.