Skip to content

Fix async cancellation failing to send TDS attention signal#4435

Open
cheenamalhotra wants to merge 10 commits into
mainfrom
dev/cheena/fix-async-cancel-attention
Open

Fix async cancellation failing to send TDS attention signal#4435
cheenamalhotra wants to merge 10 commits into
mainfrom
dev/cheena/fix-async-cancel-attention

Conversation

@cheenamalhotra

@cheenamalhotra cheenamalhotra commented Jul 9, 2026

Copy link
Copy Markdown
Member

Description

Fixes #4424
Fixes #44

When using CancellationToken with async operations like ExecuteReaderAsync/ExecuteNonQueryAsync, cancellation fails to send a TDS attention signal to SQL Server if the server is blocked (e.g., infinite WHILE loop, WAITFOR DELAY, or partial results from RAISERROR WITH NOWAIT followed by a blocking operation). The cancellation hangs until the query completes naturally — which may be forever for infinite loops.

Root Cause

EndExecuteReaderAsync (and the equivalent NonQuery/XmlReader methods) held lock(_stateObj) while calling into EndExecute*Internal. When the server hadn't yet sent result metadata (blocked on a long-running or infinite query), TryConsumeMetaData performed a synchronous network read (_syncOverAsync = true), blocking the thread while holding the monitor lock.

Meanwhile, stateObj.Cancel() uses Monitor.TryEnter(this, 100ms) in a polling loop on the same stateObj instance. Since the lock was held by EndExecute*Async for the entire duration of the server-side wait, Cancel() could never acquire the monitor and never sent the attention signal.

Fix

Remove lock(_stateObj) from the user-facing async end methods:

  • EndExecuteReaderAsync
  • EndExecuteNonQueryAsync
  • EndExecuteXmlReaderAsync

The lock is unnecessary in these paths because concurrent access is already handled by:

  1. Cancel() — uses polling Monitor.TryEnter with parser state guards in its loop
  2. Connection close — detected by TryRun checking parser Broken/Closed state
  3. StateObj internals — handle their own synchronization for read/write operations

Note: The lock(_stateObj) in CreateLocalCompletionTask (AE internal-end/retry path) is intentionally preserved — that path handles cancellation via the task's IsCanceled state rather than via stateObj.Cancel().

