Repository navigation
Scheduled retries on the SQL Server, MySQL, Oracle and SQLite queues are atomic - #5408
Merged
Merged
Conversation
…are atomic A scheduled retry of a message received on a database-backed queue deletes its inbox row and writes the envelope to the queue's scheduled table. On SQL Server, MySQL and SQLite the two statements ran autocommitted in one command, and on Oracle as two separate commands, so a scheduled write that failed left the inbox row deleted with nothing scheduled. The message then existed only in the receiver's in-memory retry block, and a node that died or shut down before that retry succeeded lost it. PostgreSQL was already atomic (one Npgsql command runs as one implicit transaction). Found by the out-of-process durability chaos rig: on SQL Server with a 5 second command timeout, ScheduleRetryAsync timed out and 3 of 18,030 messages, all ones that had gone through a scheduled retry, were in no table at the end. Each sender now runs the delete and the scheduled write in one ADO.NET transaction, like the GH-5107 outbox moves beside it. On SQL Server it is a client-side transaction rather than BEGIN TRANSACTION in the batch, so a command timeout's attention cannot leave it open on a pooled connection. Tests (one per store): a failed scheduled write -- a trigger, or on Oracle a message type too long for the column -- now leaves the inbox row in place, and the receiver's retry completes the move once the write can succeed. Each fails without its fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 6, 2026
Closed
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.
The bug
A scheduled retry of a message received on a database-backed queue (
ScheduleRetryin an error policy) deletes the message's inbox row and writes the envelope to the queue's scheduled table. On four of the five stores those two writes were not atomic:delete ...; merge ...in one batch, no transactionDELETE ...; INSERT ...in one command, autocommitDELETE ...; INSERT ...in one command, autocommitWhen the scheduled write failed (a command timeout, a deadlock), the inbox row was already gone and nothing was scheduled. The message then existed only in the receiver's in-memory
RetryBlock. If the node died, or shut down, before that retry succeeded, the message was lost with no trace in the inbox, the queue, the scheduled table or the dead letters.How it was found
The out-of-process durability chaos rig ran with EF Core on SQL Server, the SQL Server queue transport, 5 second command timeouts, and random SIGKILLs, SIGTERMs and process pauses.
SqlServerQueueSender.ScheduleRetryAsynctimed out repeatedly, and 3 of 18,030 messages were lost, all of them messages that had gone through a scheduled retry. A PostgreSQL run with the same faults stayed clean.The fix
Each sender now runs the delete and the scheduled write in one ADO.NET transaction, the same way as the GH-5107 outbox moves in the same classes. On SQL Server this is a client-side
SqlTransaction, notBEGIN TRANSACTIONin the batch: a command timeout's attention is not caught by T-SQLTRY/CATCH, and would leave a server-side transaction open on a pooled connection.Tests
There is one
scheduled_retry_is_atomicclass per store, in SqlServerTests, MySqlTests, OracleTests and SqliteTests:The failed-write test of each store fails without that store's fix and passes with it.
Run locally on net9.0:
dotnet build wolverine.slnx -c Release -f net9.0A SQL Server chaos rerun on this branch lost nothing. The timeout did not recur in that run, so the deterministic tests are the evidence for the fix.
Not in this PR
The same rerun found a separate problem. With EF Core, when a handler's commit is applied on the server but the client sees an error after a timeout, the envelope is moved to the dead letters even though its work committed, so a replay runs it twice. The commit's cascaded local-queue inbox rows also stay
Incoming, owned by the live node, and nothing runs them. That needs its own design decision.🤖 Generated with Claude Code