Add connection-creation rate limiting to ChannelDbConnectionPool#4396
Add connection-creation rate limiting to ChannelDbConnectionPool#4396mdaigle wants to merge 9 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds opt-in connection-creation rate limiting to the v2 ChannelDbConnectionPool using System.Threading.RateLimiting.RateLimiter, plus introduces a lightweight always-acquired lease for the “no limiter configured” case. The PR also wires the new dependency via central package management, adds a feature spec/diagrams, and extends unit tests to cover rate limiting + blocking-period behaviors (stacked on Part 1 / #4395).
Changes:
- Add optional
RateLimiterintegration to throttle physical connection creation inChannelDbConnectionPool, with aNoOpAcquiredLeasefallback. - Wire
System.Threading.RateLimitinginto product + test projects (andDirectory.Packages.props). - Add spec + diagrams and expand
ChannelDbConnectionPoolTestcoverage for rate limiting and blocking-period behavior.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPool.cs | Adds optional rate-limiter gating for new physical opens and integrates blocking-period error state. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/NoOpAcquiredLease.cs | Adds a singleton no-op acquired RateLimitLease for the “no limiter” path. |
| Directory.Packages.props | Adds centralized package versions for System.Threading.RateLimiting. |
| src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj | References System.Threading.RateLimiting for product build. |
| src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj | References System.Threading.RateLimiting for unit test compilation across TFMs. |
| src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTests.csproj | References System.Threading.RateLimiting for manual test compilation across TFMs. |
| src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/ChannelDbConnectionPoolTest.cs | Adds/repairs tests covering blocking-period behavior and rate-limiter permit/lease handling. |
| specs/006-pool-rate-limiting/spec.md | Documents requirements and acceptance scenarios for rate limiting + blocking period. |
| specs/006-pool-rate-limiting/diagrams.md | Adds diagrams comparing existing vs. new rate-limiting flow. |
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).
806140e to
d3e2fe7
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4396 +/- ##
==========================================
- Coverage 65.83% 64.09% -1.75%
==========================================
Files 287 285 -2
Lines 43763 66875 +23112
==========================================
+ Hits 28812 42862 +14050
- Misses 14951 24013 +9062
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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>
|
Why the rate-limit acquire uses non-blocking fast-fail (context for the We considered two alternatives and rejected both:
Non-blocking fast-fail plus the idle-channel fallback prefers connection reuse and funnels both sources of capacity (a returned connection or a released permit) through a single wait path. Rearchitecture option we also rejected: route everything through an async channel read (prototyped as the pump-based pool in #4446). Instead of the current fast-path-plus-fallback, every caller would
For a concurrency limiter, permit availability is a concrete event (a lease release), which maps cleanly onto the pool's existing "wake on returned/created connection" signaling — so the pump's main justification (covering timer-based token replenishment with no per-connection wake event) doesn't apply. See #4446 for the full design and trade-off analysis. |
…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>
Copilot review comments — resolution summaryAddressed in commit
Not applied, with rationale:
Validation: product builds clean for net8.0 + net9.0; UnitTests build clean (0 warnings); |
|
@mdaigle how does the application set the rate limit? What is the experience? |
@saurabh500 I haven't exposed it at the moment. I don't want to make it configurable by customers until we decide that it's needed. For the initial release, I will at most set a default concurrency for on-prem if testing shows it to be necessary. |
Summary
This is Part 2 of 3 splitting #4376 ("Dev/mdaigle/pool rate limit") into stacked, reviewable PRs. It stacks on #4395 (Part 1) — review/merge that first.
This PR adds an optional connection-creation rate-limiting policy to
ChannelDbConnectionPool, decoupled from the blocking-period error state extracted in Part 1.Changes
ChannelDbConnectionPool— when an optionalSystem.Threading.RateLimiting.ConcurrencyLimiteris configured (null= no limiting), a new physical connection open requires a permit. If the permit is denied, the caller waits for an existing connection to be returned instead of forcing a second create. Leases are always released — including on a failed open — so the pool cannot starve future callers. The pool takes a concreteConcurrencyLimiter?rather than the abstractRateLimiterbase because a concurrency limiter is the only throttling we currently need (pooling against on-prem SQL Server); support for other limiter types can be added later if a concrete need arises.NoOpAcquiredLease— lightweight always-acquired lease used when no limiter is configured.System.Threading.RateLimitingtoDirectory.Packages.propsand references it from the product project plus the ManualTests/UnitTests projects.specs/006-pool-rate-limiting/(spec + diagrams).ChannelDbConnectionPoolTestcovers permit-available, permit-denied/reuse, and lease-disposal-on-failure using realConcurrencyLimiterinstances (asserting viaGetStatistics()).Design alternative considered (and rejected): pump-based pool — #4446
We prototyped a more invasive pump-based, fast-path-free rewrite of
ChannelDbConnectionPoolin #4446 and decided not to adopt it for this work. It's kept open as a documented reference.See #4446 for the full design and trade-off analysis.
Stacking
mainChecklist