[Messenger] Add DispatchOnFailureStamp to dispatch a message when another one fails - #65917
Open
nicolas-grekas wants to merge 2 commits into
Open
[Messenger] Add DispatchOnFailureStamp to dispatch a message when another one fails#65917nicolas-grekas wants to merge 2 commits into
nicolas-grekas wants to merge 2 commits into
Conversation
wachterjohannes
requested changes
Sep 11, 2026
wachterjohannes
left a comment
Member
There was a problem hiding this comment.
The design avoids the obvious trap: the stamp is not copied onto the failure message, so a failing failure message cannot loop. Details inline: dispatching the failure message can itself throw, and neither call site catches that. Both may need a fix, and the right answer might differ between them.
A ChainStamp lists the messages to handle after the one it is attached to, in order. The new ChainMiddleware dispatches the next message of the chain once the current one is handled, with a DispatchAfterCurrentBusStamp, and passes the remaining messages along in a new ChainStamp. The next message keeps the bus name of the current one unless it names its own bus. When a step fails, the rest of the chain is not dispatched. The remaining steps travel with a message sent to a transport, so the chain continues from there once a failed step is retried. The Messenger bundle registers the middleware as messenger.middleware.chain, bound to the routable message bus, and adds it to the default middleware stack between send_message and handle_message.
…ther one fails A DispatchOnFailureStamp names the message to dispatch when the message it is attached to fails for good. The failure message is dispatched with a FailedMessageStamp holding the message that failed, the ErrorDetailsStamp recorded on the failed envelope or one created from the exception, and the bus name of the failed message unless it names its own bus. A worker gives up on a message when the retry listener does not retry it: the new DispatchOnFailureListener dispatches the failure message then, at priority 0, before the message is sent to a failure transport. When a message is handled synchronously, the exception is the give-up: the new DispatchOnFailureMiddleware catches it, dispatches the failure message and rethrows. The middleware also fires when the rest of the stack returns an envelope with a new SentToFailureTransportStamp, which a synchronous transport adds when it sends a failed message to a failure transport instead of throwing. A message received from a transport is skipped by the middleware, since the worker decides when it fails for good. Dispatching the failure message can itself fail, when it has no handler or when its handler throws. That failure is logged at error level and goes no further: it must not replace the exception of the message that failed, and it must not stop the other listeners of the worker event, such as the one that sends the failed message to the failure transport. ChainMiddleware copies the stamp of the current step onto the next one unless the next step carries its own, so that a chain dispatches the failure message once, whichever step fails. The Messenger bundle registers the middleware as messenger.middleware.dispatch_on_failure, at the end of the default "before" middleware so that it wraps user middleware such as the Doctrine transaction one, and the listener as messenger.failure.dispatch_on_failure_listener. Both use the routable message bus and log on the "messenger" channel.
nicolas-grekas
force-pushed
the
messenger-dispatch-on-failure
branch
from
September 13, 2026 07:37
d0967fa to
60bb704
Compare
wachterjohannes
approved these changes
Sep 13, 2026
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.
Stacked on #65903, which has to be merged first: the diff shows both until then.
A message can now declare what to dispatch when it fails for good:
This is the "catch" that #50462 asked for, without serializing a closure: the failure hook is a message, and the failed message plus the error travel with it. The hook belongs to the message that can fail, not to the chain, so it is just as useful on a single message ("notify when this import fails");
ChainMiddlewarecopies it onto each next step, so whichever step breaks fires it once.When it fires
"For good" means after the retries:
WorkerMessageFailedEventreports that the message will not be retried (DispatchOnFailureListener, priority 0, between the retry listener and the failure-transport listener).DispatchOnFailureMiddleware, at the end of the defaultbeforemiddleware so that it wrapsdoctrine_transaction: the failure message is dispatched after the rollback). The same middleware fires when async://transport with a failure transport ([Messenger] Add retry and failure transport support to the sync transport #65902) sends the message there instead of throwing, which it detects from theSentToFailureTransportStampon the returned envelope.A nested synchronous dispatch inside a worker handler has no retries of its own, so its failure fires on every attempt of the outer message.
What is dispatched
Envelope::wrap($failureMessage)with aFailedMessageStampholding the message that failed, theErrorDetailsStampof the failure, and theBusNameStampof the failed message so that the same bus handles it. With #65899, a handler reads both by type-hinting them:The failure message carries no
DispatchOnFailureStampof its own, so a failing failure message does not loop.Public API
Symfony\Component\Messenger\Stamp\DispatchOnFailureStamp(object $message)Symfony\Component\Messenger\Stamp\FailedMessageStamp(object $message)Symfony\Component\Messenger\EventListener\DispatchOnFailureListenerSymfony\Component\Messenger\Middleware\DispatchOnFailureMiddlewaredispatch_on_failurein the default middleware stack (guarded for older Messenger versions).Checks
./phpunit src/Symfony/Component/Messenger/Testsand./phpunit src/Symfony/Bundle/FrameworkBundle/Tests: green (usual missing-server skips).Documentation
ChainStampandRedispatchMessage.