Skip to content

Commit 000531b

Browse files
mdaigleCopilot
andcommitted
Address Paul's review: doc/comment clarity and test consolidation
- 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>
1 parent 36e0a51 commit 000531b

3 files changed

Lines changed: 39 additions & 66 deletions

File tree

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPool.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ namespace Microsoft.Data.SqlClient.ConnectionPool
4848
///
4949
/// The trade-off is slightly higher memory overhead per pool instance due to the channel infrastructure,
5050
/// but this is generally offset by the performance benefits in async-heavy workloads.
51+
///
52+
/// <para>
53+
/// Comments in this file reference requirements by tag (e.g. <c>FR-001</c>). These tags are
54+
/// defined in the feature spec at <c>specs/006-pool-rate-limiting/spec.md</c> (see the
55+
/// "Functional Requirements" section); consult it for the authoritative description of each
56+
/// requirement.
57+
/// </para>
5158
/// </summary>
5259
internal sealed class ChannelDbConnectionPool : IDbConnectionPool, IDisposable
5360
{
@@ -581,6 +588,13 @@ public bool TryGetConnection(
581588
// FR-001, FR-002, FR-003.
582589

583590
RateLimitLease lease = _connectionCreationRateLimiter?.AttemptAcquire(1) ?? NoOpAcquiredLease.Instance;
591+
592+
// Tracks whether we are leaving this block via an exception rather than a
593+
// normal return. It is read in the finally below to decide whether to poke
594+
// the idle channel: every return path sets it to false first, so by the time
595+
// the finally runs it is true only when an exception is propagating. This
596+
// lets us skip the wake on exception paths, where cleanupCallback already
597+
// pokes, and avoid a redundant double wake.
584598
bool faulted = true;
585599
try
586600
{
@@ -624,6 +638,9 @@ public bool TryGetConnection(
624638
// null poke and retry its acquire before the permit is actually returned,
625639
// fail to acquire, and fall back to waiting with no subsequent signal -
626640
// stalling connection creation even though the limiter has capacity.
641+
// When no limiter is configured this is NoOpAcquiredLease.Instance, whose
642+
// Dispose is an idempotent no-op, so disposing the shared singleton here
643+
// is safe.
627644
lease.Dispose();
628645

629646
// After releasing, signal a waiter on the idle channel that they may now

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/NoOpAcquiredLease.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ namespace Microsoft.Data.SqlClient.ConnectionPool
1919
internal sealed class NoOpAcquiredLease : RateLimitLease
2020
{
2121
/// <summary>
22-
/// The shared singleton instance.
22+
/// The shared singleton instance. Because it is stateless and its <see cref="Dispose"/>
23+
/// releases nothing, the same instance is handed to every caller that opens without a
24+
/// configured limiter and may be disposed any number of times, on any thread, without
25+
/// affecting other callers.
2326
/// </summary>
2427
public static readonly NoOpAcquiredLease Instance = new();
2528

@@ -39,7 +42,9 @@ public override bool TryGetMetadata(string metadataName, out object? metadata)
3942

4043
protected override void Dispose(bool disposing)
4144
{
42-
// No resources to release.
45+
// No resources to release. This is intentionally idempotent: the shared Instance is
46+
// handed to every no-limiter open, so Dispose may be called repeatedly and
47+
// concurrently with no effect.
4348
}
4449
}
4550
}

src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/ChannelDbConnectionPoolTest.cs

Lines changed: 15 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1610,46 +1610,21 @@ public void Constructor_WithValidSmallPoolSizes_WorksCorrectly()
16101610

16111611
/// <summary>
16121612
/// Verifies that a connection creation failure enters the blocking-period error state when
1613-
/// blocking is enabled for the pool.
1613+
/// blocking is enabled for the pool, and stays out of it when blocking is disabled. The
1614+
/// blocking policy is driven by the connection string's Pool Blocking Period:
1615+
/// Default/Auto enable blocking for a non-Azure host (localhost), AlwaysBlock forces it on,
1616+
/// and NeverBlock suppresses it. FR-006, FR-007.
16141617
/// </summary>
1615-
[Fact]
1616-
public void ErrorOccurred_FailureWithBlockingEnabled_BecomesTrue()
1617-
{
1618-
// Arrange
1619-
// Default PoolBlockingPeriod is Auto; localhost is non-Azure so blocking is enabled.
1620-
var dbConnectionPoolGroup = new DbConnectionPoolGroup(
1621-
new SqlConnectionOptions("Data Source=localhost;"),
1622-
new ConnectionPoolKey("TestDataSource", credential: null, accessToken: null, accessTokenCallback: null, sspiContextProvider: null),
1623-
new DbConnectionPoolGroupOptions(
1624-
poolByIdentity: false,
1625-
minPoolSize: 0,
1626-
maxPoolSize: 4,
1627-
creationTimeout: 15,
1628-
loadBalanceTimeout: 0,
1629-
hasTransactionAffinity: true,
1630-
idleTimeout: 0));
1631-
var pool = ConstructPool(TimeoutConnectionFactory, dbConnectionPoolGroup: dbConnectionPoolGroup);
1632-
1633-
// Act
1634-
Assert.False(pool.ErrorOccurred);
1635-
1636-
Assert.Throws<InvalidOperationException>(() =>
1637-
pool.TryGetConnection(new SqlConnection(), taskCompletionSource: null, TimeoutTimer.StartNew(TimeSpan.FromSeconds(15)), out _));
1638-
1639-
// Assert
1640-
Assert.True(pool.ErrorOccurred);
1641-
}
1642-
1643-
/// <summary>
1644-
/// Verifies that a connection creation failure does not enter the blocking-period error state
1645-
/// when the connection string disables blocking with NeverBlock.
1646-
/// </summary>
1647-
[Fact]
1648-
public void ErrorOccurred_FailureWithNeverBlock_StaysFalse()
1618+
[Theory]
1619+
[InlineData("", true)] // Default (unspecified) => Auto => blocks for localhost
1620+
[InlineData("Pool Blocking Period=Auto;", true)] // Auto => blocks for non-Azure host
1621+
[InlineData("Pool Blocking Period=NeverBlock;", false)]
1622+
[InlineData("Pool Blocking Period=AlwaysBlock;", true)]
1623+
public void ErrorOccurred_OnFailure_FollowsBlockingPeriod(string blockingPeriodClause, bool expectErrorOccurred)
16491624
{
16501625
// Arrange
16511626
var dbConnectionPoolGroup = new DbConnectionPoolGroup(
1652-
new SqlConnectionOptions("Data Source=localhost;Pool Blocking Period=NeverBlock;"),
1627+
new SqlConnectionOptions($"Data Source=localhost;{blockingPeriodClause}"),
16531628
new ConnectionPoolKey("TestDataSource", credential: null, accessToken: null, accessTokenCallback: null, sspiContextProvider: null),
16541629
new DbConnectionPoolGroupOptions(
16551630
poolByIdentity: false,
@@ -1662,40 +1637,13 @@ public void ErrorOccurred_FailureWithNeverBlock_StaysFalse()
16621637
var pool = ConstructPool(TimeoutConnectionFactory, dbConnectionPoolGroup: dbConnectionPoolGroup);
16631638

16641639
// Act
1665-
Assert.Throws<InvalidOperationException>(() =>
1666-
pool.TryGetConnection(new SqlConnection(), taskCompletionSource: null, TimeoutTimer.StartNew(TimeSpan.FromSeconds(15)), out _));
1667-
1668-
// Assert - FR-007: NeverBlock must not enter the error state.
16691640
Assert.False(pool.ErrorOccurred);
1670-
}
16711641

1672-
/// <summary>
1673-
/// Verifies that a connection creation failure enters the blocking-period error state when
1674-
/// the connection string explicitly enables AlwaysBlock.
1675-
/// </summary>
1676-
[Fact]
1677-
public void ErrorOccurred_FailureWithAlwaysBlock_BecomesTrue()
1678-
{
1679-
// Arrange
1680-
var dbConnectionPoolGroup = new DbConnectionPoolGroup(
1681-
new SqlConnectionOptions("Data Source=localhost;Pool Blocking Period=AlwaysBlock;"),
1682-
new ConnectionPoolKey("TestDataSource", credential: null, accessToken: null, accessTokenCallback: null, sspiContextProvider: null),
1683-
new DbConnectionPoolGroupOptions(
1684-
poolByIdentity: false,
1685-
minPoolSize: 0,
1686-
maxPoolSize: 4,
1687-
creationTimeout: 15,
1688-
loadBalanceTimeout: 0,
1689-
hasTransactionAffinity: true,
1690-
idleTimeout: 0));
1691-
var pool = ConstructPool(TimeoutConnectionFactory, dbConnectionPoolGroup: dbConnectionPoolGroup);
1692-
1693-
// Act
16941642
Assert.Throws<InvalidOperationException>(() =>
16951643
pool.TryGetConnection(new SqlConnection(), taskCompletionSource: null, TimeoutTimer.StartNew(TimeSpan.FromSeconds(15)), out _));
16961644

16971645
// Assert
1698-
Assert.True(pool.ErrorOccurred);
1646+
Assert.Equal(expectErrorOccurred, pool.ErrorOccurred);
16991647
}
17001648

17011649
/// <summary>
@@ -1956,6 +1904,9 @@ public async Task RateLimiter_LeaseReleaseWakesRateLimitedWaiter_CreatesPhysical
19561904
"Timed out waiting for the first open to begin physical creation.");
19571905

19581906
// Caller B is denied a permit (A holds it) and must fall back to the idle-channel wait.
1907+
// A denied AttemptAcquire increments the limiter's TotalFailedLeases, so watching that
1908+
// counter rise is how we confirm B was refused a permit and is now parked on the idle
1909+
// channel waiting for a wakeup (rather than still racing to acquire).
19591910
long failedLeasesBefore = rateLimiter.GetStatistics()!.TotalFailedLeases;
19601911
Task<DbConnectionInternal?> requestB = Open(new SqlConnection());
19611912
Assert.True(

0 commit comments

Comments
 (0)