Skip to content

GH-5412 The SQL Server scheduled-message poll never waits on a lock - #5422

Merged
jeremydmiller merged 1 commit into
mainfrom
gh-5412-scheduled-poll-deadlock
Nov 7, 2026
Merged

jeremydmiller merged 1 commit into
mainfrom
gh-5412-scheduled-poll-deadlock

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #5412.

Cause

The system_health deadlock graphs from a calm three-node chaos baseline (EF Core with the SQL Server queue) all had the same shape. The scheduled poll's select TOP 100 ... where status = 'Scheduled' read the inbox under shared page locks and, holding one, waited for the next page. Meanwhile handlers' update ... set status = 'Handled' statements held intent-exclusive locks on those pages and waited to convert. In a chaos run with faults, SQL Server picked a handler's commit as the victim, and that message was dead-lettered. So this was more than log noise.

Change

  • Select: the load uses WITH (READPAST, ROWLOCK). The poll skips any row or page another transaction holds and leaves it for the next poll, so it never waits and can't be part of a cycle.
  • Promotion: the poll always uses the inline statement that matches each selected row by (id, received_at). The uspMarkIncomingOwnership procedure joined a table-valued parameter with no statistics, so SQL Server scanned the Scheduled rows and waited on rows the poll had not selected. The inline statement already ran under IdAndDestination or InboxStaleTime. The procedure is left in the schema, unused by this path.

Verification

Bug_5412_scheduled_poll_never_waits_on_a_row_lock holds a lock on one due scheduled row from another transaction. It asserts the poll returns promptly, promotes the other row, and leaves the locked one Scheduled. On main the poll waits until the test cancels it. The full SqlServerTests suite passes locally: 739 passed, 2 skipped.

Calm baselines with the durability chaos rig, 3 workers for 30 seconds (CHAOS_STORE=ef CHAOS_TRANSPORT=sqlqueue CHAOS_FAULTS=):

Build Deadlock victims Slowest message
main 17 (8 in the scheduled poll) 13.6 s
READPAST only 1, between two durable pops 7.5 s
this PR, run 1 0 3.4 s
this PR, run 2 0 4.0 s

🤖 Generated with Claude Code

https://claude.ai/code/session_013nKJrqsJw3j23Xd46q37ob

Under ordinary three-node load the durability agent's scheduled poll, or a handler's commit,
was chosen as a deadlock victim several times a minute. The system_health deadlock graphs show
the poll's select reading the inbox under shared page locks and waiting for the next page, while
handlers marking their own rows Handled held intent locks on those pages and waited to convert.

- The select takes READPAST and ROWLOCK, so it skips any row or page another transaction holds
  and leaves it for the next poll.
- The promotion always uses the inline statement that matches each selected row by primary key.
  The uspMarkIncomingOwnership procedure joined a table-valued parameter with no statistics, so
  it scanned the Scheduled rows and waited on rows the poll never selected.

Closes #5412

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nKJrqsJw3j23Xd46q37ob
@jeremydmiller jeremydmiller added this to the 6.63 milestone Nov 7, 2026
@jeremydmiller
jeremydmiller merged commit 15a5299 into main Nov 7, 2026
This was referenced Nov 9, 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.

SQL Server: the scheduled-message poll is a deadlock victim under normal multi-node load

1 participant