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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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/*'/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 InternalOpenWithRetryAsync(SqlConnectionOverrides overrides, CancellationToken cancellationToken)
=> RetryLogicProvider.ExecuteAsync(this, () => InternalOpenAsync(overrides, cancellationToken), cancellationToken);

private Task InternalOpenAsync(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);
Expand Down Expand Up @@ -1710,7 +1714,7 @@ private Task InternalOpenAsync(CancellationToken cancellationToken)

try
{
completed = TryOpen(completion);
completed = TryOpen(completion, overrides);
}
catch (Exception e)
{
Expand All @@ -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;
Expand Down Expand Up @@ -1807,13 +1811,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);
}
Expand Down Expand Up @@ -1848,7 +1854,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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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/*' />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1970,16 +1970,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 InternalOpenWithRetryAsync(SqlConnectionOverrides overrides, CancellationToken cancellationToken)
=> RetryLogicProvider.ExecuteAsync(this, () => InternalOpenAsync(overrides, cancellationToken), cancellationToken);

private Task InternalOpenAsync(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);
Expand Down Expand Up @@ -2025,7 +2029,7 @@ private Task InternalOpenAsync(CancellationToken cancellationToken)

try
{
completed = TryOpen(completion);
completed = TryOpen(completion, overrides);
}
catch (Exception e)
{
Expand All @@ -2044,7 +2048,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;
Expand All @@ -2068,13 +2072,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;
}

Expand Down Expand Up @@ -2110,7 +2116,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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Data;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Xunit;

namespace Microsoft.Data.SqlClient.ManualTesting.Tests
Expand Down Expand Up @@ -368,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();
Expand All @@ -383,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)]
Expand Down
Loading