[Messenger] Let a flow carry its context in stamps: propagation, handler arguments and identity - #65899
Open
nicolas-grekas wants to merge 1 commit into
Open
Conversation
nicolas-grekas
force-pushed
the
messenger-propagated-stamps
branch
from
September 8, 2026 14:34
7981a1a to
4ac38df
Compare
nicolas-grekas
force-pushed
the
messenger-propagated-stamps
branch
from
September 8, 2026 15:00
4ac38df to
c8e1eb3
Compare
nicolas-grekas
force-pushed
the
messenger-propagated-stamps
branch
from
September 8, 2026 15:13
c8e1eb3 to
036e6fc
Compare
This was referenced Sep 8, 2026
nicolas-grekas
force-pushed
the
messenger-propagated-stamps
branch
from
September 13, 2026 06:38
036e6fc to
77c7add
Compare
…nto handlers Stamps implementing the new PropagatedStampInterface are copied by PropagateStampsMiddleware onto every message dispatched while the message carrying them is being handled. The middleware keeps a stack of the envelopes being handled, so nested dispatches at any depth inherit the stamps of the message on top of the stack, except for the stamp classes they already carry. A received message is never enriched, but the messages its handler dispatches inherit its propagated stamps. MessengerBundle registers the middleware as one shared service, listed after add_bus_name_stamp_middleware and before dispatch_after_current_bus, so that propagation works across buses and for messages queued with DispatchAfterCurrentBusStamp. Handler methods can now declare, after the message argument, arguments typed with Envelope or with a stamp class. HandlerDescriptor records them and HandleMessageMiddleware passes them as named arguments: the envelope, or the last stamp of that class. A missing stamp gives null to a nullable argument, leaves the default value of an optional argument in place, and throws a LogicException otherwise. Extra arguments from HandlerArgumentsStamp and stamp arguments must not overlap, since a named argument cannot replace a positional one. CorrelationStamp is the built-in stamp using that interface: give it the identifier of the request or of the process a flow belongs to, and every message dispatched while that one is handled carries it, across buses and transports. Three stamps carry the identity of a message: MessageIdStamp identifies the message itself and stays with it across retries and replays, CausationStamp holds the id of the message whose handling caused this one, and CorrelationStamp ties a whole flow together. AddIdentityStampsMiddleware adds them, only when the message does not carry them already, and mints ids with a closure that can be replaced. It must run before PropagateStampsMiddleware, which is what carries the correlation from a message to the ones dispatched while it is handled. MessengerBundle wires it behind the new messenger.identity_stamps option, with a UUIDv7 generator when the Uid component is installed.
nicolas-grekas
force-pushed
the
messenger-propagated-stamps
branch
from
September 13, 2026 14:00
77c7add to
e5ae412
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is a proposal, open for discussion. It started from Dariusz Gafka's article Symfony Messenger vs Ecotone: The Real Difference, which describes how Ecotone approaches this. What is proposed here is a free interpretation for Symfony, built on Messenger's own model rather than ported from Ecotone, so it departs from the article where the two models differ.
Stamps are the place for what a flow knows about a message but the message itself is not: a trace id, the tenant, who asked, which import run this row belongs to. Two things stop them from being used that way today:
HandlerArgumentsStamp.Either gap alone forces the same workaround, and it is the interesting part of this PR: the value gets added as a field to the message class, then to the next message class, and so on down the flow. Messages end up carrying data they do not use, only to pass it along. This PR closes both gaps, so a value can be attached once and read where it is needed.
The two halves together
Without the first half,
ImportCatalogHandlerhas to re-attach the stamp on every dispatch, and forgetting once loses it silently. Without the second half,ImportRowhas to carry a$tenantIdfield, and so does every other message of the flow, which is what the stamp was meant to avoid.Propagated stamps
A stamp implementing the new
PropagatedStampInterfaceis copied onto every message dispatched while the message carrying it is being handled, at any nesting depth and across buses. The copy is done by the newPropagateStampsMiddleware, which keeps a stack of the envelopes being handled. Rules:DispatchAfterCurrentBusStampinherit at dispatch time; what their own handlers dispatch later inherits from the root message being handled.CorrelationStampis what the component ships on top of the interface. Give it the identifier of the request or of the process the flow belongs to, and every message of the flow carries it:Nothing attaches it on its own, so an application that does not use it sees no change. The identity middleware below adds one when it is enabled.
FrameworkBundle enables the middleware by default as
propagate_stamps, right afteradd_bus_name_stamp_middlewareand beforedispatch_after_current_bus(that order is required, since queued messages are executed once the stack has unwound). It is one shared service for all buses, so that a command handler dispatching an event propagates to the event bus. Buses configured withdefault_middleware: falsehave to add it themselves.Stamp and envelope arguments for handlers
A handler method can declare, after the message, parameters typed with
Envelopeor with a stamp class. This half is also useful on its own, for stamps the system puts on the envelope:A missing stamp gives
nullto a nullable parameter, keeps the default value of a parameter that has one, and throws aLogicExceptionfor a required one:Message identity
Correlation says which flow a message belongs to. Two more stamps say which message this is and what caused it:
MessageIdStampidentifies the message itself, whereTransportMessageIdStampidentifies one delivery in one transport. It is kept across a retry, the failure transport and a replay, so it stays the same identifier from one end of a flow to the other.CausationStampholds the id of the message whose handling dispatched this one.AddIdentityStampsMiddlewareassigns them. A message that opens a flow gets an id and a correlation built from it. A message dispatched while another one is handled gets its own id and the id of that one as its cause. Anything the envelope already carries is left untouched, which is what lets an incoming message keep its place in the flow. It keeps its own frame of the messages in flight, so causation needs no interface of its own, and it has to run beforepropagate_stamps: the frame it pushes is what the children read, and the correlation of a nested dispatch has to come from the parent.Stamping every dispatch changes serialized envelopes, so it is opt-in:
Identifiers are UUIDv7 when the Uid component is installed, which keeps them ordered by creation and friendly to a database index, and 32 random hexadecimal characters otherwise. The generator is a closure, so decorating
messenger.message_id_generatorreplaces it.The option is global rather than per bus on purpose, because the middleware has to see every hop. A bus that skips it leaves its messages without an id, and the messages their handlers dispatch then take their cause from the nearest ancestor that did run, which is not the message that dispatched them. Enabling it on a command bus but not on the event bus it dispatches to gives this:
PlaceOrderOrderWasPlacedNotifyWarehouseNotifyWarehousewas dispatched by the handler ofOrderWasPlaced, yet it namesPlaceOrderas its cause. So a bus configured withdefault_middleware: falsehas to listadd_identity_stampsitself, immediately beforepropagate_stamps, or stay out of flows that are identified elsewhere.The stamp is looked up by exact class (
Envelope::last()), so a parameter typed with an interface or a parent class never matches. Stamp parameters are passed as named arguments, while the extra arguments ofHandlerArgumentsStampstay positional, so the two must not overlap. Batch handlers keep theirAcknowledgerparameter.Public API
Symfony\Component\Messenger\Stamp\PropagatedStampInterface(marker interface)Symfony\Component\Messenger\Middleware\PropagateStampsMiddlewareSymfony\Component\Messenger\Stamp\CorrelationStamp, the stamp of the component using that interfaceSymfony\Component\Messenger\Stamp\MessageIdStampandSymfony\Component\Messenger\Stamp\CausationStampSymfony\Component\Messenger\Middleware\AddIdentityStampsMiddleware, andframework.messenger.identity_stampsto enable itEnvelopeor stamp-typed parameters after the message.HandlerDescriptor::getStampParameters()carries the reflection result and is@internal(the class is final).The two halves are independently mergeable and touch disjoint files. They are one PR because either one alone leaves the workaround above in place. Identity is a third part, built on the first: it is the flow context the framework itself puts on a message. Say the word and I will split them.
Checks
./phpunit src/Symfony/Component/Messenger/Testsand./phpunit src/Symfony/Bundle/FrameworkBundle/Tests: green (usual missing-server skips).ArgumentCountErroron the base branch; the bundle tests fail without the wiring.class_exists()for the low-deps job (Messenger 7.4/8.0/8.1), and the identity middleware falls back to its own generator when the Uid component is absent.Documentation
CorrelationStamp: what to put in it, and that it follows a flow through transports and retries as long as it is attached at the entry point.identity_stampsoption and why it is opt-in, the three stamps and how they relate,MessageIdStampagainstTransportMessageIdStamp, the UUIDv7 generator and how to replace it, and why a bus that lists its middleware by hand has to includeadd_identity_stampsimmediately beforepropagate_stamps, with the broken causation chain above as the reason.HandlerArgumentsStamp.