Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
d3e2fe7
Add connection-creation rate limiting to ChannelDbConnectionPool
mdaigle Jun 23, 2026
4396432
Narrow pool rate limiter to ConcurrencyLimiter
mdaigle Jul 14, 2026
86b4b2a
Replace rate-limit TODOs with rationale comment
mdaigle Jul 14, 2026
144a324
Remove rate-limit TODOs from AttemptAcquire call
mdaigle Jul 14, 2026
029ce7e
Merge remote-tracking branch 'origin/main' into dev/mdaigle/pool-chan…
mdaigle Jul 14, 2026
662b36d
Inline leaseAcquired into lease.IsAcquired checks
mdaigle Jul 14, 2026
87c8631
Add test that successful create releases its rate-limiter lease
mdaigle Jul 14, 2026
19948c2
Add test for lease-release wake path (FR-004)
mdaigle Jul 14, 2026
0d60cd4
Address Copilot review: OCE handling, redundant wake, docs, test disp…
mdaigle Jul 14, 2026
9b91a45
Remove instance-level ForceNewConnection property. Replace with expli…
mdaigle Jun 29, 2026
08d24b3
Fix initialization. Add doc comments.
mdaigle Jun 29, 2026
08eff42
Remove unnecessary internal API surface.
mdaigle Jun 29, 2026
e350b42
Expose param.
mdaigle Jun 29, 2026
e2e1a72
Address broken test and copilot comments.
mdaigle Jun 29, 2026
63edefe
WIP
mdaigle Jun 29, 2026
949d9b2
Add unit tests.
mdaigle Jul 8, 2026
d684191
Wording
mdaigle Jul 8, 2026
23cfa38
improve error handling
mdaigle Jul 9, 2026
ae75925
Address copilot comments.
mdaigle Jul 10, 2026
b266402
Fix malformed XML doc comment in TryOpenInner remarks
mdaigle Jul 15, 2026
88aaffd
Prefer reusing an idle connection in ChannelDbConnectionPool.ReplaceC…
mdaigle Jul 15, 2026
19933e7
Use named forceNewConnection arguments at literal call sites
mdaigle Jul 15, 2026
ea77190
Return reused connection to the pool on activation failure via Prepar…
mdaigle Jul 15, 2026
e573441
Condense block comments in ReplaceConnection
mdaigle Jul 15, 2026
594231c
Document ReplaceConnection design rationale in one header block
mdaigle Jul 15, 2026
36e0a51
Merge remote-tracking branch 'origin/main' into dev/mdaigle/pool-chan…
mdaigle Jul 15, 2026
f7f7f63
Merge branch 'dev/mdaigle/pool-channel-rate-limiting' into dev/mdaigl…
mdaigle Jul 15, 2026
69b08b5
Restore named forceNewConnection arguments at test call sites
mdaigle Jul 15, 2026
fbf37d7
clean up comments
mdaigle Jul 15, 2026
0d19e4e
Merge branch 'dev/mdaigle/replace-conn-2' of https://github.com/dotne…
mdaigle Jul 15, 2026
f430f80
Address Copilot review: fix doc comments for ReplaceConnection
mdaigle Jul 16, 2026
81f6958
Address Copilot review: test summary + explicit Assert.Throws
mdaigle Jul 16, 2026
a0659c6
Respect blocking period in ReplaceConnection new-physical-connection …
mdaigle Jul 16, 2026
21c99a0
Condense blocking-period comments in ReplaceConnection
mdaigle Jul 16, 2026
418bf53
Merge remote-tracking branch 'origin/main' into dev/mdaigle/replace-c…
mdaigle Jul 27, 2026
5ca5fc8
Address Copilot review feedback on ReplaceConnection
mdaigle Jul 27, 2026
ef4754f
Throw localized message when pool connection replacement fails
mdaigle Jul 28, 2026
c50865c
Explain why replacement bypasses the connection-creation rate limiter
mdaigle Jul 28, 2026
36c309b
Inject FakeTimeProvider in new pool tests to prevent background races
mdaigle Jul 28, 2026
7b9e36b
Merge origin/main into dev/mdaigle/replace-conn-2
mdaigle Jul 28, 2026
8736037
Enter blocking-period error state on replacement open failure
mdaigle Jul 28, 2026
1f1e766
Address Paul's review feedback
mdaigle Jul 29, 2026
c8e7f11
Implement transaction support in ChannelDbConnectionPool
mdaigle Jul 29, 2026
c540496
Flow the ambient transaction explicitly on the async open path
mdaigle Jul 29, 2026
ca8079b
Reclaim emancipated connections in ChannelDbConnectionPool
mdaigle Aug 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using Microsoft.Data.ProviderBase;
Expand Down Expand Up @@ -170,6 +171,48 @@ internal bool TryRemove(DbConnectionInternal connection)
return false;
}

