Enable ConnectionPoolV2 by default - #4537
Conversation
There was a problem hiding this comment.
Pull request overview
This PR flips the default of the internal Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2 AppContext switch to true, making the Channel-based pool (ChannelDbConnectionPool) the default connection pooling implementation while keeping the legacy pool (WaitHandleDbConnectionPool) available via explicit opt-out.
Changes:
- Change
LocalAppContextSwitches.UseConnectionPoolV2default fromfalse→true, and update its XML doc accordingly. - Update the unit test that asserts the default switch values.
- Update the internal feature documentation table for the switch default.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs | Flips UseConnectionPoolV2 default to true and updates XML documentation. |
| src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/LocalAppContextSwitchesTest.cs | Updates default-value assertion to expect UseConnectionPoolV2 == true. |
| .github/instructions/features.instructions.md | Updates the documented default for UseConnectionPoolV2 in the AppContext switches table. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| | `Switch.Microsoft.Data.SqlClient.UseCompatibilityAsyncBehaviour` | `false` | Uses legacy async behavior for compatibility | | ||
| | `Switch.Microsoft.Data.SqlClient.UseCompatibilityProcessSni` | `false` | Uses legacy SNI processing path | |
|
CI investigation: the two failing legs ( This is not a pre-existing flake: the same test passes on #4504's CI (same base branch, Root cause: Per this PR's stated purpose, I'm not fixing pool internals here — documented the gap in the PR body so the pool-v2 workstream can add reclaim-on-open support to |
Broader CI update: failures across nearly the full manual-test matrixAs more legs of 1.
|
4e920b5 to
6c1ea47
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (1)
.github/instructions/features.instructions.md:257
- The AppContext switch default-value table is now inconsistent with the actual defaults in
LocalAppContextSwitches.cs:UseCompatibilityAsyncBehaviourandUseCompatibilityProcessSniboth default totrue(compatibility mode), but the table still listsfalse. Since this PR already edits this section, please update these rows so the table reflects real defaults and explains that setting them tofalseenables the newer behaviors.
| `Switch.Microsoft.Data.SqlClient.UseCompatibilityAsyncBehaviour` | `false` | Uses legacy async behavior for compatibility |
| `Switch.Microsoft.Data.SqlClient.UseCompatibilityProcessSni` | `false` | Uses legacy SNI processing path |
| `Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2` | `true` | Enables the new `ChannelDbConnectionPool` implementation; set to `false` to restore the legacy `WaitHandleDbConnectionPool` |
513c4d3 to
6cf4e79
Compare
6c1ea47 to
ee208e9
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (1)
.github/instructions/features.instructions.md:257
- The switch-default table still lists
UseCompatibilityAsyncBehaviourandUseCompatibilityProcessSnias defaulting tofalse, but both are asserted astruedefaults inLocalAppContextSwitchesTestand documented/implemented asdefaultValue: trueinLocalAppContextSwitches.cs. This table should be updated to avoid misleading contributors about the actual defaults.
| `Switch.Microsoft.Data.SqlClient.UseCompatibilityAsyncBehaviour` | `false` | Uses legacy async behavior for compatibility |
| `Switch.Microsoft.Data.SqlClient.UseCompatibilityProcessSni` | `false` | Uses legacy SNI processing path |
| `Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2` | `true` | Enables the new `ChannelDbConnectionPool` implementation; set to `false` to restore the legacy `WaitHandleDbConnectionPool` |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Suppressed comments (2)
.github/instructions/features.instructions.md:257
- The switch default-value table is inconsistent with the actual defaults in
LocalAppContextSwitches.cs: bothUseCompatibilityAsyncBehaviourandUseCompatibilityProcessSnidefault totrue(seeLocalAppContextSwitches.cs:539-575), but this table still lists them asfalse. This makes the switch reference misleading, especially now thatUseConnectionPoolV2is being updated here as well.
| `Switch.Microsoft.Data.SqlClient.UseCompatibilityAsyncBehaviour` | `false` | Uses legacy async behavior for compatibility |
| `Switch.Microsoft.Data.SqlClient.UseCompatibilityProcessSni` | `false` | Uses legacy SNI processing path |
| `Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2` | `true` | Enables the new `ChannelDbConnectionPool` implementation; set to `false` to restore the legacy `WaitHandleDbConnectionPool` |
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs:588
- The PR description calls out a missing “reclaim emancipated connections on open” path in PoolV2 as the primary root cause of CI failures, but
ChannelDbConnectionPool.GetInternalConnectionalready performs a reclaim sweep on the slow path before it parks on the idle channel (seeChannelDbConnectionPool.cs:1516-1526,ReclaimEmancipatedConnections()). Either the investigation summary is out of date, or the problem is that reclamation is not triggering / not freeing a usable connection; please update the PR description (or add a note) so the documented root cause matches the current code.
/// </summary>
public static bool UseConnectionPoolV2 =>
AcquireAndReturn(
UseConnectionPoolV2String,
defaultValue: true,
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Suppressed comments (3)
.github/instructions/features.instructions.md:257
- The switch-default table now claims
UseCompatibilityAsyncBehaviourandUseCompatibilityProcessSnidefault tofalse, butLocalAppContextSwitchesdefaults both totrue(see LocalAppContextSwitches.cs:557 and :575). Since this PR updates this table, it should keep these defaults accurate to avoid misleading guidance.
| `Switch.Microsoft.Data.SqlClient.UseCompatibilityAsyncBehaviour` | `false` | Uses legacy async behavior for compatibility |
| `Switch.Microsoft.Data.SqlClient.UseCompatibilityProcessSni` | `false` | Uses legacy SNI processing path |
| `Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2` | `true` | Enables the new `ChannelDbConnectionPool` implementation; set to `false` to restore the legacy `WaitHandleDbConnectionPool` |
src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/TvpTest.cs:72
Task.Factory.StartNew(..., LongRunning).Unwrap()is unnecessary here and adds thread-creation overhead;RunPacketNumberWraparoundis already async and can be started directly and raced against the timeout task.
// Task.Factory.StartNew with an async delegate returns a Task<Task>, so it must be
// unwrapped before use in Task.WhenAny below. Without Unwrap(), WhenAny would observe
// only the outer task (which completes as soon as the async lambda hits its first
// await) instead of the actual completion of RunPacketNumberWraparound.
Task actionTask = Task.Factory.StartNew(
src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/TvpTest.cs:86
- If the timeout wins, any fault from
actionTaskis never observed (and the task may keep running briefly after test failure), which can hide the real failure cause and leak background work into later tests. Consider best-effort observing faults after cancellation on the timeout path.
// Propagate any unexpected failure from the action task (e.g. a connection open
// failure) instead of letting it surface only as a low enumerator count below.
if (completedTask == actionTask)
{
await actionTask;
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs:184
- RunStressTest allocates a Task[] sized to ConcurrentConnections but does not populate any entry when ConcurrentConnections == 1 (the for-loop runs 0 iterations and the dooming worker is only created when > 1). This leaves a null in the array and causes Task.WhenAll(workers) to throw immediately, potentially leaving started workers unobserved.
var workers = new Task[ConcurrentConnections];
using Barrier barrier = new(ConcurrentConnections);
var command = string.IsNullOrWhiteSpace(WaitForDelay)
? "SELECT GETDATE()"
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs:184
workersis sized toConcurrentConnections, but whenConcurrentConnections == 1neither theforloop nor the dooming-worker branch assignsworkers[0], soTask.WhenAll(workers)will throw due to a null task. Either validateConcurrentConnections >= 2up front or populate all slots and conditionally mark one worker as the dooming worker.
var workers = new Task[ConcurrentConnections];
using Barrier barrier = new(ConcurrentConnections);
var command = string.IsNullOrWhiteSpace(WaitForDelay)
? "SELECT GETDATE()"
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs:191
RunStressTestleavesworkers[0]as null whenConcurrentConnections == 1(the loop runs 0 iterations and the dooming worker is only created when> 1).Task.WhenAll(workers)will then throwArgumentException/NullReferenceExceptioninstead of running the stress test. Create all worker tasks in a single loop and only enable the dooming behavior on the last worker whenConcurrentConnections > 1.
// Create regular threads (don't doom connections)
for (int i = 0; i < ConcurrentConnections - 1; i++)
{
workers[i] = CreateWorkerTask(
connectionString, command, barrier, doomConnections: false, async);
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs:184
RunStressTestleavesworkers[0]null whenConcurrentConnections == 1, which causesTask.WhenAll(workers)to throw. SinceConcurrentConnectionsis a public settable property, it should either be validated or all worker slots should be populated for any value >= 1.
var workers = new Task[ConcurrentConnections];
using Barrier barrier = new(ConcurrentConnections);
var command = string.IsNullOrWhiteSpace(WaitForDelay)
? "SELECT GETDATE()"
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs:202
RunStressTestassumesConcurrentConnections > 1. If it’s configured to1, no worker task is created, leaving a null entry inworkersand causingTask.WhenAll(workers)to throw (and previously would have deadlocked). Either validateConcurrentConnections >= 2up front or create a single worker whenConcurrentConnections == 1.
private void RunStressTest(
string connectionString,
Func<DbConnection, bool> doomAction,
bool async = false)
{
var workers = new Task[ConcurrentConnections];
using Barrier barrier = new(ConcurrentConnections);
var command = string.IsNullOrWhiteSpace(WaitForDelay)
? "SELECT GETDATE()"
: $"WAITFOR DELAY '{WaitForDelay}'; SELECT GETDATE()";
// Create regular threads (don't doom connections)
for (int i = 0; i < ConcurrentConnections - 1; i++)
{
workers[i] = CreateWorkerTask(
connectionString, command, barrier, doomConnections: false, async);
}
// Create special thread that dooms connections (if we have multiple threads)
if (ConcurrentConnections > 1)
{
workers[ConcurrentConnections - 1] = CreateWorkerTask(
connectionString, command, barrier, doomConnections: true, async, doomAction);
}
Task.WhenAll(workers).GetAwaiter().GetResult();
}
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
🤖 Superseded by a repository-owned branch so this work can be tracked directly in dotnet/SqlClient. |
Summary
Changes the default connection pool from
WaitHandleDbConnectionPooltoChannelDbConnectionPool. Applications can restore the legacy pool explicitly:Changes
UseConnectionPoolV2's default totrue.TvpTest.TestPacketNumberWraparoundsoTask.Factory.StartNew's nested task is unwrapped and observed.Enlist=falsepublic-API coverage using the in-process TDS server.Flaky-test stabilization and quarantine changes discovered during the CI investigation were removed in
1e2a53437for a separate PR.CI findings
The full matrix exposed these retained V1/V2 differences and latent test defects:
ReclaimEmancipatedOnOpenTestexposed missing reclaim-on-open behavior in V2. The fix merged through Channel Pool: Reclaim leaked connections #4529 and is now onmain.TvpTest.TestPacketNumberWraparoundexposed a latentTask<Task>test bug. Commit6941f0c63unwraps the task and propagates early failures correctly.0e6ca43c4keeps cancellation authoritative before physical creation and adds deterministic no-creation coverage. Commit83d26c964replaces the legacy inference-based test with direct public-API coverage.45b28ed95acquires through the normal pool path when the old connection is already detached.A prior clean matrix on
45b28ed95completed all 366 checks. Unified build 168998 succeeded. Project build 168996 and package build 168997 had only unrelated macOS SQL Server container, ARM64 Azure SQL TLS, and managed identity infrastructure failures. Current-head CI is pending after the scope cleanup.Performance comparison
An interleaved best-of-three comparison covered 162 benchmarks with a 10% threshold:
Confirmed regressions
The strongest remaining performance signals are fixed per-call overhead in pooled
OpenAsync, allocation and synchronization cost in rapid open/close loops, and contention whenMaxPoolSizeis small relative to concurrency. Higher-concurrency workloads with adequately sized pools generally improved.Validation
ChannelDbConnectionPoolTest: 64/64 passed on net8.0, net9.0, and net10.0 before the rebase; 64/64 passed on net9.0 after rebasing ontomain.ChannelDbConnectionPoolReplaceConnectionTest: 16/16 passed on net8.0 and net9.0.Open_WithEnlistDisabled_DoesNotEnlistInAmbientTransaction: sync and async cases passed on net8.0 and net9.0.TvpTest.TestPacketNumberWraparoundpassed after the test fix.Suggested release note
Checklist