diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml index f0eddd56fa..f2e03ebf26 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml @@ -827,10 +827,17 @@ The following example creates a an - Empties the connection pool. + Empties all connection pools. - resets (or empties) the connection pool. If there are connections in use at the time of the call, they are marked appropriately and will be discarded (instead of being returned to the pool) when is called on them. + resets (or empties) all connection pools. If there are connections in use at the time of the call, they are marked appropriately and will be discarded (instead of being returned to a pool) when is called on them. + +> [!CAUTION] +> Clearing the pool is an expensive operation and should only be used if required. This operation may negatively interfere with pool warmup and generate high connection churn as the warmup operation continually opens new connections to attempt to reach min pool size. This situation is especially likely if clear is called in a tight loop. + +]]> @@ -841,7 +848,14 @@ The following example creates a an Empties the connection pool associated with the specified connection. - clears the connection pool that is associated with the . If additional connections associated with are in use at the time of the call, they are marked appropriately and are discarded (instead of being returned to the pool) when is called on them. + clears the connection pool that is associated with the `connection`. If additional connections associated with `connection` are in use at the time of the call, they are marked appropriately and are discarded (instead of being returned to the pool) when is called on them. + +> [!CAUTION] +> Clearing the pool is an expensive operation and should only be used if required. This operation may negatively interfere with pool warmup and generate high connection churn as the warmup operation continually opens new connections to attempt to reach min pool size. This situation is especially likely if clear is called in a tight loop. + +]]> diff --git a/specs/001-pool-clear/spec.md b/specs/001-pool-clear/spec.md new file mode 100644 index 0000000000..3ff9488410 --- /dev/null +++ b/specs/001-pool-clear/spec.md @@ -0,0 +1,100 @@ +# Feature Specification: Pool Clear + +**Feature Branch**: `dev/mdaigle/connection-pool-designs` +**Created**: 2026-04-10 +**Status**: Draft +**Input**: User description: "Implement Clear() in ChannelDbConnectionPool so that SqlConnection.ClearPool() and SqlConnection.ClearAllPools() work with pool v2, using a generation counter approach to lazily invalidate connections." + +## User Scenarios & Testing + +### User Story 1 - Clear a Specific Connection Pool + +As an application developer, I want to call `SqlConnection.ClearPool(connection)` to invalidate all connections in a specific pool, so that the next connection request creates a fresh physical connection (e.g., after a failover, credential rotation, or configuration change on the server). + +**Independent Test**: Can be tested by opening connections, calling `ClearPool`, then verifying that subsequent connection requests do not reuse pre-clear connections. + +**Acceptance Scenarios**: + +1. **Given** a pool with 10 idle connections, **When** `ClearPool` is called, **Then** all idle connections are closed and new connection requests create fresh physical connections. +2. **Given** a pool with 5 busy (in-use) connections and 5 idle connections, **When** `ClearPool` is called, **Then** idle connections are closed immediately. + +--- + +### User Story 2 - Clear All Connection Pools + +As an application developer, I want to call `SqlConnection.ClearAllPools()` to invalidate connections across all pools in the application, so that I can recover from broad infrastructure changes (e.g., DNS migration, certificate rollover). + +**Independent Test**: Can be tested by creating connections to multiple connection strings, calling `ClearAllPools`, and verifying all pools produce fresh connections. + +**Acceptance Scenarios**: + +1. **Given** multiple pools each with idle connections, **When** `ClearAllPools` is called, **Then** all idle connections across all pools are closed. + +--- + +### User Story 3 - Lazy Invalidation of Busy Connections + +As the pool infrastructure, I need busy connections that were opened before a clear to be destroyed when they are returned to the pool, so that clearing does not interrupt active operations but still ensures all pre-clear connections are eventually removed. + +**Independent Test**: Can be tested by opening a connection, calling `ClearPool`, executing a query on the open connection (should succeed), closing the connection, then verifying the pool destroyed it rather than reusing it. + +**Acceptance Scenarios**: + +1. **Given** a connection opened and in use before a clear, **When** `ClearPool` is called, **Then** the connection continues to work normally. +2. **Given** a connection opened and in use before a pool clear, **When** the connection is returned to the pool, **Then** the pool detects that it predates the clear and destroys it. +3. **Given** a connection opened after a pool clear, **When** the connection is returned to the pool, **Then** it is returned to the idle channel normally. + +--- + +### User Story 4 - Multiple Consecutive Clears + +As an application developer, I want multiple/concurrent calls to `ClearPool` to behave correctly, so that pool state is not corrupted. + +**Independent Test**: Can be tested by calling `ClearPool` multiple times in rapid succession and verifying no exceptions, no connection leaks, and correct pool behavior afterward. + +**Acceptance Scenarios**: + +1. **Given** a pool with connections, **When** `ClearPool` is called twice rapidly, **Then** both calls complete without error, and connections predating either clear are invalidated. + +--- + +### Edge Cases + +- What happens if `ClearPool` is called on an empty pool? The generation counter increments but no connections are closed. Subsequent connections are fresh. +- What happens if `ClearPool` is called during pool shutdown? The clear should be a no-op or complete harmlessly — shutdown already destroys all connections. +- What happens with many clears causing generation counter overflow? With `int` counter, overflow at 2^31. Even at 1 clear/second, this is 68 years. Overflow is not a practical concern. If it wraps, the worst case is one stale connection survives a single retrieval cycle. +- What happens if `ClearPool` is called during pool startup? `ClearPool` can be called at any time after the pool is instantiated. If connections are being added to the pool while clearing, they are closed subject to the conditions of the clear operation. +- What happens if ClearPool is called while a SqlConnection is waiting to receive a connection from the pool? If a SqlConnection is waiting for a connection, then there are no idle connections in the pool, so `ClearPool` will not have any effect. + +## Requirements + +### Functional Requirements + +- **FR-002**: System MUST stamp each new connection with the current pool generation at creation time. +- **FR-003**: System MUST reject connections whose generation does not match the current pool generation when they are retrieved from the idle channel or returned to the pool. +- **FR-004**: System MUST drain all idle connections from the channel on `Clear()`, closing each one. +- **FR-005**: System MUST NOT interrupt busy (in-use) connections during a clear — busy connections are destroyed lazily when returned. +- **FR-006**: System MUST allow connections opened after a clear to be pooled normally. +- **FR-007**: System MUST support concurrent calls to `Clear()` without corrupting pool state. +- **FR-008**: System MUST integrate with the existing `SqlConnection.ClearPool()` and `SqlConnection.ClearAllPools()` call paths. + +### Key Entities + +- **Pool Generation Counter (`_clearGeneration`)**: A pool-level `volatile int` incremented atomically via `Interlocked.Increment` on each `Clear()` call. Represents the current "epoch" of the pool. +- **Connection Generation (`ClearGeneration`)**: A property on `DbConnectionInternal` stamped when the connection is created or added to the pool. Used to compare against the pool's current generation. +- **Stale Connection**: A connection whose `ClearGeneration` does not match the pool's `_clearGeneration`. Stale connections are destroyed rather than returned to the idle channel. + +## Success Criteria + +### Measurable Outcomes + +- **SC-001**: After `ClearPool` is called, 100% of subsequent connection acquisitions produce fresh physical connections (no pre-clear connections reused). +- **SC-002**: Busy connections continue to operate normally during and after a pool clear — no active queries are interrupted. +- **SC-003**: `ClearPool` and `ClearAllPools` work identically whether using pool v1 (WaitHandle) or pool v2 (Channel), from the caller's perspective. + +## Assumptions + +- The generation counter approach is preferred over the WaitHandle pool's `DoNotPoolThisConnection()` mark-all pattern because `ConnectionPoolSlots` is not iterable by design (CAS-based slot array). +- `DbConnectionInternal` can accommodate a new `ClearGeneration` property without breaking existing functionality or requiring changes to the WaitHandle pool. +- The existing `SqlConnection.ClearPool()` → `SqlConnectionFactory.ClearPool()` → `IDbConnectionPool.Clear()` call chain is already wired up and only requires the `ChannelDbConnectionPool.Clear()` implementation. +- Transacted connections with stale generations are handled separately as part of the transactions feature and are out of scope for Phase 1 of pool clear. 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 b4426bc4d6..5d3143783f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionInternal.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionInternal.cs @@ -92,6 +92,17 @@ internal DbConnectionInternal(ConnectionState state, bool hidePassword, bool all /// internal DateTime CreateTime { get; } + /// + /// The pool generation at the time this connection was created or added to the pool. + /// Used by to detect stale connections after a pool clear. + /// + /// + /// Not safe, should only be set by the connection pool. + /// + // TODO: Ideally this would be readonly and set in the constructor. Piping the value all the way through the connection factory is too complicated to be worth it. + // If we can expose the constructor to the connection pool in the future, it can be set in the constructor. + internal int ClearGeneration { get; set; } + internal bool AllowSetConnectionString { get; } internal bool CanBePooled => !IsConnectionDoomed && !_cannotBePooled && !_owningObject.TryGetTarget(out _); 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 553e6cd395..d6db51e5bb 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 @@ -73,11 +73,26 @@ internal sealed class ChannelDbConnectionPool : IDbConnectionPool 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. + /// The idle connection channel. Contains nulls in order to release waiting attempts after + /// a connection has been physically closed/broken. Also tracks the count of non-null idle connections. /// - private readonly ChannelReader _idleConnectionReader; - private readonly ChannelWriter _idleConnectionWriter; + private readonly IdleConnectionChannel _idleChannel; + + /// + /// The current generation of the pool. Incremented atomically on each call. + /// Connections stamped with a generation that does not match are considered stale and are destroyed + /// rather than returned to the idle channel. + /// Must be updated using operations to ensure thread safety. + /// + private volatile int _clearGeneration; + + /// + /// Guard to prevent concurrent operations from draining the idle channel + /// simultaneously. The generation counter is still incremented by every caller so stale connections + /// are always caught lazily, but only one thread performs the actual drain. + /// Must be updated using operations to ensure thread safety. + /// + private volatile int _isClearing; #endregion /// @@ -99,13 +114,7 @@ internal ChannelDbConnectionPool( TransactedConnectionPool = new(this); _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; + _idleChannel = new(); State = Running; } @@ -123,7 +132,11 @@ public ConcurrentDictionary< public int Count => _connectionSlots.ReservationCount; /// - public bool ErrorOccurred => throw new NotImplementedException(); + public int IdleCount => _idleChannel.Count; + + /// + /// This will be implemented later when we add support for the pool blocking period after errors. For now, it always returns false. + public bool ErrorOccurred => false; /// public int Id => _instanceId; @@ -162,7 +175,45 @@ public ConcurrentDictionary< /// public void Clear() { - throw new NotImplementedException(); + SqlClientEventSource.Log.TryPoolerTraceEvent( + " {0}, Clearing.", Id); + + Interlocked.Increment(ref _clearGeneration); + + // If another thread is already draining, skip the drain. The generation counter has + // already been incremented, so stale connections will still be caught lazily by + // IsLiveConnection on their next retrieval or return. + if (Interlocked.CompareExchange(ref _isClearing, 1, 0) == 1) + { + SqlClientEventSource.Log.TryPoolerTraceEvent( + " {0}, Skip drain, already clearing.", Id); + return; + } + + try + { + // Drain idle connections from the channel and destroy them. Limit iterations to + // the current idle count to prevent an unbounded loop if connections are + // concurrently returned to the channel during the drain. + // Any connections from a previous generation that are returned to the pool + // after we start draining will fail the _clearCounter comparison and will be closed. + int numToDrain = IdleCount; + while (numToDrain > 0 && _idleChannel.TryRead(out DbConnectionInternal? connection)) + { + if (connection is not null) + { + RemoveConnection(connection); + numToDrain--; + } + } + } + finally + { + Interlocked.Exchange(ref _isClearing, 0); + } + + SqlClientEventSource.Log.TryPoolerTraceEvent( + " {0}, Cleared.", Id); } /// @@ -205,7 +256,7 @@ public void ReturnInternalConnection(DbConnectionInternal connection, DbConnecti } else { - var written = _idleConnectionWriter.TryWrite(connection); + var written = _idleChannel.TryWrite(connection); Debug.Assert(written, "Failed to write returning connection to the idle channel."); } } @@ -213,13 +264,13 @@ public void ReturnInternalConnection(DbConnectionInternal connection, DbConnecti /// public void Shutdown() { - throw new NotImplementedException(); + // No-op for now, warmup will be implemented later. } /// public void Startup() { - throw new NotImplementedException(); + // No-op for now, warmup will be implemented later. } /// @@ -353,18 +404,23 @@ public bool TryGetConnection( // 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. - return ConnectionFactory.CreatePooledConnection( + var connection = ConnectionFactory.CreatePooledConnection( owningConnection, this, - PoolGroup.PoolKey, - PoolGroup.ConnectionOptions, userOptions); + + if (connection is not null) + { + connection.ClearGeneration = _clearGeneration; + } + + return connection; }, cleanupCallback: (newConnection) => { // If we fail to open a connection, we need to write a null to the idle channel to // wake up any waiters - _idleConnectionWriter?.TryWrite(null); + _idleChannel?.TryWrite(null); newConnection?.Dispose(); }); } @@ -376,16 +432,24 @@ public bool TryGetConnection( /// Returns true if the connection is live and unexpired, otherwise returns false. private bool IsLiveConnection(DbConnectionInternal connection) { + // Broken physical connection if (!connection.IsConnectionAlive()) { return false; } + // Connection has been alive longer than the load balance timeout if (LoadBalanceTimeout != TimeSpan.Zero && DateTime.UtcNow > connection.CreateTime + LoadBalanceTimeout) { return false; } + // Connection was created before the last Clear, so it's stale. + if (connection.ClearGeneration != _clearGeneration) + { + return false; + } + return true; } @@ -400,7 +464,7 @@ private void RemoveConnection(DbConnectionInternal connection) // Removing a connection from the pool opens a free slot. // 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); + _idleChannel.TryWrite(null); connection.Dispose(); } @@ -412,7 +476,7 @@ private void RemoveConnection(DbConnectionInternal connection) 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)) + while (_idleChannel.TryRead(out DbConnectionInternal? connection)) { if (connection is null) { @@ -478,7 +542,7 @@ private async Task GetInternalConnection( // (first-come, first-served), which is crucial to us. if (async) { - connection ??= await _idleConnectionReader.ReadAsync(cancellationToken).ConfigureAwait(false); + connection ??= await _idleChannel.ReadAsync(cancellationToken).ConfigureAwait(false); } else { @@ -525,7 +589,7 @@ private async Task GetInternalConnection( try { ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter = - _idleConnectionReader.ReadAsync(cancellationToken).ConfigureAwait(false).GetAwaiter(); + _idleChannel.ReadAsync(cancellationToken).ConfigureAwait(false).GetAwaiter(); using ManualResetEventSlim mres = new ManualResetEventSlim(false, 0); // Cancellation happens through the ReadAsync call, which will complete the task. 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 57597ae761..fc14e62d58 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 @@ -172,7 +172,7 @@ internal IDbConnectionPool GetConnectionPool(SqlConnectionFactory connectionFact IDbConnectionPool newPool; if (LocalAppContextSwitches.UseConnectionPoolV2) { - throw new NotImplementedException(); + newPool = new ChannelDbConnectionPool(connectionFactory, this, currentIdentity, connectionPoolProviderInfo); } else { 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 bfc5789d3f..8a6ae397ed 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 @@ -38,6 +38,11 @@ internal interface IDbConnectionPool /// int Count { get; } + /// + /// The number of connections currently sitting idle in the pool. + /// + int IdleCount { get; } + /// /// Indicates whether an error has occurred in the pool. /// Primarily used to support the pool blocking period feature. @@ -100,6 +105,13 @@ internal interface IDbConnectionPool /// /// Clears the connection pool, releasing all connections and resetting the state. /// + /// + /// Clearing the pool is an expensive operation and should only be used if required. + /// This operation may negatively interfere with pool warmup and generate high connection + /// churn as the warmup operation continually opens new connections to attempt + /// to reach min pool size. This situation is especially likely if clear is called in a + /// tight loop. + /// void Clear(); /// diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/IdleConnectionChannel.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/IdleConnectionChannel.cs new file mode 100644 index 0000000000..00748ab184 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/IdleConnectionChannel.cs @@ -0,0 +1,92 @@ +// 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.Threading; +using System.Threading.Tasks; +using System.Threading.Channels; +using Microsoft.Data.ProviderBase; + +#nullable enable + +namespace Microsoft.Data.SqlClient.ConnectionPool +{ + /// + /// Wraps an unbounded of idle connections and tracks the number of + /// non-null connections it contains. Unbounded channels do not support + /// , so this class maintains the count via + /// operations on every read and write of a non-null value. + /// + internal sealed class IdleConnectionChannel + { + private readonly ChannelReader _reader; + private readonly ChannelWriter _writer; + private volatile int _count; + + internal IdleConnectionChannel() + { + var channel = Channel.CreateUnbounded(); + _reader = channel.Reader; + //TODO: the channel should be completed on pool shutdown + _writer = channel.Writer; + } + + /// + /// The number of non-null connections currently in the channel. + /// + internal int Count => _count; + + /// + /// Writes a connection (or null wake-up signal) to the channel. + /// Increments the idle count when is not null. + /// + /// if the value was written; otherwise . + internal bool TryWrite(DbConnectionInternal? connection) + { + if (_writer.TryWrite(connection)) + { + if (connection is not null) + { + Interlocked.Increment(ref _count); + } + return true; + } + + return false; + } + + /// + /// Tries to read a value from the channel without blocking. + /// Decrements the idle count when a non-null connection is read. + /// + internal bool TryRead(out DbConnectionInternal? connection) + { + if (_reader.TryRead(out connection)) + { + if (connection is not null) + { + Interlocked.Decrement(ref _count); + } + + return true; + } + + return false; + } + + /// + /// Asynchronously reads a value from the channel. + /// Decrements the idle count when a non-null connection is read. + /// + internal async ValueTask ReadAsync(CancellationToken cancellationToken) + { + var connection = await _reader.ReadAsync(cancellationToken).ConfigureAwait(false); + + if (connection is not null) + { + Interlocked.Decrement(ref _count); + } + + return connection; + } + } +} 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 8d2311b5cd..b7fa972e85 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 @@ -259,8 +259,12 @@ private int CreationTimeout get { return PoolGroupOptions.CreationTimeout; } } + /// public int Count => _totalObjects; + /// + public int IdleCount => _stackNew.Count + _stackOld.Count; + public SqlConnectionFactory ConnectionFactory => _connectionFactory; public bool ErrorOccurred => _errorOccurred; @@ -290,7 +294,7 @@ private bool NeedToReplenish return true; } - int freeObjects = _stackNew.Count + _stackOld.Count; + int freeObjects = IdleCount; int waitingRequests = _waitCount; bool needToReplenish = (freeObjects < waitingRequests) || ((freeObjects == waitingRequests) && (totalObjects > 1)); @@ -528,8 +532,6 @@ private DbConnectionInternal CreateObject(DbConnection owningObject, DbConnectio newObj = _connectionFactory.CreatePooledConnection( owningObject, this, - _connectionPoolGroup.PoolKey, - _connectionPoolGroup.ConnectionOptions, userOptions); lock (_objectList) 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 89be9767d8..8d49f4dc30 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs @@ -141,15 +141,13 @@ internal DbConnectionInternal CreateNonPooledConnection( internal DbConnectionInternal CreatePooledConnection( DbConnection owningConnection, IDbConnectionPool pool, - DbConnectionPoolKey poolKey, - DbConnectionOptions options, DbConnectionOptions userOptions) { Debug.Assert(pool != null, "null pool?"); DbConnectionInternal newConnection = CreateConnection( - options, - poolKey, // @TODO: is pool.PoolGroup.Key the same thing? + pool.PoolGroup.ConnectionOptions, + pool.PoolGroup.PoolKey, pool.PoolGroup.ProviderInfo, pool, owningConnection, 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 d447720a1a..acaf348a52 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 @@ -16,31 +16,28 @@ internal static class ConnectionPoolHelper private static Assembly s_MicrosoftDotData = Assembly.Load(new AssemblyName(typeof(SqlConnection).GetTypeInfo().Assembly.FullName)); private static Type s_dbConnectionPool = s_MicrosoftDotData.GetType("Microsoft.Data.SqlClient.ConnectionPool.IDbConnectionPool"); private static Type s_waitHandleDbConnectionPool = s_MicrosoftDotData.GetType("Microsoft.Data.SqlClient.ConnectionPool.WaitHandleDbConnectionPool"); + private static Type s_channelDbConnectionPool = s_MicrosoftDotData.GetType("Microsoft.Data.SqlClient.ConnectionPool.ChannelDbConnectionPool"); 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_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); private static Type s_dictPoolIdentityPool = typeof(ConcurrentDictionary<,>).MakeGenericType(s_dbConnectionPoolIdentity, s_dbConnectionPool); - private static PropertyInfo s_dbConnectionPoolCount = s_waitHandleDbConnectionPool.GetProperty("Count", BindingFlags.Instance | BindingFlags.Public); + // Resolve Count from the interface so it works with both pool implementations + private static PropertyInfo s_dbConnectionPoolCount = s_dbConnectionPool.GetProperty("Count", BindingFlags.Instance | BindingFlags.Public); + private static PropertyInfo s_dbConnectionPoolIdleCount = s_dbConnectionPool.GetProperty("IdleCount", BindingFlags.Instance | BindingFlags.Public); private static PropertyInfo s_dictStringPoolGroupGetKeys = s_dictStringPoolGroup.GetProperty("Keys"); private static PropertyInfo s_dictPoolIdentityPoolValues = s_dictPoolIdentityPool.GetProperty("Values"); 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_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); private static MethodInfo s_dictStringPoolGroupTryGetValue = s_dictStringPoolGroup.GetMethod("TryGetValue"); public static int CountFreeConnections(object pool) { VerifyObjectIsPool(pool); - - ICollection oldStack = (ICollection)s_dbConnectionPoolStackOld.GetValue(pool); - ICollection newStack = (ICollection)s_dbConnectionPoolStackNew.GetValue(pool); - - return (oldStack.Count + newStack.Count); + return (int)s_dbConnectionPoolIdleCount.GetValue(pool, null); } /// @@ -107,10 +104,17 @@ public static object ConnectionPoolFromString(string connectionString) /// /// Causes the cleanup timer code in the connection pool to be invoked /// - /// A connection pool object + /// A connection pool object internal static void CleanConnectionPool(object pool) { VerifyObjectIsPool(pool); + + if (s_channelDbConnectionPool.IsInstanceOfType(pool)) + { + // ChannelDbConnectionPool does not have a cleanup timer callback. + return; + } + s_dbConnectionPoolCleanup.Invoke(pool, new object[] { null }); } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolTest.cs index 10c3939774..0d9b076dd2 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolTest.cs @@ -7,14 +7,19 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Microsoft.Data.SqlClient.Tests.Common; using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests { public class ConnectionPoolConnectionStringProvider : IEnumerable { - private static readonly string _TCPConnectionString = (new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString) { MultipleActiveResultSets = false, Pooling = true }).ConnectionString; - private static readonly string _tcpMarsConnStr = (new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString) { MultipleActiveResultSets = true, Pooling = true }).ConnectionString; + private static readonly string _TCPConnectionString = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString) { + MultipleActiveResultSets = false, + Pooling = true}.ConnectionString; + private static readonly string _tcpMarsConnStr = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString) { + MultipleActiveResultSets = true, + Pooling = true }.ConnectionString; public IEnumerator GetEnumerator() { @@ -28,6 +33,29 @@ public IEnumerator GetEnumerator() IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } + public class ConnectionPoolConnectionStringAndPoolVersionProvider : IEnumerable + { + private static readonly string _TCPConnectionString = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString) { + MultipleActiveResultSets = false, + Pooling = true }.ConnectionString; + private static readonly string _tcpMarsConnStr = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString) { + MultipleActiveResultSets = true, + Pooling = true }.ConnectionString; + + public IEnumerator GetEnumerator() + { + yield return new object[] { _TCPConnectionString, false }; + yield return new object[] { _TCPConnectionString, true }; + if (DataTestUtility.IsNotAzureSynapse()) + { + yield return new object[] { _tcpMarsConnStr, false }; + yield return new object[] { _tcpMarsConnStr, true }; + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + // TODO Synapse: Fix these tests for Azure Synapse. public static class ConnectionPoolTest { @@ -118,9 +146,12 @@ public static void AccessTokenConnectionPoolingTest() /// Tests if clearing all of the pools does actually remove the pools /// [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] - [ClassData(typeof(ConnectionPoolConnectionStringProvider))] - public static void ClearAllPoolsTest(string connectionString) + [ClassData(typeof(ConnectionPoolConnectionStringAndPoolVersionProvider))] + public static void ClearAllPoolsTest(string connectionString, bool usePoolV2) { + using LocalAppContextSwitchesHelper switchesHelper = new(); + switchesHelper.UseConnectionPoolV2 = usePoolV2; + SqlConnection.ClearAllPools(); Assert.True(0 == ConnectionPoolWrapper.AllConnectionPools().Length, "Pools exist after clearing all pools"); diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/ChannelDbConnectionPoolTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/ChannelDbConnectionPoolTest.cs index 219bb1fdf6..c7df9dc2c6 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/ChannelDbConnectionPoolTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/ChannelDbConnectionPoolTest.cs @@ -570,7 +570,7 @@ public void TestCount() public void TestErrorOccurred() { var pool = ConstructPool(SuccessfulConnectionFactory); - Assert.Throws(() => _ = pool.ErrorOccurred); + Assert.False(pool.ErrorOccurred); } [Fact] @@ -682,46 +682,253 @@ public void TestUseLoadBalancing() #region Not Implemented Method Tests [Fact] - public void TestClear() + public void TestPutObjectFromTransactedPool() { var pool = ConstructPool(SuccessfulConnectionFactory); - Assert.Throws(() => pool.Clear()); + Assert.Throws(() => pool.PutObjectFromTransactedPool(null!)); } [Fact] - public void TestPutObjectFromTransactedPool() + public void TestReplaceConnection() { var pool = ConstructPool(SuccessfulConnectionFactory); - Assert.Throws(() => pool.PutObjectFromTransactedPool(null!)); + Assert.Throws(() => pool.ReplaceConnection(null!, null!, null!)); } [Fact] - public void TestReplaceConnection() + public void TestTransactionEnded() { var pool = ConstructPool(SuccessfulConnectionFactory); - Assert.Throws(() => pool.ReplaceConnection(null!, null!, null!)); + Assert.Throws(() => pool.TransactionEnded(null!, null!)); } + #endregion + + #region Pool Clear Tests [Fact] - public void TestShutdown() + public void Clear_EmptyPool_DoesNotThrow() { + // Arrange var pool = ConstructPool(SuccessfulConnectionFactory); - Assert.Throws(() => pool.Shutdown()); + + // Act & Assert - Should complete without error + pool.Clear(); + Assert.Equal(0, pool.Count); } [Fact] - public void TestStartup() + public void Clear_MultipleIdleConnections_AllAreDestroyed() { + // Arrange + int numConnections = 5; var pool = ConstructPool(SuccessfulConnectionFactory); - Assert.Throws(() => pool.Startup()); + var owningConnections = new SqlConnection[numConnections]; + var internalConnections = new DbConnectionInternal?[numConnections]; + + for (int i = 0; i < numConnections; i++) + { + owningConnections[i] = new SqlConnection(); + pool.TryGetConnection( + owningConnections[i], + taskCompletionSource: null, + new DbConnectionOptions("", null), + out internalConnections[i] + ); + Assert.Equal(0, internalConnections[i]!.ClearGeneration); + } + + // Return all connections to the pool + for (int i = 0; i < numConnections; i++) + { + pool.ReturnInternalConnection(internalConnections[i]!, owningConnections[i]); + } + + // Act + pool.Clear(); + + // Assert + Assert.Equal(0, pool.Count); } [Fact] - public void TestTransactionEnded() + public void Clear_BusyConnection_NotDestroyedImmediately() { + // Arrange var pool = ConstructPool(SuccessfulConnectionFactory); - Assert.Throws(() => pool.TransactionEnded(null!, null!)); + SqlConnection owningConnection = new(); + + pool.TryGetConnection( + owningConnection, + taskCompletionSource: null, + new DbConnectionOptions("", null), + out DbConnectionInternal? busyConnection + ); + Assert.NotNull(busyConnection); + Assert.Equal(0, busyConnection.ClearGeneration); + + // Act - Clear while connection is still busy + pool.Clear(); + + // Assert - Busy connection is still tracked in the pool and retains its old generation + Assert.Equal(1, pool.Count); + Assert.Equal(0, busyConnection.ClearGeneration); } + + [Fact] + public void Clear_BusyConnectionReturned_IsDestroyed() + { + // Arrange + var pool = ConstructPool(SuccessfulConnectionFactory); + SqlConnection owningConnection = new(); + + pool.TryGetConnection( + owningConnection, + taskCompletionSource: null, + new DbConnectionOptions("", null), + out DbConnectionInternal? busyConnection + ); + Assert.NotNull(busyConnection); + Assert.Equal(0, busyConnection.ClearGeneration); + + // Act - Clear, then return the busy connection + pool.Clear(); + + // Assert - Busy connection is still tracked but has stale generation + Assert.Equal(1, pool.Count); + + // Act - Return the busy connection + pool.ReturnInternalConnection(busyConnection, owningConnection); + + // Assert - The connection should have been destroyed on return (generation mismatch) + Assert.Equal(0, pool.Count); + } + + [Fact] + public void Clear_MixedBusyAndIdle_OnlyIdleDestroyedImmediately() + { + // Arrange + var pool = ConstructPool(SuccessfulConnectionFactory); + SqlConnection busyOwner = new(); + SqlConnection idleOwner = new(); + + pool.TryGetConnection( + busyOwner, + taskCompletionSource: null, + new DbConnectionOptions("", null), + out DbConnectionInternal? busyConnection + ); + pool.TryGetConnection( + idleOwner, + taskCompletionSource: null, + new DbConnectionOptions("", null), + out DbConnectionInternal? idleConnection + ); + Assert.NotNull(busyConnection); + Assert.NotNull(idleConnection); + Assert.Equal(0, busyConnection.ClearGeneration); + Assert.Equal(0, idleConnection.ClearGeneration); + + // Return only the idle connection + pool.ReturnInternalConnection(idleConnection, idleOwner); + + // Act + pool.Clear(); + + // Assert - Only the busy connection remains with stale generation + Assert.Equal(1, pool.Count); + Assert.Equal(0, busyConnection.ClearGeneration); + + // Now return the busy connection - it should be destroyed (generation 0 != pool generation 1) + pool.ReturnInternalConnection(busyConnection, busyOwner); + Assert.Equal(0, pool.Count); + } + + [Fact] + public void Clear_NewConnectionsAfterClear_ArePooledNormally() + { + // Arrange + var pool = ConstructPool(SuccessfulConnectionFactory); + SqlConnection owningConnection = new(); + + pool.TryGetConnection( + owningConnection, + taskCompletionSource: null, + new DbConnectionOptions("", null), + out DbConnectionInternal? oldConnection + ); + Assert.Equal(0, oldConnection!.ClearGeneration); + pool.ReturnInternalConnection(oldConnection, owningConnection); + + // Act + pool.Clear(); + + // Get a new connection after clear + SqlConnection newOwner = new(); + pool.TryGetConnection( + newOwner, + taskCompletionSource: null, + new DbConnectionOptions("", null), + out DbConnectionInternal? newConnection + ); + Assert.NotNull(newConnection); + + // The new connection should be different from the old one and have generation 1 + Assert.NotSame(oldConnection, newConnection); + Assert.Equal(1, newConnection.ClearGeneration); + + // Return the new connection - it should be pooled normally + pool.ReturnInternalConnection(newConnection, newOwner); + Assert.Equal(1, pool.Count); + + // Get another connection - it should reuse the post-clear connection (same generation) + SqlConnection reuseOwner = new(); + pool.TryGetConnection( + reuseOwner, + taskCompletionSource: null, + new DbConnectionOptions("", null), + out DbConnectionInternal? reusedConnection + ); + Assert.Same(newConnection, reusedConnection); + Assert.Equal(1, reusedConnection!.ClearGeneration); + } + + [Fact] + public void Clear_MultipleClearCalls_DoNotCorruptState() + { + // Arrange + var pool = ConstructPool(SuccessfulConnectionFactory); + SqlConnection owningConnection = new(); + + pool.TryGetConnection( + owningConnection, + taskCompletionSource: null, + new DbConnectionOptions("", null), + out DbConnectionInternal? connection + ); + Assert.Equal(0, connection!.ClearGeneration); + pool.ReturnInternalConnection(connection, owningConnection); + + // Act - Call clear multiple times rapidly + pool.Clear(); + pool.Clear(); + pool.Clear(); + + // Assert - Pool state is still valid + Assert.Equal(0, pool.Count); + + // New connections should have generation 3 (incremented three times) + SqlConnection newOwner = new(); + pool.TryGetConnection( + newOwner, + taskCompletionSource: null, + new DbConnectionOptions("", null), + out DbConnectionInternal? newConnection + ); + Assert.NotNull(newConnection); + Assert.Equal(1, pool.Count); + Assert.Equal(3, newConnection.ClearGeneration); + } + #endregion #region Test classes diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/IdleConnectionChannelTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/IdleConnectionChannelTest.cs new file mode 100644 index 0000000000..56aa9c234a --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/IdleConnectionChannelTest.cs @@ -0,0 +1,244 @@ +// 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.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using System.Transactions; +using Microsoft.Data.ProviderBase; +using Microsoft.Data.SqlClient.ConnectionPool; +using Xunit; + +#nullable enable + +namespace Microsoft.Data.SqlClient.UnitTests.ConnectionPool +{ + public class IdleConnectionChannelTest + { + #region TryWrite + + [Fact] + public void TryWrite_NonNullConnection_IncrementsCount() + { + var channel = new IdleConnectionChannel(); + + Assert.True(channel.TryWrite(new StubDbConnectionInternal())); + Assert.Equal(1, channel.Count); + } + + [Fact] + public void TryWrite_NullConnection_DoesNotIncrementCount() + { + var channel = new IdleConnectionChannel(); + + Assert.True(channel.TryWrite(null)); + Assert.Equal(0, channel.Count); + } + + [Fact] + public void TryWrite_MultipleConnections_TracksCountCorrectly() + { + var channel = new IdleConnectionChannel(); + + channel.TryWrite(new StubDbConnectionInternal()); + channel.TryWrite(new StubDbConnectionInternal()); + channel.TryWrite(null); + channel.TryWrite(new StubDbConnectionInternal()); + + Assert.Equal(3, channel.Count); + } + + #endregion + + #region TryRead + + [Fact] + public void TryRead_NonNullConnection_DecrementsCount() + { + var channel = new IdleConnectionChannel(); + channel.TryWrite(new StubDbConnectionInternal()); + Assert.Equal(1, channel.Count); + + Assert.True(channel.TryRead(out var connection)); + Assert.NotNull(connection); + Assert.Equal(0, channel.Count); + } + + [Fact] + public void TryRead_NullConnection_DoesNotDecrementCount() + { + var channel = new IdleConnectionChannel(); + channel.TryWrite(new StubDbConnectionInternal()); + channel.TryWrite(null); + Assert.Equal(1, channel.Count); + + // Read the non-null connection first (FIFO) + Assert.True(channel.TryRead(out var first)); + Assert.NotNull(first); + Assert.Equal(0, channel.Count); + + // Read the null + Assert.True(channel.TryRead(out var second)); + Assert.Null(second); + Assert.Equal(0, channel.Count); + } + + [Fact] + public void TryRead_EmptyChannel_ReturnsFalse() + { + var channel = new IdleConnectionChannel(); + + Assert.False(channel.TryRead(out var connection)); + Assert.Null(connection); + Assert.Equal(0, channel.Count); + } + + #endregion + + #region ReadAsync + + [Fact] + public async Task ReadAsync_NonNullConnection_DecrementsCount() + { + var channel = new IdleConnectionChannel(); + channel.TryWrite(new StubDbConnectionInternal()); + Assert.Equal(1, channel.Count); + + var connection = await channel.ReadAsync(CancellationToken.None); + + Assert.NotNull(connection); + Assert.Equal(0, channel.Count); + } + + [Fact] + public async Task ReadAsync_NullConnection_DoesNotDecrementCount() + { + var channel = new IdleConnectionChannel(); + channel.TryWrite(new StubDbConnectionInternal()); + channel.TryWrite(null); + Assert.Equal(1, channel.Count); + + // First read returns the non-null connection (FIFO) + var first = await channel.ReadAsync(CancellationToken.None); + Assert.NotNull(first); + Assert.Equal(0, channel.Count); + + // Second read returns null + var second = await channel.ReadAsync(CancellationToken.None); + Assert.Null(second); + Assert.Equal(0, channel.Count); + } + + [Fact] + public async Task ReadAsync_WaitsForWrite() + { + var channel = new IdleConnectionChannel(); + var expected = new StubDbConnectionInternal(); + + var readTask = channel.ReadAsync(CancellationToken.None); + Assert.False(readTask.IsCompleted); + + channel.TryWrite(expected); + + var connection = await readTask; + Assert.Same(expected, connection); + Assert.Equal(0, channel.Count); + } + + [Fact] + public async Task ReadAsync_Cancelled_ThrowsOperationCanceledException() + { + var channel = new IdleConnectionChannel(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAnyAsync( + () => channel.ReadAsync(cts.Token).AsTask()); + } + + #endregion + + #region Mixed operations + + [Fact] + public void WriteAndReadSequence_CountStaysConsistent() + { + var channel = new IdleConnectionChannel(); + + // Write 3 + channel.TryWrite(new StubDbConnectionInternal()); + channel.TryWrite(new StubDbConnectionInternal()); + channel.TryWrite(new StubDbConnectionInternal()); + Assert.Equal(3, channel.Count); + + // Read 2 + channel.TryRead(out _); + channel.TryRead(out _); + Assert.Equal(1, channel.Count); + + // Write 1 more + channel.TryWrite(new StubDbConnectionInternal()); + Assert.Equal(2, channel.Count); + + // Read remaining 2 + channel.TryRead(out _); + channel.TryRead(out _); + Assert.Equal(0, channel.Count); + + // Channel is empty + Assert.False(channel.TryRead(out _)); + Assert.Equal(0, channel.Count); + } + + #endregion + + #region Multi-threaded Tests + + [Fact] + public async Task ConcurrentWriteAndRead_CountReturnsToZero() + { + var channel = new IdleConnectionChannel(); + var barrier = new Barrier(3); + const int iterations = 1000; + + async Task WriteAndRead() + { + barrier.SignalAndWait(); + + for (int i = 0; i < iterations; i++) + { + channel.TryWrite(new StubDbConnectionInternal()); + await channel.ReadAsync(CancellationToken.None); + } + } + + await Task.WhenAll( + Task.Run(WriteAndRead), + Task.Run(WriteAndRead), + Task.Run(WriteAndRead)); + + Assert.Equal(0, channel.Count); + } + + #endregion + + #region Helpers + + private class StubDbConnectionInternal : DbConnectionInternal + { + public override string ServerVersion => throw new NotImplementedException(); + + public override DbTransaction BeginTransaction(System.Data.IsolationLevel il) + => throw new NotImplementedException(); + + public override void EnlistTransaction(Transaction transaction) { } + protected override void Activate(Transaction transaction) { } + protected override void Deactivate() { } + internal override void ResetConnection() { } + } + + #endregion + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/TransactedConnectionPoolTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/TransactedConnectionPoolTest.cs index 18bd9c5ea3..9f284be22a 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/TransactedConnectionPoolTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/TransactedConnectionPoolTest.cs @@ -661,6 +661,7 @@ internal class MockDbConnectionPool : IDbConnectionPool public int Count => throw new NotImplementedException(); public bool ErrorOccurred => throw new NotImplementedException(); public int Id { get; } = 1; + public int IdleCount => throw new NotImplementedException(); public DbConnectionPoolIdentity Identity => throw new NotImplementedException(); public bool IsRunning => throw new NotImplementedException(); public TimeSpan LoadBalanceTimeout => throw new NotImplementedException();