Skip to content

Commit b24d4fb

Browse files
 Address PR #4302 review feedback and drop spec FR-00x labels
Code review fixes (WaitHandleDbConnectionPool): - TryGetConnection shutdown short-circuit now releases the PoolSemaphore slot when WaitAny was woken by SEMAPHORE_HANDLE (or WAIT_ABANDONED+SEMAPHORE_HANDLE), preventing a semaphore-slot leak that would starve future waiters. - Shutdown() releases Volatile.Read(ref _waitCount) slots instead of MaxPoolSize, fixing ArgumentOutOfRangeException when MaxPoolSize == 0 (unlimited pool) and ensuring every parked waiter is woken under high contention. Kept SemaphoreFullException guard. Test fix: - Shutdown_UnblocksSyncWaiter replaces Thread.Sleep(200) with a bounded poll on the private _waitCount field (via the existing reflection helper) so the test is deterministic on slow CI agents. Documentation cleanup: - Removed inline 'FR-00x' spec-traceability labels from comments and test section banners across both pool implementations and both shutdown test files. Explanatory text is preserved; the spec coverage will live in the PR description instead of the source.
1 parent 6431209 commit b24d4fb

4 files changed

Lines changed: 64 additions & 39 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ internal sealed class ChannelDbConnectionPool : IDbConnectionPool
9595