/// <summary>
/// Atomically replaces an existing connection with a new one in the same slot.
/// The reservation count is unchanged because the slot is reused.
/// </summary>
/// <param name="oldConnection">The connection currently occupying the slot.</param>
/// <param name="newConnection">The connection to place into the slot.</param>
/// <returns>True if the old connection was found and replaced; otherwise, false.</returns>
internal bool TryReplace(DbConnectionInternal oldConnection, DbConnectionInternal newConnection)
{
for (int i = 0; i < _connections.Length; i++)
{
if (Interlocked.CompareExchange(ref _connections[i], newConnection, oldConnection) == oldConnection)
{
return true;
}
}

return false;
}

/// <summary>
/// Returns a point-in-time snapshot of the connections currently tracked by this collection.
/// The snapshot is best-effort: connections may be added or removed while it is being taken,
/// so callers must tolerate entries that have since left the pool. Intended for infrequent
/// bookkeeping passes (e.g. reclaiming emancipated connections), not for hot paths.
/// </summary>
internal List<DbConnectionInternal> Snapshot()
{
List<DbConnectionInternal> snapshot = new(_connections.Length);

for (int i = 0; i < _connections.Length; i++)
{
DbConnectionInternal? connection = Volatile.Read(ref _connections[i]);
if (connection is not null)
{
snapshot.Add(connection);
}
}

return snapshot;
}

