diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 7d94425123..b5148713c5 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -10,6 +10,7 @@
+
diff --git a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj
index 60eff24c1c..c02c44829c 100644
--- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj
+++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj
@@ -45,6 +45,11 @@
+
+
+
+
+
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 9dae90ffd0..7e7b391daa 100644
--- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj
+++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj
@@ -105,6 +105,9 @@
Microsoft\Data\SqlClient\ConnectionPool\ChannelDbConnectionPool.cs
+
+ Microsoft\Data\SqlClient\ConnectionPool\ConnectionPoolSlots.cs
+
Microsoft\Data\SqlClient\ConnectionPool\DbConnectionPoolAuthenticationContext.cs
diff --git a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj
index 6b507b5a0a..bb2fe8ee4d 100644
--- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj
+++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj
@@ -46,6 +46,7 @@
+
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 9ce3b09334..0e21c57d71 100644
--- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj
+++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj
@@ -294,6 +294,9 @@
Microsoft\Data\SqlClient\ConnectionPool\ChannelDbConnectionPool.cs
+
+ Microsoft\Data\SqlClient\ConnectionPool\ConnectionPoolSlots.cs
+
Microsoft\Data\SqlClient\ConnectionPool\DbConnectionPoolAuthenticationContext.cs
@@ -974,6 +977,7 @@
+
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj
index ff8274c66f..9c9ae1db80 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj
@@ -20,7 +20,8 @@
-
+
+
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..1ed777b093 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionInternal.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionInternal.cs
@@ -46,11 +46,6 @@ internal abstract class DbConnectionInternal
///
private bool _cannotBePooled;
- ///
- /// When the connection was created.
- ///
- private DateTime _createTime;
-
///
/// [usage must be thread-safe] the transaction that we're enlisted in, either manually or automatically.
///
@@ -93,10 +88,16 @@ internal DbConnectionInternal(ConnectionState state, bool hidePassword, bool all
AllowSetConnectionString = allowSetConnectionString;
ShouldHidePassword = hidePassword;
State = state;
+ CreateTime = DateTime.UtcNow;
}
#region Properties
+ ///
+ /// When the connection was created.
+ ///
+ internal DateTime CreateTime { get; }
+
internal bool AllowSetConnectionString { get; }
internal bool CanBePooled => !IsConnectionDoomed && !_cannotBePooled && !_owningObject.TryGetTarget(out _);
@@ -531,7 +532,7 @@ internal void DeactivateConnection()
// If we're not already doomed, check the connection's lifetime and
// doom it if it's lifetime has elapsed.
DateTime now = DateTime.UtcNow;
- if (now.Ticks - _createTime.Ticks > Pool.LoadBalanceTimeout.Ticks)
+ if (now.Ticks - CreateTime.Ticks > Pool.LoadBalanceTimeout.Ticks)
{
DoNotPoolThisConnection();
}
@@ -701,7 +702,6 @@ internal void MakeNonPooledObject(DbConnection owningObject)
///
internal void MakePooledConnection(IDbConnectionPool connectionPool)
{
- _createTime = DateTime.UtcNow;
Pool = connectionPool;
}
@@ -756,7 +756,7 @@ internal virtual void PrepareForReplaceConnection()
// By default, there is no preparation required
}
- internal void PrePush(object expectedOwner)
+ internal void PrePush(DbConnection expectedOwner)
{
// Called by IDbConnectionPool when we're about to be put into it's pool, we take this
// opportunity to ensure ownership and pool counts are legit.
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..912d09da18 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
@@ -3,12 +3,18 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Concurrent;
+using System.Collections.ObjectModel;
using System.Data.Common;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Channels;
using System.Threading.Tasks;
using System.Transactions;
using Microsoft.Data.Common;
using Microsoft.Data.Common.ConnectionString;
using Microsoft.Data.ProviderBase;
+using static Microsoft.Data.SqlClient.ConnectionPool.DbConnectionPoolState;
#nullable enable
@@ -20,52 +26,107 @@ namespace Microsoft.Data.SqlClient.ConnectionPool
///
internal sealed class ChannelDbConnectionPool : IDbConnectionPool
{
+ #region Fields
+ // Limits synchronous operations which depend on async operations on managed
+ // threads from blocking on all available threads, which would stop async tasks
+ // from being scheduled and cause deadlocks. Use ProcessorCount/2 as a balance
+ // between sync and async tasks.
+ private static SemaphoreSlim _syncOverAsyncSemaphore = new(Math.Max(1, Environment.ProcessorCount / 2));
+
+ ///
+ /// Tracks the number of instances of this class. Used to generate unique IDs for each instance.
+ ///
+ private static int _instanceCount;
+
+ private readonly int _instanceId = Interlocked.Increment(ref _instanceCount);
+
+ ///
+ /// Tracks all connections currently managed by this pool, whether idle or busy.
+ /// Only updated rarely - when physical connections are opened/closed - but is read in perf-sensitive contexts.
+ ///
+ private readonly ConnectionPoolSlots _connectionSlots;
+
+ ///
+ /// Reader side for the idle connection channel. Contains nulls in order to release waiting attempts after
+ /// a connection has been physically closed/broken.
+ ///
+ private readonly ChannelReader _idleConnectionReader;
+ private readonly ChannelWriter _idleConnectionWriter;
+ #endregion
+
+ ///
+ /// Initializes a new PoolingDataSource.
+ ///
+ internal ChannelDbConnectionPool(
+ DbConnectionFactory connectionFactory,
+ DbConnectionPoolGroup connectionPoolGroup,
+ DbConnectionPoolIdentity identity,
+ DbConnectionPoolProviderInfo connectionPoolProviderInfo)
+ {
+ ConnectionFactory = connectionFactory;
+ PoolGroup = connectionPoolGroup;
+ PoolGroupOptions = connectionPoolGroup.PoolGroupOptions;
+ ProviderInfo = connectionPoolProviderInfo;
+ Identity = identity;
+ AuthenticationContexts = new();
+ MaxPoolSize = Convert.ToUInt32(PoolGroupOptions.MaxPoolSize);
+
+ _connectionSlots = new(MaxPoolSize);
+
+ // We enforce Max Pool Size, so no need to create a bounded channel (which is less efficient)
+ // On the consuming side, we have the multiplexing write loop but also non-multiplexing Rents
+ // On the producing side, we have connections being released back into the pool (both multiplexing and not)
+ var idleChannel = Channel.CreateUnbounded();
+ _idleConnectionReader = idleChannel.Reader;
+ _idleConnectionWriter = idleChannel.Writer;
+
+ State = Running;
+ }
+
#region Properties
///
- public ConcurrentDictionary AuthenticationContexts => throw new NotImplementedException();
+ public ConcurrentDictionary<
+ DbConnectionPoolAuthenticationContextKey,
+ DbConnectionPoolAuthenticationContext> AuthenticationContexts { get; }
///
- public DbConnectionFactory ConnectionFactory => throw new NotImplementedException();
+ public DbConnectionFactory ConnectionFactory { get; }
///
- public int Count => throw new NotImplementedException();
+ public int Count => _connectionSlots.ReservationCount;
///
public bool ErrorOccurred => throw new NotImplementedException();
///
- public int Id => throw new NotImplementedException();
+ public int Id => _instanceId;
///
- public DbConnectionPoolIdentity Identity => throw new NotImplementedException();
+ public DbConnectionPoolIdentity Identity { get; }
///
- public bool IsRunning => throw new NotImplementedException();
+ public bool IsRunning => State == Running;
///
- public TimeSpan LoadBalanceTimeout => throw new NotImplementedException();
+ public TimeSpan LoadBalanceTimeout => PoolGroupOptions.LoadBalanceTimeout;
///
- public DbConnectionPoolGroup PoolGroup => throw new NotImplementedException();
+ public DbConnectionPoolGroup PoolGroup { get; }
///
- public DbConnectionPoolGroupOptions PoolGroupOptions => throw new NotImplementedException();
+ public DbConnectionPoolGroupOptions PoolGroupOptions { get; }
///
- public DbConnectionPoolProviderInfo ProviderInfo => throw new NotImplementedException();
+ public DbConnectionPoolProviderInfo ProviderInfo { get; }
///
- public DbConnectionPoolState State
- {
- get => throw new NotImplementedException();
- set => throw new NotImplementedException();
- }
+ public DbConnectionPoolState State { get; private set; }
///
- public bool UseLoadBalancing => throw new NotImplementedException();
- #endregion
-
+ public bool UseLoadBalancing => PoolGroupOptions.UseLoadBalancing;
+ private uint MaxPoolSize { get; }
+ #endregion
#region Methods
///
@@ -75,21 +136,48 @@ public void Clear()
}
///
- public void PutObjectFromTransactedPool(DbConnectionInternal obj)
+ public void PutObjectFromTransactedPool(DbConnectionInternal connection)
{
throw new NotImplementedException();
}
///
- public DbConnectionInternal ReplaceConnection(DbConnection owningObject, DbConnectionOptions userOptions, DbConnectionInternal oldConnection)
+ public DbConnectionInternal ReplaceConnection(
+ DbConnection owningObject,
+ DbConnectionOptions userOptions,
+ DbConnectionInternal oldConnection)
{
throw new NotImplementedException();
}
///
- public void ReturnInternalConnection(DbConnectionInternal obj, object owningObject)
+ public void ReturnInternalConnection(DbConnectionInternal connection, DbConnection? owningObject)
{
- throw new NotImplementedException();
+ ValidateOwnershipAndSetPoolingState(connection, owningObject);
+
+ if (!IsLiveConnection(connection))
+ {
+ RemoveConnection(connection);
+ return;
+ }
+
+ SqlClientEventSource.Log.TryPoolerTraceEvent(
+ " {0}, Connection {1}, Deactivating.",
+ Id,
+ connection.ObjectID);
+ connection.DeactivateConnection();
+
+ if (connection.IsConnectionDoomed ||
+ !connection.CanBePooled ||
+ State == ShuttingDown)
+ {
+ RemoveConnection(connection);
+ }
+ else
+ {
+ var written = _idleConnectionWriter.TryWrite(connection);
+ Debug.Assert(written, "Failed to write returning connection to the idle channel.");
+ }
}
///
@@ -111,9 +199,382 @@ public void TransactionEnded(Transaction transaction, DbConnectionInternal trans
}
///
- public bool TryGetConnection(DbConnection owningObject, TaskCompletionSource taskCompletionSource, DbConnectionOptions userOptions, out DbConnectionInternal connection)
+ public bool TryGetConnection(
+ DbConnection owningObject,
+ TaskCompletionSource taskCompletionSource,
+ DbConnectionOptions userOptions,
+ out DbConnectionInternal? connection)
{
- throw new NotImplementedException();
+ var timeout = TimeSpan.FromSeconds(owningObject.ConnectionTimeout);
+
+ // If taskCompletionSource is null, we are in a sync context.
+ if (taskCompletionSource is null)
+ {
+ var task = GetInternalConnection(
+ owningObject,
+ userOptions,
+ async: false,
+ timeout);
+
+ // When running synchronously, we are guaranteed that the task is already completed.
+ // We don't need to guard the managed threadpool at this spot because we pass the async flag as false
+ // to GetInternalConnection, which means it will not use Task.Run or any async-await logic that would
+ // schedule tasks on the managed threadpool.
+ connection = task.ConfigureAwait(false).GetAwaiter().GetResult();
+ return connection is not null;
+ }
+
+ // Early exit if the task is already completed.
+ if (taskCompletionSource.Task.IsCompleted)
+ {
+ connection = null;
+ return false;
+ }
+
+ // This is ugly, but async anti-patterns above and below us in the stack necessitate a fresh task to be
+ // created. Ideally we would just return the Task from GetInternalConnection and let the caller await
+ // it as needed, but instead we need to signal to the provided TaskCompletionSource when the connection
+ // is established. This pattern has implications for connection open retry logic that are intricate
+ // enough to merit dedicated work. For now, callers that need to open many connections asynchronously
+ // and in parallel *must* pre-prevision threads in the managed thread pool to avoid exhaustion and
+ // timeouts.
+ //
+ // Also note that we don't have access to the cancellation token passed by the caller to the original
+ // OpenAsync call. This means that we cannot cancel the connection open operation if the caller's token
+ // is cancelled. We can only cancel based on our own timeout, which is set to the owningObject's
+ // ConnectionTimeout.
+ Task.Run(async () =>
+ {
+ if (taskCompletionSource.Task.IsCompleted)
+ {
+ return;
+ }
+
+ // We're potentially on a new thread, so we need to properly set the ambient transaction.
+ // We rely on the caller to capture the ambient transaction in the TaskCompletionSource's AsyncState
+ // so that we can access it here. Read: area for improvement.
+ // TODO: ADP.SetCurrentTransaction(taskCompletionSource.Task.AsyncState as Transaction);
+ DbConnectionInternal? connection = null;
+
+ try
+ {
+ connection = await GetInternalConnection(
+ owningObject,
+ userOptions,
+ async: true,
+ timeout
+ ).ConfigureAwait(false);
+
+ if (!taskCompletionSource.TrySetResult(connection))
+ {
+ // We were able to get a connection, but the task was cancelled out from under us.
+ // This can happen if the caller's CancellationToken is cancelled while we're waiting for a connection.
+ // Check the success to avoid an unnecessary exception.
+ ReturnInternalConnection(connection, owningObject);
+ }
+ }
+ catch (Exception e)
+ {
+ if (connection != null)
+ {
+ ReturnInternalConnection(connection, owningObject);
+ }
+
+ // It's possible to fail to set an exception on the TaskCompletionSource if the task is already
+ // completed. In that case, this exception will be swallowed because nobody directly awaits this
+ // task.
+ taskCompletionSource.TrySetException(e);
+ }
+ });
+
+ connection = null;
+ return false;
+ }
+
+ ///
+ /// Opens a new internal connection to the database.
+ ///
+ /// The owning connection.
+ /// The options for the connection.
+ /// Whether to open the connection asynchronously.
+ /// The cancellation token to cancel the operation.
+ /// A task representing the asynchronous operation, with a result of the new internal connection.
+ /// InvalidOperationException - when the newly created connection is invalid or already in the pool.
+ private Task OpenNewInternalConnection(
+ DbConnection? owningConnection,
+ DbConnectionOptions userOptions,
+ bool async,
+ CancellationToken cancellationToken)
+ {
+ // Opening a connection can be a slow operation and we don't want to hold a lock for the duration.
+ // Instead, we reserve a connection slot prior to attempting to open a new connection and release the slot
+ // in case of an exception.
+ if (_connectionSlots.TryReserve())
+ {
+ DbConnectionInternal? newConnection = null;
+ try
+ {
+ var startTime = Stopwatch.GetTimestamp();
+
+ // https://github.com/dotnet/SqlClient/issues/3459
+ // TODO: This blocks the thread for several network calls!
+ // When running async, the blocked thread is one allocated from the managed thread pool (due to
+ // use of Task.Run in TryGetConnection). This is why it's critical for async callers to
+ // pre-provision threads in the managed thread pool. Our options are limited because
+ // DbConnectionInternal doesn't support an async open. It's better to block this thread and keep
+ // throughput high than to queue all of our opens onto a single worker thread. Add an async path
+ // when this support is added to DbConnectionInternal.
+ newConnection = ConnectionFactory.CreatePooledConnection(
+ this,
+ owningConnection,
+ PoolGroup.ConnectionOptions,
+ PoolGroup.PoolKey,
+ userOptions);
+
+ // We don't expect these conditions to happen, but we need to check them to be thorough.
+ if (newConnection == null)
+ {
+ throw ADP.InternalError(ADP.InternalErrorCode.CreateObjectReturnedNull);
+ }
+ if (!newConnection.CanBePooled)
+ {
+ throw ADP.InternalError(ADP.InternalErrorCode.NewObjectCannotBePooled);
+ }
+
+ ValidateOwnershipAndSetPoolingState(newConnection, null);
+
+ _connectionSlots.Add(newConnection);
+
+ return Task.FromResult(newConnection);
+ }
+ catch
+ {
+ // If we failed to open a new connection, we need to reset any state we modified.
+ // Clear the reservation we made, dispose of the connection if it was created, and wake up any waiting
+ // attempts on the idle connection channel.
+ if (newConnection is not null)
+ {
+ RemoveConnection(newConnection);
+ }
+
+ throw;
+ }
+ }
+
+ return Task.FromResult(null);
+ }
+
+ ///
+ /// Checks that the provided connection is live and unexpired and closes it if needed.
+ ///
+ ///
+ /// Returns true if the connection is live and unexpired, otherwise returns false.
+ private bool IsLiveConnection(DbConnectionInternal connection)
+ {
+ if (!connection.IsConnectionAlive())
+ {
+ return false;
+ }
+
+ if (LoadBalanceTimeout != TimeSpan.Zero && DateTime.UtcNow > connection.CreateTime + LoadBalanceTimeout)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Closes the provided connection and removes it from the pool.
+ ///
+ /// The connection to be closed.
+ private void RemoveConnection(DbConnectionInternal connection)
+ {
+ _connectionSlots.TryRemove(connection);
+
+ _connectionSlots.ReleaseReservation();
+ // Closing a connection opens a free spot in the pool.
+ // Write a null to the idle connection channel to wake up a waiter, who can now open a new
+ // connection. Statement order is important since we have synchronous completions on the channel.
+ _idleConnectionWriter.TryWrite(null);
+
+ connection.Dispose();
+ }
+
+ ///
+ /// Tries to read a connection from the idle connection channel.
+ ///
+ /// A connection from the idle channel, or null if the channel is empty.
+ private DbConnectionInternal? GetIdleConnection()
+ {
+ // The channel may contain nulls. Read until we find a non-null connection or exhaust the channel.
+ while (_idleConnectionReader.TryRead(out DbConnectionInternal? connection))
+ {
+ if (connection is null)
+ {
+ continue;
+ }
+
+ if (!IsLiveConnection(connection))
+ {
+ RemoveConnection(connection);
+ continue;
+ }
+
+ return connection;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Gets an internal connection from the pool, either by retrieving an idle connection or opening a new one.
+ ///
+ /// The DbConnection that will own this internal connection
+ /// The user options to set on the internal connection
+ /// A boolean indicating whether the operation should be asynchronous.
+ /// The timeout for the operation.
+ /// Returns a DbConnectionInternal that is retrieved from the pool.
+ private async Task GetInternalConnection(
+ DbConnection owningConnection,
+ DbConnectionOptions userOptions,
+ bool async,
+ TimeSpan timeout)
+ {
+ DbConnectionInternal? connection = null;
+ using CancellationTokenSource cancellationTokenSource = new(timeout);
+ CancellationToken cancellationToken = cancellationTokenSource.Token;
+
+ // Continue looping until we create or retrieve a connection
+ do
+ {
+ try
+ {
+ // Optimistically try to get an idle connection from the channel
+ // Doesn't wait if the channel is empty, just returns null.
+ connection ??= GetIdleConnection();
+
+
+ // If we didn't find an idle connection, try to open a new one.
+ connection ??= await OpenNewInternalConnection(
+ owningConnection,
+ userOptions,
+ async,
+ cancellationToken).ConfigureAwait(false);
+
+ // If we're at max capacity and couldn't open a connection. Block on the idle channel with a
+ // timeout. Note that Channels guarantee fair FIFO behavior to callers of ReadAsync
+ // (first-come, first-served), which is crucial to us.
+ if (async)
+ {
+ connection ??= await _idleConnectionReader.ReadAsync(cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ connection ??= ReadChannelSyncOverAsync(cancellationToken);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ throw ADP.PooledOpenTimeout();
+ }
+ catch (ChannelClosedException)
+ {
+ //TODO: exceptions from resource file
+ throw new Exception("The connection pool has been shut down.");
+ }
+
+ if (connection is not null && !IsLiveConnection(connection))
+ {
+ // If the connection is not live, we need to remove it from the pool and try again.
+ RemoveConnection(connection);
+ connection = null;
+ }
+ }
+ while (connection is null);
+
+ PrepareConnection(owningConnection, connection);
+ return connection;
+ }
+
+ ///
+ /// Performs a blocking synchronous read from the idle connection channel.
+ ///
+ /// Cancels the read operation.
+ /// The connection read from the channel.
+ private DbConnectionInternal? ReadChannelSyncOverAsync(CancellationToken cancellationToken)
+ {
+ // If there are no connections in the channel, then ReadAsync will block until one is available.
+ // Channels doesn't offer a sync API, so running ReadAsync synchronously on this thread may spawn
+ // additional new async work items in the managed thread pool if there are no items available in the
+ // channel. We need to ensure that we don't block all available managed threads with these child
+ // tasks or we could deadlock. Prefer to block the current user-owned thread, and limit throughput
+ // to the managed threadpool.
+
+ _syncOverAsyncSemaphore.Wait(cancellationToken);
+ try
+ {
+ ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter =
+ _idleConnectionReader.ReadAsync(cancellationToken).ConfigureAwait(false).GetAwaiter();
+ using ManualResetEventSlim mres = new ManualResetEventSlim(false, 0);
+
+ // Cancellation happens through the ReadAsync call, which will complete the task.
+ // Even a failed task will complete and set the ManualResetEventSlim.
+ awaiter.UnsafeOnCompleted(() => mres.Set());
+ mres.Wait(CancellationToken.None);
+ return awaiter.GetResult();
+ }
+ finally
+ {
+ _syncOverAsyncSemaphore.Release();
+ }
+ }
+
+ ///
+ /// Sets connection state and activates the connection for use. Should always be called after a connection is
+ /// created or retrieved from the pool.
+ ///
+ /// The owning DbConnection instance.
+ /// The DbConnectionInternal to be activated.
+ private void PrepareConnection(DbConnection owningObject, DbConnectionInternal connection)
+ {
+ lock (connection)
+ {
+ // Protect against Clear which calls IsEmancipated, which is affected by PrePush and PostPop
+ connection.PostPop(owningObject);
+ }
+
+ try
+ {
+ //TODO: pass through transaction
+ connection.ActivateConnection(null);
+ }
+ catch
+ {
+ // At this point, the connection is "out of the pool" (the call to postpop). If we hit a transient
+ // error anywhere along the way when enlisting the connection in the transaction, we need to get
+ // the connection back into the pool so that it isn't leaked.
+ ReturnInternalConnection(connection, owningObject);
+ throw;
+ }
+ }
+
+ ///
+ /// Validates that the connection is owned by the provided DbConnection and that it is in a valid state to be returned to the pool.
+ ///
+ /// The owning DbConnection instance.
+ /// The DbConnectionInternal to be validated.
+ private void ValidateOwnershipAndSetPoolingState(DbConnectionInternal connection, DbConnection? owningObject)
+ {
+ lock (connection)
+ {
+ // Calling PrePush prevents the object from being reclaimed
+ // once we leave the lock, because it sets _pooledCount such
+ // that it won't appear to be out of the pool. What that
+ // means, is that we're now responsible for this connection:
+ // it won't get reclaimed if it gets lost.
+ connection.PrePush(owningObject);
+ }
}
#endregion
}
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/ConnectionPoolSlots.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/ConnectionPoolSlots.cs
new file mode 100644
index 0000000000..24b3389c68
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/ConnectionPoolSlots.cs
@@ -0,0 +1,139 @@
+// 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.Diagnostics;
+using System.Threading;
+using Microsoft.Data.ProviderBase;
+
+#nullable enable
+
+namespace Microsoft.Data.SqlClient.ConnectionPool
+{
+ ///
+ /// A thread-safe collection with a fixed capacity that allows reservations.
+ /// A reservation *must* be made before adding a connection to the collection.
+ /// Exceptions *must* be handled by the caller when trying to add connections
+ /// and the caller *must* release the reservation.
+ ///
+ ///
+ ///
+ /// ConnectionPoolSlots slots = new ConnectionPoolSlots(100);
+ ///
+ /// if (slots.TryReserve())
+ /// {
+ /// try {
+ /// var connection = OpenConnection();
+ /// slots.Add(connection);
+ /// }
+ /// catch (InvalidOperationException ex)
+ /// {
+ /// slots.ReleaseReservation();
+ /// throw;
+ /// }
+ /// }
+ ///
+ /// if (slots.TryRemove())
+ /// {
+ /// slots.ReleaseReservation();
+ /// }
+ ///
+ ///
+ ///
+ internal sealed class ConnectionPoolSlots
+ {
+ private readonly DbConnectionInternal?[] _connections;
+ private readonly uint _capacity;
+ private volatile int _reservations;
+
+ ///
+ /// Constructs a ConnectionPoolSlots instance with the given fixed capacity.
+ ///
+ /// The fixed capacity of the collection.
+ internal ConnectionPoolSlots(uint fixedCapacity)
+ {
+ _capacity = fixedCapacity;
+ _reservations = 0;
+ _connections = new DbConnectionInternal?[fixedCapacity];
+ }
+
+ ///
+ /// Gets the total number of reservations.
+ ///
+ internal int ReservationCount => _reservations;
+
+ ///
+ /// Adds a connection to the collection. Can only be called after a reservation has been made.
+ ///
+ /// The connection to add to the collection.
+ ///
+ /// Thrown when unable to find an empty slot.
+ /// This can occur if a reservation is not taken before adding a connection.
+ ///
+ internal void Add(DbConnectionInternal connection)
+ {
+ int i;
+ for (i = 0; i < _capacity; i++)
+ {
+ if (Interlocked.CompareExchange(ref _connections[i], connection, null) == null)
+ {
+ return;
+ }
+ }
+
+ throw new InvalidOperationException("Couldn't find an empty slot.");
+ }
+
+ ///
+ /// Releases a reservation that was previously obtained.
+ /// Must be called after removing a connection from the collection or if an exception occurs.
+ ///
+ internal void ReleaseReservation()
+ {
+ Interlocked.Decrement(ref _reservations);
+ Debug.Assert(_reservations >= 0, "Released a reservation that wasn't held");
+ }
+
+ ///
+ /// Removes a connection from the collection.
+ ///
+ /// The connection to remove from the collection.
+ /// True if the connection was found and removed; otherwise, false.
+ internal bool TryRemove(DbConnectionInternal connection)
+ {
+ for (int i = 0; i < _connections.Length; i++)
+ {
+ if (Interlocked.CompareExchange(ref _connections[i], null, connection) == connection)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Attempts to reserve a spot in the collection.
+ ///
+ /// True if a reservation was successfully obtained.
+ internal bool TryReserve()
+ {
+ for (var expected = _reservations; expected < _capacity; expected = _reservations)
+ {
+ // Try to reserve a spot in the collection by incrementing _reservations.
+ // If _reservations changed underneath us, then another thread already reserved the spot we were trying to take.
+ // Cycle back through the check above to reset expected and to make sure we don't go
+ // over capacity.
+ // Note that we purposefully don't use SpinWait for this: https://github.com/dotnet/coreclr/pull/21437
+ if (Interlocked.CompareExchange(ref _reservations, expected + 1, expected) != expected)
+ {
+ continue;
+ }
+
+ return true;
+ }
+ return false;
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolState.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolState.cs
index 1790e38a57..ca4353f2c6 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolState.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolState.cs
@@ -6,7 +6,6 @@ namespace Microsoft.Data.SqlClient.ConnectionPool
{
internal enum DbConnectionPoolState
{
- Initializing,
Running,
ShuttingDown,
}
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..b6bf23ffc5 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
@@ -10,6 +10,8 @@
using Microsoft.Data.Common.ConnectionString;
using Microsoft.Data.ProviderBase;
+#nullable enable
+
namespace Microsoft.Data.SqlClient.ConnectionPool
{
///
@@ -81,7 +83,7 @@ internal interface IDbConnectionPool
///
/// The current state of the connection pool.
///
- DbConnectionPoolState State { get; set; }
+ DbConnectionPoolState State { get; }
///
/// Indicates whether the connection pool is using load balancing.
@@ -104,7 +106,7 @@ internal interface IDbConnectionPool
/// The user options to use if a new connection must be opened.
/// The retrieved connection will be passed out via this parameter.
/// True if a connection was set in the out parameter, otherwise returns false.
- bool TryGetConnection(DbConnection owningObject, TaskCompletionSource taskCompletionSource, DbConnectionOptions userOptions, out DbConnectionInternal connection);
+ bool TryGetConnection(DbConnection owningObject, TaskCompletionSource taskCompletionSource, DbConnectionOptions userOptions, out DbConnectionInternal? connection);
///
/// Replaces the internal connection currently associated with owningObject with a new internal connection from the pool.
@@ -120,7 +122,7 @@ internal interface IDbConnectionPool
///
/// The internal connection to return to the pool.
/// The connection that currently owns this internal connection. Used to verify ownership.
- void ReturnInternalConnection(DbConnectionInternal obj, object owningObject);
+ void ReturnInternalConnection(DbConnectionInternal obj, DbConnection owningObject);
///
/// Puts an internal connection from a transacted pool back into the general 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..453696fb67 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
@@ -451,8 +451,6 @@ internal WaitHandleDbConnectionPool(
throw ADP.InternalError(ADP.InternalErrorCode.AttemptingToPoolOnRestrictedToken);
}
- State = Initializing;
-
lock (s_random)
{
// Random.Next is not thread-safe
@@ -854,10 +852,6 @@ private void DeactivateObject(DbConnectionInternal obj)
}
else
{
- // NOTE: constructor should ensure that current state cannot be State.Initializing, so it can only
- // be State.Running or State.ShuttingDown
- Debug.Assert(State is Running or ShuttingDown);
-
lock (obj)
{
// A connection with a delegated transaction cannot currently
@@ -1661,7 +1655,7 @@ private void PutNewObject(DbConnectionInternal obj)
}
- public void ReturnInternalConnection(DbConnectionInternal obj, object owningObject)
+ public void ReturnInternalConnection(DbConnectionInternal obj, DbConnection owningObject)
{
Debug.Assert(obj != null, "null obj?");
diff --git a/src/Microsoft.Data.SqlClient/src/System/Runtime/CompilerServices/IsExternalInit.netfx.cs b/src/Microsoft.Data.SqlClient/src/System/Runtime/CompilerServices/IsExternalInit.netfx.cs
index 0d0181ba6d..530d1b4096 100644
--- a/src/Microsoft.Data.SqlClient/src/System/Runtime/CompilerServices/IsExternalInit.netfx.cs
+++ b/src/Microsoft.Data.SqlClient/src/System/Runtime/CompilerServices/IsExternalInit.netfx.cs
@@ -6,7 +6,7 @@
using System.ComponentModel;
-
+// The `init` accessor was introduced in C# 9.0 and is not natively supported in .NET Framework.
// This class enables the use of the `init` property accessor in .NET framework.
namespace System.Runtime.CompilerServices
{
diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPoolTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPoolTest.cs
index 2dcfe476fe..6e3e328aaa 100644
--- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPoolTest.cs
+++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPoolTest.cs
@@ -3,9 +3,15 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Collections.Concurrent;
+using System.Collections.Specialized;
using System.Data.Common;
+using System.Threading;
using System.Threading.Tasks;
using System.Transactions;
+using Microsoft.Data.Common;
+using Microsoft.Data.Common.ConnectionString;
+using Microsoft.Data.ProviderBase;
using Microsoft.Data.SqlClient.ConnectionPool;
using Xunit;
@@ -13,143 +19,889 @@ namespace Microsoft.Data.SqlClient.UnitTests
{
public class ChannelDbConnectionPoolTest
{
- private readonly ChannelDbConnectionPool _pool;
+ private ChannelDbConnectionPool pool;
+ private DbConnectionFactory connectionFactory;
+ private DbConnectionPoolGroup dbConnectionPoolGroup;
+ private DbConnectionPoolGroupOptions poolGroupOptions;
+ private DbConnectionPoolIdentity identity;
+ private DbConnectionPoolProviderInfo connectionPoolProviderInfo;
- public ChannelDbConnectionPoolTest()
+ private static readonly DbConnectionFactory SuccessfulConnectionFactory = new SuccessfulDbConnectionFactory();
+ private static readonly DbConnectionFactory TimeoutConnectionFactory = new TimeoutDbConnectionFactory();
+
+ private void Setup(DbConnectionFactory connectionFactory)
+ {
+ this.connectionFactory = connectionFactory;
+ identity = DbConnectionPoolIdentity.NoIdentity;
+ connectionPoolProviderInfo = new DbConnectionPoolProviderInfo();
+ poolGroupOptions = new DbConnectionPoolGroupOptions(
+ poolByIdentity: false,
+ minPoolSize: 0,
+ maxPoolSize: 50,
+ creationTimeout: 15,
+ loadBalanceTimeout: 0,
+ hasTransactionAffinity: true
+ );
+ dbConnectionPoolGroup = new DbConnectionPoolGroup(
+ new DbConnectionOptions("DataSource=localhost;", null),
+ new DbConnectionPoolKey("TestDataSource"),
+ poolGroupOptions
+ );
+ pool = new ChannelDbConnectionPool(
+ connectionFactory,
+ dbConnectionPoolGroup,
+ identity,
+ connectionPoolProviderInfo
+ );
+ }
+
+ [Theory]
+ [InlineData(1)]
+ [InlineData(5)]
+ [InlineData(10)]
+ public void GetConnectionEmptyPool_ShouldCreateNewConnection(int numConnections)
+ {
+ // Arrange
+ Setup(SuccessfulConnectionFactory);
+
+ // Act
+ for (int i = 0; i < numConnections; i++)
+ {
+ DbConnectionInternal internalConnection = null;
+ var completed = pool.TryGetConnection(
+ new SqlConnection(),
+ taskCompletionSource: null,
+ new DbConnectionOptions("", null),
+ out internalConnection
+ );
+
+ // Assert
+ Assert.True(completed);
+ Assert.NotNull(internalConnection);
+ }
+
+
+ // Assert
+ Assert.Equal(numConnections, pool.Count);
+ }
+
+ [Theory]
+ [InlineData(1)]
+ [InlineData(5)]
+ [InlineData(10)]
+ public async Task GetConnectionAsyncEmptyPool_ShouldCreateNewConnection(int numConnections)
+ {
+ // Arrange
+ Setup(SuccessfulConnectionFactory);
+
+ // Act
+ for (int i = 0; i < numConnections; i++)
+ {
+ var tcs = new TaskCompletionSource();
+ DbConnectionInternal internalConnection = null;
+ var completed = pool.TryGetConnection(
+ new SqlConnection(),
+ tcs,
+ new DbConnectionOptions("", null),
+ out internalConnection
+ );
+
+ // Assert
+ Assert.False(completed);
+ Assert.Null(internalConnection);
+ Assert.NotNull(await tcs.Task);
+ }
+
+
+ // Assert
+ Assert.Equal(numConnections, pool.Count);
+ }
+
+ [Fact]
+ public void GetConnectionMaxPoolSize_ShouldTimeoutAfterPeriod()
+ {
+ // Arrange
+ Setup(SuccessfulConnectionFactory);
+
+ for (int i = 0; i < poolGroupOptions.MaxPoolSize; i++)
+ {
+ DbConnectionInternal internalConnection = null;
+ var completed = pool.TryGetConnection(
+ new SqlConnection(),
+ taskCompletionSource: null,
+ new DbConnectionOptions("", null),
+ out internalConnection
+ );
+
+ Assert.True(completed);
+ Assert.NotNull(internalConnection);
+ }
+
+ try
+ {
+ // Act
+ DbConnectionInternal extraConnection = null;
+ var exceeded = pool.TryGetConnection(
+ new SqlConnection("Timeout=1"),
+ taskCompletionSource: null,
+ new DbConnectionOptions("", null),
+ out extraConnection
+ );
+ }
+ catch (Exception ex)
+ {
+ // Assert
+ Assert.IsType(ex);
+ Assert.Equal("Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.", ex.Message);
+ }
+
+ // Assert
+ Assert.Equal(poolGroupOptions.MaxPoolSize, pool.Count);
+ }
+
+ [Fact]
+ public async Task GetConnectionAsyncMaxPoolSize_ShouldTimeoutAfterPeriod()
+ {
+ // Arrange
+ Setup(SuccessfulConnectionFactory);
+
+ for (int i = 0; i < poolGroupOptions.MaxPoolSize; i++)
+ {
+ DbConnectionInternal internalConnection = null;
+ var completed = pool.TryGetConnection(
+ new SqlConnection(),
+ taskCompletionSource: null,
+ new DbConnectionOptions("", null),
+ out internalConnection
+ );
+
+ Assert.True(completed);
+ Assert.NotNull(internalConnection);
+ }
+
+ try
+ {
+ // Act
+ TaskCompletionSource taskCompletionSource = new TaskCompletionSource();
+ DbConnectionInternal extraConnection = null;
+ var exceeded = pool.TryGetConnection(
+ new SqlConnection("Timeout=1"),
+ taskCompletionSource,
+ new DbConnectionOptions("", null),
+ out extraConnection
+ );
+ await taskCompletionSource.Task;
+ }
+ catch (Exception ex)
+ {
+ // Assert
+ Assert.IsType(ex);
+ Assert.Equal("Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.", ex.Message);
+ }
+
+ // Assert
+ Assert.Equal(poolGroupOptions.MaxPoolSize, pool.Count);
+ }
+
+ [Fact]
+ public async Task GetConnectionMaxPoolSize_ShouldReuseAfterConnectionReleased()
+ {
+ // Arrange
+ Setup(SuccessfulConnectionFactory);
+ DbConnectionInternal firstConnection = null;
+ SqlConnection firstOwningConnection = new SqlConnection();
+
+ pool.TryGetConnection(
+ firstOwningConnection,
+ taskCompletionSource: null,
+ new DbConnectionOptions("", null),
+ out firstConnection
+ );
+
+ for (int i = 1; i < poolGroupOptions.MaxPoolSize; i++)
+ {
+ DbConnectionInternal internalConnection = null;
+ var completed = pool.TryGetConnection(
+ new SqlConnection(),
+ taskCompletionSource: null,
+ new DbConnectionOptions("", null),
+ out internalConnection
+ );
+
+ Assert.True(completed);
+ Assert.NotNull(internalConnection);
+ }
+
+ TaskCompletionSource tcs = new TaskCompletionSource();
+
+ // Act
+ var task = Task.Run(() =>
+ {
+ DbConnectionInternal extraConnection = null;
+ var exceeded = pool.TryGetConnection(
+ new SqlConnection(""),
+ taskCompletionSource: null,
+ new DbConnectionOptions("", null),
+ out extraConnection
+ );
+ return extraConnection;
+ });
+ pool.ReturnInternalConnection(firstConnection, firstOwningConnection);
+ var extraConnection = await task;
+
+ // Assert
+ Assert.Equal(firstConnection, extraConnection);
+ }
+
+ [Fact]
+ public async Task GetConnectionAsyncMaxPoolSize_ShouldReuseAfterConnectionReleased()
+ {
+ // Arrange
+ Setup(SuccessfulConnectionFactory);
+ DbConnectionInternal firstConnection = null;
+ SqlConnection firstOwningConnection = new SqlConnection();
+
+ pool.TryGetConnection(
+ firstOwningConnection,
+ taskCompletionSource: null,
+ new DbConnectionOptions("", null),
+ out firstConnection
+ );
+
+ for (int i = 1; i < poolGroupOptions.MaxPoolSize; i++)
+ {
+ DbConnectionInternal internalConnection = null;
+ var completed = pool.TryGetConnection(
+ new SqlConnection(),
+ taskCompletionSource: null,
+ new DbConnectionOptions("", null),
+ out internalConnection
+ );
+
+ Assert.True(completed);
+ Assert.NotNull(internalConnection);
+ }
+
+ TaskCompletionSource taskCompletionSource = new TaskCompletionSource();
+
+ // Act
+ DbConnectionInternal recycledConnection = null;
+ var exceeded = pool.TryGetConnection(
+ new SqlConnection(""),
+ taskCompletionSource,
+ new DbConnectionOptions("", null),
+ out recycledConnection
+ );
+ pool.ReturnInternalConnection(firstConnection, firstOwningConnection);
+ recycledConnection = await taskCompletionSource.Task;
+
+ // Assert
+ Assert.Equal(firstConnection, recycledConnection);
+ }
+
+ [Fact]
+ public async Task GetConnectionMaxPoolSize_ShouldRespectOrderOfRequest()
+ {
+ // Arrange
+ Setup(SuccessfulConnectionFactory);
+ DbConnectionInternal firstConnection = null;
+ SqlConnection firstOwningConnection = new SqlConnection();
+
+ pool.TryGetConnection(
+ firstOwningConnection,
+ taskCompletionSource: null,
+ new DbConnectionOptions("", null),
+ out firstConnection
+ );
+
+ for (int i = 1; i < poolGroupOptions.MaxPoolSize; i++)
+ {
+ DbConnectionInternal internalConnection = null;
+ var completed = pool.TryGetConnection(
+ new SqlConnection(),
+ taskCompletionSource: null,
+ new DbConnectionOptions("", null),
+ out internalConnection
+ );
+
+ Assert.True(completed);
+ Assert.NotNull(internalConnection);
+ }
+
+ // Use ManualResetEventSlim to synchronize the tasks
+ // and force the request queueing order.
+ using ManualResetEventSlim mresQueueOrder = new ManualResetEventSlim();
+ using CountdownEvent allRequestsQueued = new CountdownEvent(2);
+
+ // Act
+ var recycledTask = Task.Run(() =>
+ {
+ DbConnectionInternal recycledConnection = null;
+ mresQueueOrder.Set();
+ allRequestsQueued.Signal();
+ pool.TryGetConnection(
+ new SqlConnection(""),
+ null,
+ new DbConnectionOptions("", null),
+ out recycledConnection
+ );
+ return recycledConnection;
+ });
+ var failedTask = Task.Run(() =>
+ {
+ DbConnectionInternal failedConnection = null;
+ // Force this request to be second in the queue.
+ mresQueueOrder.Wait();
+ allRequestsQueued.Signal();
+ pool.TryGetConnection(
+ new SqlConnection("Timeout=1"),
+ null,
+ new DbConnectionOptions("", null),
+ out failedConnection
+ );
+ return failedConnection;
+ });
+
+ allRequestsQueued.Wait();
+ pool.ReturnInternalConnection(firstConnection, firstOwningConnection);
+ var recycledConnection = await recycledTask;
+
+ // Assert
+ Assert.Equal(firstConnection, recycledConnection);
+ await Assert.ThrowsAsync(async () => await failedTask);
+ }
+
+ [Fact]
+ public async Task GetConnectionAsyncMaxPoolSize_ShouldRespectOrderOfRequest()
+ {
+ // Arrange
+ Setup(SuccessfulConnectionFactory);
+ DbConnectionInternal firstConnection = null;
+ SqlConnection firstOwningConnection = new SqlConnection();
+
+ pool.TryGetConnection(
+ firstOwningConnection,
+ taskCompletionSource: null,
+ new DbConnectionOptions("", null),
+ out firstConnection
+ );
+
+ for (int i = 1; i < poolGroupOptions.MaxPoolSize; i++)
+ {
+ DbConnectionInternal internalConnection = null;
+ var completed = pool.TryGetConnection(
+ new SqlConnection(),
+ taskCompletionSource: null,
+ new DbConnectionOptions("", null),
+ out internalConnection
+ );
+
+ Assert.True(completed);
+ Assert.NotNull(internalConnection);
+ }
+
+ TaskCompletionSource recycledTaskCompletionSource = new TaskCompletionSource();
+ TaskCompletionSource failedCompletionSource = new TaskCompletionSource();
+
+ // Act
+ DbConnectionInternal recycledConnection = null;
+ var exceeded = pool.TryGetConnection(
+ new SqlConnection(""),
+ recycledTaskCompletionSource,
+ new DbConnectionOptions("", null),
+ out recycledConnection
+ );
+ DbConnectionInternal failedConnection = null;
+ var exceeded2 = pool.TryGetConnection(
+ new SqlConnection("Timeout=1"),
+ failedCompletionSource,
+ new DbConnectionOptions("", null),
+ out failedConnection
+ );
+
+ pool.ReturnInternalConnection(firstConnection, firstOwningConnection);
+ recycledConnection = await recycledTaskCompletionSource.Task;
+
+ // Assert
+ Assert.Equal(firstConnection, recycledConnection);
+ await Assert.ThrowsAsync(async () => failedConnection = await failedCompletionSource.Task);
+ }
+
+ [Fact]
+ public void ConnectionsAreReused()
+ {
+ // Arrange
+ Setup(SuccessfulConnectionFactory);
+ SqlConnection owningConnection = new SqlConnection();
+ DbConnectionInternal internalConnection1 = null;
+ DbConnectionInternal internalConnection2 = null;
+
+ // Act: Get the first connection
+ var completed1 = pool.TryGetConnection(
+ owningConnection,
+ null,
+ new DbConnectionOptions("", null),
+ out internalConnection1
+ );
+
+ // Assert: First connection should succeed
+ Assert.True(completed1);
+ Assert.NotNull(internalConnection1);
+
+ // Act: Return the first connection to the pool
+ pool.ReturnInternalConnection(internalConnection1, owningConnection);
+
+ // Act: Get the second connection (should reuse the first one)
+ var completed2 = pool.TryGetConnection(
+ owningConnection,
+ null,
+ new DbConnectionOptions("", null),
+ out internalConnection2
+ );
+
+ // Assert: Second connection should succeed and reuse the first connection
+ Assert.True(completed2);
+ Assert.NotNull(internalConnection2);
+ Assert.Same(internalConnection1, internalConnection2);
+ }
+
+ [Fact]
+ public void GetConnectionTimeout_ShouldThrowTimeoutException()
+ {
+ // Arrange
+ Setup(TimeoutConnectionFactory);
+ DbConnectionInternal internalConnection = null;
+
+ // Act & Assert
+ var ex = Assert.Throws(() =>
+ {
+ var completed = pool.TryGetConnection(
+ new SqlConnection(),
+ taskCompletionSource: null,
+ new DbConnectionOptions("", null),
+ out internalConnection
+ );
+ });
+
+ Assert.Equal("Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.", ex.Message);
+ }
+
+ [Fact]
+ public async Task GetConnectionAsyncTimeout_ShouldThrowTimeoutException()
+ {
+ // Arrange
+ Setup(TimeoutConnectionFactory);
+ DbConnectionInternal internalConnection = null;
+ TaskCompletionSource taskCompletionSource = new TaskCompletionSource();
+
+ // Act & Assert
+ var ex = await Assert.ThrowsAsync(async () =>
+ {
+ var completed = pool.TryGetConnection(
+ new SqlConnection(),
+ taskCompletionSource,
+ new DbConnectionOptions("", null),
+ out internalConnection
+ );
+
+ await taskCompletionSource.Task;
+ });
+
+ Assert.Equal("Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.", ex.Message);
+ }
+
+ [Fact]
+ public void StressTest()
{
- _pool = new ChannelDbConnectionPool();
+ //Arrange
+ Setup(SuccessfulConnectionFactory);
+ ConcurrentBag tasks = new ConcurrentBag();
+
+
+ for (int i = 1; i < poolGroupOptions.MaxPoolSize * 3; i++)
+ {
+ var t = Task.Run(() =>
+ {
+ DbConnectionInternal internalConnection = null;
+ SqlConnection owningObject = new SqlConnection();
+ var completed = pool.TryGetConnection(
+ owningObject,
+ taskCompletionSource: null,
+ new DbConnectionOptions("", null),
+ out internalConnection
+ );
+ if (completed)
+ {
+ pool.ReturnInternalConnection(internalConnection, owningObject);
+ }
+
+ Assert.True(completed);
+ Assert.NotNull(internalConnection);
+ });
+ tasks.Add(t);
+ }
+
+ Task.WaitAll(tasks.ToArray());
+ Assert.True(pool.Count <= poolGroupOptions.MaxPoolSize, "Pool size exceeded max pool size after stress test.");
}
[Fact]
- public void TestAuthenticationContexts()
+ public void StressTestAsync()
{
- Assert.Throws(() => _ = _pool.AuthenticationContexts);
+ //Arrange
+ Setup(SuccessfulConnectionFactory);
+ ConcurrentBag tasks = new ConcurrentBag();
+
+
+ for (int i = 1; i < poolGroupOptions.MaxPoolSize * 3; i++)
+ {
+ var t = Task.Run(async () =>
+ {
+ DbConnectionInternal internalConnection = null;
+ SqlConnection owningObject = new SqlConnection();
+ TaskCompletionSource taskCompletionSource = new TaskCompletionSource();
+ var completed = pool.TryGetConnection(
+ owningObject,
+ taskCompletionSource,
+ new DbConnectionOptions("", null),
+ out internalConnection
+ );
+ internalConnection = await taskCompletionSource.Task;
+ pool.ReturnInternalConnection(internalConnection, owningObject);
+
+ Assert.NotNull(internalConnection);
+ });
+ tasks.Add(t);
+ }
+
+ Task.WaitAll(tasks.ToArray());
+ Assert.True(pool.Count <= poolGroupOptions.MaxPoolSize, "Pool size exceeded max pool size after stress test.");
}
+
+ #region Property Tests
+
[Fact]
public void TestConnectionFactory()
{
- Assert.Throws(() => _ = _pool.ConnectionFactory);
+ Setup(SuccessfulConnectionFactory);
+ Assert.Equal(connectionFactory, pool.ConnectionFactory);
}
[Fact]
public void TestCount()
{
- Assert.Throws(() => _ = _pool.Count);
+ Setup(SuccessfulConnectionFactory);
+ Assert.Equal(0, pool.Count);
}
[Fact]
public void TestErrorOccurred()
{
- Assert.Throws(() => _ = _pool.ErrorOccurred);
+ Setup(SuccessfulConnectionFactory);
+ Assert.Throws(() => _ = pool.ErrorOccurred);
}
[Fact]
public void TestId()
{
- Assert.Throws(() => _ = _pool.Id);
+ Setup(SuccessfulConnectionFactory);
+ Assert.True(pool.Id >= 1);
}
[Fact]
public void TestIdentity()
{
- Assert.Throws(() => _ = _pool.Identity);
+ Setup(SuccessfulConnectionFactory);
+ Assert.Equal(identity, pool.Identity);
}
[Fact]
public void TestIsRunning()
{
- Assert.Throws(() => _ = _pool.IsRunning);
+ Setup(SuccessfulConnectionFactory);
+ Assert.True(pool.IsRunning);
}
[Fact]
public void TestLoadBalanceTimeout()
{
- Assert.Throws(() => _ = _pool.LoadBalanceTimeout);
+ Setup(SuccessfulConnectionFactory);
+ Assert.Equal(poolGroupOptions.LoadBalanceTimeout, pool.LoadBalanceTimeout);
}
[Fact]
public void TestPoolGroup()
{
- Assert.Throws(() => _ = _pool.PoolGroup);
+ Setup(SuccessfulConnectionFactory);
+ Assert.Equal(dbConnectionPoolGroup, pool.PoolGroup);
}
[Fact]
public void TestPoolGroupOptions()
{
- Assert.Throws(() => _ = _pool.PoolGroupOptions);
+ Setup(SuccessfulConnectionFactory);
+ Assert.Equal(poolGroupOptions, pool.PoolGroupOptions);
}
[Fact]
public void TestProviderInfo()
{
- Assert.Throws(() => _ = _pool.ProviderInfo);
+ Setup(SuccessfulConnectionFactory);
+ Assert.Equal(connectionPoolProviderInfo, pool.ProviderInfo);
}
[Fact]
public void TestStateGetter()
{
- Assert.Throws(() => _ = _pool.State);
+ Setup(SuccessfulConnectionFactory);
+ Assert.Equal(DbConnectionPoolState.Running, pool.State);
}
[Fact]
public void TestStateSetter()
{
- Assert.Throws(() => _pool.State = DbConnectionPoolState.Running);
+ Setup(SuccessfulConnectionFactory);
+ Assert.Equal(DbConnectionPoolState.Running, pool.State);
}
[Fact]
public void TestUseLoadBalancing()
{
- Assert.Throws(() => _ = _pool.UseLoadBalancing);
+ Setup(SuccessfulConnectionFactory);
+ Assert.Equal(poolGroupOptions.UseLoadBalancing, pool.UseLoadBalancing);
}
+ #endregion
+
+ #region Not Implemented Method Tests
+
[Fact]
public void TestClear()
{
- Assert.Throws(() => _pool.Clear());
+ Setup(SuccessfulConnectionFactory);
+ Assert.Throws(() => pool.Clear());
}
[Fact]
public void TestPutObjectFromTransactedPool()
{
- Assert.Throws(() => _pool.PutObjectFromTransactedPool(null!));
+ Setup(SuccessfulConnectionFactory);
+ Assert.Throws(() => pool.PutObjectFromTransactedPool(null!));
}
[Fact]
public void TestReplaceConnection()
{
- Assert.Throws(() => _pool.ReplaceConnection(null!, null!, null!));
- }
-
- [Fact]
- public void TestReturnInternalConnection()
- {
- Assert.Throws(() => _pool.ReturnInternalConnection(null!, null!));
+ Setup(SuccessfulConnectionFactory);
+ Assert.Throws(() => pool.ReplaceConnection(null!, null!, null!));
}
[Fact]
public void TestShutdown()
{
- Assert.Throws(() => _pool.Shutdown());
+ Setup(SuccessfulConnectionFactory);
+ Assert.Throws(() => pool.Shutdown());
}
[Fact]
public void TestStartup()
{
- Assert.Throws(() => _pool.Startup());
+ Setup(SuccessfulConnectionFactory);
+ Assert.Throws(() => pool.Startup());
}
[Fact]
public void TestTransactionEnded()
{
- Assert.Throws(() => _pool.TransactionEnded(null!, null!));
+ Setup(SuccessfulConnectionFactory);
+ Assert.Throws(() => pool.TransactionEnded(null!, null!));
}
+ #endregion
- [Fact]
- public void TestTryGetConnection()
+ #region Test classes
+ internal class SuccessfulDbConnectionFactory : DbConnectionFactory
+ {
+ protected override DbConnectionInternal CreateConnection(
+ DbConnectionOptions options,
+ DbConnectionPoolKey poolKey,
+ DbConnectionPoolGroupProviderInfo poolGroupProviderInfo,
+ IDbConnectionPool pool,
+ DbConnection owningConnection,
+ DbConnectionOptions userOptions)
+ {
+ return new StubDbConnectionInternal();
+ }
+
+ #region Not Implemented Members
+ public override DbProviderFactory ProviderFactory => throw new NotImplementedException();
+
+ protected override DbConnectionOptions CreateConnectionOptions(string connectionString, DbConnectionOptions previous)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override DbConnectionPoolGroupOptions CreateConnectionPoolGroupOptions(DbConnectionOptions options)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override int GetObjectId(DbConnection connection)
+ {
+ throw new NotImplementedException();
+ }
+
+ internal override DbConnectionPoolGroup GetConnectionPoolGroup(DbConnection connection)
+ {
+ throw new NotImplementedException();
+ }
+
+ internal override DbConnectionInternal GetInnerConnection(DbConnection connection)
+ {
+ throw new NotImplementedException();
+ }
+
+ internal override void PermissionDemand(DbConnection outerConnection)
+ {
+ throw new NotImplementedException();
+ }
+
+ internal override void SetConnectionPoolGroup(DbConnection outerConnection, DbConnectionPoolGroup poolGroup)
+ {
+ throw new NotImplementedException();
+ }
+
+ internal override void SetInnerConnectionEvent(DbConnection owningObject, DbConnectionInternal to)
+ {
+ throw new NotImplementedException();
+ }
+
+ internal override bool SetInnerConnectionFrom(DbConnection owningObject, DbConnectionInternal to, DbConnectionInternal from)
+ {
+ throw new NotImplementedException();
+ }
+
+ internal override void SetInnerConnectionTo(DbConnection owningObject, DbConnectionInternal to)
+ {
+ throw new NotImplementedException();
+ }
+
+ internal override DbConnectionPoolProviderInfo CreateConnectionPoolProviderInfo(DbConnectionOptions connectionOptions)
+ {
+ throw new NotImplementedException();
+ }
+
+ internal override DbConnectionPoolGroupProviderInfo CreateConnectionPoolGroupProviderInfo(DbConnectionOptions connectionOptions)
+ {
+ throw new NotImplementedException();
+ }
+ #endregion
+ }
+
+ internal class TimeoutDbConnectionFactory : DbConnectionFactory
+ {
+ protected override DbConnectionInternal CreateConnection(
+ DbConnectionOptions options,
+ DbConnectionPoolKey poolKey,
+ DbConnectionPoolGroupProviderInfo poolGroupProviderInfo,
+ IDbConnectionPool pool,
+ DbConnection owningConnection,
+ DbConnectionOptions userOptions)
+ {
+ throw ADP.PooledOpenTimeout();
+ }
+
+ #region Not Implemented Members
+ public override DbProviderFactory ProviderFactory => throw new NotImplementedException();
+
+ protected override DbConnectionOptions CreateConnectionOptions(string connectionString, DbConnectionOptions previous)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override DbConnectionPoolGroupOptions CreateConnectionPoolGroupOptions(DbConnectionOptions options)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override int GetObjectId(DbConnection connection)
+ {
+ throw new NotImplementedException();
+ }
+
+ internal override DbConnectionPoolGroup GetConnectionPoolGroup(DbConnection connection)
+ {
+ throw new NotImplementedException();
+ }
+
+ internal override DbConnectionInternal GetInnerConnection(DbConnection connection)
+ {
+ throw new NotImplementedException();
+ }
+
+ internal override void PermissionDemand(DbConnection outerConnection)
+ {
+ throw new NotImplementedException();
+ }
+
+ internal override void SetConnectionPoolGroup(DbConnection outerConnection, DbConnectionPoolGroup poolGroup)
+ {
+ throw new NotImplementedException();
+ }
+
+ internal override void SetInnerConnectionEvent(DbConnection owningObject, DbConnectionInternal to)
+ {
+ throw new NotImplementedException();
+ }
+
+ internal override bool SetInnerConnectionFrom(DbConnection owningObject, DbConnectionInternal to, DbConnectionInternal from)
+ {
+ throw new NotImplementedException();
+ }
+
+ internal override void SetInnerConnectionTo(DbConnection owningObject, DbConnectionInternal to)
+ {
+ throw new NotImplementedException();
+ }
+
+ internal override DbConnectionPoolProviderInfo CreateConnectionPoolProviderInfo(DbConnectionOptions connectionOptions)
+ {
+ throw new NotImplementedException();
+ }
+
+ internal override DbConnectionPoolGroupProviderInfo CreateConnectionPoolGroupProviderInfo(DbConnectionOptions connectionOptions)
+ {
+ throw new NotImplementedException();
+ }
+ #endregion
+ }
+
+ internal class StubDbConnectionInternal : DbConnectionInternal
{
- Assert.Throws(() => _pool.TryGetConnection(null!, null!, null!, out _));
+ #region Not Implemented Members
+ public override string ServerVersion => throw new NotImplementedException();
+
+ public override DbTransaction BeginTransaction(System.Data.IsolationLevel il)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override void EnlistTransaction(Transaction transaction)
+ {
+ return;
+ }
+
+ protected override void Activate(Transaction transaction)
+ {
+ return;
+ }
+
+ protected override void Deactivate()
+ {
+ return;
+ }
+ #endregion
}
+ #endregion
}
}
diff --git a/tools/specs/Microsoft.Data.SqlClient.nuspec b/tools/specs/Microsoft.Data.SqlClient.nuspec
index 0f029b4c19..ad04728342 100644
--- a/tools/specs/Microsoft.Data.SqlClient.nuspec
+++ b/tools/specs/Microsoft.Data.SqlClient.nuspec
@@ -40,6 +40,7 @@
+
@@ -76,6 +77,7 @@
+