Repository navigation
GH-5105 Durable receiver awaits the dead-letter write; a failed write is never reported as dead lettered - #5110
Merged
Conversation
…ts a failed one as dead lettered DurableReceiver.MoveToErrorsAsync only posted the write to a background RetryBlock, so MoveToErrorQueue completed the envelope (marking the inbox row, the only copy, handled) and recorded MovedToErrorQueue before anything was written. A write that kept failing was discarded after 3 attempts at Information. - The write is now awaited inline with a short retry budget - A write that cannot be made throws DeadLetterMoveFailedException, so MoveToErrorQueue neither completes nor reports the envelope, and the row is rescheduled for any node (retried until the store returns, as mark-as-handled does since GH-4664), logged at Error with id, message type and tenant - Envelopes dead lettered on receipt are settled only after the write, else deferred to the broker - Batches attempt every member before surfacing the failure - The missing-message-type receipt path no longer NREs building an ErrorReport from a null Failure Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCggXgxnG3EBXoR9rrq37U
This was referenced Oct 23, 2026
This was referenced Oct 26, 2026
This was referenced Oct 27, 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.
Closes #5105
Mechanism
On a durable listener,
MessageContext.MoveToDeadLetterQueueAsynchands the envelope toDurableReceiver.MoveToErrorsAsync. That method only posted the write to a backgroundRetryBlockand returned.MoveToErrorQueuethen completed the envelope, which marked the inbox row handled, and recordedMovedToErrorQueue. The broker delivery had already been settled when the inbox row was written, so that row was the only copy. If the write kept failing (a database outage, or a tenant database that could not be resolved as in the report), the block discarded it after 3 attempts with a line at Information. No dead-letter row existed, yet the logs, tracker,wolverine.error.queuedand the dead-letter counter all said the message had been dead lettered.Fix (
DurableReceiver,MessageContext)MoveToErrorsAsyncmakes the dead-letter write (or the_deadLetterSendersend) inline, retrying with pauses of 50ms, 250ms, 1s and 2s.DeadLetterMoveFailedException, the same contract NATS uses since 6.50.0, soMoveToErrorQueueandNoHandlerContinuationneither complete the envelope, publish the fault, nor recordMovedToErrorQueue. They recordRequeuedinstead. The failure is logged at Error with the envelope id, message type, tenant id and listener URI.ScheduleExecutionAsync, orScheduleRetryAsyncon a database-backed endpoint), so it is executed again and dead lettered once the store returns. Otherwise the row would sitIncomingand owned by a live node, which nothing reclaims (An envelope whose completion could not be written stays owned by the live node until the node stops (6.41.0) #4664). The reschedule retries until the store comes back or the host stops, the same way mark-as-handled has since An envelope whose completion could not be written stays owned by the live node until the node stops (6.41.0) #4664.new ErrorReport(envelope, envelope.Failure!)with a nullFailure, which threw a NullReferenceException inside the block, so those envelopes were never dead lettered either.Reproduction
CoreTests/Runtime/WorkerQueues/dead_letter_write_failures_5105.cshas 8 unit tests with a substituted inbox. On unmodifiedmain, 6 of the 8 fail. The key one showsMarkIncomingEnvelopeAsHandledAsynccalled on an envelope whose dead-letter write had failed, and nothing surfacing. The two that pass on main are a control and a regression guard.PostgresqlTests/Transport/Bug_5105_failed_dead_letter_write.csis end to end: a PostgreSQL durable queue listener with a handler that always throws andMoveToErrorQueue. The test renameswolverine_dead_lettersaway while the message fails, then renames it back. On main the dead-letter row never appears (the message was lost). With the fix the message runs again and lands in dead-letter storage, in about 40s.Tests run locally (
-f net9.0)Bug_1942...inline_rabbitmq_listener_replay_does_not_loopfailed because RabbitMQ was not running locally.inbox_outbox_usage.cascaded_response_with_outboxtimed out under load and passed 3/3 on rerun; it does not touch the dead-letter path.dotnet build wolverine.slnx -c Release -f net9.0builds cleanFollow-ups (not in this PR)
MultiTenantedMessageStorehas no fallback for the dead-letter write. A fallback to Main would need the row'sTenantIdcleared, becauseMarkIncomingEnvelopeAsHandledAsyncalso routes byTenantId, as the reporter found. With this PR such a message is retried rather than lost.IDeadLetterInterceptorgap: the native-DLQ branch ofMessageContext.MoveToDeadLetterQueueAsync, which aDurableReceivertakes, does not runIDeadLetterInterceptor. Only the non-nativeStorage.Inboxbranch does. Interceptors therefore don't apply to durable listeners' dead-letter rows today. This is pre-existing and worth its own issue.DurabilitySettingswould be an API decision.🤖 Generated with Claude Code
https://claude.ai/code/session_01DCggXgxnG3EBXoR9rrq37U