OrderPlaced dispatché, 14:02:11. Rollback Doctrine, 14:02:11. Deux lignes de log avec le même horodatage et, entre les deux, un client qui a reçu un e-mail de confirmation pour une commande qui n'existe pas. On a presque tous vécu une variante de cette nuit-là. Tu enregistres une ligne et tu pousses un message vers AMQP ou SQS, les deux opérations ne forment pas un tout atomique, et tôt ou tard l'écart entre les deux te fait mal. Ma position sur Symfony 8.2 est simple. De tout ce que contient cette version, c'est l'outbox transactionnelle qui compte le plus pour les équipes sur le terrain, et je pense qu'elle devrait devenir la façon par défaut de brancher Messenger sur un broker. Ce ne devrait pas être l'astuce qu'on sort après le troisième incident.
php bin/console messenger:consume db_outbox
# handles the messages sent to "orders" as usual
php bin/console messenger:consume ordersD'abord les faits. La 8.2 est prévue pour fin novembre 2026, demande PHP 8.4.0 ou plus récent et sera maintenue jusqu'en juillet 2027. La branche bouge encore, donc ne déploie rien dessus pour l'instant. L'outbox est l'œuvre de Nicolas Grekas, et c'est une option de transport. Tu désignes un transport Doctrine comme outbox, les messages y sont insérés dans ta transaction de base de données, et un worker relais les transmet plus tard au vrai broker. Tes consommateurs continuent de lire depuis RabbitMQ ou SQS exactement comme avant. Un rollback emporte le message avec les données. Si le broker est en panne, le message attend dans une table qu'il revienne.
On pouvait déjà obtenir l'essentiel de ça avant. En routant tout vers le transport Doctrine, tu avais des insertions transactionnelles, et pas mal d'équipes l'ont fait sans le crier sur les toits. Le prix à payer, c'est que ta base de données devenait la file d'attente, avec chaque worker qui venait l'interroger en boucle. J'ai vu un modeste serveur Postgres consacrer une part non négligeable de son CPU à répondre au même SELECT vide envoyé par une douzaine de consommateurs inactifs. L'outbox garde la partie utile de cette astuce, l'insertion qui est validée ou annulée avec tes données, et rend la vraie gestion de file à des logiciels conçus pour ça.
Voici l'objection la plus solide, et je la trouve légitime. Tu ajoutes une pièce mobile. Le relais est un processus qu'il faut superviser, monitorer et dimensionner, et s'il se bloque, tes messages s'accumulent dans une table que personne ne regarde. L'ordre disparaît aussi : la doc précise que les messages transmis peuvent arriver dans le désordre. Les retries se compliquent également. Un échec de transmission passe par la stratégie de retry et le failure transport de l'outbox, alors qu'un échec dans le handler est retenté sur le transport cible. Ça fait deux chemins d'échec à comprendre au lieu d'un, et quiconque a déjà débogué une tempête de retries sait que chaque chemin en plus a un coût.
Je penche malgré tout du côté de l'outbox, parce que l'alternative ne paraît plus simple que parce que ses échecs sont invisibles. Un message dispatché avant le commit ne te donne aucune métrique quand ça tourne mal. Tu l'apprends par un ticket de support une semaine plus tard. Un relais bloqué, lui, te donne un nombre de lignes qui grimpe et sur lequel tu peux poser une alerte. Entre les deux, je prends l'échec que je peux voir. Côté ordre : si tes handlers dépendent de l'ordre d'arrivée sur un broker distribué, ils étaient déjà fragiles, et l'outbox t'oblige simplement à l'admettre. Les délais continuent aussi de fonctionner. Un message différé attend dans l'outbox et part vers le broker à l'échéance.
La même version apporte une fonctionnalité compagnon qui découle de la même honnêteté sur la livraison. Yanick Witschi a contribué une option claim_check : tout ce qui dépasse une limite en octets que tu fixes par transport part dans un pool de cache PSR-6, et le broker ne transporte qu'une référence accompagnée d'une somme de contrôle. Là encore, tu échanges un échec implicite contre un échec explicite. Ton pool de cache se retrouve sur le chemin de livraison, son default_lifetime doit couvrir les délais, les retries et le temps passé dans le failure transport, et si un claim expire, tu obtiens une ClaimCheckNotFoundException enveloppée dans une MessageDecodingFailedException qui suit le traitement normal des retries. C'est mieux qu'un broker qui rejette en silence un payload trop gros, tant que personne ne lance cache:pool:clear sur le mauvais pool.
Concrètement, le changement, c'est deux workers au lieu d'un. C'est tout le coût opérationnel, et je préfère de loin justifier une entrée de plus dans le superviseur en code review plutôt que d'expliquer un e-mail de commande fantôme à un product manager.
Alors voilà ce que j'aimerais savoir, surtout si tu fais tourner Messenger à gros volume : est-ce que tu activerais l'outbox sur chaque transport qui accompagne une écriture en base, ou est-ce qu'il existe des charges pour lesquelles tu garderais volontairement l'envoi direct depuis la transaction, en acceptant le risque ? Si tu en gardes, dis-moi lesquelles et pourquoi.




Commentaires
Pas encore de commentaire — écris le premier.
Lance la discussion
Pas de compte ni de mot de passe — saisis simplement ton adresse e-mail et nous t’envoyons un lien de connexion à usage unique. Première visite ? Tout se met en place automatiquement.
Ton évaluation sera appliquée automatiquement après ta connexion.
Vérifie ta boîte mail
Nous avons envoyé un lien de connexion à …. Ouvre-le sur cet appareil — cet onglet te connectera automatiquement.
Rien reçu ? Vérifiez le dossier spam — et marquez le message « Non spam » pour qu'il arrive directement la prochaine fois.