OrderPlaced despachado, 14:02:11. Rollback de Doctrine, 14:02:11. Dos líneas de log con la misma marca de tiempo y, entre ellas, un cliente que recibió un correo de confirmación de un pedido que no existe. Casi todos hemos vivido alguna versión de esa noche. Guardas una fila y envías un mensaje a AMQP o SQS, las dos operaciones no forman una sola unidad atómica y, tarde o temprano, el hueco entre ellas te hace daño. Mi postura sobre Symfony 8.2 es sencilla. De todo lo que trae esta versión, el outbox transaccional es lo que más importa a los equipos que trabajan con esto a diario, y creo que debería convertirse en la forma predeterminada de conectar Messenger con un broker. No debería ser el truco ingenioso al que recurres después del tercer incidente.
php bin/console messenger:consume db_outbox
# handles the messages sent to "orders" as usual
php bin/console messenger:consume ordersPrimero, los hechos. La 8.2 está prevista para finales de noviembre de 2026, requiere PHP 8.4.0 o superior y tendrá soporte hasta julio de 2027. La rama todavía se está moviendo, así que no despliegues nada desde ella por ahora. El outbox es obra de Nicolas Grekas y es una opción de transporte. Designas un transporte de Doctrine como outbox, los mensajes se insertan ahí dentro de tu transacción de base de datos y un worker de relay los reenvía más tarde al broker real. Tus consumidores siguen leyendo de RabbitMQ o SQS exactamente igual que antes. Un rollback se lleva el mensaje junto con los datos. Si el broker está caído, el mensaje espera en una tabla hasta que vuelva.
La mayor parte de esto ya se podía conseguir antes. Enrutar todo al transporte de Doctrine te daba inserciones transaccionales, y muchos equipos lo hacían sin hacer ruido. El precio era que tu base de datos se convertía en la cola, con todos los workers haciendo polling contra ella. He visto un Postgres modesto dedicar una parte considerable de su CPU a responder el mismo SELECT vacío de una docena de consumidores ociosos. El outbox se queda con la parte útil de aquel truco, la inserción que se confirma o se revierte junto con tus datos, y devuelve el trabajo de cola de verdad al software pensado para ello.
Esta es la objeción más fuerte, y me parece justa. Tienes una pieza móvil más. El relay es un proceso que tienes que supervisar, monitorizar y escalar, y si se atasca, tus mensajes se acumulan en una tabla que nadie está mirando. También se pierde el orden: la documentación dice que los mensajes reenviados pueden llegar desordenados. Además, los reintentos se complican. Un fallo al reenviar usa la estrategia de reintentos y el transporte de fallos del transporte outbox, mientras que un fallo en el handler se reintenta en el transporte de destino. Eso te deja dos caminos de fallo que entender en lugar de uno, y cualquiera que haya depurado una tormenta de reintentos sabe que cada camino extra tiene su coste.
Aun así, me quedo con el outbox, porque la alternativa parece más sencilla por una única razón: sus fallos no se ven. Un mensaje despachado antes del commit no te da ninguna métrica cuando algo sale mal. Te enteras por un ticket de soporte una semana después. Un relay atascado te da un número de filas creciente sobre el que puedes poner una alerta. Puestos a elegir, prefiero el fallo que puedo ver. Sobre el orden: si tus handlers dependen del orden de llegada en un broker distribuido, ya eran frágiles, y el outbox solo te obliga a reconocerlo. Los retrasos también siguen funcionando. Un mensaje con retraso espera en el outbox y sale hacia el broker cuando le toca.
La misma versión incluye una función complementaria que nace de esa misma honestidad sobre la entrega. Yanick Witschi aportó una opción claim_check: todo lo que supere un límite de bytes que defines por transporte va a un pool de caché PSR-6, y el broker solo transporta una referencia con un checksum. De nuevo cambias un fallo implícito por uno explícito. Tu pool de caché pasa a estar en el camino de entrega, su default_lifetime tiene que cubrir retrasos, reintentos y el tiempo en el transporte de fallos, y si un claim caduca obtienes una ClaimCheckNotFoundException envuelta en una MessageDecodingFailedException que pasa por el manejo normal de reintentos. Es mejor que un broker rechazando en silencio un payload enorme, siempre que nadie ejecute cache:pool:clear sobre el pool equivocado.
En la práctica, el cambio son dos workers en lugar de uno. Ese es todo el coste operativo, y prefiero mil veces justificar una entrada más en el supervisor durante una code review que explicarle a un product manager el correo de un pedido fantasma.
Así que esto es lo que me gustaría saber de ti, sobre todo si usas Messenger con mucho volumen: ¿activarías el outbox en todos los transportes que conviven con una escritura en base de datos, o hay cargas de trabajo en las que seguirías enviando directamente desde dentro de la transacción a propósito, asumiendo el riesgo? Si mantendrías algunas, me gustaría saber cuáles y por qué.




Comentarios
Aún no hay comentarios — escribe el primero.
Inicia la conversación
Sin cuenta ni contraseña — introduce tu correo y te enviamos un enlace de acceso de un solo uso. ¿Primera vez? Todo se configura automáticamente.
Tu valoración se aplicará automáticamente al iniciar sesión.
Revisa tu bandeja de entrada
Hemos enviado un enlace de acceso a …. Ábrelo en este dispositivo — esta pestaña te conectará automáticamente.
¿No llega nada? Mira en la carpeta de spam — y marca el correo como «No es spam» para que la próxima vez llegue directo.