diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index 89fbdaa759..64bea7d5fb 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -99,9 +99,6 @@ Microsoft\Data\ProviderBase\DbConnectionClosed.cs - - Microsoft\Data\ProviderBase\DbConnectionFactory.cs - Microsoft\Data\SqlClient\ConnectionPool\ChannelDbConnectionPool.cs 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 45df50badb..dae580e0bb 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 @@ -1272,7 +1272,7 @@ public override void ChangeDatabase(string database) /// public static void ClearAllPools() { - SqlConnectionFactory.SingletonInstance.ClearAllPools(); + SqlConnectionFactory.Instance.ClearAllPools(); } /// @@ -1283,7 +1283,7 @@ public static void ClearPool(SqlConnection connection) DbConnectionOptions connectionOptions = connection.UserConnectionOptions; if (connectionOptions != null) { - SqlConnectionFactory.SingletonInstance.ClearPool(connection); + SqlConnectionFactory.Instance.ClearPool(connection); } } @@ -2265,7 +2265,7 @@ public static void ChangePassword(string connectionString, string newPassword) SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential: null, accessToken: null, accessTokenCallback: null); - SqlConnectionString connectionOptions = SqlConnectionFactory.FindSqlConnectionOptions(key); + SqlConnectionString connectionOptions = SqlConnectionFactory.Instance.FindSqlConnectionOptions(key); if (connectionOptions.IntegratedSecurity) { throw SQL.ChangePasswordConflictsWithSSPI(); @@ -2314,7 +2314,7 @@ public static void ChangePassword(string connectionString, SqlCredential credent SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential, accessToken: null, accessTokenCallback: null); - SqlConnectionString connectionOptions = SqlConnectionFactory.FindSqlConnectionOptions(key); + SqlConnectionString connectionOptions = SqlConnectionFactory.Instance.FindSqlConnectionOptions(key); // Check for connection string values incompatible with SqlCredential if (!string.IsNullOrEmpty(connectionOptions.UserID) || !string.IsNullOrEmpty(connectionOptions.Password)) @@ -2352,7 +2352,7 @@ private static void ChangePassword(string connectionString, SqlConnectionString } SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential, accessToken: null, accessTokenCallback: null); - SqlConnectionFactory.SingletonInstance.ClearPool(key); + SqlConnectionFactory.Instance.ClearPool(key); } internal Task RegisterForConnectionCloseNotification(Task outerTask, object value, int tag) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionHelper.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionHelper.cs index 133854c1f0..9095c9b454 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionHelper.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionHelper.cs @@ -17,7 +17,7 @@ namespace Microsoft.Data.SqlClient { public sealed partial class SqlConnection : DbConnection { - private static readonly DbConnectionFactory s_connectionFactory = SqlConnectionFactory.SingletonInstance; + private static readonly SqlConnectionFactory s_connectionFactory = SqlConnectionFactory.Instance; private DbConnectionOptions _userConnectionOptions; private DbConnectionPoolGroup _poolGroup; @@ -42,7 +42,7 @@ internal int CloseCount } } - internal DbConnectionFactory ConnectionFactory + internal SqlConnectionFactory ConnectionFactory { get { @@ -156,9 +156,7 @@ override protected DbCommand CreateDbCommand() { using (TryEventScope.Create(" {0}", ObjectID)) { - DbCommand command = null; - DbProviderFactory providerFactory = ConnectionFactory.ProviderFactory; - command = providerFactory.CreateCommand(); + DbCommand command = SqlClientFactory.Instance.CreateCommand(); command.Connection = this; return command; } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 0d1da114cc..8e3cb1bef3 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -3049,9 +3049,13 @@ internal bool ThreadHasParserLockForClose } } - internal override bool TryReplaceConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource retry, DbConnectionOptions userOptions) + internal override bool TryReplaceConnection( + DbConnection outerConnection, + SqlConnectionFactory connectionFactory, + TaskCompletionSource retry, + DbConnectionOptions userOptions) { - return base.TryOpenConnectionInternal(outerConnection, connectionFactory, retry, userOptions); + return TryOpenConnectionInternal(outerConnection, connectionFactory, retry, userOptions); } } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index 78466acbc2..3904bfa16c 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -285,9 +285,6 @@ Microsoft\Data\ProviderBase\DbConnectionClosed.cs - - Microsoft\Data\ProviderBase\DbConnectionFactory.cs - Microsoft\Data\ProviderBase\DbConnectionInternal.cs 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 9681a6a3ae..ff5efa0ef9 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 @@ -1266,7 +1266,7 @@ public override void ChangeDatabase(string database) public static void ClearAllPools() { (new SqlClientPermission(PermissionState.Unrestricted)).Demand(); - SqlConnectionFactory.SingletonInstance.ClearAllPools(); + SqlConnectionFactory.Instance.ClearAllPools(); } /// @@ -1278,7 +1278,7 @@ public static void ClearPool(SqlConnection connection) if (connectionOptions != null) { connectionOptions.DemandPermission(); - SqlConnectionFactory.SingletonInstance.ClearPool(connection); + SqlConnectionFactory.Instance.ClearPool(connection); } } @@ -2197,7 +2197,7 @@ public static void ChangePassword(string connectionString, string newPassword) SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential: null, accessToken: null, accessTokenCallback: null); - SqlConnectionString connectionOptions = SqlConnectionFactory.FindSqlConnectionOptions(key); + SqlConnectionString connectionOptions = SqlConnectionFactory.Instance.FindSqlConnectionOptions(key); if (connectionOptions.IntegratedSecurity || connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated) { throw SQL.ChangePasswordConflictsWithSSPI(); @@ -2249,7 +2249,7 @@ public static void ChangePassword(string connectionString, SqlCredential credent SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential, accessToken: null, accessTokenCallback: null); - SqlConnectionString connectionOptions = SqlConnectionFactory.FindSqlConnectionOptions(key); + SqlConnectionString connectionOptions = SqlConnectionFactory.Instance.FindSqlConnectionOptions(key); // Check for connection string values incompatible with SqlCredential if (!string.IsNullOrEmpty(connectionOptions.UserID) || !string.IsNullOrEmpty(connectionOptions.Password)) @@ -2290,7 +2290,7 @@ private static void ChangePassword(string connectionString, SqlConnectionString } SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential, accessToken: null, accessTokenCallback: null); - SqlConnectionFactory.SingletonInstance.ClearPool(key); + SqlConnectionFactory.Instance.ClearPool(key); } internal Task RegisterForConnectionCloseNotification(Task outerTask, object value, int tag) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionHelper.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionHelper.cs index eb5cb511be..68c7d24ea2 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionHelper.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionHelper.cs @@ -18,7 +18,7 @@ namespace Microsoft.Data.SqlClient { public sealed partial class SqlConnection : DbConnection { - private static readonly DbConnectionFactory _connectionFactory = SqlConnectionFactory.SingletonInstance; + private static readonly SqlConnectionFactory _connectionFactory = SqlConnectionFactory.Instance; internal static readonly System.Security.CodeAccessPermission ExecutePermission = SqlConnection.CreateExecutePermission(); private DbConnectionOptions _userConnectionOptions; @@ -69,7 +69,7 @@ internal int CloseCount } } - internal DbConnectionFactory ConnectionFactory + internal SqlConnectionFactory ConnectionFactory { get { @@ -203,8 +203,7 @@ override protected DbCommand CreateDbCommand() { using (TryEventScope.Create(" {0}", ObjectID)) { - DbProviderFactory providerFactory = ConnectionFactory.ProviderFactory; - DbCommand command = providerFactory.CreateCommand(); + DbCommand command = SqlClientFactory.Instance.CreateCommand(); command.Connection = this; return command; } @@ -212,8 +211,8 @@ override protected DbCommand CreateDbCommand() private static System.Security.CodeAccessPermission CreateExecutePermission() { - DBDataPermission p = (DBDataPermission)SqlConnectionFactory.SingletonInstance.ProviderFactory.CreatePermission(System.Security.Permissions.PermissionState.None); - p.Add(String.Empty, String.Empty, KeyRestrictionBehavior.AllowOnly); + DBDataPermission p = (DBDataPermission)SqlClientFactory.Instance.CreatePermission(System.Security.Permissions.PermissionState.None); + p.Add(string.Empty, string.Empty, KeyRestrictionBehavior.AllowOnly); return p; } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index d460c61619..391c204c60 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -3081,9 +3081,13 @@ internal bool ThreadHasParserLockForClose } } - internal override bool TryReplaceConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource retry, DbConnectionOptions userOptions) + internal override bool TryReplaceConnection( + DbConnection outerConnection, + SqlConnectionFactory connectionFactory, + TaskCompletionSource retry, + DbConnectionOptions userOptions) { - return base.TryOpenConnectionInternal(outerConnection, connectionFactory, retry, userOptions); + return TryOpenConnectionInternal(outerConnection, connectionFactory, retry, userOptions); } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs index 46e6091ad4..b59e5cb33d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs @@ -130,7 +130,18 @@ internal static Exception ExceptionWithStackTrace(Exception e) } } - internal static Timer UnsafeCreateTimer(TimerCallback callback, object state, int dueTime, int period) + internal static Timer UnsafeCreateTimer( + TimerCallback callback, + object state, + int dueTimeMilliseconds, + int periodMilliseconds) => + UnsafeCreateTimer( + callback, + state, + TimeSpan.FromMilliseconds(dueTimeMilliseconds), + TimeSpan.FromMilliseconds(periodMilliseconds)); + + internal static Timer UnsafeCreateTimer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period) { // Don't capture the current ExecutionContext and its AsyncLocals onto // a global timer causing them to live forever @@ -154,6 +165,7 @@ internal static Timer UnsafeCreateTimer(TimerCallback callback, object state, in } } } + #region COM+ exceptions internal static ArgumentException Argument(string error) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionClosed.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionClosed.cs index d4ea183312..8549c48a8d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionClosed.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionClosed.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.Data.Common; using Microsoft.Data.Common.ConnectionString; +using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient.ConnectionPool; namespace Microsoft.Data.ProviderBase @@ -27,7 +28,7 @@ protected DbConnectionClosed(ConnectionState state, bool hidePassword, bool allo public override void ChangeDatabase(string database) => throw ADP.ClosedConnectionError(); - internal override void CloseConnection(DbConnection owningObject, DbConnectionFactory connectionFactory) + internal override void CloseConnection(DbConnection owningObject, SqlConnectionFactory connectionFactory) { // not much to do here... } @@ -36,13 +37,24 @@ internal override void CloseConnection(DbConnection owningObject, DbConnectionFa public override void EnlistTransaction(System.Transactions.Transaction transaction) => throw ADP.ClosedConnectionError(); - protected internal override DataTable GetSchema(DbConnectionFactory factory, DbConnectionPoolGroup poolGroup, DbConnection outerConnection, string collectionName, string[] restrictions) - => throw ADP.ClosedConnectionError(); + protected internal override DataTable GetSchema( + SqlConnectionFactory factory, + DbConnectionPoolGroup poolGroup, + DbConnection outerConnection, + string collectionName, + string[] restrictions) + { + throw ADP.ClosedConnectionError(); + } protected override DbReferenceCollection CreateReferenceCollection() => throw ADP.ClosedConnectionError(); - internal override bool TryOpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource retry, DbConnectionOptions userOptions) - => base.TryOpenConnectionInternal(outerConnection, connectionFactory, retry, userOptions); + internal override bool TryOpenConnection( + DbConnection outerConnection, + SqlConnectionFactory connectionFactory, + TaskCompletionSource retry, + DbConnectionOptions userOptions) => + TryOpenConnectionInternal(outerConnection, connectionFactory, retry, userOptions); } internal abstract class DbConnectionBusy : DbConnectionClosed @@ -51,7 +63,11 @@ protected DbConnectionBusy(ConnectionState state) : base(state, true, false) { } - internal override bool TryOpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource retry, DbConnectionOptions userOptions) + internal override bool TryOpenConnection( + DbConnection outerConnection, + SqlConnectionFactory connectionFactory, + TaskCompletionSource retry, + DbConnectionOptions userOptions) => throw ADP.ConnectionAlreadyOpen(State); } @@ -84,15 +100,23 @@ private DbConnectionClosedConnecting() : base(ConnectionState.Connecting) { } - internal override void CloseConnection(DbConnection owningObject, DbConnectionFactory connectionFactory) + internal override void CloseConnection(DbConnection owningObject, SqlConnectionFactory connectionFactory) { connectionFactory.SetInnerConnectionTo(owningObject, DbConnectionClosedPreviouslyOpened.SingletonInstance); } - internal override bool TryReplaceConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource retry, DbConnectionOptions userOptions) - => TryOpenConnection(outerConnection, connectionFactory, retry, userOptions); - - internal override bool TryOpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource retry, DbConnectionOptions userOptions) + internal override bool TryReplaceConnection( + DbConnection outerConnection, + SqlConnectionFactory connectionFactory, + TaskCompletionSource retry, + DbConnectionOptions userOptions) => + TryOpenConnection(outerConnection, connectionFactory, retry, userOptions); + + internal override bool TryOpenConnection( + DbConnection outerConnection, + SqlConnectionFactory connectionFactory, + TaskCompletionSource retry, + DbConnectionOptions userOptions) { if (retry == null || !retry.Task.IsCompleted) { @@ -137,7 +161,11 @@ private DbConnectionClosedPreviouslyOpened() : base(ConnectionState.Closed, true { } - internal override bool TryReplaceConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource retry, DbConnectionOptions userOptions) - => TryOpenConnection(outerConnection, connectionFactory, retry, userOptions); + internal override bool TryReplaceConnection( + DbConnection outerConnection, + SqlConnectionFactory connectionFactory, + TaskCompletionSource retry, + DbConnectionOptions userOptions) => + TryOpenConnection(outerConnection, connectionFactory, retry, userOptions); } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionFactory.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionFactory.cs deleted file mode 100644 index 399b98435b..0000000000 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionFactory.cs +++ /dev/null @@ -1,678 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Data; -using System.Data.Common; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Data.Common; -using Microsoft.Data.Common.ConnectionString; -using Microsoft.Data.SqlClient; -using Microsoft.Data.SqlClient.ConnectionPool; - -namespace Microsoft.Data.ProviderBase -{ - internal abstract class DbConnectionFactory - { - private Dictionary _connectionPoolGroups; - private readonly List _poolsToRelease; - private readonly List _poolGroupsToRelease; - private readonly Timer _pruningTimer; - - private const int PruningDueTime = 4 * 60 * 1000; // 4 minutes - private const int PruningPeriod = 30 * 1000; // thirty seconds - - private static int _objectTypeCount; // EventSource counter - internal int ObjectID { get; } = Interlocked.Increment(ref _objectTypeCount); - - // s_pendingOpenNonPooled is an array of tasks used to throttle creation of non-pooled connections to - // a maximum of Environment.ProcessorCount at a time. - private static uint s_pendingOpenNonPooledNext = 0; - private static Task[] s_pendingOpenNonPooled = new Task[Environment.ProcessorCount]; - private static Task s_completedTask; - - protected DbConnectionFactory() - { - _connectionPoolGroups = new Dictionary(); - _poolsToRelease = new List(); - _poolGroupsToRelease = new List(); - _pruningTimer = CreatePruningTimer(); - } - - public abstract DbProviderFactory ProviderFactory - { - get; - } - - public void ClearAllPools() - { - using (TryEventScope.Create(nameof(DbConnectionFactory))) - { - Dictionary connectionPoolGroups = _connectionPoolGroups; - foreach (KeyValuePair entry in connectionPoolGroups) - { - DbConnectionPoolGroup poolGroup = entry.Value; - if (poolGroup != null) - { - poolGroup.Clear(); - } - } - } - } - - public void ClearPool(DbConnection connection) - { - ADP.CheckArgumentNull(connection, nameof(connection)); - using (TryEventScope.Create(" {0}", GetObjectId(connection))) - { - DbConnectionPoolGroup poolGroup = GetConnectionPoolGroup(connection); - if (poolGroup != null) - { - poolGroup.Clear(); - } - } - } - - public void ClearPool(DbConnectionPoolKey key) - { - Debug.Assert(key != null, "key cannot be null"); - ADP.CheckArgumentNull(key.ConnectionString, $"{nameof(key)}.{nameof(key.ConnectionString)}"); - using (TryEventScope.Create(" connectionString")) - { - Dictionary connectionPoolGroups = _connectionPoolGroups; - if (connectionPoolGroups.TryGetValue(key, out DbConnectionPoolGroup poolGroup)) - { - poolGroup.Clear(); - } - } - } - - internal abstract DbConnectionPoolProviderInfo CreateConnectionPoolProviderInfo( - DbConnectionOptions connectionOptions); - - protected virtual DbMetaDataFactory CreateMetaDataFactory(DbConnectionInternal internalConnection, out bool cacheMetaDataFactory) - { - // providers that support GetSchema must override this with a method that creates a meta data - // factory appropriate for them. - cacheMetaDataFactory = false; - throw ADP.NotSupported(); - } - - internal DbConnectionInternal CreateNonPooledConnection(DbConnection owningConnection, DbConnectionPoolGroup poolGroup, DbConnectionOptions userOptions) - { - Debug.Assert(owningConnection != null, "null owningConnection?"); - Debug.Assert(poolGroup != null, "null poolGroup?"); - - DbConnectionOptions connectionOptions = poolGroup.ConnectionOptions; - DbConnectionPoolGroupProviderInfo poolGroupProviderInfo = poolGroup.ProviderInfo; - DbConnectionPoolKey poolKey = poolGroup.PoolKey; - - DbConnectionInternal newConnection = CreateConnection(connectionOptions, poolKey, poolGroupProviderInfo, null, owningConnection, userOptions); - if (newConnection != null) - { - SqlClientEventSource.Metrics.HardConnectRequest(); - newConnection.MakeNonPooledObject(owningConnection); - } - SqlClientEventSource.Log.TryTraceEvent(" {0}, Non-pooled database connection created.", ObjectID); - return newConnection; - } - - internal DbConnectionInternal CreatePooledConnection(IDbConnectionPool pool, DbConnection owningObject, DbConnectionOptions options, DbConnectionPoolKey poolKey, DbConnectionOptions userOptions) - { - Debug.Assert(pool != null, "null pool?"); - DbConnectionPoolGroupProviderInfo poolGroupProviderInfo = pool.PoolGroup.ProviderInfo; - DbConnectionInternal newConnection = CreateConnection(options, poolKey, poolGroupProviderInfo, pool, owningObject, userOptions); - - if (newConnection != null) - { - SqlClientEventSource.Metrics.HardConnectRequest(); - - newConnection.MakePooledConnection(pool); - } - SqlClientEventSource.Log.TryTraceEvent(" {0}, Pooled database connection created.", ObjectID); - return newConnection; - } - - internal abstract DbConnectionPoolGroupProviderInfo CreateConnectionPoolGroupProviderInfo( - DbConnectionOptions connectionOptions); - - private Timer CreatePruningTimer() => - ADP.UnsafeCreateTimer( - new TimerCallback(PruneConnectionPoolGroups), - null, - PruningDueTime, - PruningPeriod); - - protected DbConnectionOptions FindConnectionOptions(DbConnectionPoolKey key) - { - Debug.Assert(key != null, "key cannot be null"); - if (!string.IsNullOrEmpty(key.ConnectionString)) - { - DbConnectionPoolGroup connectionPoolGroup; - Dictionary connectionPoolGroups = _connectionPoolGroups; - if (connectionPoolGroups.TryGetValue(key, out connectionPoolGroup)) - { - return connectionPoolGroup.ConnectionOptions; - } - } - return null; - } - - private static Task GetCompletedTask() - { - Debug.Assert(Monitor.IsEntered(s_pendingOpenNonPooled), $"Expected {nameof(s_pendingOpenNonPooled)} lock to be held."); - return s_completedTask ?? (s_completedTask = Task.FromResult(null)); - } - - internal bool TryGetConnection(DbConnection owningConnection, TaskCompletionSource retry, DbConnectionOptions userOptions, DbConnectionInternal oldConnection, out DbConnectionInternal connection) - { - Debug.Assert(owningConnection != null, "null owningConnection?"); - - DbConnectionPoolGroup poolGroup; - IDbConnectionPool connectionPool; - connection = null; - - // Work around race condition with clearing the pool between GetConnectionPool obtaining pool - // and GetConnection on the pool checking the pool state. Clearing the pool in this window - // will switch the pool into the ShuttingDown state, and GetConnection will return null. - // There is probably a better solution involving locking the pool/group, but that entails a major - // re-design of the connection pooling synchronization, so is postponed for now. - - // Use retriesLeft to prevent CPU spikes with incremental sleep - // start with one msec, double the time every retry - // max time is: 1 + 2 + 4 + ... + 2^(retries-1) == 2^retries -1 == 1023ms (for 10 retries) - int retriesLeft = 10; - int timeBetweenRetriesMilliseconds = 1; - - do - { - poolGroup = GetConnectionPoolGroup(owningConnection); - // Doing this on the callers thread is important because it looks up the WindowsIdentity from the thread. - connectionPool = GetConnectionPool(owningConnection, poolGroup); - if (connectionPool == null) - { - // If GetConnectionPool returns null, we can be certain that - // this connection should not be pooled via DbConnectionPool - // or have a disabled pool entry. - poolGroup = GetConnectionPoolGroup(owningConnection); // previous entry have been disabled - - if (retry != null) - { - Task newTask; - CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); - lock (s_pendingOpenNonPooled) - { - // look for an available task slot (completed or empty) - int idx; - for (idx = 0; idx < s_pendingOpenNonPooled.Length; idx++) - { - Task task = s_pendingOpenNonPooled[idx]; - if (task == null) - { - s_pendingOpenNonPooled[idx] = GetCompletedTask(); - break; - } - else if (task.IsCompleted) - { - break; - } - } - - // if didn't find one, pick the next one in round-robin fashion - if (idx == s_pendingOpenNonPooled.Length) - { - idx = (int)(s_pendingOpenNonPooledNext % s_pendingOpenNonPooled.Length); - unchecked - { - s_pendingOpenNonPooledNext++; - } - } - - // now that we have an antecedent task, schedule our work when it is completed. - // If it is a new slot or a completed task, this continuation will start right away. - newTask = CreateReplaceConnectionContinuation(s_pendingOpenNonPooled[idx], owningConnection, retry, userOptions, oldConnection, poolGroup, cancellationTokenSource); - - // Place this new task in the slot so any future work will be queued behind it - s_pendingOpenNonPooled[idx] = newTask; - } - - // Set up the timeout (if needed) - if (owningConnection.ConnectionTimeout > 0) - { - int connectionTimeoutMilliseconds = owningConnection.ConnectionTimeout * 1000; - cancellationTokenSource.CancelAfter(connectionTimeoutMilliseconds); - } - - // once the task is done, propagate the final results to the original caller - newTask.ContinueWith( - continuationAction: TryGetConnectionCompletedContinuation, - state: Tuple.Create(cancellationTokenSource, retry), - scheduler: TaskScheduler.Default - ); - - return false; - } - - connection = CreateNonPooledConnection(owningConnection, poolGroup, userOptions); - - SqlClientEventSource.Metrics.EnterNonPooledConnection(); - } - else - { - if (((SqlClient.SqlConnection)owningConnection).ForceNewConnection) - { - Debug.Assert(!(oldConnection is DbConnectionClosed), "Force new connection, but there is no old connection"); - connection = connectionPool.ReplaceConnection(owningConnection, userOptions, oldConnection); - } - else - { - if (!connectionPool.TryGetConnection(owningConnection, retry, userOptions, out connection)) - { - return false; - } - } - - if (connection == null) - { - // connection creation failed on semaphore waiting or if max pool reached - if (connectionPool.IsRunning) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, GetConnection failed because a pool timeout occurred.", ObjectID); - // If GetConnection failed while the pool is running, the pool timeout occurred. - throw ADP.PooledOpenTimeout(); - } - else - { - // We've hit the race condition, where the pool was shut down after we got it from the group. - // Yield time slice to allow shut down activities to complete and a new, running pool to be instantiated - // before retrying. - System.Threading.Thread.Sleep(timeBetweenRetriesMilliseconds); - timeBetweenRetriesMilliseconds *= 2; // double the wait time for next iteration - } - } - } - } while (connection == null && retriesLeft-- > 0); - - if (connection == null) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, GetConnection failed because a pool timeout occurred and all retries were exhausted.", ObjectID); - // exhausted all retries or timed out - give up - throw ADP.PooledOpenTimeout(); - } - - return true; - } - - private Task CreateReplaceConnectionContinuation(Task task, DbConnection owningConnection, TaskCompletionSource retry, DbConnectionOptions userOptions, DbConnectionInternal oldConnection, DbConnectionPoolGroup poolGroup, CancellationTokenSource cancellationTokenSource) - { - return task.ContinueWith( - (_) => - { - System.Transactions.Transaction originalTransaction = ADP.GetCurrentTransaction(); - try - { - ADP.SetCurrentTransaction(retry.Task.AsyncState as System.Transactions.Transaction); - var newConnection = CreateNonPooledConnection(owningConnection, poolGroup, userOptions); - if ((oldConnection != null) && (oldConnection.State == ConnectionState.Open)) - { - oldConnection.PrepareForReplaceConnection(); - oldConnection.Dispose(); - } - return newConnection; - } - finally - { - ADP.SetCurrentTransaction(originalTransaction); - } - }, - cancellationTokenSource.Token, - TaskContinuationOptions.LongRunning, - TaskScheduler.Default - ); - } - - private void TryGetConnectionCompletedContinuation(Task task, object state) - { - Tuple> parameters = (Tuple>)state; - CancellationTokenSource source = parameters.Item1; - source.Dispose(); - - TaskCompletionSource retryCompletionSource = parameters.Item2; - - if (task.IsCanceled) - { - retryCompletionSource.TrySetException(ADP.ExceptionWithStackTrace(ADP.NonPooledOpenTimeout())); - } - else if (task.IsFaulted) - { - retryCompletionSource.TrySetException(task.Exception.InnerException); - } - else - { - if (!retryCompletionSource.TrySetResult(task.Result)) - { - // The outer TaskCompletionSource was already completed - // Which means that we don't know if someone has messed with the outer connection in the middle of creation - // So the best thing to do now is to destroy the newly created connection - task.Result.DoomThisConnection(); - task.Result.Dispose(); - } - else - { - SqlClientEventSource.Metrics.EnterNonPooledConnection(); - } - } - } - - private IDbConnectionPool GetConnectionPool(DbConnection owningObject, DbConnectionPoolGroup connectionPoolGroup) - { - // if poolgroup is disabled, it will be replaced with a new entry - - Debug.Assert(owningObject != null, "null owningObject?"); - Debug.Assert(connectionPoolGroup != null, "null connectionPoolGroup?"); - - // It is possible that while the outer connection object has - // been sitting around in a closed and unused state in some long - // running app, the pruner may have come along and remove this - // the pool entry from the master list. If we were to use a - // pool entry in this state, we would create "unmanaged" pools, - // which would be bad. To avoid this problem, we automagically - // re-create the pool entry whenever it's disabled. - - // however, don't rebuild connectionOptions if no pooling is involved - let new connections do that work - if (connectionPoolGroup.IsDisabled && connectionPoolGroup.PoolGroupOptions != null) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, DisabledPoolGroup={1}", ObjectID, connectionPoolGroup?.ObjectID); - - // reusing existing pool option in case user originally used SetConnectionPoolOptions - DbConnectionPoolGroupOptions poolOptions = connectionPoolGroup.PoolGroupOptions; - - // get the string to hash on again - DbConnectionOptions connectionOptions = connectionPoolGroup.ConnectionOptions; - Debug.Assert(connectionOptions != null, "prevent expansion of connectionString"); - - connectionPoolGroup = GetConnectionPoolGroup(connectionPoolGroup.PoolKey, poolOptions, ref connectionOptions); - Debug.Assert(connectionPoolGroup != null, "null connectionPoolGroup?"); - SetConnectionPoolGroup(owningObject, connectionPoolGroup); - } - IDbConnectionPool connectionPool = connectionPoolGroup.GetConnectionPool(this); - return connectionPool; - } - - internal DbConnectionPoolGroup GetConnectionPoolGroup(DbConnectionPoolKey key, DbConnectionPoolGroupOptions poolOptions, ref DbConnectionOptions userConnectionOptions) - { - if (string.IsNullOrEmpty(key.ConnectionString)) - { - return (DbConnectionPoolGroup)null; - } - - DbConnectionPoolGroup connectionPoolGroup; - Dictionary connectionPoolGroups = _connectionPoolGroups; - if (!connectionPoolGroups.TryGetValue(key, out connectionPoolGroup) || (connectionPoolGroup.IsDisabled && connectionPoolGroup.PoolGroupOptions != null)) - { - // If we can't find an entry for the connection string in - // our collection of pool entries, then we need to create a - // new pool entry and add it to our collection. - - DbConnectionOptions connectionOptions = CreateConnectionOptions(key.ConnectionString, userConnectionOptions); - if (connectionOptions == null) - { - throw ADP.InternalConnectionError(ADP.ConnectionError.ConnectionOptionsMissing); - } - - if (userConnectionOptions == null) - { - // we only allow one expansion on the connection string - - userConnectionOptions = connectionOptions; - string expandedConnectionString = connectionOptions.Expand(); - - // if the expanded string is same instance (default implementation), then use the already created options - if ((object)expandedConnectionString != (object)key.ConnectionString) - { - // CONSIDER: caching the original string to reduce future parsing - DbConnectionPoolKey newKey = (DbConnectionPoolKey)((ICloneable)key).Clone(); - newKey.ConnectionString = expandedConnectionString; - return GetConnectionPoolGroup(newKey, null, ref userConnectionOptions); - } - } - - if (poolOptions == null) - { - if (connectionPoolGroup != null) - { - // reusing existing pool option in case user originally used SetConnectionPoolOptions - poolOptions = connectionPoolGroup.PoolGroupOptions; - } - else - { - // Note: may return null for non-pooled connections - poolOptions = CreateConnectionPoolGroupOptions(connectionOptions); - } - } - - lock (this) - { - connectionPoolGroups = _connectionPoolGroups; - if (!connectionPoolGroups.TryGetValue(key, out connectionPoolGroup)) - { - DbConnectionPoolGroup newConnectionPoolGroup = new DbConnectionPoolGroup(connectionOptions, key, poolOptions); - newConnectionPoolGroup.ProviderInfo = CreateConnectionPoolGroupProviderInfo(connectionOptions); - - // build new dictionary with space for new connection string - Dictionary newConnectionPoolGroups = new Dictionary(1 + connectionPoolGroups.Count); - foreach (KeyValuePair entry in connectionPoolGroups) - { - newConnectionPoolGroups.Add(entry.Key, entry.Value); - } - - // lock prevents race condition with PruneConnectionPoolGroups - newConnectionPoolGroups.Add(key, newConnectionPoolGroup); - - SqlClientEventSource.Metrics.EnterActiveConnectionPoolGroup(); - connectionPoolGroup = newConnectionPoolGroup; - _connectionPoolGroups = newConnectionPoolGroups; - } - else - { - Debug.Assert(!connectionPoolGroup.IsDisabled, "Disabled pool entry discovered"); - } - } - Debug.Assert(connectionPoolGroup != null, "how did we not create a pool entry?"); - Debug.Assert(userConnectionOptions != null, "how did we not have user connection options?"); - } - else if (userConnectionOptions == null) - { - userConnectionOptions = connectionPoolGroup.ConnectionOptions; - } - return connectionPoolGroup; - } - - internal DbMetaDataFactory GetMetaDataFactory(DbConnectionPoolGroup connectionPoolGroup, DbConnectionInternal internalConnection) - { - Debug.Assert(connectionPoolGroup != null, "connectionPoolGroup may not be null."); - - // get the matadatafactory from the pool entry. If it does not already have one - // create one and save it on the pool entry - DbMetaDataFactory metaDataFactory = connectionPoolGroup.MetaDataFactory; - - // consider serializing this so we don't construct multiple metadata factories - // if two threads happen to hit this at the same time. One will be GC'd - if (metaDataFactory == null) - { - bool allowCache = false; - metaDataFactory = CreateMetaDataFactory(internalConnection, out allowCache); - if (allowCache) - { - connectionPoolGroup.MetaDataFactory = metaDataFactory; - } - } - return metaDataFactory; - } - - private void PruneConnectionPoolGroups(object state) - { - // when debugging this method, expect multiple threads at the same time - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}", ObjectID); - - // First, walk the pool release list and attempt to clear each - // pool, when the pool is finally empty, we dispose of it. If the - // pool isn't empty, it's because there are active connections or - // distributed transactions that need it. - lock (_poolsToRelease) - { - if (0 != _poolsToRelease.Count) - { - IDbConnectionPool[] poolsToRelease = _poolsToRelease.ToArray(); - foreach (IDbConnectionPool pool in poolsToRelease) - { - if (pool != null) - { - pool.Clear(); - - if (0 == pool.Count) - { - _poolsToRelease.Remove(pool); - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, ReleasePool={1}", ObjectID, pool.Id); - - SqlClientEventSource.Metrics.ExitInactiveConnectionPool(); - } - } - } - } - } - - // Next, walk the pool entry release list and dispose of each - // pool entry when it is finally empty. If the pool entry isn't - // empty, it's because there are active pools that need it. - lock (_poolGroupsToRelease) - { - if (0 != _poolGroupsToRelease.Count) - { - DbConnectionPoolGroup[] poolGroupsToRelease = _poolGroupsToRelease.ToArray(); - foreach (DbConnectionPoolGroup poolGroup in poolGroupsToRelease) - { - if (poolGroup != null) - { - int poolsLeft = poolGroup.Clear(); // may add entries to _poolsToRelease - - if (0 == poolsLeft) - { - _poolGroupsToRelease.Remove(poolGroup); - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, ReleasePoolGroup={1}", ObjectID, poolGroup.ObjectID); - - SqlClientEventSource.Metrics.ExitInactiveConnectionPoolGroup(); - } - } - } - } - } - - // Finally, we walk through the collection of connection pool entries - // and prune each one. This will cause any empty pools to be put - // into the release list. - lock (this) - { - Dictionary connectionPoolGroups = _connectionPoolGroups; - Dictionary newConnectionPoolGroups = new Dictionary(connectionPoolGroups.Count); - - foreach (KeyValuePair entry in connectionPoolGroups) - { - if (entry.Value != null) - { - Debug.Assert(!entry.Value.IsDisabled, "Disabled pool entry discovered"); - - // entries start active and go idle during prune if all pools are gone - // move idle entries from last prune pass to a queue for pending release - // otherwise process entry which may move it from active to idle - if (entry.Value.Prune()) - { - // may add entries to _poolsToRelease - QueuePoolGroupForRelease(entry.Value); - } - else - { - newConnectionPoolGroups.Add(entry.Key, entry.Value); - } - } - } - _connectionPoolGroups = newConnectionPoolGroups; - } - } - - internal void QueuePoolForRelease(IDbConnectionPool pool, bool clearing) - { - // Queue the pool up for release -- we'll clear it out and dispose - // of it as the last part of the pruning timer callback so we don't - // do it with the pool entry or the pool collection locked. - Debug.Assert(pool != null, "null pool?"); - - // set the pool to the shutdown state to force all active - // connections to be automatically disposed when they - // are returned to the pool - pool.Shutdown(); - - lock (_poolsToRelease) - { - if (clearing) - { - pool.Clear(); - } - _poolsToRelease.Add(pool); - } - SqlClientEventSource.Metrics.EnterInactiveConnectionPool(); - SqlClientEventSource.Metrics.ExitActiveConnectionPool(); - } - - internal void QueuePoolGroupForRelease(DbConnectionPoolGroup poolGroup) - { - Debug.Assert(poolGroup != null, "null poolGroup?"); - SqlClientEventSource.Log.TryTraceEvent(" {0}, poolGroup={1}", ObjectID, poolGroup.ObjectID); - - lock (_poolGroupsToRelease) - { - _poolGroupsToRelease.Add(poolGroup); - } - - SqlClientEventSource.Metrics.EnterInactiveConnectionPoolGroup(); - SqlClientEventSource.Metrics.ExitActiveConnectionPoolGroup(); - } - - protected abstract DbConnectionInternal CreateConnection( - DbConnectionOptions options, - DbConnectionPoolKey poolKey, - DbConnectionPoolGroupProviderInfo poolGroupProviderInfo, - IDbConnectionPool pool, - DbConnection owningConnection, - DbConnectionOptions userOptions); - - abstract protected DbConnectionOptions CreateConnectionOptions(string connectionString, DbConnectionOptions previous); - - abstract protected DbConnectionPoolGroupOptions CreateConnectionPoolGroupOptions(DbConnectionOptions options); - - abstract internal DbConnectionPoolGroup GetConnectionPoolGroup(DbConnection connection); - - abstract internal DbConnectionInternal GetInnerConnection(DbConnection connection); - - abstract protected int GetObjectId(DbConnection connection); - - abstract internal void PermissionDemand(DbConnection outerConnection); - - abstract internal void SetConnectionPoolGroup(DbConnection outerConnection, DbConnectionPoolGroup poolGroup); - - abstract internal void SetInnerConnectionEvent(DbConnection owningObject, DbConnectionInternal to); - - abstract internal bool SetInnerConnectionFrom(DbConnection owningObject, DbConnectionInternal to, DbConnectionInternal from); - - abstract internal void SetInnerConnectionTo(DbConnection owningObject, DbConnectionInternal to); - - virtual internal void Unload() - { - _pruningTimer.Dispose(); - } - } -} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionInternal.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionInternal.cs index b4dfb4f214..31f207e5ce 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionInternal.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionInternal.cs @@ -398,7 +398,7 @@ internal void CleanupConnectionOnTransactionCompletion(Transaction transaction) pool?.TransactionEnded(transaction, this); } - internal virtual void CloseConnection(DbConnection owningObject, DbConnectionFactory connectionFactory) + internal virtual void CloseConnection(DbConnection owningObject, SqlConnectionFactory connectionFactory) { // The implementation here is the implementation required for the // "open" internal connections, since our own private "closed" @@ -708,7 +708,7 @@ internal void MakePooledConnection(IDbConnectionPool connectionPool) internal void NotifyWeakReference(int message) => ReferenceCollection?.Notify(message); - internal virtual void OpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory) + internal virtual void OpenConnection(DbConnection outerConnection, SqlConnectionFactory connectionFactory) { if (!TryOpenConnection(outerConnection, connectionFactory, null, null)) { @@ -814,7 +814,7 @@ internal void SetInStasis() /// internal virtual bool TryOpenConnection( DbConnection outerConnection, - DbConnectionFactory connectionFactory, + SqlConnectionFactory connectionFactory, TaskCompletionSource retry, DbConnectionOptions userOptions) { @@ -823,7 +823,7 @@ internal virtual bool TryOpenConnection( internal virtual bool TryReplaceConnection( DbConnection outerConnection, - DbConnectionFactory connectionFactory, + SqlConnectionFactory connectionFactory, TaskCompletionSource retry, DbConnectionOptions userOptions) { @@ -871,7 +871,7 @@ protected internal void DoomThisConnection() } protected internal virtual DataTable GetSchema( - DbConnectionFactory factory, + SqlConnectionFactory factory, DbConnectionPoolGroup poolGroup, DbConnection outerConnection, string collectionName, @@ -901,7 +901,11 @@ protected virtual void ReleaseAdditionalLocksForClose(bool lockToken) // No additional locks in default implementation } - protected bool TryOpenConnectionInternal(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource retry, DbConnectionOptions userOptions) + protected bool TryOpenConnectionInternal( + DbConnection outerConnection, + SqlConnectionFactory connectionFactory, + TaskCompletionSource retry, + DbConnectionOptions userOptions) { // ?->Connecting: prevent set_ConnectionString during Open if (connectionFactory.SetInnerConnectionFrom(outerConnection, DbConnectionClosedConnecting.SingletonInstance, this)) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPool.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPool.cs index c4772bb736..8b0efe8763 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPool.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPool.cs @@ -25,7 +25,7 @@ internal sealed class ChannelDbConnectionPool : IDbConnectionPool public ConcurrentDictionary AuthenticationContexts => throw new NotImplementedException(); /// - public DbConnectionFactory ConnectionFactory => throw new NotImplementedException(); + public SqlConnectionFactory ConnectionFactory => throw new NotImplementedException(); /// public int Count => throw new NotImplementedException(); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolGroup.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolGroup.cs index 805beed2b5..226a5749b4 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolGroup.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolGroup.cs @@ -132,7 +132,7 @@ internal int Clear() IDbConnectionPool pool = entry.Value; if (pool != null) { - DbConnectionFactory connectionFactory = pool.ConnectionFactory; + SqlConnectionFactory connectionFactory = pool.ConnectionFactory; connectionFactory.QueuePoolForRelease(pool, true); } @@ -143,7 +143,7 @@ internal int Clear() return _poolCollection.Count; } - internal IDbConnectionPool GetConnectionPool(DbConnectionFactory connectionFactory) + internal IDbConnectionPool GetConnectionPool(SqlConnectionFactory connectionFactory) { // When this method returns null it indicates that the connection // factory should not use pooling. @@ -282,7 +282,7 @@ internal bool Prune() // to use it while we're processing and finally we put the // pool into a list of pools to be released when they // are completely empty. - DbConnectionFactory connectionFactory = pool.ConnectionFactory; + SqlConnectionFactory connectionFactory = pool.ConnectionFactory; connectionFactory.QueuePoolForRelease(pool, false); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/IDbConnectionPool.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/IDbConnectionPool.cs index d6647f832e..066318fce2 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/IDbConnectionPool.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/IDbConnectionPool.cs @@ -28,7 +28,7 @@ internal interface IDbConnectionPool /// /// Gets the factory used to create database connections. /// - DbConnectionFactory ConnectionFactory { get; } + SqlConnectionFactory ConnectionFactory { get; } /// /// The number of connections currently managed by the pool. diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs index 77d2b3bae1..bd6ebf708e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs @@ -403,7 +403,7 @@ internal WaitHandle[] GetHandles(bool withCreate) private readonly int _cleanupWait; private readonly DbConnectionPoolIdentity _identity; - private readonly DbConnectionFactory _connectionFactory; + private readonly SqlConnectionFactory _connectionFactory; private readonly DbConnectionPoolGroup _connectionPoolGroup; private readonly DbConnectionPoolGroupOptions _connectionPoolGroupOptions; private DbConnectionPoolProviderInfo _connectionPoolProviderInfo; @@ -439,10 +439,10 @@ internal WaitHandle[] GetHandles(bool withCreate) // only created by DbConnectionPoolGroup.GetConnectionPool internal WaitHandleDbConnectionPool( - DbConnectionFactory connectionFactory, - DbConnectionPoolGroup connectionPoolGroup, - DbConnectionPoolIdentity identity, - DbConnectionPoolProviderInfo connectionPoolProviderInfo) + SqlConnectionFactory connectionFactory, + DbConnectionPoolGroup connectionPoolGroup, + DbConnectionPoolIdentity identity, + DbConnectionPoolProviderInfo connectionPoolProviderInfo) { Debug.Assert(connectionPoolGroup != null, "null connectionPoolGroup"); @@ -492,7 +492,7 @@ private int CreationTimeout public int Count => _totalObjects; - public DbConnectionFactory ConnectionFactory => _connectionFactory; + public SqlConnectionFactory ConnectionFactory => _connectionFactory; public bool ErrorOccurred => _errorOccurred; @@ -746,7 +746,12 @@ private DbConnectionInternal CreateObject(DbConnection owningObject, DbConnectio try { - newObj = _connectionFactory.CreatePooledConnection(this, owningObject, _connectionPoolGroup.ConnectionOptions, _connectionPoolGroup.PoolKey, userOptions); + newObj = _connectionFactory.CreatePooledConnection( + owningObject, + this, + _connectionPoolGroup.PoolKey, + _connectionPoolGroup.ConnectionOptions, + userOptions); if (newObj == null) { throw ADP.InternalError(ADP.InternalErrorCode.CreateObjectReturnedNull); // CreateObject succeeded, but null object diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs index ae4c7bc98d..fd2633a4b9 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs @@ -3,10 +3,14 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; +using System.Data; using System.Data.Common; using System.Diagnostics; using System.IO; using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Data.Common; using Microsoft.Data.Common.ConnectionString; using Microsoft.Data.ProviderBase; @@ -18,26 +22,561 @@ namespace Microsoft.Data.SqlClient { - sealed internal class SqlConnectionFactory : DbConnectionFactory + // @TODO: Facade pattern (interface, use interface, add constructor overloads for providing non-default interface, reseal) + internal sealed class SqlConnectionFactory { - public static readonly SqlConnectionFactory SingletonInstance = new SqlConnectionFactory(); + #region Member Variables - private SqlConnectionFactory() : base() + private static readonly TimeSpan PruningDueTime = TimeSpan.FromMinutes(4); + private static readonly TimeSpan PruningPeriod = TimeSpan.FromSeconds(30); + private static readonly Task CompletedTask = + Task.FromResult(null); + + // s_pendingOpenNonPooled is an array of tasks used to throttle creation of non-pooled + // connections to a maximum of Environment.ProcessorCount at a time. + private static readonly Task[] s_pendingOpenNonPooled = + new Task[Environment.ProcessorCount]; + + private static int s_objectTypeCount; + private static uint s_pendingOpenNonPooledNext = 0; + + private readonly List _poolGroupsToRelease; + private readonly List _poolsToRelease; + private readonly Timer _pruningTimer; + private Dictionary _connectionPoolGroups; + + #endregion + + #region Constructors + + private SqlConnectionFactory() { + _connectionPoolGroups = new Dictionary(); + _poolsToRelease = new List(); + _poolGroupsToRelease = new List(); + _pruningTimer = ADP.UnsafeCreateTimer( + PruneConnectionPoolGroups, + state: null, + PruningDueTime, + PruningPeriod); + #if NET SubscribeToAssemblyLoadContextUnload(); #endif } + + #endregion + + #region Properties + + internal static SqlConnectionFactory Instance { get; } = new SqlConnectionFactory(); + + internal int ObjectId { get; } = Interlocked.Increment(ref s_objectTypeCount); + + #endregion - override public DbProviderFactory ProviderFactory + #region Public Methods + + internal void ClearAllPools() { - get + using TryEventScope scope = TryEventScope.Create(nameof(SqlConnectionFactory)); + foreach (DbConnectionPoolGroup group in _connectionPoolGroups.Values) { - return SqlClientFactory.Instance; + group?.Clear(); } } - protected override DbConnectionInternal CreateConnection( + internal void ClearPool(DbConnection connection) + { + ADP.CheckArgumentNull(connection, nameof(connection)); + + using TryEventScope scope = TryEventScope.Create(" {0}", GetObjectId(connection)); + DbConnectionPoolGroup poolGroup = GetConnectionPoolGroup(connection); + poolGroup?.Clear(); + } + + internal void ClearPool(DbConnectionPoolKey key) + { + ADP.CheckArgumentNull(key.ConnectionString, $"{nameof(key)}.{nameof(key.ConnectionString)}"); + + using TryEventScope scope = TryEventScope.Create(" connectionString"); + if (_connectionPoolGroups.TryGetValue(key, out DbConnectionPoolGroup poolGroup)) + { + poolGroup?.Clear(); + } + } + + internal DbConnectionPoolProviderInfo CreateConnectionPoolProviderInfo(DbConnectionOptions connectionOptions) => + ((SqlConnectionString)connectionOptions).UserInstance + ? new SqlConnectionPoolProviderInfo() + : null; + + internal SqlInternalConnectionTds CreateNonPooledConnection( + DbConnection owningConnection, + DbConnectionPoolGroup poolGroup, + DbConnectionOptions userOptions) + { + Debug.Assert(owningConnection is not null, "null owningConnection?"); + Debug.Assert(poolGroup is not null, "null poolGroup?"); + + SqlInternalConnectionTds newConnection = CreateConnection( + poolGroup.ConnectionOptions, + poolGroup.PoolKey, + poolGroup.ProviderInfo, + pool: null, + owningConnection, + userOptions); + if (newConnection is not null) + { + SqlClientEventSource.Metrics.HardConnectRequest(); + newConnection.MakeNonPooledObject(owningConnection); + } + + SqlClientEventSource.Log.TryTraceEvent(" {0}, Non-pooled database connection created.", ObjectId); + return newConnection; + } + + internal SqlInternalConnectionTds CreatePooledConnection( + DbConnection owningConnection, + IDbConnectionPool pool, + DbConnectionPoolKey poolKey, + DbConnectionOptions options, + DbConnectionOptions userOptions) + { + Debug.Assert(pool != null, "null pool?"); + + SqlInternalConnectionTds newConnection = CreateConnection( + options, + poolKey, // @TODO: is pool.PoolGroup.Key the same thing? + pool.PoolGroup.ProviderInfo, + pool, + owningConnection, + userOptions); + if (newConnection is not null) + { + SqlClientEventSource.Metrics.HardConnectRequest(); + newConnection.MakePooledConnection(pool); + } + + SqlClientEventSource.Log.TryTraceEvent(" {0}, Pooled database connection created.", ObjectId); + return newConnection; + } + + internal DbConnectionPoolGroup GetConnectionPoolGroup( + DbConnectionPoolKey key, + DbConnectionPoolGroupOptions poolOptions, + ref DbConnectionOptions userConnectionOptions) + { + if (string.IsNullOrEmpty(key.ConnectionString)) + { + return null; + } + + if (!_connectionPoolGroups.TryGetValue(key, out DbConnectionPoolGroup connectionPoolGroup) || + (connectionPoolGroup.IsDisabled && connectionPoolGroup.PoolGroupOptions != null)) + { + // If we can't find an entry for the connection string in + // our collection of pool entries, then we need to create a + // new pool entry and add it to our collection. + + SqlConnectionString connectionOptions = new SqlConnectionString(key.ConnectionString); + + if (userConnectionOptions is null) + { + // We only allow one expansion on the connection string + userConnectionOptions = connectionOptions; + string expandedConnectionString = connectionOptions.Expand(); + + // if the expanded string is same instance (default implementation), then use the already created options + if ((object)expandedConnectionString != (object)key.ConnectionString) + { + // CONSIDER: caching the original string to reduce future parsing + DbConnectionPoolKey newKey = (DbConnectionPoolKey)key.Clone(); + newKey.ConnectionString = expandedConnectionString; + return GetConnectionPoolGroup(newKey, null, ref userConnectionOptions); + } + } + + if (poolOptions is null) + { + if (connectionPoolGroup is not null) + { + // reusing existing pool option in case user originally used SetConnectionPoolOptions + poolOptions = connectionPoolGroup.PoolGroupOptions; + } + else + { + // Note: may return null for non-pooled connections + poolOptions = CreateConnectionPoolGroupOptions(connectionOptions); + } + } + + lock (this) + { + if (!_connectionPoolGroups.TryGetValue(key, out connectionPoolGroup)) + { + DbConnectionPoolGroup newConnectionPoolGroup = + new DbConnectionPoolGroup(connectionOptions, key, poolOptions) + { + ProviderInfo = CreateConnectionPoolGroupProviderInfo(connectionOptions) + }; + + // build new dictionary with space for new connection string + Dictionary newConnectionPoolGroups = + new Dictionary(1 + _connectionPoolGroups.Count); + foreach (KeyValuePair entry in _connectionPoolGroups) + { + newConnectionPoolGroups.Add(entry.Key, entry.Value); + } + + // lock prevents race condition with PruneConnectionPoolGroups + newConnectionPoolGroups.Add(key, newConnectionPoolGroup); + + SqlClientEventSource.Metrics.EnterActiveConnectionPoolGroup(); + connectionPoolGroup = newConnectionPoolGroup; + _connectionPoolGroups = newConnectionPoolGroups; + } + else + { + Debug.Assert(!connectionPoolGroup.IsDisabled, "Disabled pool entry discovered"); + } + } + + Debug.Assert(connectionPoolGroup != null, "how did we not create a pool entry?"); + Debug.Assert(userConnectionOptions != null, "how did we not have user connection options?"); + } + else if (userConnectionOptions is null) + { + userConnectionOptions = connectionPoolGroup.ConnectionOptions; + } + + return connectionPoolGroup; + } + + internal DbMetaDataFactory GetMetaDataFactory( + DbConnectionPoolGroup poolGroup, + DbConnectionInternal internalConnection) + { + Debug.Assert(poolGroup is not null, "connectionPoolGroup may not be null."); + + // Get the matadatafactory from the pool entry. If it does not already have one + // create one and save it on the pool entry + DbMetaDataFactory metaDataFactory = poolGroup.MetaDataFactory; + + // CONSIDER: serializing this so we don't construct multiple metadata factories + // if two threads happen to hit this at the same time. One will be GC'd + if (metaDataFactory is null) + { + metaDataFactory = CreateMetaDataFactory(internalConnection, out bool allowCache); + if (allowCache) + { + poolGroup.MetaDataFactory = metaDataFactory; + } + } + + return metaDataFactory; + } + + internal void QueuePoolForRelease(IDbConnectionPool pool, bool clearing) + { + // Queue the pool up for release -- we'll clear it out and dispose of it as the last + // part of the pruning timer callback so we don't do it with the pool entry or the pool + // collection locked. + Debug.Assert(pool != null, "null pool?"); + + // Set the pool to the shutdown state to force all active connections to be + // automatically disposed when they are returned to the pool + pool.Shutdown(); + + lock (_poolsToRelease) + { + if (clearing) + { + pool.Clear(); + } + _poolsToRelease.Add(pool); + } + + SqlClientEventSource.Metrics.EnterInactiveConnectionPool(); + SqlClientEventSource.Metrics.ExitActiveConnectionPool(); + } + + internal void QueuePoolGroupForRelease(DbConnectionPoolGroup poolGroup) + { + Debug.Assert(poolGroup != null, "null poolGroup?"); + SqlClientEventSource.Log.TryTraceEvent(" {0}, poolGroup={1}", ObjectId, poolGroup.ObjectID); + + lock (_poolGroupsToRelease) + { + _poolGroupsToRelease.Add(poolGroup); + } + + SqlClientEventSource.Metrics.EnterInactiveConnectionPoolGroup(); + SqlClientEventSource.Metrics.ExitActiveConnectionPoolGroup(); + } + + internal bool TryGetConnection( + DbConnection owningConnection, + TaskCompletionSource retry, + DbConnectionOptions userOptions, + DbConnectionInternal oldConnection, + out DbConnectionInternal connection) + { + Debug.Assert(owningConnection is not null, "null owningConnection?"); + + connection = null; + + // Work around race condition with clearing the pool between GetConnectionPool obtaining pool + // and GetConnection on the pool checking the pool state. Clearing the pool in this window + // will switch the pool into the ShuttingDown state, and GetConnection will return null. + // There is probably a better solution involving locking the pool/group, but that entails a major + // re-design of the connection pooling synchronization, so is postponed for now. + + // Use retriesLeft to prevent CPU spikes with incremental sleep + // start with one msec, double the time every retry + // max time is: 1 + 2 + 4 + ... + 2^(retries-1) == 2^retries -1 == 1023ms (for 10 retries) + int retriesLeft = 10; + int timeBetweenRetriesMilliseconds = 1; + + do + { + DbConnectionPoolGroup poolGroup = GetConnectionPoolGroup(owningConnection); + + // Doing this on the callers thread is important because it looks up the WindowsIdentity from the thread. + IDbConnectionPool connectionPool = GetConnectionPool(owningConnection, poolGroup); + if (connectionPool == null) + { + // If GetConnectionPool returns null, we can be certain that this connection + // should not be pooled via DbConnectionPool or have a disabled pool entry. + poolGroup = GetConnectionPoolGroup(owningConnection); // previous entry have been disabled + + if (retry is not null) + { + Task newTask; + CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + lock (s_pendingOpenNonPooled) + { + // look for an available task slot (completed or empty) + int idx; + for (idx = 0; idx < s_pendingOpenNonPooled.Length; idx++) + { + Task task = s_pendingOpenNonPooled[idx]; + if (task is null) + { + s_pendingOpenNonPooled[idx] = CompletedTask; + break; + } + + if (task.IsCompleted) + { + break; + } + } + + // if didn't find one, pick the next one in round-robin fashion + if (idx == s_pendingOpenNonPooled.Length) + { + idx = (int)(s_pendingOpenNonPooledNext % s_pendingOpenNonPooled.Length); + unchecked + { + s_pendingOpenNonPooledNext++; + } + } + + // now that we have an antecedent task, schedule our work when it is completed. + // If it is a new slot or a completed task, this continuation will start right away. + newTask = CreateReplaceConnectionContinuation( + s_pendingOpenNonPooled[idx], + owningConnection, + retry, + userOptions, + oldConnection, + poolGroup, + cancellationTokenSource); + + // Place this new task in the slot so any future work will be queued behind it + s_pendingOpenNonPooled[idx] = newTask; + } + + // Set up the timeout (if needed) + if (owningConnection.ConnectionTimeout > 0) + { + int connectionTimeoutMilliseconds = owningConnection.ConnectionTimeout * 1000; + cancellationTokenSource.CancelAfter(connectionTimeoutMilliseconds); + } + + // once the task is done, propagate the final results to the original caller + newTask.ContinueWith( + continuationAction: TryGetConnectionCompletedContinuation, + state: Tuple.Create(cancellationTokenSource, retry), + scheduler: TaskScheduler.Default + ); + + return false; + } + + connection = CreateNonPooledConnection(owningConnection, poolGroup, userOptions); + + SqlClientEventSource.Metrics.EnterNonPooledConnection(); + } + else + { + if (((SqlConnection)owningConnection).ForceNewConnection) + { + Debug.Assert(oldConnection is not DbConnectionClosed, "Force new connection, but there is no old connection"); + + connection = connectionPool.ReplaceConnection(owningConnection, userOptions, oldConnection); + } + else + { + if (!connectionPool.TryGetConnection(owningConnection, retry, userOptions, out connection)) + { + return false; + } + } + + if (connection is null) + { + // connection creation failed on semaphore waiting or if max pool reached + if (connectionPool.IsRunning) + { + SqlClientEventSource.Log.TryTraceEvent(" {0}, GetConnection failed because a pool timeout occurred.", ObjectId); + // If GetConnection failed while the pool is running, the pool timeout occurred. + throw ADP.PooledOpenTimeout(); + } + + // We've hit the race condition, where the pool was shut down after we + // got it from the group. Yield time slice to allow shutdown activities + // to complete and a new, running pool to be instantiated before + // retrying. + Thread.Sleep(timeBetweenRetriesMilliseconds); + timeBetweenRetriesMilliseconds *= 2; // double the wait time for next iteration + } + } + } while (connection == null && retriesLeft-- > 0); + + if (connection == null) + { + SqlClientEventSource.Log.TryTraceEvent(" {0}, GetConnection failed because a pool timeout occurred and all retries were exhausted.", ObjectId); + // exhausted all retries or timed out - give up + throw ADP.PooledOpenTimeout(); + } + + return true; + } + + #endregion + + internal DbConnectionPoolGroupProviderInfo CreateConnectionPoolGroupProviderInfo( + DbConnectionOptions connectionOptions) => + new SqlConnectionPoolGroupProviderInfo((SqlConnectionString)connectionOptions); + + internal SqlConnectionString FindSqlConnectionOptions(SqlConnectionPoolKey key) + { + Debug.Assert(key is not null, "Key cannot be null"); + + DbConnectionOptions connectionOptions = null; + + if (!string.IsNullOrEmpty(key.ConnectionString) && + _connectionPoolGroups.TryGetValue(key, out DbConnectionPoolGroup poolGroup)) + { + connectionOptions = poolGroup.ConnectionOptions; + } + + if (connectionOptions is null) + { + connectionOptions = new SqlConnectionString(key.ConnectionString); + } + + if (connectionOptions.IsEmpty) + { + throw ADP.NoConnectionString(); + } + + return (SqlConnectionString)connectionOptions; + } + + // @TODO: All these methods seem redundant ... shouldn't we always have a SqlConnection? + internal DbConnectionPoolGroup GetConnectionPoolGroup(DbConnection connection) + { + SqlConnection c = (connection as SqlConnection); + if (c != null) + { + return c.PoolGroup; + } + return null; + } + + internal DbConnectionInternal GetInnerConnection(DbConnection connection) + { + SqlConnection c = (connection as SqlConnection); + if (c != null) + { + return c.InnerConnection; + } + return null; + } + + internal int GetObjectId(DbConnection connection) + { + SqlConnection c = (connection as SqlConnection); + if (c != null) + { + return c.ObjectID; + } + return 0; + } + + internal void PermissionDemand(DbConnection outerConnection) + { + SqlConnection c = (outerConnection as SqlConnection); + if (c != null) + { + c.PermissionDemand(); + } + } + + internal void SetConnectionPoolGroup(DbConnection outerConnection, DbConnectionPoolGroup poolGroup) + { + SqlConnection c = (outerConnection as SqlConnection); + if (c != null) + { + c.PoolGroup = poolGroup; + } + } + + internal void SetInnerConnectionEvent(DbConnection owningObject, DbConnectionInternal to) + { + SqlConnection c = (owningObject as SqlConnection); + if (c != null) + { + c.SetInnerConnectionEvent(to); + } + } + + internal bool SetInnerConnectionFrom(DbConnection owningObject, DbConnectionInternal to, DbConnectionInternal from) + { + SqlConnection c = (owningObject as SqlConnection); + if (c != null) + { + return c.SetInnerConnectionFrom(to, from); + } + return false; + } + + internal void SetInnerConnectionTo(DbConnection owningObject, DbConnectionInternal to) + { + SqlConnection c = (owningObject as SqlConnection); + if (c != null) + { + c.SetInnerConnectionTo(to); + } + } + + #region Private Methods + + // @TODO: I think this could be broken down into methods more specific to use cases above + private static SqlInternalConnectionTds CreateConnection( DbConnectionOptions options, DbConnectionPoolKey poolKey, DbConnectionPoolGroupProviderInfo poolGroupProviderInfo, @@ -70,10 +609,9 @@ protected override DbConnectionInternal CreateConnection( bool redirectedUserInstance = false; DbConnectionPoolIdentity identity = null; - // Pass DbConnectionPoolIdentity to SqlInternalConnectionTds if using integrated security - // or active directory integrated security. + // Pass DbConnectionPoolIdentity to SqlInternalConnectionTds if using integrated security. // Used by notifications. - if (opt.IntegratedSecurity || opt.Authentication is SqlAuthenticationMethod.ActiveDirectoryIntegrated) + if (opt.IntegratedSecurity || opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated) { if (pool != null) { @@ -160,26 +698,7 @@ protected override DbConnectionInternal CreateConnection( key.AccessTokenCallback); } - protected override DbConnectionOptions CreateConnectionOptions(string connectionString, DbConnectionOptions previous) - { - Debug.Assert(!string.IsNullOrEmpty(connectionString), "empty connectionString"); - SqlConnectionString result = new SqlConnectionString(connectionString); - return result; - } - - internal override DbConnectionPoolProviderInfo CreateConnectionPoolProviderInfo(DbConnectionOptions connectionOptions) - { - DbConnectionPoolProviderInfo providerInfo = null; - - if (((SqlConnectionString)connectionOptions).UserInstance) - { - providerInfo = new SqlConnectionPoolProviderInfo(); - } - - return providerInfo; - } - - protected override DbConnectionPoolGroupOptions CreateConnectionPoolGroupOptions(DbConnectionOptions connectionOptions) + private static DbConnectionPoolGroupOptions CreateConnectionPoolGroupOptions(SqlConnectionString connectionOptions) { SqlConnectionString opt = (SqlConnectionString)connectionOptions; @@ -224,123 +743,222 @@ protected override DbConnectionPoolGroupOptions CreateConnectionPoolGroupOptions return poolingOptions; } - internal override DbConnectionPoolGroupProviderInfo CreateConnectionPoolGroupProviderInfo( - DbConnectionOptions connectionOptions) + private static DbMetaDataFactory CreateMetaDataFactory( + DbConnectionInternal internalConnection, + out bool cacheMetaDataFactory) { - return new SqlConnectionPoolGroupProviderInfo((SqlConnectionString)connectionOptions); - } + Debug.Assert(internalConnection is not null, "internalConnection may not be null."); - internal static SqlConnectionString FindSqlConnectionOptions(SqlConnectionPoolKey key) - { - SqlConnectionString connectionOptions = (SqlConnectionString)SingletonInstance.FindConnectionOptions(key); - if (connectionOptions == null) - { - connectionOptions = new SqlConnectionString(key.ConnectionString); - } - if (connectionOptions.IsEmpty) - { - throw ADP.NoConnectionString(); - } - return connectionOptions; + Stream xmlStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Microsoft.Data.SqlClient.SqlMetaData.xml"); + Debug.Assert(xmlStream is not null, $"{nameof(xmlStream)} may not be null."); + + cacheMetaDataFactory = true; + return new SqlMetaDataFactory(xmlStream, + internalConnection.ServerVersion, + internalConnection.ServerVersion); } - - // @TODO: All these methods seem redundant ... shouldn't we always have a SqlConnection? - override internal DbConnectionPoolGroup GetConnectionPoolGroup(DbConnection connection) + + private Task CreateReplaceConnectionContinuation( + Task task, + DbConnection owningConnection, + TaskCompletionSource retry, + DbConnectionOptions userOptions, + DbConnectionInternal oldConnection, + DbConnectionPoolGroup poolGroup, + CancellationTokenSource cancellationTokenSource) { - SqlConnection c = (connection as SqlConnection); - if (c != null) - { - return c.PoolGroup; - } - return null; + return task.ContinueWith( + _ => + { + System.Transactions.Transaction originalTransaction = ADP.GetCurrentTransaction(); + try + { + ADP.SetCurrentTransaction(retry.Task.AsyncState as System.Transactions.Transaction); + + DbConnectionInternal newConnection = CreateNonPooledConnection(owningConnection, poolGroup, userOptions); + + if (oldConnection?.State == ConnectionState.Open) + { + oldConnection.PrepareForReplaceConnection(); + oldConnection.Dispose(); + } + + return newConnection; + } + finally + { + ADP.SetCurrentTransaction(originalTransaction); + } + }, + cancellationTokenSource.Token, + TaskContinuationOptions.LongRunning, + TaskScheduler.Default + ); } - override internal DbConnectionInternal GetInnerConnection(DbConnection connection) + private IDbConnectionPool GetConnectionPool( + DbConnection owningObject, + DbConnectionPoolGroup connectionPoolGroup) { - SqlConnection c = (connection as SqlConnection); - if (c != null) + // If poolgroup is disabled, it will be replaced with a new entry + + Debug.Assert(owningObject != null, "null owningObject?"); + Debug.Assert(connectionPoolGroup != null, "null connectionPoolGroup?"); + + // It is possible that while the outer connection object has been sitting around in a + // closed and unused state in some long-running app, the pruner may have come along and + // remove this the pool entry from the master list. If we were to use a pool entry in + // this state, we would create "unmanaged" pools, which would be bad. To avoid this + // problem, we automagically re-create the pool entry whenever it's disabled. + + // however, don't rebuild connectionOptions if no pooling is involved - let new connections do that work + if (connectionPoolGroup.IsDisabled && connectionPoolGroup.PoolGroupOptions != null) { - return c.InnerConnection; + SqlClientEventSource.Log.TryTraceEvent(" {0}, DisabledPoolGroup={1}", ObjectId, connectionPoolGroup.ObjectID); + + // reusing existing pool option in case user originally used SetConnectionPoolOptions + DbConnectionPoolGroupOptions poolOptions = connectionPoolGroup.PoolGroupOptions; + + // get the string to hash on again + DbConnectionOptions connectionOptions = connectionPoolGroup.ConnectionOptions; + Debug.Assert(connectionOptions != null, "prevent expansion of connectionString"); + + connectionPoolGroup = GetConnectionPoolGroup(connectionPoolGroup.PoolKey, poolOptions, ref connectionOptions); + Debug.Assert(connectionPoolGroup != null, "null connectionPoolGroup?"); + SetConnectionPoolGroup(owningObject, connectionPoolGroup); } - return null; + + IDbConnectionPool connectionPool = connectionPoolGroup.GetConnectionPool(this); + return connectionPool; } - override protected int GetObjectId(DbConnection connection) + private void PruneConnectionPoolGroups(object state) { - SqlConnection c = (connection as SqlConnection); - if (c != null) + // When debugging this method, expect multiple threads at the same time + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}", ObjectId); + + // First, walk the pool release list and attempt to clear each pool, when the pool is + // finally empty, we dispose of it. If the pool isn't empty, it's because there are + // active connections or distributed transactions that need it. + lock (_poolsToRelease) { - return c.ObjectID; + if (_poolsToRelease.Count != 0) + { + IDbConnectionPool[] poolsToRelease = _poolsToRelease.ToArray(); + foreach (IDbConnectionPool pool in poolsToRelease) + { + if (pool is not null) + { + pool.Clear(); + + if (pool.Count == 0) + { + _poolsToRelease.Remove(pool); + + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, ReleasePool={1}", ObjectId, pool.Id); + SqlClientEventSource.Metrics.ExitInactiveConnectionPool(); + } + } + } + } } - return 0; - } - override internal void PermissionDemand(DbConnection outerConnection) - { - SqlConnection c = (outerConnection as SqlConnection); - if (c != null) + // Next, walk the pool entry release list and dispose of each pool entry when it is + // finally empty. If the pool entry isn't empty, it's because there are active pools + // that need it. + lock (_poolGroupsToRelease) { - c.PermissionDemand(); + if (_poolGroupsToRelease.Count != 0) + { + DbConnectionPoolGroup[] poolGroupsToRelease = _poolGroupsToRelease.ToArray(); + foreach (DbConnectionPoolGroup poolGroup in poolGroupsToRelease) + { + if (poolGroup != null) + { + int poolsLeft = poolGroup.Clear(); // may add entries to _poolsToRelease + + if (poolsLeft == 0) + { + _poolGroupsToRelease.Remove(poolGroup); + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, ReleasePoolGroup={1}", ObjectId, poolGroup.ObjectID); + + SqlClientEventSource.Metrics.ExitInactiveConnectionPoolGroup(); + } + } + } + } } - } - override internal void SetConnectionPoolGroup(DbConnection outerConnection, DbConnectionPoolGroup poolGroup) - { - SqlConnection c = (outerConnection as SqlConnection); - if (c != null) + // Finally, we walk through the collection of connection pool entries and prune each + // one. This will cause any empty pools to be put into the release list. + lock (this) { - c.PoolGroup = poolGroup; + Dictionary connectionPoolGroups = _connectionPoolGroups; + Dictionary newConnectionPoolGroups = new Dictionary(connectionPoolGroups.Count); + + foreach (KeyValuePair entry in connectionPoolGroups) + { + if (entry.Value != null) + { + Debug.Assert(!entry.Value.IsDisabled, "Disabled pool entry discovered"); + + // entries start active and go idle during prune if all pools are gone + // move idle entries from last prune pass to a queue for pending release + // otherwise process entry which may move it from active to idle + if (entry.Value.Prune()) + { + // may add entries to _poolsToRelease + QueuePoolGroupForRelease(entry.Value); + } + else + { + newConnectionPoolGroups.Add(entry.Key, entry.Value); + } + } + } + _connectionPoolGroups = newConnectionPoolGroups; } } - - override internal void SetInnerConnectionEvent(DbConnection owningObject, DbConnectionInternal to) + + private void TryGetConnectionCompletedContinuation(Task task, object state) { - SqlConnection c = (owningObject as SqlConnection); - if (c != null) + // Decompose the state into the parameters we want + (CancellationTokenSource cts, TaskCompletionSource tcs) = + (Tuple>)state; + + cts.Dispose(); + + if (task.IsCanceled) { - c.SetInnerConnectionEvent(to); + tcs.TrySetException(ADP.ExceptionWithStackTrace(ADP.NonPooledOpenTimeout())); } - } - - override internal bool SetInnerConnectionFrom(DbConnection owningObject, DbConnectionInternal to, DbConnectionInternal from) - { - SqlConnection c = (owningObject as SqlConnection); - if (c != null) + else if (task.IsFaulted) { - return c.SetInnerConnectionFrom(to, from); + tcs.TrySetException(task.Exception.InnerException); } - return false; - } - - override internal void SetInnerConnectionTo(DbConnection owningObject, DbConnectionInternal to) - { - SqlConnection c = (owningObject as SqlConnection); - if (c != null) + else { - c.SetInnerConnectionTo(to); + if (!tcs.TrySetResult(task.Result)) + { + // The outer TaskCompletionSource was already completed + // Which means that we don't know if someone has messed with the outer connection in the middle of creation + // So the best thing to do now is to destroy the newly created connection + task.Result.DoomThisConnection(); + task.Result.Dispose(); + } + else + { + SqlClientEventSource.Metrics.EnterNonPooledConnection(); + } } } - - protected override DbMetaDataFactory CreateMetaDataFactory(DbConnectionInternal internalConnection, out bool cacheMetaDataFactory) - { - Debug.Assert(internalConnection != null, "internalConnection may not be null."); - - Stream xmlStream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("Microsoft.Data.SqlClient.SqlMetaData.xml"); - cacheMetaDataFactory = true; - - Debug.Assert(xmlStream != null, nameof(xmlStream) + " may not be null."); - - return new SqlMetaDataFactory(xmlStream, - internalConnection.ServerVersion, - internalConnection.ServerVersion); - } - + #if NET private void Unload(object sender, EventArgs e) { try { - Unload(); + _pruningTimer.Dispose(); } finally { @@ -359,6 +977,8 @@ private void SubscribeToAssemblyLoadContextUnload() SqlConnectionFactoryAssemblyLoadContext_Unloading; } #endif + + #endregion } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Common/SystemDataInternals/ConnectionPoolHelper.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Common/SystemDataInternals/ConnectionPoolHelper.cs index 6c828b188b..34b9070946 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Common/SystemDataInternals/ConnectionPoolHelper.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Common/SystemDataInternals/ConnectionPoolHelper.cs @@ -18,7 +18,6 @@ internal static class ConnectionPoolHelper private static Type s_waitHandleDbConnectionPool = s_MicrosoftDotData.GetType("Microsoft.Data.SqlClient.ConnectionPool.WaitHandleDbConnectionPool"); private static Type s_dbConnectionPoolGroup = s_MicrosoftDotData.GetType("Microsoft.Data.SqlClient.ConnectionPool.DbConnectionPoolGroup"); private static Type s_dbConnectionPoolIdentity = s_MicrosoftDotData.GetType("Microsoft.Data.SqlClient.ConnectionPool.DbConnectionPoolIdentity"); - private static Type s_dbConnectionFactory = s_MicrosoftDotData.GetType("Microsoft.Data.ProviderBase.DbConnectionFactory"); private static Type s_sqlConnectionFactory = s_MicrosoftDotData.GetType("Microsoft.Data.SqlClient.SqlConnectionFactory"); private static Type s_dbConnectionPoolKey = s_MicrosoftDotData.GetType("Microsoft.Data.SqlClient.ConnectionPool.DbConnectionPoolKey"); private static Type s_dictStringPoolGroup = typeof(Dictionary<,>).MakeGenericType(s_dbConnectionPoolKey, s_dbConnectionPoolGroup); @@ -26,9 +25,9 @@ internal static class ConnectionPoolHelper private static PropertyInfo s_dbConnectionPoolCount = s_waitHandleDbConnectionPool.GetProperty("Count", BindingFlags.Instance | BindingFlags.Public); private static PropertyInfo s_dictStringPoolGroupGetKeys = s_dictStringPoolGroup.GetProperty("Keys"); private static PropertyInfo s_dictPoolIdentityPoolValues = s_dictPoolIdentityPool.GetProperty("Values"); - private static FieldInfo s_dbConnectionFactoryPoolGroupList = s_dbConnectionFactory.GetField("_connectionPoolGroups", BindingFlags.Instance | BindingFlags.NonPublic); + private static PropertyInfo s_sqlConnectionFactorySingleton = s_sqlConnectionFactory.GetProperty("Instance", BindingFlags.Static | BindingFlags.NonPublic); + private static FieldInfo s_dbConnectionFactoryPoolGroupList = s_sqlConnectionFactory.GetField("_connectionPoolGroups", BindingFlags.Instance | BindingFlags.NonPublic); private static FieldInfo s_dbConnectionPoolGroupPoolCollection = s_dbConnectionPoolGroup.GetField("_poolCollection", BindingFlags.Instance | BindingFlags.NonPublic); - private static FieldInfo s_sqlConnectionFactorySingleton = s_sqlConnectionFactory.GetField("SingletonInstance", BindingFlags.Static | BindingFlags.Public); private static FieldInfo s_dbConnectionPoolStackOld = s_waitHandleDbConnectionPool.GetField("_stackOld", BindingFlags.Instance | BindingFlags.NonPublic); private static FieldInfo s_dbConnectionPoolStackNew = s_waitHandleDbConnectionPool.GetField("_stackNew", BindingFlags.Instance | BindingFlags.NonPublic); private static MethodInfo s_dbConnectionPoolCleanup = s_waitHandleDbConnectionPool.GetMethod("CleanupCallback", BindingFlags.Instance | BindingFlags.NonPublic);