ediverse Explorer la plateforme

À la une PEPPOL BIS Billing 3.0 L’obligation européenne d’e-invoicing arrive : France sept 2026, Belgique janv 2026, Allemagne 2025.

Circuit Breaker

Quand un partenaire EDI tombe, on a deux mauvais réflexes : (a) retenter en boucle et amplifier sa panne, (b) abandonner trop tôt et perdre les messages. Le Circuit Breaker tranche en formalisant « échouer vite, ne pas marteler, sonder prudemment avant de rétablir ».

Problème

Un partenaire EDI est down — son AS2 endpoint renvoie 503, son SFTP refuse les connexions, son broker ne répond plus. Sans précaution, le pipeline va : (a) accumuler des tentatives qui timeoutent toutes, chacune consommant un thread / une connexion ; (b) déclencher le backoff sur chaque envoi mais en ordre indépendant — un thundering herd quand le partenaire revient ; (c) propager l'erreur au reste de la plateforme, qui peut alors perdre des messages d'autres partenaires non-impactés. La panne se diffuse au-delà de sa source.

Forces

  • Coût des appels en panne. Chaque appel qui finit en timeout consomme un thread / une connexion pendant 30-60 secondes. À 100 messages en queue, on sature la pool en quelques minutes.
  • Cascade failure. Une panne d'un partenaire peut bloquer le traitement des messages destinés à d'autres partenaires qui passent par la même infrastructure.
  • Visibilité opérationnelle. L'état « circuit ouvert » est un signal opérationnel clair pour l'astreinte — bien plus utile que « erreurs en hausse ».
  • Asymétrie partenaire. La plupart des partenaires sont en bonne santé. Ne pas appliquer le même retry brutal à tous quand un seul a un problème.

Solution

Fowler (2014, CircuitBreaker bliki) formalise le pattern repris auparavant par Nygard dans Release It! (2007). Une machine à états par dépendance externe (par partenaire EDI), avec trois états et des transitions explicites. En production le circuit breaker s'incarne dans des bibliothèques matures (resilience4j en Java, Polly en .NET, opossum en Node).

plaintext states.txt
┌───────────────────┐
   │      CLOSED      │  ◀────────────┐
   │ (passing calls)  │               │
   └────────┬─────────┘               │
            │ failures >= threshold   │ success in HALF_OPEN
            │ in window               │
            ▼                          │
   ┌───────────────────┐               │
   │      OPEN         │               │
   │ (fail fast)       │               │
   └────────┬──────────┘               │
            │ cooldown elapsed         │
            ▼                          │
   ┌───────────────────┐               │
   │   HALF_OPEN       │ ──────────────┘
   │ (1 probe call)    │
   └────────┬──────────┘
            │ probe fails

   back to OPEN

États du circuit

  • CLOSED (normal). Les appels passent. On compte les échecs sur une fenêtre glissante (par exemple 10 minutes). Si le taux d'erreur dépasse un seuil (par exemple 50 % sur 20 appels), on bascule en OPEN.
  • OPEN (échec rapide). Aucun appel n'est tenté. Toute requête échoue immédiatement avec une erreur CircuitOpenError. Le pipeline route ces messages vers une file d'attente patiente ou vers la DLC selon le criticité.
  • HALF_OPEN (sonde). Après un cooldown configuré (1 minute, 5 minutes), on autorise un seul appel à passer. Si cet appel-sonde réussit, on revient en CLOSED. S'il échoue, on retourne en OPEN pour un nouveau cooldown.

Implémentation EDI

Deux niveaux d'application :

  • Par partenaire. Un circuit breaker par paire (sender, receiver). Permet d'isoler la panne d'un partenaire sans impacter les autres. C'est le modèle canonique pour un hub d'intégration EDI.
  • Par dépendance interne. Un circuit breaker sur l'ERP cible (SAP, NetSuite, Dynamics). Si l'ERP est en indisponibilité, on bascule les intégrations en attente plutôt que d'accumuler les échecs.
