Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for SqlConnectionOverrides for OpenAsync() API #2433

Merged
33 changes: 33 additions & 0 deletions doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml
Original file line number Diff line number Diff line change
@@ -2853,6 +2853,39 @@
Any error returned by SQL Server that occurred while opening the connection.
</exception>
</OpenAsync>
<OpenAsyncWithOverrides>
<param name="overrides">
Options to override default connection open behavior.
</param>
<param name="cancellationToken">
The cancellation instruction.
</param>
<summary>
An asynchronous version of <see cref="M:Microsoft.Data.SqlClient.SqlConnection.Open" />, which opens a database connection with the property settings specified by the <see cref="P:Microsoft.Data.SqlClient.SqlConnection.ConnectionString" />. The cancellation token can be used to request that the operation be abandoned before the connection timeout elapses. Exceptions will be propagated via the returned Task. If the connection timeout time elapses without successfully connecting, the returned Task will be marked as faulted with an Exception. The implementation returns a Task without blocking the calling thread for both pooled and non-pooled connections.
</summary>
<returns>
A task representing the asynchronous operation.
</returns>
<remarks>
<para>
After calling <see cref="M:Microsoft.Data.SqlClient.SqlConnection.OpenAsync" />, <see cref="P:Microsoft.Data.SqlClient.SqlConnection.State" /> must return <see cref="F:System.Data.ConnectionState.Connecting" /> until the returned <see cref="T:System.Threading.Tasks.Task" /> is completed. Then, if the connection was successful, <see cref="P:Microsoft.Data.SqlClient.SqlConnection.State" /> must return <see cref="F:System.Data.ConnectionState.Open" />. If the connection fails, <see cref="P:Microsoft.Data.SqlClient.SqlConnection.State" /> must return <see cref="F:System.Data.ConnectionState.Closed" />.
</para>
<para>
A call to <see cref="M:Microsoft.Data.SqlClient.SqlConnection.Close" /> will attempt to cancel or close the corresponding <see cref="M:Microsoft.Data.SqlClient.SqlConnection.OpenAsync" /> call. For more information about asynchronous programming in the .NET Framework Data Provider for SQL Server, see <see href="/sql/connect/ado-net/asynchronous-programming">Asynchronous Programming</see>.
</para>
</remarks>
<exception cref="T:System.InvalidOperationException">
<para>
Calling <see cref="M:Microsoft.Data.SqlClient.SqlConnection.OpenAsync(Microsoft.Data.SqlClient.SqlConnectionOverrides, System.Threading.CancellationToken)" /> more than once for the same instance before task completion.
</para>
<para>
A connection was not available from the connection pool before the connection time out elapsed.
</para>
</exception>
<exception cref="T:Microsoft.Data.SqlClient.SqlException">
Any error returned by SQL Server that occurred while opening the connection.
</exception>
</OpenAsyncWithOverrides>
<PacketSize>
<summary>
Gets the size (in bytes) of network packets used to communicate with an instance of SQL Server.
Original file line number Diff line number Diff line change
@@ -975,6 +975,8 @@ public override void Open() { }
public void Open(SqlConnectionOverrides overrides) { }
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/OpenAsync/*'/>
public override System.Threading.Tasks.Task OpenAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/OpenAsyncWithOverrides/*'/>
public System.Threading.Tasks.Task OpenAsync(Microsoft.Data.SqlClient.SqlConnectionOverrides overrides, System.Threading.CancellationToken cancellationToken) { throw null; }
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/ResetStatistics/*'/>
public void ResetStatistics() { }
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/RetrieveStatistics/*'/>
Original file line number Diff line number Diff line change
@@ -1662,16 +1662,20 @@ private void CancelOpenAndWait()
Debug.Assert(_currentCompletion == null, "After waiting for an async call to complete, there should be no completion source");
}

private Task InternalOpenWithRetryAsync(CancellationToken cancellationToken)
=> RetryLogicProvider.ExecuteAsync(this, () => InternalOpenAsync(cancellationToken), cancellationToken);

/// <include file='../../../../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/OpenAsync/*' />
public override Task OpenAsync(CancellationToken cancellationToken)
public override Task OpenAsync(CancellationToken cancellationToken)
=> OpenAsync(SqlConnectionOverrides.None, cancellationToken);

/// <include file='../../../../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/OpenAsyncWithOverrides/*' />
public Task OpenAsync(SqlConnectionOverrides overrides, CancellationToken cancellationToken)
=> IsProviderRetriable ?
InternalOpenWithRetryAsync(cancellationToken) :
InternalOpenAsync(cancellationToken);
InternalOpenWithRetryAsync(overrides, cancellationToken) :
InternalOpenAsync(overrides, cancellationToken);

private Task InternalOpenAsync(CancellationToken cancellationToken)
private Task InternalOpenWithRetryAsync(SqlConnectionOverrides overrides, CancellationToken cancellationToken)
=> RetryLogicProvider.ExecuteAsync(this, () => InternalOpenAsync(overrides, cancellationToken), cancellationToken);

private Task InternalOpenAsync(SqlConnectionOverrides overrides, CancellationToken cancellationToken)
{
long scopeID = SqlClientEventSource.Log.TryPoolerScopeEnterEvent("SqlConnection.InternalOpenAsync | API | Object Id {0}", ObjectID);
SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlConnection.InternalOpenAsync | API | Correlation | Object Id {0}, Activity Id {1}", ObjectID, ActivityCorrelator.Current);
@@ -1710,7 +1714,7 @@ private Task InternalOpenAsync(CancellationToken cancellationToken)

try
{
completed = TryOpen(completion);
completed = TryOpen(completion, overrides);
}
catch (Exception e)
{
@@ -1730,7 +1734,7 @@ private Task InternalOpenAsync(CancellationToken cancellationToken)
{
registration = cancellationToken.Register(s_openAsyncCancel, completion);
}
OpenAsyncRetry retry = new OpenAsyncRetry(this, completion, result, registration);
OpenAsyncRetry retry = new OpenAsyncRetry(this, completion, result, overrides, registration);
_currentCompletion = new Tuple<TaskCompletionSource<DbConnectionInternal>, Task>(completion, result.Task);
completion.Task.ContinueWith(retry.Retry, TaskScheduler.Default);
return result.Task;
@@ -1805,13 +1809,15 @@ private class OpenAsyncRetry
private SqlConnection _parent;
private TaskCompletionSource<DbConnectionInternal> _retry;
private TaskCompletionSource<object> _result;
private SqlConnectionOverrides _overrides;
private CancellationTokenRegistration _registration;

public OpenAsyncRetry(SqlConnection parent, TaskCompletionSource<DbConnectionInternal> retry, TaskCompletionSource<object> result, CancellationTokenRegistration registration)
public OpenAsyncRetry(SqlConnection parent, TaskCompletionSource<DbConnectionInternal> retry, TaskCompletionSource<object> result, SqlConnectionOverrides overrides, CancellationTokenRegistration registration)
{
_parent = parent;
_retry = retry;
_result = result;
_overrides = overrides;
_registration = registration;
SqlClientEventSource.Log.TryTraceEvent("SqlConnection.OpenAsyncRetry | Info | Object Id {0}", _parent?.ObjectID);
}
@@ -1846,7 +1852,7 @@ internal void Retry(Task<DbConnectionInternal> retryTask)
// protect continuation from races with close and cancel
lock (_parent.InnerConnection)
{
result = _parent.TryOpen(_retry);
result = _parent.TryOpen(_retry, _overrides);
}
if (result)
{
@@ -1968,9 +1974,6 @@ private bool TryOpen(TaskCompletionSource<DbConnectionInternal> retry, SqlConnec
}
// does not require GC.KeepAlive(this) because of ReRegisterForFinalize below.

// Set future transient fault handling based on connection options
_applyTransientFaultHandling = connectionOptions != null && connectionOptions.ConnectRetryCount > 0;

var tdsInnerConnection = (SqlInternalConnectionTds)InnerConnection;

Debug.Assert(tdsInnerConnection.Parser != null, "Where's the parser?");
Original file line number Diff line number Diff line change
@@ -99,6 +99,9 @@ override protected DbConnectionInternal CreateConnection(DbConnectionOptions opt
// NOTE: Retrieve <UserInstanceName> here. This user instance name will be used below to connect to the Sql Express User Instance.
instanceName = sseConnection.InstanceName;

// Set future transient fault handling based on connection options
sqlOwningConnection._applyTransientFaultHandling = opt != null && opt.ConnectRetryCount > 0;
Copy link
Member Author

@cheenamalhotra cheenamalhotra Nov 15, 2024

Choose a reason for hiding this comment

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

This had to be moved here as in async flow, connection attempt happens on another thread and the value gets reset very quickly to be captured again when retry is attempted. So fail fast wasn't really working. This change makes sure the value gets reset after the login flow is complete.

cc @David-Engel for second set of eyes


if (!instanceName.StartsWith("\\\\.\\", StringComparison.Ordinal))
{
throw SQL.NonLocalSSEInstance();
Original file line number Diff line number Diff line change
@@ -885,6 +885,8 @@ public override void Open() { }
public void Open(SqlConnectionOverrides overrides) { }
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/OpenAsync/*'/>
public override System.Threading.Tasks.Task OpenAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/OpenAsyncWithOverrides/*'/>
public System.Threading.Tasks.Task OpenAsync(Microsoft.Data.SqlClient.SqlConnectionOverrides overrides, System.Threading.CancellationToken cancellationToken) { throw null; }
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/RegisterColumnEncryptionKeyStoreProviders/*'/>
public static void RegisterColumnEncryptionKeyStoreProviders(System.Collections.Generic.IDictionary<string, Microsoft.Data.SqlClient.SqlColumnEncryptionKeyStoreProvider> customProviders) { }
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/RegisterColumnEncryptionKeyStoreProvidersOnConnection/*' />
Original file line number Diff line number Diff line change
@@ -1922,16 +1922,20 @@ void CancelOpenAndWait()
Debug.Assert(_currentCompletion == null, "After waiting for an async call to complete, there should be no completion source");
}

private Task InternalOpenWithRetryAsync(CancellationToken cancellationToken)
=> RetryLogicProvider.ExecuteAsync(this, () => InternalOpenAsync(cancellationToken), cancellationToken);

/// <include file='../../../../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/OpenAsync/*' />
public override Task OpenAsync(CancellationToken cancellationToken)
=> OpenAsync(SqlConnectionOverrides.None, cancellationToken);

/// <include file='../../../../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/OpenAsyncWithOverrides/*' />
public Task OpenAsync(SqlConnectionOverrides overrides, CancellationToken cancellationToken)
=> IsProviderRetriable ?
InternalOpenWithRetryAsync(cancellationToken) :
InternalOpenAsync(cancellationToken);
InternalOpenWithRetryAsync(overrides, cancellationToken) :
InternalOpenAsync(overrides, cancellationToken);

private Task InternalOpenAsync(CancellationToken cancellationToken)
private Task InternalOpenWithRetryAsync(SqlConnectionOverrides overrides, CancellationToken cancellationToken)
=> RetryLogicProvider.ExecuteAsync(this, () => InternalOpenAsync(overrides, cancellationToken), cancellationToken);

private Task InternalOpenAsync(SqlConnectionOverrides overrides, CancellationToken cancellationToken)
{
long scopeID = SqlClientEventSource.Log.TryPoolerScopeEnterEvent("<sc.SqlConnection.OpenAsync|API> {0}", ObjectID);
SqlClientEventSource.Log.TryCorrelationTraceEvent("<sc.SqlConnection.OpenAsync|API|Correlation> ObjectID {0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current);
@@ -1977,7 +1981,7 @@ private Task InternalOpenAsync(CancellationToken cancellationToken)

try
{
completed = TryOpen(completion);
completed = TryOpen(completion, overrides);
}
catch (Exception e)
{
@@ -1996,7 +2000,7 @@ private Task InternalOpenAsync(CancellationToken cancellationToken)
{
registration = cancellationToken.Register(() => completion.TrySetCanceled());
}
OpenAsyncRetry retry = new OpenAsyncRetry(this, completion, result, registration);
OpenAsyncRetry retry = new OpenAsyncRetry(this, completion, result, overrides, registration);
_currentCompletion = new Tuple<TaskCompletionSource<DbConnectionInternal>, Task>(completion, result.Task);
completion.Task.ContinueWith(retry.Retry, TaskScheduler.Default);
return result.Task;
@@ -2020,13 +2024,15 @@ private class OpenAsyncRetry
SqlConnection _parent;
TaskCompletionSource<DbConnectionInternal> _retry;
TaskCompletionSource<object> _result;
SqlConnectionOverrides _overrides;
CancellationTokenRegistration _registration;

public OpenAsyncRetry(SqlConnection parent, TaskCompletionSource<DbConnectionInternal> retry, TaskCompletionSource<object> result, CancellationTokenRegistration registration)
public OpenAsyncRetry(SqlConnection parent, TaskCompletionSource<DbConnectionInternal> retry, TaskCompletionSource<object> result, SqlConnectionOverrides overrides, CancellationTokenRegistration registration)
{
_parent = parent;
_retry = retry;
_result = result;
_overrides = overrides;
_registration = registration;
}

@@ -2062,7 +2068,7 @@ internal void Retry(Task<DbConnectionInternal> retryTask)
// protect continuation from races with close and cancel
lock (_parent.InnerConnection)
{
result = _parent.TryOpen(_retry);
result = _parent.TryOpen(_retry, _overrides);
}
if (result)
{
@@ -2139,9 +2145,6 @@ private bool TryOpen(TaskCompletionSource<DbConnectionInternal> retry, SqlConnec
result = TryOpenInner(retry);
}

// Set future transient fault handling based on connection options
_applyTransientFaultHandling = connectionOptions != null && connectionOptions.ConnectRetryCount > 0;

return result;
}

Original file line number Diff line number Diff line change
@@ -108,6 +108,9 @@ override protected DbConnectionInternal CreateConnection(DbConnectionOptions opt
// NOTE: Retrieve <UserInstanceName> here. This user instance name will be used below to connect to the Sql Express User Instance.
instanceName = sseConnection.InstanceName;

// Set future transient fault handling based on connection options
sqlOwningConnection._applyTransientFaultHandling = opt != null && opt.ConnectRetryCount > 0;

if (!instanceName.StartsWith("\\\\.\\", StringComparison.Ordinal))
{
throw SQL.NonLocalSSEInstance();
Original file line number Diff line number Diff line change
@@ -369,7 +369,8 @@ public static void ConnectionOpenDisableRetry()
{
InitialCatalog = "DoesNotExist0982532435423",
Pooling = false,
ConnectTimeout = 15
ConnectTimeout = 15,
ConnectRetryCount = 3
};
using SqlConnection sqlConnection = new(connectionStringBuilder.ConnectionString);
Stopwatch timer = new();
@@ -384,7 +385,33 @@ public static void ConnectionOpenDisableRetry()
Assert.Throws<SqlException>(() => sqlConnection.Open());
timer.Stop();
duration = timer.Elapsed;
Assert.True(duration.Seconds > 5, $"Connection Open() with retries took less time than expected. Expect > 5 sec with transient fault handling. Took {duration.Seconds} sec."); // sqlConnection.Open();
Assert.True(duration.Seconds > 5, $"Connection Open() with retries took less time than expected. Expect > 5 sec with transient fault handling. Took {duration.Seconds} sec."); // sqlConnection.Open();
}

[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer), nameof(DataTestUtility.TcpConnectionStringDoesNotUseAadAuth))]
public static async Task ConnectionOpenAsyncDisableRetry()
{
SqlConnectionStringBuilder connectionStringBuilder = new(DataTestUtility.TCPConnectionString)
{
InitialCatalog = DataTestUtility.GetUniqueNameForSqlServer("DoesNotExist", false),
Pooling = false,
ConnectTimeout = 15,
ConnectRetryCount = 3
};
using SqlConnection sqlConnection = new(connectionStringBuilder.ConnectionString);
Stopwatch timer = new();

timer.Start();
await Assert.ThrowsAsync<SqlException>(async () => await sqlConnection.OpenAsync(SqlConnectionOverrides.OpenWithoutRetry, CancellationToken.None));
timer.Stop();
TimeSpan duration = timer.Elapsed;
Assert.True(duration.Seconds < 2, $"Connection OpenAsync() without retries took longer than expected. Expected < 2 sec. Took {duration.Seconds} sec.");

timer.Restart();
await Assert.ThrowsAsync<SqlException>(async () => await sqlConnection.OpenAsync(CancellationToken.None));
timer.Stop();
duration = timer.Elapsed;
Assert.True(duration.Seconds > 5, $"Connection OpenAsync() with retries took less time than expected. Expect > 5 sec with transient fault handling. Took {duration.Seconds} sec.");
}

[PlatformSpecific(TestPlatforms.Windows)]

Unchanged files with check annotations Beta

<Target Name="RunManualTests">
<!-- Windows -->
<Exec ConsoleToMsBuild="true" Command="$(DotnetPath)dotnet test &quot;@(ManualTestsProj)&quot; -p:Configuration=$(Configuration) -p:Target$(TFGroup)Version=$(TF) -p:ReferenceType=$(ReferenceType) --no-build -l &quot;console;verbosity=normal&quot; --collect &quot;Code coverage&quot; -p:TestSet=$(TestSet) --results-directory $(ResultsDirectory) -p:TestTargetOS=Windows$(TargetGroup) --filter &quot;category!=non$(TargetGroup)tests&amp;category!=failing&amp;category!=nonwindowstests&quot; &quot;--logger:trx;LogFilePrefix=Manual-Windows$(TargetGroup)-$(TestSet)&quot;" Condition="'$(IsEnabledWindows)' == 'true'"/>

Check failure on line 190 in build.proj

Azure Pipelines / CI-SqlClient (Win22_Azure_Sql net8_0_AnyCPU_NativeSNI_3)

build.proj#L190

build.proj(190,5): Error MSB3073: The command "dotnet test "src\Microsoft.Data.SqlClient\tests\ManualTests\Microsoft.Data.SqlClient.ManualTesting.Tests.csproj" -p:Configuration=Release -p:TargetnetcoreVersion=net8.0 -p:ReferenceType=Project --no-build -l "console;verbosity=normal" --collect "Code coverage" -p:TestSet=3 --results-directory TestResults -p:TestTargetOS=Windowsnetcoreapp --filter "category!=nonnetcoreapptests&category!=failing&category!=nonwindowstests" "--logger:trx;LogFilePrefix=Manual-Windowsnetcoreapp-3"" exited with code 1.

Check failure on line 190 in build.proj

Azure Pipelines / CI-SqlClient (Win22_Sql22_named_instance net8_0_AnyCPU_ManagedSNI_3)

build.proj#L190

build.proj(190,5): Error MSB3073: The command "dotnet test "src\Microsoft.Data.SqlClient\tests\ManualTests\Microsoft.Data.SqlClient.ManualTesting.Tests.csproj" -p:Configuration=Release -p:TargetnetcoreVersion=net8.0 -p:ReferenceType=Project --no-build -l "console;verbosity=normal" --collect "Code coverage" -p:TestSet=3 --results-directory TestResults -p:TestTargetOS=Windowsnetcoreapp --filter "category!=nonnetcoreapptests&category!=failing&category!=nonwindowstests" "--logger:trx;LogFilePrefix=Manual-Windowsnetcoreapp-3"" exited with code 1.

Check failure on line 190 in build.proj

Azure Pipelines / CI-SqlClient (Win22_Azure_ARM64_Sql net8_0_AnyCPU_ManagedSNI_1)

build.proj#L190

build.proj(190,5): Error MSB3073: The command "dotnet test "src\Microsoft.Data.SqlClient\tests\ManualTests\Microsoft.Data.SqlClient.ManualTesting.Tests.csproj" -p:Configuration=Release -p:TargetnetcoreVersion=net8.0 -p:ReferenceType=Project --no-build -l "console;verbosity=normal" --collect "Code coverage" -p:TestSet=1 --results-directory TestResults -p:TestTargetOS=Windowsnetcoreapp --filter "category!=nonnetcoreapptests&category!=failing&category!=nonwindowstests" "--logger:trx;LogFilePrefix=Manual-Windowsnetcoreapp-1"" exited with code 1.

Check failure on line 190 in build.proj

Azure Pipelines / CI-SqlClient

build.proj#L190

build.proj(190,5): Error MSB3073: The command "dotnet test "src\Microsoft.Data.SqlClient\tests\ManualTests\Microsoft.Data.SqlClient.ManualTesting.Tests.csproj" -p:Configuration=Release -p:TargetnetcoreVersion=net8.0 -p:ReferenceType=Project --no-build -l "console;verbosity=normal" --collect "Code coverage" -p:TestSet=3 --results-directory TestResults -p:TestTargetOS=Windowsnetcoreapp --filter "category!=nonnetcoreapptests&category!=failing&category!=nonwindowstests" "--logger:trx;LogFilePrefix=Manual-Windowsnetcoreapp-3"" exited with code 1.
<!-- Unix -->
<Exec ConsoleToMsBuild="true" Command="$(DotnetPath)dotnet test &quot;@(ManualTestsProj)&quot; -p:Configuration=$(Configuration) -p:TargetNetCoreVersion=$(TF) -p:ReferenceType=$(ReferenceType) --no-build -l &quot;console;verbosity=normal&quot; --collect &quot;Code coverage&quot; -p:TestSet=$(TestSet) --results-directory $(ResultsDirectory) -p:TestTargetOS=Unixnetcoreapp --filter &quot;category!=nonnetcoreapptests&amp;category!=failing&amp;category!=nonlinuxtests&amp;category!=nonuaptests&quot; &quot;--logger:trx;LogFilePrefix=Manual-Unixnetcoreapp-$(TestSet)&quot;" Condition="'$(IsEnabledWindows)' != 'true'"/>
</Target>