Failed streamed TVP request poison other requests in the application
メンテナーはふだん 2 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 45/100
調査の方向性
Start with TdsParser.FailureCleanup in src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient/Parser/TdsParser.cs, then trace how WritePacket updates _outputPacketNumber and how attention cleanup is handled. Use the supplied TVP reproduction with a row source that throws around row 256,000, in both sync and async modes. Done means the failure sends attention or dooms the connection so a subsequent command on the pooled connection succeeds.
索引モデルが issue の本文から書いたものです。
説明
Describe the bug
Pooled connection left with an unfinished RPC when a streamed TVP throws in a packet whose number wrapped to 1
When the IEnumerable<SqlDataRecord> value of a table-valued parameter throws while SqlClient is writing the RPC,
TdsParser.FailureCleanup sends an attention only if stateObj._outputPacketNumber != 1, i.e. if it thinks a packet
has already been sent:
// src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Parser/TdsParser.cs, FailureCleanup
int old_outputPacketNumber = stateObj._outputPacketNumber;
...
stateObj.ResetBuffer();
stateObj.ResetPacketCounters();
if (old_outputPacketNumber != 1 && _state == TdsParserState.OpenLoggedIn)
{
...
// If _outputPacketNumber prior to ResetBuffer was not equal to 1, a packet was already
// sent to the server and so we need to send an attention and process the attention ack.
stateObj.SendAttention();
ProcessAttention(stateObj);
}
_outputPacketNumber is a byte incremented for every packet (TdsParserStateObject.WritePacket), so it wraps
around: while the 257th (513th, ...) packet of the request is being filled it is 1 again, although 256 packets were
already sent. In that case no attention is sent, the server keeps waiting for the rest of the RPC, and the connection
is returned to the pool as healthy. The next command on that pooled connection (any caller) fails, because the server
parses its bytes as the continuation of the unfinished TVP:
Microsoft.Data.SqlClient.SqlException (0x80131904): The incoming tabular data stream (TDS) remote procedure call (RPC)
protocol stream is incorrect. Parameter 4 (""): Data type 0x12 is unknown.
The exact error depends on the bytes; in production we get 8037:
The incoming tabular data stream (TDS) remote procedure call (RPC) protocol stream is incorrect. Table-valued parameter 3 (""),
row 213890, column 4: Data type 0x68 has an invalid data length or metadata length.
The data for table-valued parameter "@ImportTable" doesn't conform to the table type of the parameter. SQL Server error is: 8037, state: 44
When the exception is thrown in any other packet, SqlClient sends the attention and the connection is fine.
To reproduce
TvpPoolRepro.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<SqlClientVersion Condition="'$(SqlClientVersion)' == ''">6.1.2</SqlClientVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.SqlClient" Version="$(SqlClientVersion)" />
</ItemGroup>
</Project>
Program.cs
using System.Data;
using System.Reflection;
using Microsoft.Data.SqlClient;
using Microsoft.Data.SqlClient.Server;
var connectionString = args.FirstOrDefault(arg => !arg.StartsWith("--"))
?? @"Server=(localdb)\MSSQLLocalDB;Database=tempdb;Integrated Security=true";
var useSync = args.Contains("--sync");
Console.WriteLine(
$"Microsoft.Data.SqlClient {typeof(SqlConnection).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion}, "
+ (useSync ? "sync" : "async"));
await using (var connection = new SqlConnection(connectionString))
{
await connection.OpenAsync();
await using var command = new SqlCommand(
"IF TYPE_ID('dbo.ReproTvp') IS NULL CREATE TYPE dbo.ReproTvp AS TABLE (Id int NOT NULL, Flag bit NOT NULL)",
connection);
await command.ExecuteNonQueryAsync();
}
// 1. TVP streamed from an enumerable that throws mid-request.
// A row is 8 bytes, so an 8000 byte packet holds 999 rows: rows 255717..256715 are written into the 257th packet,
// whose TDS packet number wrapped around to 1 (any row in this window reproduces, rows outside it don't)
await using (var connection = new SqlConnection(connectionString))
{
await connection.OpenAsync();
Console.WriteLine($"TVP command on connection {connection.ClientConnectionId}");
await using var command = new SqlCommand("SELECT COUNT(*) FROM @Rows", connection);
command.Parameters.Add(
new SqlParameter("@Rows", SqlDbType.Structured) { TypeName = "dbo.ReproTvp", Value = GetRows(failAtRow: 256_000) });
try
{
if (useSync)
command.ExecuteNonQuery();
else
await command.ExecuteNonQueryAsync();
}
catch (Exception exception)
{
Console.WriteLine($" expected: {exception.GetType().Name}: {exception.Message}");
}
}
// 2. unrelated parameterized command; the pool hands out the same physical connection
await using (var connection = new SqlConnection(connectionString))
{
await connection.OpenAsync();
Console.WriteLine($"Next command on connection {connection.ClientConnectionId}");
await using var command = new SqlCommand("SELECT @Value", connection);
command.Parameters.Add(new SqlParameter("@Value", SqlDbType.Int) { Value = 1 });
try
{
var result = useSync ? command.ExecuteScalar() : await command.ExecuteScalarAsync();
Console.WriteLine($" OK, result {result}");
}
catch (SqlException exception)
{
Console.WriteLine($" BUG, error {exception.Number}: {exception.Message}");
}
}
static IEnumerable<SqlDataRecord> GetRows(int failAtRow)
{
SqlMetaData[] metaData = [new("Id", SqlDbType.Int), new("Flag", SqlDbType.Bit)];
for (var row = 1; ; row++)
{
// e.g. the rows come from an HTTP request body and the client disconnects
if (row == failAtRow)
throw new InvalidOperationException($"Row source failed at row {row}");
var record = new SqlDataRecord(metaData);
record.SetInt32(0, row);
record.SetBoolean(1, row % 2 == 0);
yield return record;
}
}
Output (dotnet run, same with -- --sync, -p:SqlClientVersion=7.1.1, -p:SqlClientVersion=5.1.6):
Microsoft.Data.SqlClient 6.1.2+607b8c975a0866b674a63aa5ee389c07f85f88e8, async
TVP command on connection ad96acab-3318-47a5-864c-3ee0a3935a0a
expected: InvalidOperationException: Row source failed at row 256000
Next command on connection ad96acab-3318-47a5-864c-3ee0a3935a0a
BUG, error 8009: The incoming tabular data stream (TDS) remote procedure call (RPC) protocol stream is incorrect. Parameter 4 (""): Data type 0x12 is unknown.
Sweeping failAtRow from 1 to 400 000 (step 997) fails only inside one window; a step-1 sweep of its edges gives exactly
rows 255 717..256 715, i.e. the 999 rows of the 257th packet. The next window is around 511 500..512 500 (the next wrap). Reading TdsParserStateObject._outputPacketNumber via reflection at the moment of the throw shows 1 for every
failing row. With failAtRow: 10_000 the next command succeeds.
Expected behavior
SqlClient sends an attention (as it does when the row source throws in any other packet), or dooms the connection,
so the next command on the pooled connection succeeds.
Further technical details
- Microsoft.Data.SqlClient version: 6.1.2; also reproduced with 7.1.1 and 5.1.6
- .NET target: net9.0
- SQL Server version: LocalDB 15.0.4382 (repro); Amazon RDS SQL Server 16.00.4255 (production)
- Operating system: Windows 11 (repro); Windows Server on EC2 (production)
- Both
ExecuteNonQueryAsyncandExecuteNonQueryare affected
Additional context
In production the TVP rows are streamed from an ASP.NET Core request body, so the row source throws when
the server rejects the body ("Request body too large") or the client disconnects. The failure then surfaces seconds later
in unrelated requests that get the pooled connection. We confirmed via ClientConnectionId that the victims run on the same physical connection.
- 主要言語
- C#
- スター
- 1k
- フォーク
- 352
- 平均マージ
- 4日 12時間
- マージ済み PR(30日)
- 87
環境構築
このプロジェクトの開発コンテナを、あなたの GitHub アカウントでブラウザ上に起動します。
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
dotnet/SqlClient のほかの issue
-
Sync ExecuteReader on an STA thread deadlocks against a StateChange handler that marshals to the UI thread (OpenAsyncRetry path)再び着手できるかも @mdaigle が 43 日前に担当しましたが、オープン中のプルリクエストはありません。 オープンExternal :link:
難易度 2/5 1〜3時間 初心者へのやさしさ 64/100
dotnet/SqlClient#4534 · コメント 2 件 · 担当者 1 名 ·
メンテナーはふだん 2 日以内に返信
-
Area\Tests
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
dotnet/SqlClient#4377 · コメント 2 件 ·
メンテナーはふだん 2 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 62/100
dotnet/SqlClient#4957 · コメント 2 件 ·
メンテナーはふだん 2 日以内に返信
-
難易度 3/5 1〜2日 初心者へのやさしさ 54/100
メンテナーはふだん 2 日以内に返信
-
Approval Needed Code Health :pill: Common Project 🚮 Design Review Needed Up-for-Grabs :raised_hands:
難易度 5/5 1週間以上 初心者へのやさしさ 25/100
dotnet/SqlClient#4940 · コメント 1 件 · リアクション 1 件 ·
メンテナーはふだん 2 日以内に返信
dotnet/SqlClient の issue をすべて見る
似ている issue
-
请让 FPS 叠加层的字号大小可单独设置オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 66/100
MahdiyarGHD/GheychiApp#39 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
-
難易度 1/5 1時間未満 初心者へのやさしさ 90/100
-
bug effort:S P3
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
nightscout/nocturne#3136 ·
メンテナーはふだん 1 日以内に返信