Une commande est enregistrée, la réponse HTTP confirme son numéro, mais rien ne part vers la préparation. Le processus s’est arrêté entre le commit SQL et l’envoi du message. Relancer le serveur ne suffit pas : aucune intention durable ne dit au système qu’il reste quelque chose à publier. La panne laisse une commande parfaitement visible dans la base et absente du circuit qui doit la traiter. Le client attend sa préparation, tandis que le support cherche la cause de ce retard.
En pratique, une outbox transactionnelle doit conserver l’intention avec la donnée métier, puis accepter que sa transmission puisse être répétée. La reprise se joue ensuite à deux endroits : le relais doit retrouver le travail abandonné, et le destinataire doit reconnaître une livraison déjà appliquée. Le mot « transactionnel » ne couvre pas automatiquement ces deux frontières.
Le scénario fictif ci-dessous suit une commande C42 et son événement E42. Un programme téléchargeable provoque un véritable arrêt de processus après commit, simule une réponse perdue et vérifie la reprise d’une réservation expirée. Il utilise SQLite localement, sans broker ni API externe : les conclusions portent sur les invariants exécutés, pas sur une certification de Symfony ou PostgreSQL.
Pour un projet de développement d’application métier sur mesure, cette frontière conditionne la confiance dans les confirmations affichées. Un middleware conçu pour vos échanges doit rendre les intentions en attente consultables et récupérables, avec un propriétaire et une preuve de traitement, même lorsque le réseau ne permet plus de connaître immédiatement le résultat.
Dans quel cas une outbox répond au bon problème
Identifier les deux écritures qui peuvent diverger
Le besoin apparaît lorsqu’une même décision doit modifier une base locale et déclencher une opération dans un autre système. Par exemple, valider une commande puis prévenir le WMS, accepter un dossier puis lancer sa vérification, ou enregistrer une modification puis actualiser une projection. Les deux écritures n’appartiennent généralement pas à la même transaction locale.
Commencez par nommer ce qui doit être engagé ensemble. Si la commande peut exister sans préparation immédiate, son état peut devenir « enregistrée, transmission en attente ». Si le métier exige une réservation externe préalable, l’outbox ne supprime pas cette dépendance : il faut modéliser une étape provisoire et son issue avant d’annoncer une disponibilité garantie.
Écarter les usages qui ne nécessitent pas cette mécanique
Une lecture synchrone sans effet durable n’a pas besoin d’une outbox. Une notification purement informative, reconstruisible depuis un état fiable, peut accepter une politique plus simple si sa perte est assumée. À l’inverse, une instruction de débit ou de réservation ne doit pas être rangée dans cette catégorie par commodité technique.
Comparez le coût de l’exploitation au coût d’une intention oubliée : stockage, supervision, rétention, reprise et gestion des versions font partie de la solution. La décision doit couvrir le service rendu après la panne, pas seulement la facilité d’ajouter une table dans une migration Doctrine.
Engager la commande et son intention dans la même transaction
Donner une identité durable à l’événement
Notre commande C42 porte un montant fictif de 12 000 centimes. Sa validation insère également E42 dans l’outbox avec l’état « pending ». Ces deux écritures utilisent la même connexion et la même transaction. Si une exception survient avant commit, un observateur indépendant ne doit voir ni commande ni événement. Après commit, il doit retrouver les deux.
E42 identifie cette intention précise. Ce numéro reste stable pendant les reprises, alors que chaque tentative reçoit sa propre référence. Réutiliser le seul numéro de commande pour tous ses événements confondrait création, annulation et correction. Générer un nouvel identifiant d’événement à chaque envoi empêcherait le destinataire de reconnaître une redélivrance.
Conserver un contrat exploitable après un déploiement
Une implémentation complète ajoute le type d’événement, sa version de schéma, l’agrégat concerné et les données nécessaires au destinataire. Le payload doit représenter l’intention convenue ; une simple référence vers une ligne mutable peut changer de sens entre l’enregistrement et la lecture. Décidez explicitement si le consommateur attend un fait historique ou l’état courant.
La démonstration conserve seulement l’identité de commande, car elle mesure un effet local abstrait. Elle ne sérialise pas une commande réelle. Dans une application PHP, testez aussi la compatibilité entre un ancien événement encore en attente et le nouveau code du consommateur. Un déploiement réussi ne justifie pas de supprimer les messages devenus illisibles.
Lire les fenêtres de panne avant d’écrire le relais
Placer la preuve au bon endroit
La question utile est : « Quel état durable reste visible si le processus s’arrête ici ? » Avant le commit, aucune promesse n’est acquise. Après le commit, une intention doit être retrouvable. Après l’envoi mais avant l’enregistrement de son résultat, le relais peut ignorer que le destinataire a déjà appliqué l’opération. Cette dernière fenêtre impose la tolérance aux doublons.
| Interruption | État attendu | Reprise |
|---|---|---|
| Avant commit local | Ni C42 ni E42 | Reprendre la demande selon sa clé métier |
| Après commit, avant envoi | C42 et E42 en attente | Réserver E42 puis transmettre |
| Après effet distant, réponse perdue | Effet possible, E42 non acquitté localement | Renvoyer E42 avec déduplication côté réception |
| Après acquittement durable | E42 transmis | Ne plus sélectionner E42 comme travail courant |
Refuser le marquage anticipé comme raccourci
Marquer « envoyé » avant de publier évite certains doublons visibles, mais réintroduit une perte silencieuse si le processus tombe avant le réseau. Marquer après publication permet la reprise, au prix d’une possible répétition. Le risque est de croire que le premier choix est plus fiable parce que les compteurs de doublons diminuent.
Les deux ordres ne constituent donc pas deux variantes équivalentes. Le choix doit préserver une intention récupérable et faire porter la protection contre les répétitions à l’endroit où l’effet est réellement appliqué. Un tableau de bord vert n’est pas une preuve si les commandes oubliées ont simplement quitté la sélection.
Réserver le travail sans laisser un ancien worker le terminer
Utiliser un bail et une référence de tentative
Dans la recette, le worker A réserve E42 à l’instant fictif 100 pour dix unités de temps. À 105, B ne peut pas prendre la même entrée. À 111, le bail a expiré : B peut la reprendre avec son propre jeton. Ces nombres servent à vérifier une frontière ; ils ne recommandent pas un délai universel de dix secondes.
Le marquage final vérifie l’identité de l’événement, son état et le jeton de la réservation courante. Un retour tardif de A ne peut alors pas terminer la tentative de B. L’expiration seule ne suffit pas : sans cette comparaison, un ancien worker pourrait écraser un résultat plus récent après avoir retrouvé le réseau.
Ne pas confondre exclusion locale et arrêt du monde extérieur
Un bail expiré ne tue pas le worker précédent et n’annule pas son appel externe. A et B peuvent encore transmettre E42. Le jeton protège la transition dans l’outbox, tandis que l’identité stable E42 protège l’effet côté consommateur. Supprimer l’un de ces contrôles parce que l’autre existe ouvre une fenêtre différente.
Le producteur doit utiliser une horloge cohérente pour décider l’expiration, borner la durée de ses appels et renouveler prudemment les traitements longs. Sur une architecture distribuée, une réservation peut être perdue pendant une pause du processus. La reprise doit rester correcte même lorsque l’ancien worker reprend son exécution après cette pause.
Rendre l’effet local du consommateur idempotent
Enregistrer la réception et l’effet ensemble
La base du consommateur possède une inbox dont la clé unique est E42. Dans la même transaction, elle inscrit cette clé puis incrémente le compteur de préparation simulé. Si E42 est déjà présent, l’effet n’est pas appliqué une deuxième fois. Si une erreur intervient avant commit, la clé et l’effet sont tous deux annulés.
La recette provoque cette erreur avant de retenter. Une inbox écrite séparément de l’effet pourrait bloquer définitivement une opération jamais réalisée ; une inbox écrite après un effet non transactionnel pourrait laisser ce dernier se répéter. La preuve recherchée porte donc sur leur atomicité locale, pas uniquement sur l’existence d’une contrainte unique.
Reconnaître la nouvelle frontière lorsqu’un autre service intervient
Le compteur de la démonstration appartient à la même base que l’inbox. Un envoi d’email, un paiement ou une réservation dans un WMS ne partage pas cette transaction. Pour ces effets, il faut une clé d’idempotence reconnue par le fournisseur, une nouvelle intention durable, ou une procédure de rapprochement lorsque le résultat reste inconnu.
Le dossier sur l’idempotence et la reprise après erreur détaille cette distinction. La présence d’une inbox dans votre application ne démontre pas qu’un système tiers ne prélèvera jamais deux fois. La garantie doit suivre la chaîne jusqu’à l’opération que le métier cherche à protéger.
Exécuter l’arrêt après commit et la réponse perdue
Observer une base depuis un nouveau processus
Le programme de recette de l’outbox s’exécute avec Python et sa bibliothèque standard. Il crée deux bases temporaires, lance un processus enfant qui écrit puis quitte avec le code 81 après commit, et rouvre la base depuis le parent. C42 et E42 doivent alors être présents, sans avoir besoin de reconstruire l’intention à partir des logs.
Le programme simule ensuite une livraison appliquée dont la réponse n’est pas enregistrée par le producteur. B reprend la réservation expirée et livre à nouveau E42. Le compteur reste à un, l’inbox contient une seule entrée et le nombre de tentatives atteint deux. Un témoin volontairement naïf, sans déduplication, applique deux effets pour les deux livraisons.
python3 outbox-reprise-pannes.py
# Arrêt réel du processus enfant après commit : code 81.
# Deux tentatives de livraison ; un effet local protégé.
# Acquittement de l’ancien worker refusé.
# Témoin sans inbox : deux effets.
Délimiter précisément ce qui a été éprouvé
Les assertions couvrent le rollback producteur, la persistance après arrêt, l’exclusion avant expiration, la reprise du bail, le refus du jeton périmé et le rollback consommateur. Les entrelacements sont déterministes. Ce test n’exécute pas deux workers réellement concurrents et ne mesure ni débit, ni durabilité matérielle, ni comportement d’un broker.
Pour réceptionner votre installation, ajoutez les coupures de connexion, redémarrages de conteneurs, verrous concurrents et limites de stockage sur les composants retenus. Conservez un observateur indépendant de la transaction testée. Une assertion effectuée uniquement dans la connexion qui vient d’écrire peut manquer une différence entre données visibles localement et données réellement engagées.
Brancher le mécanisme dans Symfony sans surpromettre
Vérifier la connexion qui porte les écritures
La documentation Messenger sur Doctrine, vérifiée le 25 octobre 2026, décrit le middleware transactionnel et le rollback des handlers concernés. Cela ne rend pas atomique un envoi vers un broker externe avec votre commit SQL. La configuration effective du bus et du transport doit donc être examinée, pas seulement le nom du middleware.
Dans notre architecture proposée, le handler métier écrit sa donnée et l’outbox sur la même connexion. Un service distinct assure la publication. L’entrée du relais est une intention persistée ; sa sortie est une transmission confirmée selon le contrat du transport ou une attente attribuée. La journalisation conserve événement, tentative et erreur, sans confondre publication et exécution distante.
Garder la reprise visible dans l’interface
Le backend doit exposer « transmission en attente » lorsque cette nuance change l’action de l’utilisateur. Un bouton de relance ne crée pas une nouvelle commande : il demande une reprise contrôlée de l’intention existante. Le support doit pouvoir voir l’ancienneté, le prochain essai et le motif de blocage sans éditer directement une ligne SQL.
La responsabilité du relais couvre réservation, repli et traçabilité. Celle du consommateur couvre l’idempotence de son effet. Le contrat d’exploitation précise qui prend une exception en charge et comment la clôturer avec une preuve. L’article sur Messenger dans l’exploitation d’une application Symfony aide à relier ces états techniques à une promesse utilisateur compréhensible.
Choisir un relais SQL ou une capture de changements
Partager une file SQL sans prétendre préserver tout ordre
PostgreSQL documente SKIP LOCKED pour des usages de type file : les lignes déjà verrouillées sont ignorées. Ce comportement réduit certaines attentes entre consommateurs, mais donne une vue incomplète et ne convient pas à une lecture générale censée représenter toutes les données. La recette SQLite ne teste pas cette primitive PostgreSQL.
Avec plusieurs relais, la réservation de lignes distinctes ne garantit pas l’ordre de publication des événements d’une même commande. Si cet ordre compte, prévoyez une séquence par agrégat et un traitement des trous, ou une stratégie de partitionnement adaptée. Une clause de tri dans la sélection ne suffit pas à ordonner les appels réseau exécutés ensuite par différents workers.
Évaluer le CDC avec ses propres responsabilités
Le routeur outbox de Debezium exploite les changements de la table et distingue notamment l’identifiant d’événement de la clé d’agrégat. Cette dernière joue un rôle dans la répartition des messages Kafka. Le choix peut être pertinent lorsqu’une infrastructure de capture est déjà exploitée avec des garanties et une supervision connues.
Il ne faut pas additionner sans analyse un relais qui modifie les états et une capture conçue pour des événements immuables. Définissez quel composant publie, quel format il attend, comment il reprend et comment la rétention est pilotée. L’économie de code applicatif doit être comparée à l’exploitation du connecteur, des positions de lecture et des éventuels journaux accumulés.
Repérer le retard que les métriques de débit cachent
Surveiller la plus ancienne intention encore active
Un débit moyen satisfaisant peut masquer une poignée de commandes bloquées depuis longtemps. Premier signal faible : l’âge maximal progresse alors que le volume total diminue. Deuxième signal : les reprises de baux expirés augmentent sans hausse comparable des nouveaux événements. Ces observations orientent vers une famine de certaines lignes ou des workers incapables de terminer à temps.
Mesurez séparément attente de publication, nombre de tentatives et confirmation métier distante lorsqu’elle existe. Fixez un seuil en fonction de la prochaine échéance opérationnelle. Une préparation avant départ transport et une actualisation de tableau de bord n’ont pas le même budget de retard ; un unique seuil technique peut rester vert alors que la commande devient inutilisable.
Dimensionner aussi le rattrapage
Par exemple, une arrivée hypothétique de 50 événements par seconde crée 180 000 intentions pendant une heure d’arrêt. Si le relais reprend à 80 par seconde pendant que l’arrivée reste à 50, la capacité nette de rattrapage est de 30 par seconde. Il faut alors 6 000 secondes, soit 100 minutes, pour absorber ce retard dans ces conditions constantes.
Ce calcul ne représente pas un benchmark. Il rend visible un coût souvent oublié : revenir en ligne ne signifie pas revenir à jour. La capacité du destinataire, les limites API et la taille des messages peuvent réduire ce débit. Réservez une marge et donnez à l’exploitation un indicateur de délai estimé, assorti de ses hypothèses.
Erreurs fréquentes qui annulent la protection attendue
Faire disparaître l’intention pour nettoyer le tableau de bord
Une suppression automatique des entrées anciennes peut effacer précisément les dossiers qui demandent une intervention. Séparez événements terminés, tentatives échouées et intentions encore actives. La rétention doit conserver la preuve nécessaire au rapprochement et les identifiants de déduplication pendant la fenêtre où un rejeu reste possible.
- Écrire l’outbox avec une autre connexion hors de la transaction métier.
- Changer l’identifiant d’événement lors de chaque retry.
- Marquer « envoyé » avant d’obtenir la confirmation attendue du transport.
- Autoriser un jeton de réservation périmé à clore une tentative récente.
- Purger l’inbox alors que des événements anciens peuvent encore revenir.
Rejouer sans connaître la portée du résultat
Une réponse perdue demande de déterminer ce qui a pu être appliqué. Pour un consommateur idempotent éprouvé, la redélivrance peut être prévue par contrat. Pour une API qui ne propose ni clé stable ni consultation fiable, la même action peut exiger un rapprochement humain. L’outbox garde le problème visible ; elle ne fabrique pas une garantie absente chez le destinataire.
Consignez la procédure dans le runbook avec les références à vérifier, le responsable et l’action interdite tant que l’incertitude subsiste. Un bouton « tout rejouer » ne doit pas mélanger événements sûrs à répéter et opérations susceptibles de produire un nouvel effet commercial.
Plan d’action pour réceptionner une outbox récupérable
Construire la preuve sur une seule décision métier
Choisissez une commande représentative, son événement et le destinataire concerné. Décrivez les états visibles après chaque interruption avant de sélectionner un transport. Vérifiez la transaction réelle, la connexion utilisée et la stabilité de l’identité d’événement. Faites approuver la différence entre commande enregistrée, message transmis et opération réalisée par les équipes qui répondront au client.
Exécutez ensuite les pannes dans un environnement de test : avant commit, après commit, après application distante et avant acquittement. Ajoutez une reprise de bail et un ancien worker qui revient. Chaque test doit observer un état durable depuis une nouvelle session, puis comparer le nombre d’effets au nombre d’intentions distinctes. Un simple message de succès dans les logs n’est pas un critère de réception.
Ouvrir le relais avec une capacité de reprise mesurée
Limitez le premier périmètre et préparez les alertes avant la bascule. Mesurez le retard initial, le débit soutenable du destinataire et la capacité nette de rattrapage. Conservez les événements en attente lors du déploiement et testez leur décodage par le nouveau consommateur. Le responsable d’exploitation doit savoir interrompre les nouvelles publications sans supprimer le travail à reprendre.
Avant une intervention, le passage d’une application en lecture seule doit également couvrir ce relais et ses consommateurs. Le bandeau de maintenance ne suspend pas les intentions déjà persistées.
La décision d’étendre repose sur les exceptions réellement observées et sur leur résolution. Vérifiez notamment les reprises manuelles, la rétention de l’inbox et le comportement des messages trop anciens. Refusez l’élargissement si un arrêt banal laisse des intentions invisibles ou si un rejeu peut doubler un effet sans alerte. La qualité se mesure dans la récupération démontrée, au-delà du fonctionnement nominal.
Guides complémentaires pour éprouver les traitements différés
La cohérence de l’outbox ne dispense pas de vérifier la qualité des tests et les règles de lecture des messages conservés. Deux approfondissements aident à préparer les déploiements sans confondre ces responsabilités.
Construire un résultat attendu indépendant du code testé
La méthode des tests avec un oracle indépendant aide à choisir une preuve qui ne répète pas l’algorithme. Pour l’outbox, observer une nouvelle connexion et compter les effets réels évite de valider uniquement les valeurs internes du relais.
Conservez aussi un témoin incorrect qui doit échouer à votre critère. Ici, deux livraisons sans inbox produisent deux effets : cette différence montre que l’assertion sait distinguer une reprise protégée d’une simple répétition.
Faire survivre les messages aux changements de secrets
La rotation des secrets de signature dans Messenger traite la lecture des messages déjà présents pendant une évolution de configuration. Elle complète la recette lorsque le transport utilisé signe ses enveloppes et conserve une file ancienne.
Une intention durable mais impossible à décoder ou à vérifier reste bloquée. Préparez donc la compatibilité de lecture avant de changer les producteurs, puis exercez la reprise sur des messages créés avant le déploiement.
Conclusion : conserver l’intention et maîtriser sa répétition
Une outbox utile relie la décision métier à une intention durable. Elle ferme la perte entre commit local et publication, à condition que les deux écritures partagent réellement la transaction. Elle ne supprime ni les réponses inconnues ni les livraisons répétées qui apparaissent ensuite.
La recette montre comment retrouver une intention après un arrêt de processus, reprendre un bail expiré et refuser l’acquittement d’un ancien worker. Elle démontre aussi deux livraisons pour un seul effet local protégé. Ces preuves restent bornées au mécanisme exécuté et doivent être complétées sur les composants de votre architecture.
Le bon périmètre associe identité stable, transaction vérifiée, consommateur idempotent, journal de reprise et indicateurs de retard. Il permet au support d’expliquer une commande en attente et de la faire avancer sans recréer l’opération. C’est cette continuité qui donne de la valeur au dispositif.
Dawap accompagne le développement et la fiabilisation de vos applications métier pour transformer ces frontières techniques en comportements vérifiables. Le premier chantier consiste à choisir une décision critique, provoquer ses pannes et établir les preuves nécessaires avant d’étendre les traitements asynchrones.