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.

Async Bulkhead

L'isolation par sémaphore plutôt que par thread-pool — l'option qui rend le Bulkhead viable sur un hub à 500 partenaires.

Problème

Le Bulkhead classique alloue un pool de threads par partenaire. Sur un hub avec 500 partenaires actifs, à 20 threads par pool = 10 000 threads. À 1 Mo de stack par thread, on est à 10 Go RAM rien que pour les stacks. La majorité des threads dorment en attente d'I/O. Le context-switching kernel devient le bottleneck. Ce n'est plus viable.

Forces

  • L'isolation par partenaire reste nécessaire. Une saturation Walmart ne doit pas affecter Carrefour.
  • Les threads OS sont coûteux. Stack mémoire, scheduling kernel, context-switch.
  • L'I/O est presque toujours bloquant historiquement. Mais Project Reactor, Vert.x, Netty, async Python ont changé la donne.
  • Une coroutine ou un Future ne coûte rien. Quelques octets de stack, pas de scheduling kernel.

Solution

Remplacer le pool de threads par un sémaphore qui plafonne le nombre de calls concurrents. Le code applicatif appelle semaphore.acquire() avant l'appel partenaire (typiquement non-bloquant, retourne un CompletableFuture/Mono), exécute l'appel sur l'event-loop partagée, et libère le permit en fin. Si tous les permits sont pris, la requête échoue rapidement (BulkheadFullException) ou est mise en file d'attente bornée. L'isolation est logique, pas physique. Resilience4j propose les deux modes ; en Reactive Streams, le pattern est intégré nativement via la backpressure.

Variante Thread-pool Bulkhead :       Variante Semaphore Bulkhead :

  partenaire Walmart                     partenaire Walmart
   ─────────────────                      ─────────────────
   ThreadPool(20)                         Semaphore(20)
   ↓ 20 threads dédiés                    ↓ 20 permits
   ↓ ~1 Mo stack par thread               ↓ quelques octets de comptage
   ↓ blocage I/O OK                       ↓ I/O non-bloquant requis
   ↓ context-switch coûteux               ↓ ordonnancement par event-loop
   ↓ overhead 20 Mo/partenaire            ↓ overhead négligeable

  Pour 500 partenaires :
  - thread-pool  → 10 Go RAM rien que les stacks
  - semaphore   → quelques Mo total

Implémentation EDI

Cas concret : hub EDI Spring WebFlux avec Resilience4j en mode semaphore. Configuration resilience4j.bulkhead.instances.walmart.maxConcurrentCalls=20 et idem par partenaire. Le client AS2 utilise WebClient + Reactor Netty (event-loop partagée). Quand Walmart est lent, ses 20 permits sont utilisés, nouvelles requêtes vers Walmart sont rejetées avec BulkheadFullException, redirigées sur queue de retry avec backoff. Pour Carrefour, ses propres 20 permits restent disponibles. Coût mémoire : 500 partenaires × 20 permits = 10 000 entiers atomiques, soit ~80 ko. Vs 10 Go avec le mode thread-pool. Sur Kafka Streams, équivalent : partitions par clé partenaire, parallélisme borné par max.poll.records. Sur Python asyncio : asyncio.Semaphore(20) par partenaire.

Anti-patterns

  • Semaphore bulkhead sur du code bloquant. Le sémaphore ne protège pas le thread bloquant : il faut un thread-pool. Choisir le bon variant selon le code.
  • Pas de queue bornée. Si les requêtes en attente du sémaphore s'accumulent sans limite, on revient à la saturation mémoire. Définir maxWaitDuration et maxQueueSize.
  • Mélanger semaphore et thread-pool incorrectement. Mettre un semaphore-bulkhead devant un thread-pool-bulkhead sans coordonner les tailles : deux limites qui se chevauchent et qui produisent un comportement imprévisible.
  • Une seule event-loop pour tous les partenaires. Un CPU-bound dans un thread event-loop fait tomber tous les partenaires. Réserver l'event-loop à de l'I/O strict.

Patterns liés

  • Bulkhead — le pattern parent (variante thread-pool).
  • Back-pressure — souvent combiné en mode réactif : la backpressure régule l'amont.
  • Rate Limiter — le sémaphore plafonne la concurrence, le rate limiter plafonne le débit.
  • Circuit Breaker — souvent composé : si un partenaire échoue, le circuit s'ouvre avant que le bulkhead ne sature.

Sources

  • Resilience4j — Bulkhead. La documentation détaille les deux variants : SemaphoreBulkhead et ThreadPoolBulkhead. resilience4j.readme.io/docs/bulkhead
  • Nygard M.Release It!, Pragmatic Bookshelf, 2e éd. 2018. Le pattern parent.
  • Reactive Streams Specification. Backpressure native qui rend explicite la borne de concurrence. reactive-streams.org
  • Project Reactor — Reactor Netty bulkhead. Implémentation idiomatique en Reactor. projectreactor.io
  • Microsoft Architecture Center — Bulkhead pattern (mentionne les deux variants). learn.microsoft.com — Bulkhead