Skip to content

Commit 2cc9e3f

Browse files
[Release 5.0] Fixes ReadAsync() behavior to register Cancellation token action before streaming results (#1785)
1 parent 914caf5 commit 2cc9e3f

File tree

4 files changed

+98
-12
lines changed

4 files changed

+98
-12
lines changed

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDataReader.cs

+7-6
Original file line numberDiff line numberDiff line change
@@ -4733,6 +4733,13 @@ public override Task<bool> ReadAsync(CancellationToken cancellationToken)
47334733
return Task.FromException<bool>(ADP.ExceptionWithStackTrace(ADP.DataReaderClosed()));
47344734
}
47354735

4736+
// Register first to catch any already expired tokens to be able to trigger cancellation event.
4737+
IDisposable registration = null;
4738+
if (cancellationToken.CanBeCanceled)
4739+
{
4740+
registration = cancellationToken.Register(SqlCommand.s_cancelIgnoreFailure, _command);
4741+
}
4742+
47364743
// If user's token is canceled, return a canceled task
47374744
if (cancellationToken.IsCancellationRequested)
47384745
{
@@ -4831,12 +4838,6 @@ public override Task<bool> ReadAsync(CancellationToken cancellationToken)
48314838
return source.Task;
48324839
}
48334840

4834-
IDisposable registration = null;
4835-
if (cancellationToken.CanBeCanceled)
4836-
{
4837-
registration = cancellationToken.Register(SqlCommand.s_cancelIgnoreFailure, _command);
4838-
}
4839-
48404841
ReadAsyncCallContext context = null;
48414842
if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection)
48424843
{

src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlDataReader.cs

+7-6
Original file line numberDiff line numberDiff line change
@@ -5326,6 +5326,13 @@ public override Task<bool> ReadAsync(CancellationToken cancellationToken)
53265326
return ADP.CreatedTaskWithException<bool>(ADP.ExceptionWithStackTrace(ADP.DataReaderClosed("ReadAsync")));
53275327
}
53285328

5329+
// Register first to catch any already expired tokens to be able to trigger cancellation event.
5330+
IDisposable registration = null;
5331+
if (cancellationToken.CanBeCanceled)
5332+
{
5333+
registration = cancellationToken.Register(SqlCommand.s_cancelIgnoreFailure, _command);
5334+
}
5335+
53295336
// If user's token is canceled, return a canceled task
53305337
if (cancellationToken.IsCancellationRequested)
53315338
{
@@ -5425,12 +5432,6 @@ public override Task<bool> ReadAsync(CancellationToken cancellationToken)
54255432
return source.Task;
54265433
}
54275434

5428-
IDisposable registration = null;
5429-
if (cancellationToken.CanBeCanceled)
5430-
{
5431-
registration = cancellationToken.Register(SqlCommand.s_cancelIgnoreFailure, _command);
5432-
}
5433-
54345435
var context = Interlocked.Exchange(ref _cachedReadAsyncContext, null) ?? new ReadAsyncCallContext();
54355436

54365437
Debug.Assert(context._reader == null && context._source == null && context._disposable == null, "cached ReadAsyncCallContext was not properly disposed");

src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
<Compile Include="SQL\ConnectivityTests\TcpDefaultForAzureTest.cs" />
8787
<Compile Include="SQL\DataBaseSchemaTest\ConnectionSchemaTest.cs" />
8888
<Compile Include="SQL\DataClassificationTest\DataClassificationTest.cs" />
89+
<Compile Include="SQL\DataReaderTest\DataReaderCancellationTest.cs" />
8990
<Compile Include="SQL\DataReaderTest\DataReaderStreamsTest.cs" />
9091
<Compile Include="SQL\DataReaderTest\DataReaderTest.cs" />
9192
<Compile Include="SQL\DataStreamTest\DataStreamTest.cs" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Diagnostics;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using Xunit;
9+
10+
namespace Microsoft.Data.SqlClient.ManualTesting.Tests
11+
{
12+
public class DataReaderCancellationTest
13+
{
14+
/// <summary>
15+
/// Test ensures cancellation token is registered before ReadAsync starts processing results from TDS Stream,
16+
/// such that when Cancel is triggered, the token is capable of canceling reading further results.
17+
/// </summary>
18+
/// <returns>Async Task</returns>
19+
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
20+
public static async Task CancellationTokenIsRespected_ReadAsync()
21+
{
22+
const string longRunningQuery = @"
23+
with TenRows as (select Value from (values (1), (2), (3), (4), (5), (6), (7), (8), (9), (10)) as TenRows (Value)),
24+
ThousandRows as (select A.Value as A, B.Value as B, C.Value as C from TenRows as A, TenRows as B, TenRows as C)
25+
select *
26+
from ThousandRows as A, ThousandRows as B, ThousandRows as C;";
27+
28+
using (var source = new CancellationTokenSource())
29+
using (var connection = new SqlConnection(DataTestUtility.TCPConnectionString))
30+
{
31+
await connection.OpenAsync(source.Token);
32+
33+
Stopwatch stopwatch = Stopwatch.StartNew();
34+
await Assert.ThrowsAsync<TaskCanceledException>(async () =>
35+
{
36+
using (var command = new SqlCommand(longRunningQuery, connection))
37+
using (var reader = await command.ExecuteReaderAsync(source.Token))
38+
{
39+
while (await reader.ReadAsync(source.Token))
40+
{
41+
source.Cancel();
42+
}
43+
}
44+
});
45+
Assert.True(stopwatch.ElapsedMilliseconds < 10000, "Cancellation did not trigger on time.");
46+
}
47+
}
48+
49+
/// <summary>
50+
/// Test ensures cancellation token is registered before ReadAsync starts processing results from TDS Stream,
51+
/// such that when Cancel is triggered, the token is capable of canceling reading further results.
52+
/// </summary>
53+
/// <returns>Async Task</returns>
54+
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
55+
public static async Task CancelledCancellationTokenIsRespected_ReadAsync()
56+
{
57+
const string longRunningQuery = @"
58+
with TenRows as (select Value from (values (1), (2), (3), (4), (5), (6), (7), (8), (9), (10)) as TenRows (Value)),
59+
ThousandRows as (select A.Value as A, B.Value as B, C.Value as C from TenRows as A, TenRows as B, TenRows as C)
60+
select *
61+
from ThousandRows as A, ThousandRows as B, ThousandRows as C;";
62+
63+
using (var source = new CancellationTokenSource())
64+
using (var connection = new SqlConnection(DataTestUtility.TCPConnectionString))
65+
{
66+
await connection.OpenAsync(source.Token);
67+
68+
Stopwatch stopwatch = Stopwatch.StartNew();
69+
await Assert.ThrowsAsync<TaskCanceledException>(async () =>
70+
{
71+
using (var command = new SqlCommand(longRunningQuery, connection))
72+
using (var reader = await command.ExecuteReaderAsync(source.Token))
73+
{
74+
source.Cancel();
75+
while (await reader.ReadAsync(source.Token))
76+
{ }
77+
}
78+
});
79+
Assert.True(stopwatch.ElapsedMilliseconds < 10000, "Cancellation did not trigger on time.");
80+
}
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)