Competing Consumers
Plusieurs consommateurs tirent depuis un même canal ; le canal n'envoie chaque message qu'à un seul d'entre eux. C'est l'unité de scaling horizontal des hubs EDI : pour absorber un pic, on ajoute des workers, pas un plus gros worker.
Problème
Un hub EDI reçoit des centaines de milliers de messages par jour : INVOIC EDIFACT, 850 X12, UBL via PEPPOL. Un seul processus consommateur ne tient pas le débit, ne survit pas aux pics de fin de mois et devient un point de panne unique. Il faut pouvoir scaler horizontalement le traitement : ajouter des workers identiques qui prennent la charge en parallèle, sans réécrire la file ni le producteur.
Forces
- Le débit du producteur peut dépasser le débit d'un consommateur unique. À 800 messages/seconde en pointe, un seul parser est en sous-capacité dès la fin du premier mois.
- Le coût d'une instance plus grosse augmente non-linéairement. Doubler la RAM d'un worker coûte plus cher que doubler le nombre de workers identiques.
- La résilience est portée par le nombre. Si un worker crashe, les N-1 restants continuent de drainer la file pendant que l'orchestrateur en redémarre un.
- Chaque message ne doit être traité qu'une fois. Le canal doit assurer la livraison exclusive, sinon on retombe sur l'idempotence côté application.
Solution
EIP §502 (Hohpe & Woolf, 2003) définit Competing Consumers comme un canal point-à-point doté de N consommateurs, où le broker décide à chaque message lequel le reçoit. C'est l'implémentation historique des queues JMS, AMQP (RabbitMQ work queue), Amazon SQS, Azure Service Bus queue, et de tout consumer group Kafka pointé sur un topic à plusieurs partitions. Chaque message est livré à un seul consommateur ; l'acknowledgement du consommateur lance la suppression du message ou la libération du verrou de visibilité.
one queue N competing consumers
───────── ────────────────────
┌──▶ worker A (parser)
│
┌──────────┐ ┌────────────┐ │
│ producer │ ──▶│ queue │ ─────┼──▶ worker B (parser)
└──────────┘ └────────────┘ │
│
└──▶ worker C (parser)
The queue picks ONE worker per message. Add workers to scale out. Implémentation EDI
- Pool de parsers EDIFACT. Une queue
edi.in.rawreçoit chaque interchange tel quel à l'arrivée d'AS2. Un pool de N parsers tire en parallèle, tokenise l'interchange en transactions, écrit dans la queue suivante (edi.in.tx). L'auto-scaling provisionne plus de parsers en fin de mois. - Validation EN 16931 sur plusieurs workers. En PEPPOL,
la validation Schematron BIS 3.0 coûte plusieurs centaines de
millisecondes par facture. Un pool de validateurs concurrents lit
peppol.in.validateet écrit dans une queue de routage, transformant un coût p99 en débit additif. - Routage AS2 / OFTP2 sortant. Chaque envoi sortant
est une tâche I/O-bound (handshake AS2, chiffrement, attente du
MDN). Des workers concurrents partagent une queue
edi.out.partner.*et passent à l'échelle linéairement tant que le partenaire suit. - Workers Kafka par partition. Un topic
edi.events.invoic.validatedà 12 partitions admet 12 consommateurs dans le même consumer group : c'est une instance de Competing Consumers à granularité de partition, avec ordering garanti à l'intérieur de chaque partition.
Ordre, idempotence et FIFO partiel
L'ordre des messages est perdu dès qu'il y a plusieurs consommateurs concurrents : deux messages tirés en parallèle peuvent finir leur traitement dans n'importe quel ordre. C'est rarement un problème en EDI (chaque commande, chaque facture est indépendante), mais cela impose deux disciplines :
- Sticky partitioning quand l'ordre par partenaire
importe. Kafka et SQS FIFO permettent un partitioning par
MessageGroupId(par partenaire, ou par bulk ID) pour garder l'ordre intra-groupe tout en parallélisant entre groupes. - Idempotence systématique chez chaque consommateur. Si une livraison échoue après acknowledgement, certains brokers redélivrent. Voir Idempotence.
Anti-patterns
- Single consumer pinned to a queue. Ajouter un seul consommateur revient à un point-à-point classique ; on ne gagne ni en débit ni en résilience. Le pattern n'a de sens qu'avec N ≥ 2 workers.
- Order strictement requis sur tous les messages. Avec un strict ordering, soit on retombe sur un seul consommateur (et on perd le scaling), soit on partitionne sur un attribut stable. Forcer l'ordre global avec plusieurs workers donne des bogues subtils de réordonnancement.
- Acknowledgement avant le travail. ACK-before-work casse la garantie at-least-once : si le worker crashe après ACK mais avant écriture, le message est perdu silencieusement.
- Dimensionner sur la moyenne, pas sur le pic. Un pool de workers cadencé sur le débit moyen sature lors du pic de fin de mois et fait gonfler la queue à plusieurs millions de messages avant rattrapage. Auto-scaler sur la longueur de queue, pas sur le CPU des workers.
Patterns liés
- Point-to-Point Channel — le canal sous-jacent, étendu à N consommateurs.
- Message Channel — la généralisation.
- Idempotence — pré-requis dès qu'il y a redélivrance possible.
- Dead Letter Channel — destination des messages que les workers ne traitent pas.
Sources
- Hohpe G., Woolf B. — Enterprise Integration Patterns, pattern Competing Consumers (§502). enterpriseintegrationpatterns.com — Competing Consumers
- OASIS AMQP 1.0. La sémantique link / credit de RabbitMQ et Azure Service Bus formalise l'allocation d'un message à un seul subscriber parmi plusieurs. docs.oasis-open.org/amqp
- Apache Kafka — Consumer Groups. L'allocation de partitions aux consommateurs d'un même group est l'implémentation moderne du pattern. kafka.apache.org — Consumers
- AWS — SQS FIFO Queues. MessageGroupId permet le compromis ordering intra-group + scaling inter-group. docs.aws.amazon.com — SQS FIFO