9696
/// <summary>
9797
/// Tracks whether <see cref="Shutdown"/> has already initiated the shutdown sequence so that
98-
/// repeated calls are observed as no-ops (FR-006). Updated atomically via
98+
/// repeated calls are observed as no-ops. Updated atomically via
9999
/// <see cref="Interlocked.CompareExchange(ref int, int, int)"/>.
100100
/// </summary>
101101
private int _shutdownInitiated;
@@ -273,7 +273,7 @@ public void ReturnInternalConnection(DbConnectionInternal connection, DbConnecti
273273
/// <inheritdoc />
274274
public void Shutdown()
275275
{
276-
// FR-006: idempotent. Compare-and-exchange ensures only one caller performs shutdown work.
276+
// idempotent. Compare-and-exchange ensures only one caller performs shutdown work.
277277
if (Interlocked.CompareExchange(ref _shutdownInitiated, 1, 0) != 0)
278278
{
279279
return;

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

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ public bool IsRunning
329329

330330
private void CleanupCallback(object state)
331331
{
332-
// FR-005: if the pool is shutting down, skip work. Shutdown disposes the timer, but
332+
// If the pool is shutting down, skip work. Shutdown disposes the timer, but
333333
// a callback may already be in-flight when Shutdown runs; this guard ensures it does
334334
// not perform pruning or re-arm pool create requests.
335335
if (State == ShuttingDown)
@@ -775,7 +775,7 @@ private void DestroyObject(DbConnectionInternal obj)
775775

776776
private void ErrorCallback(object state)
777777
{
778-
// FR-005: skip work if the pool is shutting down. The shutdown path disposes the
778+
// Skip work if the pool is shutting down. The shutdown path disposes the
779779
// timer; this guard handles the in-flight-callback race.
780780
if (State == ShuttingDown)
781781
{
@@ -971,11 +971,26 @@ private bool TryGetConnection(DbConnection owningObject, uint waitForMultipleObj
971971
{
972972
waitResult = WaitHandle.WaitAny(_waitHandles.GetHandles(allowCreate), unchecked((int)waitForMultipleObjectsTimeout));
973973

974-
// FR-007: after waking, observe shutdown state and bail out so waiters
975-
// do not spin against a drained pool.
974+
// After waking, observe shutdown state and bail out so waiters
975+
// do not spin against a drained pool. If WaitAny consumed a
976+
// PoolSemaphore slot, release it back so the accounting stays
977+
// balanced; otherwise the slot would leak and other waiters
978+
// (or callers that arrive after Shutdown completes its own
979+
// Release loop) would starve.
976980
if (State != Running)
977981
{
978982
SqlClientEventSource.Log.TryPoolerTraceEvent("<prov.DbConnectionPool.GetConnection|RES|CPOOL> {0}, Pool is shutting down; abandoning wait.", Id);
983+
if (waitResult == SEMAPHORE_HANDLE || waitResult == WAIT_ABANDONED + SEMAPHORE_HANDLE)
984+
{
985+
try
986+
{
987+
_waitHandles.PoolSemaphore.Release(1);
988+
}
989+
catch (SemaphoreFullException)
990+
{
991+
// Pool semaphore was already saturated by Shutdown's bulk release; safe to ignore.
992+
}
993+
}
979994
Interlocked.Decrement(ref _waitCount);
980995
connection = null;
981996
return false;
@@ -1507,35 +1522,40 @@ public void Shutdown()
15071522
{
15081523
SqlClientEventSource.Log.TryPoolerTraceEvent("<prov.DbConnectionPool.Shutdown|RES|INFO|CPOOL> {0}", Id);
15091524

1510-
// FR-006: idempotent. Subsequent calls observe ShuttingDown and bail.
1525+
// Idempotent: subsequent calls observe ShuttingDown and bail.
15111526
if (State == ShuttingDown)
15121527
{
15131528
return;
15141529
}
15151530
State = ShuttingDown;
15161531

1517-
// FR-005: dispose all background timers so they no longer schedule new work.
1532+
// Dispose all background timers so they no longer schedule new work.
15181533
// Note that any timer callback already in flight may still observe State == ShuttingDown
15191534
// and short-circuit (see CleanupCallback / ErrorCallback).
15201535
Timer cleanup = Interlocked.Exchange(ref _cleanupTimer, null);
15211536
cleanup?.Dispose();
15221537
Timer error = Interlocked.Exchange(ref _errorTimer, null);
15231538
error?.Dispose();
15241539

1525-
// FR-007: wake any threads parked in WaitHandle.WaitAny by releasing the pool semaphore
1526-
// up to its max count. Waiters check State == Running after wake-up and bail.
1527-
// We may release more than necessary; SemaphoreFullException is harmless when the
1528-
// semaphore is already saturated.
1529-
try
1540+
// Wake any threads parked in WaitHandle.WaitAny by releasing as many semaphore
1541+
// slots as there are recorded waiters. Using _waitCount (rather than MaxPoolSize)
1542+
// avoids ArgumentOutOfRangeException when MaxPoolSize == 0 (unlimited) and ensures
1543+
// we wake every parked waiter even when _waitCount exceeds MaxPoolSize. Waiters
1544+
// observe State != Running after wake-up and bail.
1545+
int waitersToWake = Volatile.Read(ref _waitCount);
1546+
if (waitersToWake > 0)
15301547
{
1531-
_waitHandles.PoolSemaphore.Release(MaxPoolSize);
1532-
}
1533-
catch (SemaphoreFullException)
1534-
{
1535-
// Already at max count - all slots free. Nothing to do.
1548+
try
1549+
{
1550+
_waitHandles.PoolSemaphore.Release(waitersToWake);
1551+
}
1552+
catch (SemaphoreFullException)
1553+
{
1554+
// Semaphore already saturated; nothing to do.
1555+
}
15361556
}
15371557

1538-
// FR-003: drain idle stacks and destroy contained connections. Active checked-out
1558+
// Drain idle stacks and destroy contained connections. Active checked-out
15391559
// connections are destroyed when they are returned (see DeactivateObject's
15401560
// State is ShuttingDown branch).
15411561
while (_stackNew.TryPop(out DbConnectionInternal newObj))

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ namespace Microsoft.Data.SqlClient.UnitTests.ConnectionPool
1515
{
1616
/// <summary>
1717
/// Deterministic tests for <see cref="ChannelDbConnectionPool"/> shutdown behavior.
18-
/// Verifies the contract defined by spec 004-pool-shutdown FR-001 .. FR-007 (P1 scope).
1918
/// </summary>
2019
public class ChannelDbConnectionPoolShutdownTest
2120
{
@@ -41,7 +40,7 @@ private static ChannelDbConnectionPool ConstructPool(int maxPoolSize = 5)
4140
new DbConnectionPoolProviderInfo());
4241
}
4342

44-
// FR-001
43+
// State transitions to ShuttingDown on Shutdown.
4544
[Fact]
4645
public void Shutdown_TransitionsState_ToShuttingDown()
4746
{
@@ -55,7 +54,7 @@ public void Shutdown_TransitionsState_ToShuttingDown()
5554
Assert.Equal(DbConnectionPoolState.ShuttingDown, pool.State);
5655
}
5756

58-
// FR-003
57+
// Drains buffered idle connections.
5958
[Fact]
6059
public void Shutdown_DrainsIdleConnections()
6160
{
@@ -84,7 +83,7 @@ public void Shutdown_DrainsIdleConnections()
8483
Assert.Equal(0, pool.Count);
8584
}
8685

87-
// FR-004 - returned connection while shutting down is destroyed, not pooled.
86+
// Returned connection while shutting down is destroyed, not pooled.
8887
[Fact]
8988
public void Shutdown_ReturnedConnection_IsDestroyedNotPooled()
9089
{
@@ -104,7 +103,7 @@ public void Shutdown_ReturnedConnection_IsDestroyedNotPooled()
104103
Assert.Equal(0, pool.Count);
105104
}
106105

107-
// FR-006
106+
// Shutdown is idempotent.
108107
[Fact]
109108
public void Shutdown_IsIdempotent()
110109
{
@@ -116,7 +115,7 @@ public void Shutdown_IsIdempotent()
116115
Assert.Equal(DbConnectionPoolState.ShuttingDown, pool.State);
117116
}
118117

119-
// FR-007 - async waiter is unblocked when the pool shuts down.
118+
// Async waiter is unblocked when the pool shuts down.
120119
[Fact]
121120
public async Task Shutdown_UnblocksAsyncWaiter()
122121
{
@@ -153,7 +152,7 @@ public async Task Shutdown_UnblocksAsyncWaiter()
153152
}
154153
}
155154

156-
// Story 3 acceptance #3 - sync get fails fast after shutdown.
155+
// Sync get fails fast after shutdown.
157156
// The factory-level retry guard checks IsRunning, but the pool itself must not vend
158157
// new connections after Shutdown. We verify by exhausting the pool first then
159158
// checking that returned connections are destroyed (Count goes back to 0).

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

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@
1414
namespace Microsoft.Data.SqlClient.UnitTests.ConnectionPool
1515
{
1616
/// <summary>
17-
/// Deterministic tests for <see cref="WaitHandleDbConnectionPool"/> shutdown behavior
18-
/// per spec 004-pool-shutdown FR-001 .. FR-007 (P1 scope).
17+
/// Deterministic tests for <see cref="WaitHandleDbConnectionPool"/> shutdown behavior.
1918
/// </summary>
2019
public class WaitHandleDbConnectionPoolShutdownTest
2120
{
@@ -50,7 +49,7 @@ private static T GetPrivateField<T>(object instance, string fieldName)
5049
return (T)field!.GetValue(instance)!;
5150
}
5251

53-
// FR-001
52+
// State transitions to ShuttingDown on Shutdown.
5453
[Fact]
5554
public void Shutdown_TransitionsState_ToShuttingDown()
5655
{
@@ -63,7 +62,7 @@ public void Shutdown_TransitionsState_ToShuttingDown()
6362
Assert.Equal(DbConnectionPoolState.ShuttingDown, pool.State);
6463
}
6564

66-
// FR-005 - cleanup timer is disposed.
65+
// Cleanup timer is disposed.
6766
[Fact]
6867
public void Shutdown_DisposesCleanupTimer()
6968
{
@@ -77,7 +76,7 @@ public void Shutdown_DisposesCleanupTimer()
7776
Assert.Null(afterTimer);
7877
}
7978

80-
// FR-005 - error timer is disposed when present.
79+
// Error timer is disposed when present.
8180
[Fact]
8281
public void Shutdown_DisposesErrorTimer_WhenPresent()
8382
{
@@ -94,7 +93,7 @@ public void Shutdown_DisposesErrorTimer_WhenPresent()
9493
Assert.Null(afterTimer);
9594
}
9695

97-
// FR-003 - drains idle stacks.
96+
// Drains idle stacks.
9897
[Fact]
9998
public void Shutdown_DrainsIdleStacks()
10099
{
@@ -119,7 +118,7 @@ public void Shutdown_DrainsIdleStacks()
119118
Assert.Equal(0, pool.Count);
120119
}
121120

122-
// FR-006
121+
// Shutdown is idempotent.
123122
[Fact]
124123
public void Shutdown_IsIdempotent()
125124
{
@@ -130,7 +129,7 @@ public void Shutdown_IsIdempotent()
130129
Assert.Equal(DbConnectionPoolState.ShuttingDown, pool.State);
131130
}
132131

133-
// FR-005 acceptance #3 - cleanup-callback after shutdown is a no-op.
132+
// Cleanup callback after shutdown is a no-op.
134133
[Fact]
135134
public void CleanupCallback_AfterShutdown_IsNoOp()
136135
{
@@ -147,7 +146,7 @@ public void CleanupCallback_AfterShutdown_IsNoOp()
147146
Assert.Equal(DbConnectionPoolState.ShuttingDown, pool.State);
148147
}
149148

150-
// FR-005 acceptance #3 - error-callback after shutdown is a no-op.
149+
// Error callback after shutdown is a no-op.
151150
[Fact]
152151
public void ErrorCallback_AfterShutdown_IsNoOp()
153152
{
@@ -161,7 +160,7 @@ public void ErrorCallback_AfterShutdown_IsNoOp()
161160
Assert.Null(ex);
162161
}
163162

164-
// FR-007 - sync caller arriving after shutdown gets a null connection (factory will
163+
// Sync caller arriving after shutdown gets a null connection (factory will
165164
// see this and return up the retry chain). The pool's TryGetConnection short-circuits
166165
// on State != Running.
167166
[Fact]
@@ -180,7 +179,7 @@ public void TryGetConnection_AfterShutdown_ReturnsNullWithoutBlocking()
180179
Assert.Null(conn);
181180
}
182181

183-
// FR-007 - shutdown wakes up a thread parked in WaitHandle.WaitAny.
182+
// Shutdown wakes up a thread parked in WaitHandle.WaitAny.
184183
[Fact]
185184
public void Shutdown_UnblocksSyncWaiter()
186185
{
@@ -213,8 +212,15 @@ public void Shutdown_UnblocksSyncWaiter()
213212
{ IsBackground = true };
214213
t.Start();
215214

216-
// Give the waiter time to enter WaitAny.
217-
Thread.Sleep(200);
215+
// Wait deterministically until the worker has incremented _waitCount, which
216+
// happens immediately before it enters WaitHandle.WaitAny. Polling avoids the
217+
// CI-flakiness of a fixed Thread.Sleep on slow agents.
218+
var deadline = DateTime.UtcNow.AddSeconds(5);
219+
while (DateTime.UtcNow < deadline && GetPrivateField<int>(pool, "_waitCount") < 1)
220+
{
221+
Thread.Yield();
222+
}
223+
Assert.True(GetPrivateField<int>(pool, "_waitCount") >= 1, "Waiter did not park within 5s.");
218224
Assert.True(t.IsAlive, "Waiter should be parked, but thread already exited.");
219225

220226
pool.Shutdown();

0 commit comments

Comments
 (0)