Retry, backoff exponentiel et circuit breaker
Quand un partenaire ne répond pas, retenter est tentant et dangereux. Cinq règles tirées des piles TCP, AWS et Fowler suffisent à ne pas transformer un incident partenaire en panne géante côté plateforme.
Problème
Un envoi AS2 expire en timeout. L'AS4 retourne une 500. L'OFTP2 ferme la session sur abort. Que faire ? La réponse naïve est de retenter immédiatement. C'est exactement comme ça que naissent les retry storms : chaque envoi en attente, dès qu'il est libéré, retape simultanément sur le partenaire déjà saturé, ce qui aggrave la panne pour tout le monde — émetteur compris, qui voit ses files d'attente exploser.
Forces
- Le partenaire peut être temporairement défaillant. Plein de causes : maintenance, certificat expiré, pic de charge. La panne n'est presque jamais permanente.
- L'amplification est réelle. Si 1 000 envois simultanés retentent toutes les 30 secondes, le partenaire reçoit 33 requêtes/s — tant qu'il n'est pas remonté. C'est suffisant pour empêcher la remontée.
- L'attente est elle aussi coûteuse. Côté émetteur, tenir des envois en cours pendant des heures consomme du stockage et des ressources. Il faut un plafond.
- Certains messages ne doivent jamais être perdus. Une facture B2G impose une trace. Au-delà de N retentes, le message doit aller en DLQ avec alerte, pas être abandonné.
Backoff exponentiel et jitter
Le pattern canonique remonte à RFC 5681 (TCP Congestion Control) : à
chaque échec, attendre 2n · base avant la
tentative suivante. Pour des envois EDI typiques : base = 1 s,
plafond = 30 min, 7 tentatives — soit 1, 2, 4, 8, 16, 32, 64 secondes
(puis plafonné). On couvre ainsi 2 minutes de retentes au début,
puis on espace.
Le backoff seul ne suffit pas. Si tous les envois en file étaient bloqués sur la même panne (incident à T0), ils vont tous retenter à T0+1, T0+3, T0+7 — exactement en phase. Le pic se reproduit à chaque palier. L'ajout d'un jitter aléatoire casse cette synchronisation. L'article de référence de Marc Brooker (AWS, 2015) montre que le full jitter (multiplier l'intervalle par un nombre aléatoire entre 0 et 1) est typiquement supérieur au equal jitter ou pas de jitter du tout, parce qu'il aplatit complètement la distribution des tentatives.
Circuit breaker
Le circuit breaker (Martin Fowler, 2014) coupe complètement la transmission quand un partenaire est manifestement KO. Il a trois états :
- Closed — passage normal. On compte les échecs.
- Open — au-delà d'un seuil d'échecs (par ex. 5 en 60
secondes), on bascule en Open. Tous les envois suivants vers ce
partenaire vont directement en DLQ avec un message
circuit-open. Le partenaire n'est plus sollicité pendant la cooldown period (5 à 30 minutes typiquement). - Half-Open — après le cooldown, on laisse passer un seul envoi de test. S'il réussit → Closed. S'il échoue → Open pour un cooldown supplémentaire.
Le circuit breaker protège les deux côtés : il évite à la plateforme d'empiler des envois inutiles, et il évite au partenaire d'être pilonné pendant qu'il essaie de remonter. C'est pourquoi il est plus utile qu'on ne le pense en EDI, où les partenaires sont souvent des systèmes legacy fragiles.
Dead-letter queue
La dead-letter queue (Hohpe & Woolf, 2003) est l'endroit où finissent les messages qui ont épuisé leurs retentes ou qui ont échoué de manière non-retryable (erreur 4xx, signature invalide, payload corrompu). C'est une file persistante visible — pas un simple log d'erreur. Trois propriétés essentielles :
- Persistance. Un redémarrage de la plateforme ne perd rien. La DLQ est typiquement sur disque, voire en base avec backup.
- Replay-friendly. Un opérateur doit pouvoir ré-injecter un message DLQ d'un clic une fois le partenaire remonté.
- Métadonnées d'erreur. Stocker la dernière erreur, l'heure, le nombre de tentatives, l'identifiant du partenaire — pour faciliter le triage par les équipes ops.
Application aux flux EDI
| Protocole | Erreur | Action |
|---|---|---|
| AS2 | HTTP 200 mais pas de MDN dans le SLA | Retry avec même Message-Id jusqu'à N=7 ; DLQ ensuite. Vérifier expiration cert partenaire entretemps. |
| AS2 | HTTP 5xx, TCP reset | Retry exponentiel + jitter. Circuit breaker si ≥5 échecs en 60s. |
| AS2 | HTTP 401, 403, signature invalide | Non-retryable. DLQ immédiat + alerte certificat. |
| AS4 | SOAP fault, timeout | Retry exponentiel + jitter. Reception awareness AS4 propose un retry intégré. |
| OFTP2 | Session abort en cours de transfert | Reprise sur l'offset négocié, puis retry exponentiel sur la session. |
| EDIFACT | CONTRL action 7 (rejet syntaxique) | Non-retryable. DLQ + alerte équipe métier — le fichier doit être corrigé manuellement. |
Pseudo-code de la politique
// Politique de retry — pseudo-code
const MAX_ATTEMPTS = 7;
const BASE_DELAY_MS = 1_000; // 1 s
const MAX_DELAY_MS = 30 * 60 * 1_000; // 30 min (plafond)
const JITTER_RATIO = 0.5; // ±50 %
async function sendWithRetry(message) {
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
if (circuitBreakerOpen(partner)) {
await deadLetterQueue.push(message, 'circuit-open');
return;
}
try {
const ack = await sendOverAs2(message);
recordSuccess(partner);
return ack;
} catch (err) {
recordFailure(partner, err);
if (!isRetryable(err)) {
await deadLetterQueue.push(message, err.code);
return;
}
}
// exponential backoff with full jitter
const exp = Math.min(BASE_DELAY_MS * 2 ** attempt, MAX_DELAY_MS);
const jitter = exp * (Math.random() * JITTER_RATIO * 2 - JITTER_RATIO);
await sleep(exp + jitter);
}
await deadLetterQueue.push(message, 'max-attempts');
} Anti-patterns
- Retry immédiat sans backoff. Ressort le problème partenaire à pleine puissance. À bannir.
- Retry uniforme sur tout type d'erreur. Une signature invalide ne disparaîtra pas en retentant. Distinguer retryable (timeout, 5xx) de non-retryable (4xx, 401/403, signature/format).
- Retry infini. Pas de plafond = files d'attente qui explosent et incidents qui s'aggravent. Toujours fixer un N maximal et une DLQ ensuite.
- Circuit breaker trop sensible. Si le seuil est à 2 échecs, le moindre micro-incident bascule en Open et bloque tout. Garder le seuil au-dessus de la variabilité normale du partenaire.
- DLQ silencieuse. Un message en DLQ qui n'alerte personne reste perdu jusqu'à ce qu'un client appelle. La DLQ doit systématiquement émettre une alerte sortante (mail, Slack, ticket).
- Backoff sans jitter. Tous les envois se synchronisent sur les paliers et reproduisent le pic à chaque tentative. Toujours ajouter du jitter.
Patterns liés
- Idempotence — prérequis indispensable du retry.
- Acquittements — pour savoir quand retenter (sur quel niveau d'ACK manquant).
- Flux d'exception — la matrice d'escalade quand le retry échoue.
Sources
- RFC 5681 — TCP Congestion Control (Allman, Paxson, Blanton, septembre 2009). La référence canonique du backoff exponentiel. rfc-editor.org/rfc/rfc5681
- AWS Architecture Blog — « Exponential Backoff And Jitter » (Marc Brooker, 2015). Les comparaisons quantifiées no-jitter / equal-jitter / full-jitter sur 100 000 retentes simulées. aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter
- Fowler M. — Circuit Breaker (martinfowler.com, 2014). La description canonique du pattern. martinfowler.com/bliki/CircuitBreaker.html
- Hohpe G., Woolf B. — Enterprise Integration Patterns, Dead Letter Channel et Guaranteed Delivery. enterpriseintegrationpatterns.com — Dead Letter Channel
- RFC 6749 §5.2 — OAuth 2.0 error responses, qui codifie la distinction retryable / non-retryable au niveau HTTP.