Add pool tracing/metrics parity and surface pooled-open timeout cause - #4505
Closed
mdaigle wants to merge 47 commits into
Closed
Add pool tracing/metrics parity and surface pooled-open timeout cause#4505mdaigle wants to merge 47 commits into
mdaigle wants to merge 47 commits into
Conversation
Introduce an optional System.Threading.RateLimiting policy that throttles new physical connection opens in the channel pool: when a permit is denied the caller waits for a returned connection instead of forcing a create, and leases are always released (including on failure) to avoid starvation. Adds NoOpAcquiredLease, wires the RateLimiting package into the product and test projects, and includes the 006-pool-rate-limiting spec. Also repairs two pre-existing build breaks in ChannelDbConnectionPoolTest (a dropped CountingSuccessfulConnectionFactory declaration and DbConnectionPoolGroupOptions calls missing the new idleTimeout argument).
The connection pool only needs a concurrency limiter (pooling against on-prem SQL Server), so change ChannelDbConnectionPool to take a concrete System.Threading.RateLimiting.ConcurrencyLimiter? instead of the abstract RateLimiter base. The limiter remains optional (null = no limiting), and AttemptAcquire(1)/RateLimitLease usage is unchanged (both inherited). Rework the three rate-limiter unit tests to use real ConcurrencyLimiter instances and assert via GetStatistics() (CurrentAvailablePermits, TotalFailedLeases) instead of the now-removed TestRateLimiter double. Update the spec and diagram to describe a concurrency limiter specifically, noting other limiter types can be added later if needed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Remove the two "options to consider" TODOs above the AttemptAcquire call and replace them with a comment explaining why non-blocking fast-fail was chosen over failing immediately or blocking on the limiter. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Drop the two "options to consider" TODOs above the AttemptAcquire call. The rationale for choosing non-blocking fast-fail lives in the PR discussion rather than in code. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…nel-rate-limiting
Remove the redundant leaseAcquired local; read lease.IsAcquired directly in the early-return guard and the finally-block poke condition. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
RateLimiter_SuccessfulCreate_ReleasesLeaseForNextCreate exercises a single-permit ConcurrencyLimiter with two sequential opens against distinct owners. A leaked lease on the success path would deny the second open, so asserting both create physical connections (CreateCount == 2) guards the release-on-success behavior at the behavioral level rather than only via the permit counter. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Cover the previously untested concurrency behavior where a caller blocked purely by rate limiting is woken by another caller's lease release (the finally-block null poke) and then creates its own physical connection. RateLimiter_LeaseReleaseWakesRateLimitedWaiter_CreatesPhysicalConnection is a [Theory] over the sync and async idle-channel wait mechanisms. It uses a new GatedSuccessfulConnectionFactory that blocks the first physical create so the permit is held in-flight while a second caller is denied and parks on the idle channel; releasing the gate triggers the release poke that must wake and satisfy the waiter. Verified the test fails (waiter times out) when the poke is disabled. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…osal - Exclude OperationCanceledException from the creation-failure catch so a caller's own timeout/cancellation no longer poisons the pool blocking period. - Gate the finally idle-channel poke to non-faulted completion via a faulted flag, avoiding a redundant double wake on exception paths (cleanupCallback already writes a wake). - Document that the pool does not own the injected ConcurrencyLimiter and never disposes it (caller owns its lifetime). - Fix comment typo (rather then -> rather than) and trailing whitespace. - Reword spec User Story 1 / FR-002 from strict FIFO to best-effort idle-channel wait, matching the non-blocking AttemptAcquire implementation. - Dispose ConcurrencyLimiter instances in tests (using var) and drop the unused System.Collections.Generic using. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…cit method parameters.
A blank line inside the <remarks> block was missing its '///' prefix, causing CS1570 and breaking the build. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…onnection ReplaceConnection now tries GetIdleConnection() before establishing a new physical connection. When a live idle connection is available it is checked out and activated under the old connection's ambient transaction, then the replaced connection's slot is freed and it is disposed. This avoids an unnecessary physical connect and keeps the reserved slot count strictly decreasing, so the pool never exceeds MaxPoolSize. When no idle connection is available the previous create-and-swap path is used unchanged. In both paths the old connection is left untouched until the replacement is activated, so a failure leaves it reusable by the caller's reconnect retry loop. Adds ReplaceConnection_PrefersIdleOverNewConnection and ReplaceConnection_IdleReuse_AtMaxCapacity_FreesOldSlot unit tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Passing the boolean forceNewConnection flag positionally as a bare true/false obscures intent at the call site. Name the argument at every literal call site so the open/reconnect paths read clearly. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…eConnection The idle-reuse branch of ReplaceConnection previously deactivated and removed the reused connection if activation failed, unconditionally discarding a connection that was healthy moments earlier. Route the failure through ReturnInternalConnection instead so a still-healthy connection is re-pooled and only a genuinely dead one is removed, matching the normal get path. Since the reuse branch's check-out + activate + return-on-failure is now identical to PrepareConnection, call PrepareConnection directly to remove the duplication. Adds ReplaceConnection_IdleReuse_ActivationFails_ReturnedToPool. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Trim the large explanatory comments in ReplaceConnection so they no longer dominate the method, keeping the non-obvious rationale (slot accounting, reuse-on-failure, never over MaxPoolSize) in a couple of lines each. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Consolidate the scattered per-branch rationale in ReplaceConnection into a single header comment explaining the two invariants that shape the method (forward progress under pool saturation via atomic reservation handoff, and oldConnection as the failure anchor) and why the create branch cannot delegate to PrepareConnection. Slim the inline branch comments to short pointers so the control flow reads cleanly. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…nel-rate-limiting Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…e/replace-conn-2 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Revert the named-to-positional reversions that crept into the TryOpenInner call sites in SqlConnectionConcurrentOpenTests and SqlConnectionStateTransitionTests, restoring the readable forceNewConnection: false/true form to match the rest of the branch and the call sites on main. Also restore the accidental missing space after the comma in the TryOpenWithRetry parameter list. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…t/SqlClient into dev/mdaigle/replace-conn-2
- Remove stray blank doc line that split the forceNewConnection <remarks> sentence into two paragraphs in generated docs. - Correct the TestReplaceConnection summary (it no longer asserts NotImplementedException) and move it out of the 'Not Implemented Method Tests' region into a dedicated 'Replace Connection Tests' region. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Add a class-level XML summary to ChannelDbConnectionPoolReplaceConnectionTest describing the behavior under test. - Replace the try/catch that swallowed the expected InvalidOperationException in ReplaceConnection_ActivationFails_NewConnectionReturnedToPool with an explicit Assert.Throws, matching the sibling failure-path tests and making the intent fail-safe. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…path The create branch of ReplaceConnection now honors the pool's blocking-period error state (ThrowIfActive) before opening a new physical connection and clears the backoff ramp on a successful open, mirroring OpenNewInternalConnection. Idle reuse stays exempt, and a reconnect failure still does not enter the error state by design, so a targeted reconnect cannot poison the pool. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…onn-2 Resolve conflicts in the connection-pool area: - NoOpAcquiredLease.cs: keep main's fuller doc comments (code identical). - ChannelDbConnectionPool.cs: take main's refined rate-limiting/blocking-period and background-warmup code; the branch's ReplaceConnection implementation and PrepareConnection transaction parameter live in non-conflicting regions. - ChannelDbConnectionPoolTest.cs: adopt main's consolidated/deterministic blocking-period and rate-limiter tests; drop the now-obsolete TestReplaceConnection stub (ReplaceConnection is implemented and covered by ChannelDbConnectionPoolReplaceConnectionTest.cs). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Localize the "slot could not be replaced" guard in ChannelDbConnectionPool.ReplaceConnection: replace the hard-coded InvalidOperationException with ADP.InternalError using a new InternalErrorCode.ConnectionSlotReplacementFailed (still an InvalidOperationException, so behavior is unchanged). - Correct the TryOpenInner forceNewConnection XML remarks: the flag is also valid when the connection was previously opened and is now disconnected (the reconnect path via DbConnectionClosedPreviouslyOpened / DbConnectionClosedConnecting), not only when already open. Also removes a stray blank doc line by using <para> blocks. - Fix the activation-failure test so its name, summary, and inline comment match the implementation: the new connection is disposed (never slotted) and the old connection is left intact, so pool count is unchanged. - Remove unused usings (Microsoft.Data.Common, Microsoft.Data.Common.ConnectionString) from the ReplaceConnection tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Replace the opaque ADP.InternalError(ConnectionSlotReplacementFailed) at the ReplaceConnection !replaced guard with a localized InvalidOperationException (SQL_ConnectionPoolReplaceConnectionFailed). Removes the now-unused InternalErrorCode.ConnectionSlotReplacementFailed enum value. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Document that the ReplaceConnection create branch intentionally skips _connectionCreationRateLimiter: a replacement is a 1-for-1 swap (not pool growth) and must make forward progress for an already checked-out caller's reconnect, so the limiter's fast-fail-then-wait-for-idle contract does not apply. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The new ReplaceConnection tests built pools on TimeProvider.System, letting time-driven background maintenance (idle-timeout pruning, warmup/replenishment, blocking-period expiry) advance in real time and potentially race the assertions. Thread a frozen FakeTimeProvider through the replacement test helper (default) and the TestReplaceConnection case so the pool clock only moves when a test drives it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Brings in the completed connection-pool pruning work (Story 2/3/4, #4463), which reworks PoolPruner to be driven by Connection Idle Timeout and only constructs a Pruner when IdleTimeout != 0. The single overlapping file, ChannelDbConnectionPool.cs, auto-merged cleanly: main's constructor pruner block coexists with this branch's ReplaceConnection additions. Also pulls in #4460 (unobserved-exception repro), #4347 (vector test refactor), and #4459 (pool benchmark coverage). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Mirror WaitHandleDbConnectionPool: when the physical open of a replacement connection fails, enter the blocking-period error state so subsequent opens fast-fail until it expires. Activation failures are excluded (the server proved reachable), matching the WaitHandle pool where PrepareConnection runs outside CreateObject's error-state catch. Adds two tests and updates the creation-failure retry test to reflect that the failed open now enters the blocking period. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- ConnectionPoolSlotsTest: move null-forgiveness to the Add() assignment instead of every use site; confirm the untouched occupant survives a failed TryReplace; add a self-replace test (benign no-op). - SqlConnection: name all args in the Open overrides ternary; drop a stray blank line. - ReplaceConnection tests: assert the replacement is not the old connection; assert the blocking-period throw is the same cached exception instance (with the factory flipped back to succeeding to prove the create path never ran). - Collapse the three test factories into one TunableSqlConnectionFactory (FailOnCreate/FailOnActivate) and fold ActivationFailDbConnectionInternal into StubDbConnectionInternal, which now reads the factory's flag live so idle-reuse tests can toggle it after creation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Ports the transacted-pool state machine from WaitHandleDbConnectionPool so the channel pool honors ambient System.Transactions enlistment: - Implement PutObjectFromTransactedPool and TransactionEnded (previously NotImplementedException). - Rewrite ReturnInternalConnection to mirror DeactivateObject: deactivate first, then route the connection to the transacted pool, stasis, the idle channel, or destruction under the connection lock. - Vend connections already enlisted in the ambient transaction via a new GetFromTransactedPool helper, and pass the transaction through to PrepareConnection/ActivateConnection. - Set the ambient transaction on the async acquisition path from the TaskCompletionSource's AsyncState. - Guard RemoveConnection against disposing a transaction root that is still waiting for its delegated transaction to end. Adds ChannelDbConnectionPoolTransactionTest mirroring the WaitHandle pool's transaction test suite, and drops the stale NotImplementedException tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The async open path ran GetInternalConnection inside a Task.Run and restored the ambient transaction by assigning Transaction.Current on that thread pool thread. That assignment writes to thread-static storage which ExecutionContext does not unwind, so the transaction outlived the open and was observable by unrelated work later scheduled onto the same thread -- including the login-time auto-enlistment that non-pooled connections perform against Transaction.Current. A try/finally restore is not sufficient either, because the continuation may resume on a different thread than the one that was polluted. Instead, capture the ambient transaction on the caller's thread (from the TaskCompletionSource's AsyncState, which is where SqlConnection.OpenAsync puts it) and thread it explicitly through GetInternalConnection into GetFromTransactedPool and PrepareConnection. The sync path passes ADP.GetCurrentTransaction() directly since it runs on the caller's thread. Also gate the transaction on HasTransactionAffinity in one place so a pool without automatic enlistment neither reads from nor writes to the transacted store. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A SqlConnection that is garbage collected without ever being closed or disposed leaves its internal connection "emancipated": still tracked by the pool, but with no owner that could ever return it. WaitHandleDbConnectionPool sweeps for these before waiting for a free connection; ChannelDbConnectionPool did not, so an emancipated connection permanently occupied a pool slot. At MaxPoolSize that meant every subsequent Open timed out -- forever, not just once. GetInternalConnection now performs the same sweep just before parking on the idle channel. This is deliberately confined to the slow path: it is O(MaxPoolSize) and allocates a snapshot, so it must not run on the hot acquire path. The sweep takes the connection lock with Monitor.TryEnter rather than Enter. IsEmancipated has to be read under that lock to avoid racing PrePush/PostPop, but a connection that is currently locked is being actively handed out or returned and therefore is not emancipated anyway, so skipping it costs nothing and keeps the sweep from blocking the caller. Only PrePush happens under the lock; deactivation, which can make server round trips, is deferred until all locks are released. Deactivating and routing a returned connection is now factored out of ReturnInternalConnection into DeactivateAndRouteConnection so reclamation can share it. Reclamation must not go through ReturnInternalConnection itself because it has already performed the PrePush and there is no owning object left to validate against. Tests: - Added ConnectionPoolVersionScope, which flips the pool version switch and clears all pools on both entry and exit. Clearing is required because a pool binds to its implementation at creation time, so without it pools leak across tests. - Parameterized ReclaimEmancipatedOnOpenTest and MaxPoolWaitForConnectionTest by pool version. ReclaimEmancipatedOnOpenTest fails against ChannelDbConnectionPool without this fix. - Three pool-exhaustion unit tests let their owning SqlConnections go out of scope, so reclamation could legitimately hand the "should time out" waiter a connection. They now keep the owners alive, which is what they meant anyway. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Three remaining behavioural gaps between ChannelDbConnectionPool and WaitHandleDbConnectionPool, none of which had test coverage. 1. Pool metrics were never emitted. PooledConnections, FreeConnections, ActiveConnections and the soft/hard connect and disconnect counters all read zero under this pool. Wired up the same call sites the wait handle pool uses. IdleConnectionChannel is a convenient single choke point for the free connection counters, since every idle enqueue and dequeue passes through it. 2. Count reported reservations rather than connections. Reservations include connections that are still being opened, whereas the wait handle pool's Count is its total object count. This broke the SQL Express user instance path in SqlConnectionFactory.CreateConnection, which branches on `pool.Count <= 0`: it took the wrong branch and threw a NullReferenceException out of SqlConnectionOptions.ValidateValueLength because providerInfo.InstanceName was never populated. Added ConnectionPoolSlots.ConnectionCount, which tracks slot occupancy rather than reservations, and pointed Count at it. 3. Async opens always completed asynchronously. WaitHandleDbConnectionPool makes a non-blocking, non-creating attempt at an idle connection before enqueuing a pending open; this pool did not, so OpenAsync against a warm pool always took a thread pool hop. Added the same fast path. It deliberately does not try to *create* a connection, which can block on the wire and must stay off the caller's thread. Transactional requests are excluded from the fast path. They have to consult the transacted store first for a connection already enlisted in the same transaction, which only GetInternalConnection does; taking a plain idle connection would both miss that affinity and skip enlistment. Tests: - Parameterized ConnectionResiliencySPIDTest and MetricsTest.PooledConnectionsCounters_Functional by pool version. - ChannelDbConnectionPoolTest.StressTestAsync awaited its TaskCompletionSource unconditionally, which hangs now that TryGetConnection can complete synchronously. - TvpTest.TestPacketNumberWraparound passed an async lambda to Task.Factory.StartNew and so awaited a Task<Task>, never observing the inner task or its failures. Added the missing Unwrap. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Instrument ChannelDbConnectionPool with TryPoolerTraceEvent calls across the connection lifecycle so it matches the categories traced by the WaitHandle pool: construction, get, create, return, remove/dispose, clear, startup, shutdown, prune, rate-limit throttle, error state, wait timeout, and the reason a connection was rejected as not live. Fill the two remaining metric gaps in ReplaceConnection, which disposed the old and failed-new connections without counting a hard disconnect. Also address GH#3545: record the last physical-connection-create exception on each pool and attach it as the inner exception of the pooled-open timeout, so callers see why the pool could not produce a connection. Fixes #3545 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Improves connection-pool diagnostics by bringing ChannelDbConnectionPool up to parity with the legacy WaitHandleDbConnectionPool for pooler tracing and metrics, and by surfacing the last physical connection creation failure as the inner exception on pooled-open timeouts (GH#3545).
Changes:
- Adds pool-lifecycle
TryPoolerTraceEventemissions throughoutChannelDbConnectionPoolto match legacy pool trace conventions. - Completes metric parity for the channel pool (notably around
ReplaceConnectionhard-disconnect accounting). - Introduces
IDbConnectionPool.LastConnectionCreateExceptionplusADP.PooledOpenTimeout(Exception inner)plumbing so pooled-open timeouts can carry the most recent physical open failure as an inner exception.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolBlockingPeriodTest.cs | Adds coverage that the legacy pool records and clears the last-create exception correctly. |
| src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/TransactedConnectionPoolTest.cs | Updates the test mock pool to implement the new LastConnectionCreateException interface member. |
| src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/ChannelDbConnectionPoolInstrumentationTest.cs | New test suite validating channel-pool trace emission, metric deltas, and last-create-exception timeout plumbing. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs | Attaches the pool’s last-create exception to pooled-open timeouts thrown from the factory path. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs | Tracks/clears the last physical create exception and uses it when pending opens time out. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/IDbConnectionPool.cs | Adds LastConnectionCreateException to standardize timeout-cause reporting across pool implementations. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPool.cs | Adds trace parity, last-create-exception storage/clearing, and missing hard-disconnect accounting on replace paths. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs | Adds ADP.PooledOpenTimeout(Exception inner) overload to optionally carry an inner exception. |
Comment on lines
+582
to
+587
| private const string SqlClientEventSourceName = "Microsoft.Data.SqlClient.EventSource"; | ||
|
|
||
| // Mirrors SqlClientEventSource.Keywords.PoolerTrace. Duplicated as a literal because | ||
| // that type is not visible to this assembly. | ||
| private const EventKeywords PoolerTraceKeyword = (EventKeywords)32; | ||
|
|
mdaigle
marked this pull request as draft
August 5, 2026 16:48
Open
14 tasks
mdaigle
force-pushed
the
dev/automation/channel-pool-v2-followups
branch
from
August 12, 2026 19:17
ebe1433 to
0b0054f
Compare
3 tasks
Contributor
Author
|
Folded into #4504, which is now rebased onto main and contains this work. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #4504.
Summary
Brings
ChannelDbConnectionPoolto trace and metric parity withWaitHandleDbConnectionPool, and surfaces the cause of a pooled-open timeout.Trace parity
Adds
TryPoolerTraceEventcalls across the connection lifecycle, following the WaitHandle pool's message conventions:Constructed. MinPoolSize=..., MaxPoolSize=...GetInternalConnection:Getting connection.,Wait timed out.,Pool is shutting down; abandoning wait.GetIdleConnection:Popped from general pool.OpenNewInternalConnection:Errors are set.,Creating new connection.,Added to pool., rate-limiter saturation, pool-full, and create-threwPutConnectionInIdleChannel:Pushing to general pool.DeactivateAndRouteConnection: the stasis/transacted routing decisionRemoveConnection:Removing from pool.,Removed from pool.,Disposed.PruneConnections: prune start and resultIsLiveConnection: the reason a connection was rejected (idle timeout, dead, load balance timeout, stale generation)Metric parity
Most metric wiring landed in #4504 via
IdleConnectionChannel. This closes the two remaining gaps inReplaceConnection, which disposed the old connection and the failed new connection without counting aHardDisconnectRequest.Note: the WaitHandle pool leaks an
EnterPooledConnectionon replace. The channel pool swaps in place viaConnectionPoolSlots.TryReplace, so the pooled gauge is correctly left untouched. That behavior is intentionally not replicated.Pooled-open timeout cause (#3545)
Today a pooled-open timeout hides why the pool could not produce a connection. This adds
IDbConnectionPool.LastConnectionCreateException, recorded on create failure and cleared on success in both pools, and a newADP.PooledOpenTimeout(Exception inner)overload.SqlConnectionFactoryand the WaitHandle pool's pending-opens path now attach it as the inner exception.Fixes #3545
Out of scope
OpenTelemetry
db.client.connections.*semantic-convention metrics (Story 4 of the original request) are deferred to a follow-up.Suggested release note
UseConnectionPoolV2) now emits the same pooler trace events and connection metrics as the default pool.Testing
New
ChannelDbConnectionPoolInstrumentationTest(17 tests) covering trace emission for each operation category, metric deltas for soft connect/disconnect and hard disconnect, and the last-create-exception plumbing. Adds a matching WaitHandle-pool test. 343 connection-pool unit tests pass onnet9.0;net8.0builds clean.Checklist
IDbConnectionPoolis internal)