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)

Aperta
#4,969 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 2 giorni

@poizan42 ci sta già lavorando.

Dal 4/11/2026.

  • #4995 di @poizan42 — aperta

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
15/100
Tipo di issue
Bug
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
csharp, sql

Direzione di ricerca

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.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

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)
Lingua principale
C#
Stelle
1k
Fork
352
Merge medio
4g 8h
PR unite (30g)
104

Preparare l'ambiente

Apri in Codespaces

Avvia il container di sviluppo del progetto nel browser, con il tuo account GitHub.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di dotnet/SqlClient

Tutte le issue di dotnet/SqlClient

Issue simili

Altre issue su C#

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.