Suspected: async MARS command can lose its own fatal error, and sync MARS Read waits for CommandTimeout, after another session tears down the connection (managed SNI)

Đang mở
#4,969 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 2 ngày

@poizan42 đang làm issue này rồi.

Từ ngày 4/11/2026.

  • #4995 của @poizan42 — đang mở

Đánh giá

Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức phù hợp với người mới
15/100
Loại issue
Lỗi
Độ rõ ràng
Khá rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
csharp, sql
Lĩnh vực
databases, networking

Hướng nghiên cứu

Start with the race described in TdsParser.TryRun (Parser/TdsParser.cs, around the early return on Broken/Closed) and SqlConnection.OnError (SqlConnection.cs, around the Close call on breakConnection). The managed SNI MARS receive path under ManagedSni/SniMarsConnection*.cs is where a pending sync Read would need a wake-up on teardown. The repro is the ManualTest MarsReceiveTeardownTest.cs, which is flaky and needs sysadmin and a non-Azure server. Done means both suspected behaviors are confirmed or ruled out with a dedicated repro.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

Triage Needed :new:
Describe the bug

These are two suspected (not confirmed) driver behaviors, found while investigating the flaky ManualTest MarsReceiveTeardownTest.FatalError_WhileMarsReaderStreams_DoesNotFaultReceivePump (added in #4680). They were observed with managed SNI and MARS enabled, when one MARS session raises a severity-20 error while another session is streaming a large result.

Suspected bug 1: an async MARS command can complete successfully even though its own result contained a fatal error.
RAISERROR('...', 20, 1) WITH LOG on MARS session 2 makes the server send the error token and then close the TCP connection. Often, the streaming reader on session 1 sees the transport close first:

  1. It raises a fatal (class 20) SqlException.
  2. SqlConnection.OnError breaks the parser and calls Close() (SqlConnection.cs ~2668).
  3. The async terminate command's End step then runs on a continuation.
  4. TdsParser.TryRun (Parser/TdsParser.cs ~2403) returns Done immediately when the parser state is Broken or Closed.

Step 4 means the command's own error token is never parsed, so ExecuteNonQueryAsync() completes without an exception. In other interleavings, the same command fails with InvalidOperationException: The connection is closed, either from the stored Begin-stage exception or from _activeConnection.Parser → GetOpenTdsConnection. The caller of the failing command never observes the sev-20 error it caused. The sync path does not show this, because the sync path parses the error token on the calling thread.

Suspected bug 2: on managed SNI, a sync MARS reader.Read() waits for CommandTimeout after teardown.
In the sync variants of the same test, the streaming session's reader.Read() sometimes takes about 60 s (the test's CommandTimeout = 60) to fail after the physical connection has been torn down. It does not fail promptly. This suggests a missing wake-up for pending sync MARS receives when the connection breaks.

Exceptions observed in the async CI failures (these are the "connection closed" variants of bug 1):

Exception message: System.InvalidOperationException : The connection is closed.
Stack trace:
   at Microsoft.Data.SqlClient.SqlCommand.EndExecuteNonQueryAsync(IAsyncResult asyncResult)  (line ~380, rethrow of asyncException.InnerException)
   -- or --
   at Microsoft.Data.SqlClient.SqlConnection.GetOpenTdsConnection()
   at Microsoft.Data.SqlClient.SqlConnection.get_Parser()
   at Microsoft.Data.SqlClient.SqlCommand.InternalEndExecuteNonQuery(...)  (line ~445)

The silent-success variant of bug 1 surfaced in the test as:
Assert.Throws() Failure: No exception was thrown. Expected: typeof(SqlException).

To reproduce

Use the ManualTest src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/MarsReceiveTeardownTest.cs as it was before the test fix. It needs sysadmin, a non-Azure SQL Server, and Switch.Microsoft.Data.SqlClient.UseManagedNetworkingOnWindows=true.

Simplified flow:

var cs = new SqlConnectionStringBuilder(baseConnectionString)
{
    MultipleActiveResultSets = true,
    CommandTimeout = 60
}.ConnectionString;

using var connection = new SqlConnection(cs);
await connection.OpenAsync();

// Session 1: stream a large result.
using var stream = new SqlCommand(
    "SELECT TOP (200000) a.object_id, b.name, REPLICATE('x', 200) AS pad " +
    "FROM sys.all_objects a CROSS JOIN sys.all_columns b", connection);
var reader = await stream.ExecuteReaderAsync();
var consumer = Task.Run(async () => { while (await reader.ReadAsync()) { } });

// Session 2: fatal error that terminates the physical connection.
using var terminate = new SqlCommand(
    "RAISERROR('teardown', 20, 1) WITH LOG", connection);
await terminate.ExecuteNonQueryAsync(); // expected SqlException (Class 20);
                                        // sometimes: no exception, or IOE "connection is closed"

Repro notes:

  • CI (public ADO, CI-SqlClient): the test failed in builds 188518, 188399, 188372, 188312, 188092 and 187614 (twice). All failures were async: True variants. Two failures were pooling False, delay 29 with "No exception was thrown". The rest were delay 1 with "The connection is closed".
  • Local loopback (SQL Server 2022 16.0.1210.3, net10.0, managed SNI): 0 failures in 56 runs. The race does not reproduce on its own locally.
  • Local, forced race: saturating the thread pool just before the async terminate command (ProcessorCount * 4 work items, each sleeping 300 ms) reproduced the "connection is closed" failure in 2 of 3 iterations.
  • Bug 2: the 60 s sync durations show up in ordinary local runs as test iterations taking about 2 min instead of about 1 min (the async False cases).
Expected behavior
  1. A command whose own batch raised a severity-20 error should surface that error as a SqlException (Class >= 20). If that is not possible, it should at least fail. It should never complete successfully because another MARS session observed the teardown first.
  2. A pending sync Read() on a MARS session should fail promptly when the physical connection is broken. It should not wait for CommandTimeout.
Further technical details

Microsoft.Data.SqlClient version: main (8.1.0-dev, after #4680 / 266a32e92)
.NET target: .NET 10.0 (managed SNI on Windows; CI job net10_0_AnyCPU_ManagedSNI_1)
SQL Server version: SQL Server 2022 (16.0.1210.3) locally; SQL Server 2025 named instance in CI (Win25_Sql25_Named_Instance)
Operating system: Windows 11 locally; Windows Server 2025 in CI

Additional context

  • Both items are suspected. Neither has been confirmed with a dedicated repro outside the test.
  • The test is being made robust to this race in a separate test-only PR. After that change, the test accepts either session reporting the fatal error, so it no longer detects suspected bug 1.
  • Relevant code:
    • TdsParser.TryRun early return on Broken/Closed
    • SqlConnection.OnError close on breakConnection
    • SqlCommand.NonQuery.cs EndExecuteNonQueryAsync / InternalEndExecuteNonQuery
    • managed SNI MARS receive path (ManagedSni/SniMarsConnection*.cs)
Ngôn ngữ chính
C#
Star
1k
Fork
352
Merge trung bình
3 ngày 21 giờ
Pull request đã merge (30 ngày)
112

Chuẩn bị môi trường

Mở trong Codespaces

Khởi chạy dev container của dự án ngay trên trình duyệt, bằng tài khoản GitHub của bạn.

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của dotnet/SqlClient

Tất cả issue của dotnet/SqlClient

Issue tương tự

Thêm issue về C#

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.