Testing

  • Added manual test for partial results scenario (severity 10 RAISERROR WITH NOWAIT + WAITFOR + CancellationToken)
  • Added manual test for cancellation during ExecuteReaderAsync (WAITFOR as first statement — also covers infinite loop scenario from Canceling SQL Server query with while loop hangs forever #44)
  • Added AE manual test exercising EndExecuteReaderAsync with AE-enabled commands (cache warmup + parameterized WAITFOR delay)
  • All existing unit tests pass (904/908; 4 pre-existing failures unrelated to this change)
  • Existing AE cancellation tests (TestSqlCommandCancellationToken) pass
  • Public API unchanged — no ref assembly updates needed
  • Verified against customer repro

Changes

File Change
SqlCommand.Reader.cs Removed lock(_stateObj) in EndExecuteReaderAsync
SqlCommand.NonQuery.cs Removed lock(_stateObj) in EndExecuteNonQueryAsync
SqlCommand.Xml.cs Removed lock(_stateObj) in EndExecuteXmlReaderAsync
DataReaderCancellationTest.cs Added CancellationSendsAttention_WhenPartialResultsReceived and CancellationDuringExecuteReaderAsync_SendsAttention tests
ApiShould.cs Added TestAsyncCancellationSendsAttention_WithAlwaysEncryptedCommand test

cheenamalhotra and others added 2 commits July 8, 2026 20:14
Remove lock(_stateObj) from EndExecuteReaderAsync, EndExecuteNonQueryAsync,
and EndExecuteXmlReaderAsync. The lock prevented Cancel() from acquiring
the stateObj monitor to send a TDS attention signal when the async
completion path was blocked on a synchronous network read (e.g., waiting
for metadata during WAITFOR). This caused cancellation via CancellationToken
to hang until the query completed naturally.

Concurrent close/cancel safety is maintained by:
- Parser state checks within TryRun (detects Broken/Closed state)
- Cancel()'s polling loop via Monitor.TryEnter with parser state guards
- The stateObj's internal synchronization mechanisms

Fixes #4424

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Validates that CancellationToken triggers TDS attention when the server
has sent partial results (RAISERROR WITH NOWAIT) but is blocked on
WAITFOR. This test would previously hang for 60+ seconds without the fix.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 9, 2026 06:09
@github-project-automation github-project-automation Bot moved this to To triage in SqlClient Board Jul 9, 2026
@cheenamalhotra cheenamalhotra added this to the 7.1.0-preview3 milestone Jul 9, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a deadlock/hang in async cancellation by removing lock(_stateObj) from the async EndExecute* paths so that TdsParserStateObject.Cancel() can acquire the monitor and send a TDS attention signal even when the async end-path is blocked on synchronous network reads (e.g., partial results + WAITFOR).

Changes:

  • Removed lock(_stateObj) from EndExecuteReaderAsync, EndExecuteNonQueryAsync, and EndExecuteXmlReaderAsync to avoid blocking cancellation’s attention-send path.
  • Added a new manual regression test intended to reproduce the “partial results + server blocked” cancellation hang scenario from #4424.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs Removes lock(_stateObj) from EndExecuteReaderAsync and documents rationale tied to #4424.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.NonQuery.cs Removes lock(_stateObj) from EndExecuteNonQueryAsync to keep cancellation from being blocked by the monitor.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs Removes lock(_stateObj) from EndExecuteXmlReaderAsync consistent with reader/nonquery.
src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderCancellationTest.cs Adds a manual regression test for cancellation sending attention after partial results are received.

…pt SqlException

Move CancelAfter() before ExecuteReaderAsync so cancellation fires during
the async completion path (not just ReadAsync). Also accept SqlException
in addition to OperationCanceledException since attention acknowledgment
from the server surfaces as SqlException.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 9, 2026 06:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Adds TestAsyncCancellationSendsAttention_WithAlwaysEncryptedCommand that
exercises the internal-end path in CreateLocalCompletionTask by warming
the query metadata cache first, then running a long-running AE query with
CancellationToken. Also removes the lock from CreateLocalCompletionTask
to fix the same contention issue in the AE retry path.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 9, 2026 06:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

Comment thread src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs Outdated
Comment thread src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs Outdated
- Use severity 10 in RAISERROR (matches real-world repro from #4424)
- Assert.Fail if reader is returned in WAITFOR-first test (should never happen)
- Fix AE test cache key: use same CommandText with parameterized @delay
  so warmup and cancel executions share the metadata cache entry

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 9, 2026 06:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 9, 2026 06:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

- Assert cts.IsCancellationRequested in all tests to guard against
  false positives from unrelated SqlExceptions
- Reorder AE test query to WAITFOR DELAY @delay; SELECT ... so that
  EndExecuteReaderInternal blocks on metadata (exercises the
  CreateLocalCompletionTask internal-end path as intended)
- Add Assert.Fail in AE test if reader is unexpectedly returned

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 9, 2026 07:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

@cheenamalhotra cheenamalhotra marked this pull request as ready for review July 9, 2026 07:12
@cheenamalhotra cheenamalhotra requested a review from a team as a code owner July 9, 2026 07:12
The lock in CreateLocalCompletionTask (AE internal-end/retry path) is
intentionally needed — that path handles cancellation via the task's
IsCanceled state, not via stateObj.Cancel(). Removing it caused
TestSqlCommandCancellationToken to fail with unexpected exception types.

The fix for #4424 only needs lock removal in the user-facing
EndExecuteReaderAsync/NonQuery/XmlReader paths where Cancel() contends
with synchronous network reads.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 9, 2026 07:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs Outdated
@cheenamalhotra cheenamalhotra marked this pull request as draft July 9, 2026 17:26
… comment

- Add CancellationOfInfiniteWhileLoop_DoesNotHang test (repro from #44)
- Fix CTS timing: start CancelAfter after OpenAsync to avoid false
  positives when connection open is slow
- Clarify CreateLocalCompletionTask lock comment to accurately describe
  why the lock is retained (timing differs from user-facing path)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 9, 2026 17:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs Outdated
- Reword CreateLocalCompletionTask lock comment to accurately state that
  this lock CAN block Cancel(), and explain why it's retained (data is
  already buffered when this continuation fires, unlike the user-facing
  path where blocking reads caused #4424)
- Add 45s watchdog timeout to CancellationOfInfiniteWhileLoop test with
  best-effort cleanup (Cancel/Close) to prevent hanging the test suite
  if the regression reappears

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 9, 2026 20:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs:2465

  • The retained lock(_stateObj) in CreateLocalCompletionTask can still recreate the #4424 cancellation hang in the Always Encrypted internal-end path when the server sends any early packet (e.g., RAISERROR ... WITH NOWAIT) before blocking on metadata. In that case localCompletion can complete after the first packet (BeginExecuteInternalReadStage uses _stateObj.ReadSni), this continuation can then enter lock(_stateObj) and block inside InternalEndExecute while waiting synchronously for metadata—preventing TdsParserStateObject.Cancel() from acquiring the same monitor to send ATTENTION.
                        // Lock on _stateObj serializes this internal-end path with
                        // close/cancel. Note: stateObj.Cancel() also acquires this monitor,
                        // so this lock CAN block Cancel() while endFunc is executing.
                        // This is retained here (unlike the user-facing EndExecute* methods)
                        // because this continuation runs after the initial async I/O has
                        // already completed — the blocking metadata read that caused #4424
                        // in the user-facing path does not apply here since the data is
                        // already buffered by the time this continuation fires.
                        lock (_stateObj)
                        {
                            endFunc(this, task, /*isInternal:*/ true, endMethod);
                        }

@cheenamalhotra cheenamalhotra marked this pull request as ready for review July 10, 2026 01:35
@cheenamalhotra cheenamalhotra moved this from To triage to In review in SqlClient Board Jul 10, 2026
@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 63.91%. Comparing base (fdebcd2) to head (22528a8).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
...lClient/src/Microsoft/Data/SqlClient/SqlCommand.cs 0.00% 8 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4435      +/-   ##
==========================================
- Coverage   65.83%   63.91%   -1.93%     
==========================================
  Files         287      282       -5     
  Lines       43763    66650   +22887     
==========================================
+ Hits        28812    42599   +13787     
- Misses      14951    24051    +9100     
Flag Coverage Δ
CI-SqlClient ?
PR-SqlClient-Project 63.91% <0.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In review

4 participants