Skip to content

GH-5107 SQL Server queue sender moves the outbox row atomically for every durable send - #5112

Merged
jeremydmiller merged 1 commit into
mainfrom
gh-5107-sqlserver-queue-sender
Oct 21, 2026
Merged

jeremydmiller merged 1 commit into
mainfrom
gh-5107-sqlserver-queue-sender

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Follow-up to #5107 / #5109 for the SQL Server queue sender. Independent of #5109; each database engine gets its own PR.

The gap

SqlServerQueueSender.SendAsync(Envelope) only took the atomic MoveFromOutgoingTo* path when Envelope.WasPersistedInOutbox was true. Envelopes stored by the EF Core / Marten outbox and envelopes resent by outbox recovery are not marked, so they were inserted into the queue on their own and the outbox row was deleted later, in a separate transaction by DurableSendingAgent. A node that dies between the two leaves the outbox row behind; outbox recovery resends it, and once the receiver's handled inbox row has expired (KeepAfterMessageHandling) the copy is handled a second time.

Fix

A durable send that isn't marked WasPersistedInOutbox now goes through MoveOrWriteToQueueAsync, which writes the queue (or scheduled) row and deletes the outbox row, if there is one, in one transaction. One batch in BEGIN TRY / BEGIN TRANSACTION … COMMIT / CATCH ROLLBACK; THROW; the queue insert is skipped if the row is already there and the MERGE stays for scheduled sends. A key violation from a concurrent duplicate send (2627/2601) is treated as a duplicate. Marked envelopes keep the existing path; non-durable endpoints are unchanged. The DurableSendingAgent delete that follows becomes a no-op.

Tests

New Transport/Bug_5107_outbox_row_survives_the_queue_insert.cs:

  • the queue insert removes the outbox row
  • a scheduled send removes the outbox row
  • an envelope with no outbox row still reaches the queue
  • sending the same envelope twice leaves one queue row and no outbox row

Before the fix 3 of 4 failed ("outbox count should be 0 but was 1"); the no-outbox-row test was the control. After: 4/4. Locally on net9.0 the provider's Transport tests pass (130 passed, 1 skipped), and wolverine.slnx builds clean with -f net9.0.

Refs #5107

🤖 Generated with Claude Code

https://claude.ai/code/session_01DCggXgxnG3EBXoR9rrq37U

…very durable send

Durable sends without WasPersistedInOutbox (EF Core outbox, outbox recovery) now insert into the queue or
scheduled table and delete the outbox row in one transactional batch, instead of leaving the delete to
DurableSendingAgent in a second transaction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCggXgxnG3EBXoR9rrq37U
(cherry picked from commit 49b64e2)
@jeremydmiller
jeremydmiller merged commit 4e45f8a into main Oct 21, 2026
47 checks passed
This was referenced Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant