[DO NOT MERGE] 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
Flip the default value of the UseConnectionPoolV2 AppContext switch from false to true, making the new Channel-based connection pool (ChannelDbConnectionPool) the default implementation. The legacy V1 pool (WaitHandleDbConnectionPool) remains available by explicitly setting the switch to false. - Update XML doc comment on the switch to reflect the new default - Update features.instructions.md default value table - Update LocalAppContextSwitchesTest default-value assertion Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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` |
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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,
Task.Factory.StartNew with an async lambda returns Task<Task>. Without Unwrap(), Task.WhenAny observed only the outer task, which completed as soon as the async lambda hit its first await, rather than waiting for RunPacketNumberWraparound to actually finish. This masked itself under the legacy WaitHandleDbConnectionPool's synchronous-leaning timing, but was exposed by ChannelDbConnectionPool's genuinely asynchronous pooled open path, producing spurious low-enumerator-count failures. Also capture and await the winning task when it is actionTask so any unexpected failure (e.g. a connection open failure) propagates as a real exception instead of surfacing only as a generic count mismatch. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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;
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/TvpTest.cs:86
- If the timeout task wins, the test cancels the CTS and continues without ever observing the eventual completion/fault of actionTask. If OpenAsync faults after cancellation (e.g., OperationCanceledException), that exception can become unobserved and show up later as UnobservedTaskException noise. Consider attaching a fault-only continuation in the timeout case to observe exceptions without blocking the test.
if (completedTask == actionTask)
{
await actionTask;
}
CI results summary
This PR flips
UseConnectionPoolV2totrueso the full CI matrix runs againstChannelDbConnectionPoolby default. That surfaced three differences:ReclaimEmancipatedOnOpenTestexposed missing reclaim-on-open behavior in V2. The fix is now supplied by this PR's base, Channel Pool: Reclaim leaked connections #4529.TvpTest.TestPacketNumberWraparoundexposed a latentTask<Task>test bug. Commit443b4f00eunwraps the task and propagates early failures correctly.AmbientTransactionFailureTestexposed a V2 timeout race on Windows. Commitd80ca6845preserves the physical connection error instead of replacing it with a pool-exhaustion timeout.CI is rerunning with all three findings addressed. The title remains
[DO NOT MERGE]until the matrix confirms the default flip is clean.Summary
Changes the default connection pool from
WaitHandleDbConnectionPooltoChannelDbConnectionPool. Applications can restore the legacy pool explicitly:This PR is stacked on #4529 (
dev/automation/channel-pool-reclaim-timer), which contains the V2 emancipated-connection reclamation fix discovered by the first CI run.Changes
UseConnectionPoolV2's default totrue.TvpTest.TestPacketNumberWraparoundsoTask.Factory.StartNew's nested task is unwrapped and observed.Ambient transaction failure root cause
The failing Windows legs expected the invalid server open to throw
SqlException, but V2 returned:ChannelDbConnectionPool.GetInternalConnectioncreates a cancellation token from the caller'sTimeoutTimer. It previously passed that token intoOpenNewInternalConnection, which checks cancellation before callingCreatePooledConnection. With the test's one-second timeout, Windows could consume the remaining budget before that check. V2 then converted the resultingOperationCanceledExceptionintoADP.PooledOpenTimeout(), even though the pool was empty and no pool contention occurred.V1 does not preempt physical creation this way. It passes the remaining or expired
TimeoutTimerintoCreatePooledConnection, allowing the physical-open failure to remain aSqlException.The fix keeps the caller's timeout-derived cancellation token on actual pool waits, but does not use it to preempt an empty-pool physical connection attempt. Background warmup still passes its own cancellation token. The physical factory receives the original timeout budget, preserving V1 behavior without extending the timeout.
TVP test root cause
The test used
Task.Factory.StartNew(async () => ...), which returnsTask<Task>.Task.WhenAnyobserved only the outer task and could continue after the async delegate's first suspension, while the TVP operation was still running. V2's async timing made that latent test bug reproducible.The test now uses a non-async delegate plus
.Unwrap()and awaits the action task when it wins, so earlyOpenAsyncfailures propagate directly and the enumerator assertion runs only after the operation completes.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.AmbientTransactionFailureTest: 2/2 passed locally on net8.0 and net9.0.TvpTest.TestPacketNumberWraparoundpassed after commit443b4f00e.Suggested release note
Checklist