diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml index b34a61386b..41635418bb 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 3ceb56da17..20997a5b31 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 555cbc051c..9da249c7d4 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 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, 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 _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); } @@ -1846,7 +1852,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) { @@ -1968,9 +1974,6 @@ private bool TryOpen(TaskCompletionSource 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?"); diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs index d84b433d68..dfa10c3b07 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs @@ -99,6 +99,9 @@ override protected DbConnectionInternal CreateConnection(DbConnectionOptions opt // NOTE: Retrieve 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(); 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 9cbc4b136d..9ad6c68967 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 81bbb1ea85..263036277b 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 @@ -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); - /// 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 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(" {0}", ObjectID); SqlClientEventSource.Log.TryCorrelationTraceEvent(" 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, Task>(completion, result.Task); completion.Task.ContinueWith(retry.Retry, TaskScheduler.Default); return result.Task; @@ -2020,13 +2024,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; } @@ -2062,7 +2068,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) { @@ -2139,9 +2145,6 @@ private bool TryOpen(TaskCompletionSource retry, SqlConnec result = TryOpenInner(retry); } - // Set future transient fault handling based on connection options - _applyTransientFaultHandling = connectionOptions != null && connectionOptions.ConnectRetryCount > 0; - return result; } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs index e0132dabd6..755e2dd747 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs @@ -108,6 +108,9 @@ override protected DbConnectionInternal CreateConnection(DbConnectionOptions opt // NOTE: Retrieve 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(); 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 5aa1c4ec3f..e9e29e5940 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs @@ -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(() => 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)]