diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml index f7ca3d1237..95688779fe 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml @@ -2853,6 +2853,39 @@ Any error returned by SQL Server that occurred while opening the connection. + + + Options to override default connection open behavior. + + + The cancellation instruction. + + + An asynchronous version of , which opens a database connection with the property settings specified by the . 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. + + + A task representing the asynchronous operation. + + + + After calling , must return until the returned is completed. Then, if the connection was successful, must return . If the connection fails, must return . + + + A call to will attempt to cancel or close the corresponding call. For more information about asynchronous programming in the .NET Framework Data Provider for SQL Server, see Asynchronous Programming. + + + + + Calling more than once for the same instance before task completion. + + + A connection was not available from the connection pool before the connection time out elapsed. + + + + Any error returned by SQL Server that occurred while opening the connection. + + Gets the size (in bytes) of network packets used to communicate with an instance of SQL Server. diff --git a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs index b234cdc3dd..2eba0bbe7d 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs @@ -975,6 +975,8 @@ public override void Open() { } public void Open(SqlConnectionOverrides overrides) { } /// public override System.Threading.Tasks.Task OpenAsync(System.Threading.CancellationToken cancellationToken) { throw null; } + /// + public System.Threading.Tasks.Task OpenAsync(Microsoft.Data.SqlClient.SqlConnectionOverrides overrides, System.Threading.CancellationToken cancellationToken) { throw null; } /// public void ResetStatistics() { } /// diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs index 7da530b12a..22da9c3c29 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -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); - /// - public override Task OpenAsync(CancellationToken cancellationToken) + public override Task OpenAsync(CancellationToken cancellationToken) + => OpenAsync(SqlConnectionOverrides.None, cancellationToken); + + /// + 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); @@ -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, Task>(completion, result.Task); completion.Task.ContinueWith(retry.Retry, TaskScheduler.Default); return result.Task; @@ -1807,13 +1811,15 @@ private class OpenAsyncRetry private SqlConnection _parent; private TaskCompletionSource _retry; private TaskCompletionSource _result; + private SqlConnectionOverrides _overrides; private CancellationTokenRegistration _registration; - public OpenAsyncRetry(SqlConnection parent, TaskCompletionSource retry, TaskCompletionSource result, CancellationTokenRegistration registration) + public OpenAsyncRetry(SqlConnection parent, TaskCompletionSource retry, TaskCompletionSource 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); } @@ -1848,7 +1854,7 @@ internal void Retry(Task retryTask) // protect continuation from races with close and cancel lock (_parent.InnerConnection) { - result = _parent.TryOpen(_retry); + result = _parent.TryOpen(_retry, _overrides); } if (result) { diff --git a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs index 8e64378330..582f694e95 100644 --- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs @@ -885,6 +885,8 @@ public override void Open() { } public void Open(SqlConnectionOverrides overrides) { } /// public override System.Threading.Tasks.Task OpenAsync(System.Threading.CancellationToken cancellationToken) { throw null; } + /// + public System.Threading.Tasks.Task OpenAsync(Microsoft.Data.SqlClient.SqlConnectionOverrides overrides, System.Threading.CancellationToken cancellationToken) { throw null; } /// public static void RegisterColumnEncryptionKeyStoreProviders(System.Collections.Generic.IDictionary customProviders) { } /// diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs index 62145bc69e..bdd66096eb 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -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); - /// public override Task OpenAsync(CancellationToken cancellationToken) + => OpenAsync(SqlConnectionOverrides.None, cancellationToken); + + /// + 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(" {0}", ObjectID); SqlClientEventSource.Log.TryCorrelationTraceEvent(" ObjectID {0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); @@ -2025,7 +2029,7 @@ private Task InternalOpenAsync(CancellationToken cancellationToken) try { - completed = TryOpen(completion); + completed = TryOpen(completion, overrides); } catch (Exception e) { @@ -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, Task>(completion, result.Task); completion.Task.ContinueWith(retry.Retry, TaskScheduler.Default); return result.Task; @@ -2068,13 +2072,15 @@ private class OpenAsyncRetry SqlConnection _parent; TaskCompletionSource _retry; TaskCompletionSource _result; + SqlConnectionOverrides _overrides; CancellationTokenRegistration _registration; - public OpenAsyncRetry(SqlConnection parent, TaskCompletionSource retry, TaskCompletionSource result, CancellationTokenRegistration registration) + public OpenAsyncRetry(SqlConnection parent, TaskCompletionSource retry, TaskCompletionSource result, SqlConnectionOverrides overrides, CancellationTokenRegistration registration) { _parent = parent; _retry = retry; _result = result; + _overrides = overrides; _registration = registration; } @@ -2110,7 +2116,7 @@ internal void Retry(Task retryTask) // protect continuation from races with close and cancel lock (_parent.InnerConnection) { - result = _parent.TryOpen(_retry); + result = _parent.TryOpen(_retry, _overrides); } if (result) { diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs index da09f3406c..36bac82fb1 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs @@ -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 @@ -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(); @@ -383,7 +385,33 @@ public static void ConnectionOpenDisableRetry() Assert.Throws(() => 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(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(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)]