/// <summary>
/// Attempts to reserve a spot in the collection.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1616,7 +1616,9 @@ public void Open(SqlConnectionOverrides overrides)
{
statistics = SqlStatistics.StartTimer(Statistics);

if (!(IsProviderRetriable ? TryOpenWithRetry(null, false, overrides) : TryOpen(null, false, overrides)))
if (!(IsProviderRetriable ?
TryOpenWithRetry(retry: null, forceNewConnection: false, overrides: overrides) :
TryOpen(retry: null, forceNewConnection: false, overrides: overrides)))
{
throw ADP.InternalError(ADP.InternalErrorCode.SynchronousConnectReturnedPending);
}
Expand Down Expand Up @@ -2252,16 +2254,22 @@ private bool TryOpen(TaskCompletionSource<DbConnectionInternal> retry, bool forc
/// Completes the inner open/replace operation and initializes parser state for the active inner connection.
/// </summary>
/// <param name="retry">Retry continuation used by async open paths.</param>
/// <param name="forceNewConnection">Provide true to forcibly overwrite the existing connection. Provide false if connecting for the first time.</param>
/// <param name="forceNewConnection">Provide <see langword="true"/> to replace the existing inner connection with a freshly established one (for example, during reconnect after a transient fault); provide <see langword="false"/> when opening for the first time.</param>
/// <returns><see langword="true"/> when open initialization completed synchronously; otherwise <see langword="false"/>.</returns>
/// <remarks>
/// The inner connection is snapshotted after the open call so downstream parser access uses a single observed
/// instance and does not rely on a second racy read of <see cref="InnerConnection"/>.
///
/// forceNewConnection may only be true when the connection is already open (or was open) and needs to be replaced. If the connection has never
/// been opened, passing true will result in an exception. It may only be false when the connection has never been opened or is
/// currently disconnected. If the connection is currently open, passing false will result in an exception. See SqlConnection state
/// transitions and subclasses for more details.
/// <para>
/// <paramref name="forceNewConnection"/> may be <see langword="true"/> when the connection is currently open, or when
/// it was previously opened and is now disconnected (the reconnect case handled by
/// <c>DbConnectionClosedPreviouslyOpened</c> and <c>DbConnectionClosedConnecting</c>). Passing <see langword="true"/>
/// on a connection that has never been opened will result in an exception.
/// </para>
/// <para>
/// <paramref name="forceNewConnection"/> may be <see langword="false"/> when the connection has never been opened or is
/// currently disconnected. Passing <see langword="false"/> on a connection that is already open will result in an
/// exception.
/// </para>
/// </remarks>
internal bool TryOpenInner(TaskCompletionSource<DbConnectionInternal> retry, bool forceNewConnection)
{
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Microsoft.Data.SqlClient/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -2157,6 +2157,9 @@
<data name="SQL_ConnectionPoolNoEmptySlot" xml:space="preserve">
<value>Could not find an empty slot in the connection pool.</value>
</data>
<data name="SQL_ConnectionPoolReplaceConnectionFailed" xml:space="preserve">
<value>Could not replace the connection because it is no longer in the connection pool.</value>
</data>
<data name="SQL_ConnectionPoolShutDown" xml:space="preserve">
<value>The connection pool has been shut down.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// 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.

namespace Microsoft.Data.SqlClient.Tests.Common;

Comment on lines +1 to +6
/// <summary>
/// Selects the connection pool implementation (<c>WaitHandleDbConnectionPool</c> or
/// <c>ChannelDbConnectionPool</c>) for the duration of a test.
///
/// A pool is bound to an implementation when it is created, so simply flipping the
/// <c>UseConnectionPoolV2</c> switch is not enough: pools created before the switch was flipped
/// keep their original implementation, and pools created inside the scope would otherwise outlive
/// it and leak the chosen implementation into unrelated tests. This scope therefore clears all
/// pools both on entry and on exit.
///
/// This follows the RAII pattern; construct it at the start of a test and dispose it at the end.
/// Like <see cref="LocalAppContextSwitchesHelper"/>, it manipulates global state and enforces a
/// single-instance policy, so it must not be held for longer than necessary.
/// </summary>
public sealed class ConnectionPoolVersionScope : IDisposable
{
private readonly LocalAppContextSwitchesHelper _switches;

/// <summary>
/// Clears all existing pools and selects the requested pool implementation.
/// </summary>
/// <param name="usePoolV2">
/// True to use <c>ChannelDbConnectionPool</c>; false to use <c>WaitHandleDbConnectionPool</c>.
/// </param>
public ConnectionPoolVersionScope(bool usePoolV2)
{
_switches = new LocalAppContextSwitchesHelper();

try
{
SqlConnection.ClearAllPools();
_switches.UseConnectionPoolV2 = usePoolV2;
}
catch
{
_switches.Dispose();
throw;
}
}

/// <summary>
/// Clears all pools created under the selected implementation and restores the original
/// switch values.
/// </summary>
public void Dispose()
{
try
{
SqlConnection.ClearAllPools();
}
finally
{
_switches.Dispose();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,7 @@ public static void AccessTokenConnectionPoolingTest()
[ClassData(typeof(ConnectionPoolConnectionStringAndPoolVersionProvider))]
public static void ClearAllPoolsTest(string connectionString, bool usePoolV2)
{
using LocalAppContextSwitchesHelper switchesHelper = new();
switchesHelper.UseConnectionPoolV2 = usePoolV2;
using ConnectionPoolVersionScope poolVersion = new(usePoolV2);

SqlConnection.ClearAllPools();
Assert.True(0 == ConnectionPoolWrapper.AllConnectionPools().Length, "Pools exist after clearing all pools");
Expand All @@ -178,9 +177,11 @@ public static void ClearAllPoolsTest(string connectionString, bool usePoolV2)
/// NOTE: 'emancipated' means that the internal connection's SqlConnection has fallen out of scope and has no references, but was not explicitly disposed\closed
/// </summary>
[ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
[ClassData(typeof(ConnectionPoolConnectionStringProvider))]
public static void ReclaimEmancipatedOnOpenTest(string connectionString)
[ClassData(typeof(ConnectionPoolConnectionStringAndPoolVersionProvider))]
public static void ReclaimEmancipatedOnOpenTest(string connectionString, bool usePoolV2)
{
using ConnectionPoolVersionScope poolVersion = new(usePoolV2);

string newConnectionString = (new SqlConnectionStringBuilder(connectionString) { MaxPoolSize = 1 }).ConnectionString;
SqlConnection.ClearAllPools();

Expand All @@ -205,9 +206,11 @@ public static void ReclaimEmancipatedOnOpenTest(string connectionString)
/// Tests if, when max pool size is reached, Open() will block until a connection becomes available
/// </summary>
[ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
[ClassData(typeof(ConnectionPoolConnectionStringProvider))]
public static void MaxPoolWaitForConnectionTest(string connectionString)
[ClassData(typeof(ConnectionPoolConnectionStringAndPoolVersionProvider))]
public static void MaxPoolWaitForConnectionTest(string connectionString, bool usePoolV2)
{
using ConnectionPoolVersionScope poolVersion = new(usePoolV2);

string newConnectionString = (new SqlConnectionStringBuilder(connectionString) { MaxPoolSize = 1 }).ConnectionString;
SqlConnection.ClearAllPools();

Expand Down
Loading