Add connection-creation rate limiting to ChannelDbConnectionPool#4396
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% 65.26% -0.58%
==========================================
Files 287 285 -2
Lines 43763 66887 +23124
==========================================
+ Hits 28812 43654 +14842
- Misses 14951 23233 +8282
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>
- NoOpAcquiredLease: document that the shared Instance is stateless and its Dispose is an idempotent no-op, safe to dispose repeatedly/concurrently. - ChannelDbConnectionPool: add a class-level pointer to the feature spec so the FR-0xx requirement tags in comments are resolvable; note at lease.Dispose() that the no-limiter case disposes the idempotent singleton; document the faulted flag and how the finally uses it to avoid a redundant idle-channel poke. - Tests: merge the three ErrorOccurred_* blocking-period facts into a single Theory parameterized by the connection string's Pool Blocking Period; comment that a rising TotalFailedLeases is how the wake test detects requestB was denied a permit and parked on the idle channel. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Replaces the hollow SuccessfulCreate_AfterFailure_ClearsErrorState test (which called pool.Clear() and made its final assertion vacuous) with Failure_ThenBlockingPeriodExpiry_AllowsSuccessfulCreate. The new test drives the blocking-period exit timer deterministically via an injected FakeTimeProvider, verifying the real recovery path: failure enters the blocking period, in-window requests fast-fail without touching the factory, and once the backoff elapses a create succeeds and clears the error state. Adds an optional TimeProvider parameter to the ChannelDbConnectionPool constructor (forwarded to BlockingPeriodErrorState) and to the ConstructPool test helper. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The ConcurrencyLimiter field doc previously suggested a limiter could be shared across pools. Because the permit-release wake is signaled on this pool's own idle channel, a permit released by another pool would not wake waiters parked here. Reword the doc to state the limiter is expected to be scoped to a single pool and that cross-pool sharing is unsupported, while keeping the caller-owns-lifetime note. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The permit-release poke condition read lease.IsAcquired after lease.Dispose() had run. Even though the current ConcurrencyLimiter lease keeps IsAcquired stable post-dispose, using an object after disposal is fragile if the lease implementation changes. Capture the value into a local before disposing and use the captured bool in the poke condition. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
priyankatiwari08
left a comment
There was a problem hiding this comment.
LGTM. Most of my observations were already raised in the latest review comments and have been addressed in the recent resolution commits.
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