javascript breaker.js
// Circuit breaker simplifié, état persistant par partenaire
function sendToPartner(partner, message) {
  const cb = getBreaker(partner);

  if (cb.state === 'OPEN') {
    if (now() - cb.openedAt < cb.cooldownMs) {
      throw new CircuitOpenError(partner);
    }
    cb.state = 'HALF_OPEN';
  }

  try {
    const result = transport.send(partner, message);
    if (cb.state === 'HALF_OPEN') {
      cb.state = 'CLOSED';
      cb.failures = 0;
    }
    return result;
  } catch (err) {
    cb.failures++;
    cb.lastFailureAt = now();
    if (cb.failures >= cb.threshold) {
      cb.state = 'OPEN';
      cb.openedAt = now();
      alertOps('circuit opened for ' + partner);
    }
    throw err;
  }
}

En contexte EDI réel : un partenaire dont l'AS2 endpoint renvoie 503 sur 10 envois consécutifs déclenche l'ouverture du circuit. Les messages sont mis en queue patiente (partner.X.parked avec TTL 24h). Après 5 minutes, une sonde tente un envoi. Si elle réussit, on draine la queue patiente progressivement avec rate limiting pour ne pas saturer le partenaire qui vient de revenir.

Paramètres clés

  • Failure threshold. Combien d'échecs pour ouvrir ? Trop bas (1 échec) déclenche en faux positif ; trop haut (50 échecs) attend trop. Valeur typique : 5 à 10 sur une fenêtre de 1 minute, ou taux d'erreur ≥ 50% sur une fenêtre de 20+ appels.
  • Cooldown. Combien de temps avant la sonde ? Trop court (5s) re-stresse un partenaire qui est encore en panne ; trop long (1h) prolonge inutilement l'indisponibilité quand le partenaire est revenu. Valeur typique : 1 à 5 minutes.
  • Half-open probe count. Combien d'appels-sondes avant de fermer ? 1 suffit dans la plupart des cas. Plus, si le partenaire est connu pour avoir des récupérations en dents de scie.
  • Granularité du circuit. Par partenaire, par flux ? La granularité (partenaire, type de message) permet de différencier « l'envoi ORDERS échoue mais le téléchargement INVOIC marche » — utile quand le partenaire a plusieurs endpoints séparés.

Anti-patterns

  • Circuit global. Un seul circuit pour tous les partenaires : la panne d'un partenaire bloque les autres. Toujours scinder.
  • État en mémoire seulement. Si l'état du circuit est en mémoire d'une instance, un redémarrage le réinitialise et le partenaire est remarketé. Persister l'état dans un store partagé.
  • Pas de visibilité. Un circuit ouvert qui n'émet pas d'alerte est un trou noir. Toujours notifier l'opération à l'ouverture, ré-confirmer à la fermeture.
  • Confondre erreur transitoire et erreur métier. Un 429 (rate limit) ou un 503 (service unavailable) doivent compter pour le circuit breaker ; un 400 (bad request) ou 422 (unprocessable entity) sont des erreurs métier qui ne doivent pas ouvrir le circuit — elles vont en DLC.
  • Pas de drain progressif. À la fermeture du circuit, balancer toute la queue d'un coup re-déclenche la panne. Toujours rate-limiter le drain.

Patterns liés

  • Retry & backoff — couvre les erreurs transitoires courtes ; le circuit breaker couvre les pannes durables.
  • Dead Letter Channel — où vont les messages quand le circuit reste ouvert au-delà du TTL.
  • Flux d'exception — la matrice d'escalade qui consomme les alertes circuit-ouvert.

Sources

  • Fowler M.CircuitBreaker, martinfowler.com, 2014. La description canonique du pattern, reprise depuis dans toutes les piles cloud-native. martinfowler.com/bliki/CircuitBreaker.html
  • Nygard M.Release It! Design and Deploy Production-Ready Software, Pragmatic Bookshelf, 2007 (2e éd. 2018). La référence d'origine sur les patterns de stabilité, dont le Circuit Breaker.
  • Netflix Hystrix (2012, archivé). Bibliothèque de référence qui a popularisé le pattern dans le cloud-native. github.com/Netflix/Hystrix
  • resilience4j. Successeur moderne, librairie de référence sur la JVM. resilience4j.readme.io — Circuit Breaker
  • AWS Architecture BlogTimeouts, retries, and backoff with jitter (Marc Brooker), avec mention du circuit breaker comme complément. aws.amazon.com — Builders' Library