Add configurable idle connection timeout (ADO #39970)#4295
Add configurable idle connection timeout (ADO #39970)#4295priyankatiwari08 wants to merge 7 commits into
Conversation
Implements spec User Stories 1, 2, 4 + FR-009 of US3 from specs/003-pool-idle-timeout/spec.md. Adds 'Connection Idle Timeout' keyword (synonym: 'Pool Idle Timeout') exposed via SqlConnectionStringBuilder.IdleTimeout. When > 0, connections that have sat idle in the pool longer than the configured number of seconds are discarded on retrieval and a fresh connection is returned. Default 0 (disabled) matches the existing convention used by LoadBalanceTimeout and ConnectionLifetime. Covers both pool designs (ChannelDbConnectionPool, WaitHandleDbConnectionPool). Deferred to follow-up: proactive timer sweep (FR-008, FR-010) which the spec assumes is built on top of the pruning feature (#37338).
There was a problem hiding this comment.
Pull request overview
Adds a new pooling-related connection-string keyword, Connection Idle Timeout (synonym: Pool Idle Timeout), enabling lazy eviction of pooled connections that have sat idle longer than the configured number of seconds. This targets stale/half-open pooled connections (e.g., behind firewalls/load balancers) by discarding expired idle connections on the retrieval path in both pool implementations.
Changes:
- Introduces
Connection Idle Timeoutparsing +SqlConnectionStringBuilder.IdleTimeoutproperty (default0disables). - Tracks per-connection idle timestamp (
DbConnectionInternal.IdleSinceUtc) and stamps it on return-to-pool; evicts idle-expired connections on retrieval in both pool designs. - Adds unit/functional tests for the new builder keyword/property and Channel pool idle-expiry behavior.
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml | Adds XML doc snippet for SqlConnectionStringBuilder.IdleTimeout. |
| src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.cs | Updates reference assembly surface with IdleTimeout property. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/ConnectionString/DbConnectionStringKeywords.cs | Adds canonical keyword string Connection Idle Timeout. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/ConnectionString/DbConnectionStringDefaults.cs | Adds default IdleTimeout = 0. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/ConnectionString/DbConnectionStringSynonyms.cs | Adds synonym pool idle timeout. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionInternal.cs | Adds IdleSinceUtc tracking + MarkPooledIdle() stamping helper. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPool.cs | Stamps idle time on return and adds idle-expiry check in IsLiveConnection. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolOptions.cs | Adds IdleTimeout option (as TimeSpan) to pool group options. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs | Stamps idle time on return and adds IsIdleExpired check at retrieval sites. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs | Wires parsed idle-timeout option into pool group options creation. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionOptions.cs | Parses/validates idle-timeout integer from connection string. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionStringBuilder.cs | Adds IdleTimeout keyword/property support + synonym mapping. |
| src/Microsoft.Data.SqlClient/src/Resources/Strings.resx | Adds localized description string for the new keyword/property. |
| src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs | Regenerates resource accessor for DbConnectionString_IdleTimeout. |
| src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/ChannelDbConnectionPoolTest.cs | Adds unit tests for Channel pool idle-timeout behavior and stamping. |
| src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConnectionStringBuilderTest.cs | Adds functional tests for keyword parsing, round-trip, default, and invalid values. |
Files not reviewed (1)
- src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs: Language not supported
Comments suppressed due to low confidence (2)
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPool.cs:445
- The comment says that when
IdleSinceUtcis the default (DateTime.MinValue) the idle-timeout check is a no-op and indicates the connection has never been pooled. In this PRIdleSinceUtcis initialized toCreateTimeinDbConnectionInternal, so it will not beDateTime.MinValue, and even if it were, the current comparison would not be a no-op. Please update/remove this comment to match the actual semantics (e.g., describe the create-time initialization and that the check applies to any connection read from the idle channel).
// Connection has been sitting idle longer than the configured idle timeout.
// IdleSinceUtc is stamped by ReturnInternalConnection on each return; if it is the default
// (DateTime.MinValue), the connection has never been pooled yet and the check is a no-op.
TimeSpan idleTimeout = PoolGroupOptions.IdleTimeout;
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs:1351
- Idle-timeout enforcement was added to the legacy
WaitHandleDbConnectionPool(newIsIdleExpiredcheck +MarkPooledIdlestamping), but there doesn’t appear to be any unit test coverage exercising this path (existing idle-timeout unit tests are only forChannelDbConnectionPool). Please add at least one focused test validating that an idle-expired connection is discarded and replaced inWaitHandleDbConnectionPool, and that IdleTimeout==0 leaves behavior unchanged.
/// <summary>
/// Returns true when the supplied connection has been sitting idle in the pool longer than the
/// configured <see cref="DbConnectionPoolGroupOptions.IdleTimeout"/>. Returns false when idle timeout
/// is disabled (zero).
/// </summary>
private bool IsIdleExpired(DbConnectionInternal obj)
{
TimeSpan idleTimeout = PoolGroupOptions.IdleTimeout;
return idleTimeout != TimeSpan.Zero && DateTime.UtcNow > obj.IdleSinceUtc + idleTimeout;
}
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4295 +/- ##
==========================================
- Coverage 66.04% 65.89% -0.16%
==========================================
Files 275 272 -3
Lines 42976 66035 +23059
==========================================
+ Hits 28383 43512 +15129
- Misses 14593 22523 +7930
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
- Fix stale comment in ChannelDbConnectionPool.IsLiveConnection: IdleSinceUtc is initialized to CreateTime, not DateTime.MinValue. - Add WaitHandleDbConnectionPoolIdleTimeoutTest mirroring the existing channel-pool idle-timeout coverage (stamp on return, zero disables expiry, expired connection is replaced, fresh connection is reused).
- Skip MarkPooledIdle on return when IdleTimeout == TimeSpan.Zero so the default config has no per-return DateTime.UtcNow on the hot path. Applies to ChannelDbConnectionPool.ReturnInternalConnection and WaitHandleDbConnectionPool.PutNewObject. - Stamp IdleSinceUtc when returning a connection into the transacted pool (WaitHandleDbConnectionPool.DeactivateObject before TransactedConnectionPool.PutTransactedObject) so idle expiry on the next retrieval measures time spent parked in the transacted pool, not time since create-time / last general-pool return. - Add 2 WaitHandle pool tests covering the new behavior: IdleTimeout_TransactedPool_StampsOnReturn and IdleTimeout_Zero_DoesNotStampOnReturn.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 17 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs: Language not supported
Comments suppressed due to low confidence (2)
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs:1043
- The idle-timeout eviction check is evaluated after obj.IsConnectionAlive(). For long-idle pooled connections, IsConnectionAlive() may perform an expensive SNI-level liveness probe; if the connection is already idle-expired, that work is unnecessary. Consider checking IsIdleExpired(obj) first (or reordering the condition) so expired connections are discarded without doing a liveness check.
This issue also appears on line 1218 of the same file.
if ((obj != null) && (!obj.IsConnectionAlive() || IsIdleExpired(obj)))
{
SqlClientEventSource.Log.TryPoolerTraceEvent("<prov.DbConnectionPool.GetConnection|RES|CPOOL> {0}, Connection {1}, found dead and removed.", Id, obj.ObjectID);
DestroyObject(obj);
obj = null; // Setting to null in case creating a new object fails
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs:1222
- Similar to the general-pool path: in the transacted-pool retrieval, the code calls obj.IsConnectionAlive() before IsIdleExpired(obj). Since IsConnectionAlive() can trigger an SNI liveness probe for idle connections, consider checking IsIdleExpired(obj) first to avoid extra work when the idle-timeout policy is what causes the connection to be recycled.
else if (!obj.IsConnectionAlive() || IsIdleExpired(obj))
{
SqlClientEventSource.Log.TryPoolerTraceEvent("<prov.DbConnectionPool.GetFromTransactedPool|RES|CPOOL> {0}, Connection {1}, found dead and removed.", Id, obj.ObjectID);
DestroyObject(obj);
obj = null;
…sacted idle handling - Default Connection Idle Timeout 0 -> 300 (5 min, matches Npgsql); 0 disables. - Remove 'Pool Idle Timeout' synonym; the canonical keyword is the only accepted form. - Make idleTimeout a required parameter on DbConnectionPoolGroupOptions; defaults now live in DbConnectionStringDefaults. - Use TimeSpan.FromSeconds for the ctor body conversion. - WaitHandle pool: drop MarkPooledIdle() on transacted-pool return and remove the idle-expiry check on transacted-pool retrieval (transacting connections must never be proactively closed). - WaitHandle pool: reorder general-pool retrieval to check idle expiry before the liveness probe; derive _cleanupWait from IdleTimeout when set. - Channel pool: reorder IsLiveConnection so the idle check runs before IsConnectionAlive(). - Tests, doc snippet, and release notes updated accordingly.
|
All review feedback has been addressed:
Ready for re-review. Please confirm if any further changes are needed. |
mdaigle
left a comment
There was a problem hiding this comment.
Thanks for making those changes. I have a few more updates that I'd like to see. Otherwise it's very close.
mdaigle
left a comment
There was a problem hiding this comment.
Behavior is looking good, just one important comment for the WaitHandle pool. Other comments relate to doc comments.
- Colocate idleTimeout < 0 validation with the _idleTimeout assignment in DbConnectionPoolOptions - Clean up WaitHandle cleanup-wait conditional and halve the interval (two pruning cycles per eviction) - Vague-up IdleTimeout doc/resx wording per reviewer (drop 'on the next retrieval attempt' / lifecycle commitments) - Toggle UseLegacyIdleTimeoutBehavior in WaitHandle idle-timeout tests and replace reflection-based BackdateIdleSince with direct internal setter
paulmedynski
left a comment
There was a problem hiding this comment.
I've looked through the non-test code and have some qestions, comments, and suggestions.
I'll look at the tests once we have discussed and resovled this batch.
| A value of zero (0) disables idle expiration; connections are kept in the pool indefinitely (subject to other expiry rules such as <see cref="P:Microsoft.Data.SqlClient.SqlConnectionStringBuilder.LoadBalanceTimeout" />). | ||
| </para> | ||
| <para> | ||
| Idle timeout operates independently of <see cref="P:Microsoft.Data.SqlClient.SqlConnectionStringBuilder.LoadBalanceTimeout" />. Whichever threshold is exceeded first causes the connection to be recycled. |
There was a problem hiding this comment.
recycled -> discarded
| Gets or sets the maximum time, in seconds, that a connection can sit unused (idle) in the connection pool before it is discarded. | ||
| </summary> | ||
| <value> | ||
| The value of the <see cref="P:Microsoft.Data.SqlClient.SqlConnectionStringBuilder.IdleTimeout" /> property, or 300 (5 minutes) if none has been supplied. |
There was a problem hiding this comment.
The value is obviously the value of this property, so no need to state that. Maybe just:
"The idle timeout for pooled connections, in seconds."
And then mention the default value in the summary instead.
| This property corresponds to the "Connection Idle Timeout" key within the connection string. | ||
| </para> | ||
| <para> | ||
| The driver makes a best effort to close connections that have remained idle in the pool for longer than this value. The exact point in the connection lifecycle at which the check occurs is an implementation detail and may change over time. This protects callers from receiving connections that may have been silently closed by firewalls, load balancers, or server-side inactivity thresholds. |
| internal const string IntegratedSecurity = "Integrated Security"; | ||
| internal const string IpAddressPreference = "IP Address Preference"; | ||
| internal const string LoadBalanceTimeout = "Load Balance Timeout"; | ||
| internal const string IdleTimeout = "Connection Idle Timeout"; |
There was a problem hiding this comment.
Does this only apply to pooled connections? What happens if I specify this for a non-pooled connection?
If this only applies to pools, then maybe we should have Pool in the name somewhere? "Pooled Connection Idle Timeout"
| /// Internal setter exists to support deterministic unit tests without reflection. | ||
| /// Used by the pool to discard connections that have sat unused longer than the configured idle timeout. | ||
| /// </summary> | ||
| internal DateTime IdleSinceUtc { get; set; } |
There was a problem hiding this comment.
I'm not sure we should have "Utc" in this property name. If you change how CreateTime is initialized later, will "Utc" still be guaranteed - not likely.
The docs make it clear that the timestamp is in UTC, so I would suggest omitting the "Utc" suffix. If you decide to use local timestamps later, then you don't have to rename the property.
|
|
||
| if (0 != idleTimeout) | ||
| { | ||
| _idleTimeout = TimeSpan.FromSeconds(idleTimeout); |
There was a problem hiding this comment.
Can this throw? If yes, did we document it?
| // Random.Next is not thread-safe | ||
| _cleanupWait = s_random.Next(12, 24) * 10 * 1000; // 2-4 minutes in 10 sec intervals, WebData 103603 | ||
| TimeSpan idleTimeout = connectionPoolGroup.PoolGroupOptions.IdleTimeout; | ||
| if (!LocalAppContextSwitches.UseLegacyIdleTimeoutBehavior && idleTimeout != TimeSpan.Zero) |
There was a problem hiding this comment.
This condition is wrong - we will incorrectly apply the legacy behaviour if the switch is false and the idle timeout is zero.
We have 3 states:
- Legacy (switch is
true) - New (switch is
falseand timeout isn't zero) - None (switch is
falseand timeout is zero)
The last 2 are new behaviour we are introducing here.
| // pool so that IsLiveConnection can later evict it if it sits idle past the configured limit. | ||
| // Skip the stamp when idle expiry is disabled or legacy idle-timeout behavior is in effect | ||
| // to avoid the per-return DateTime.UtcNow on the hot return path. | ||
| if (!LocalAppContextSwitches.UseLegacyIdleTimeoutBehavior && |
There was a problem hiding this comment.
This new connection pool has never had "legacy idle timeout behaviour", so should we even be checking the switch in this class? You can see this by the lack of an else block in IsLiveConnection() below - there is no legacy behaviour to preserve!
| TimeSpan idleTimeout = PoolGroupOptions.IdleTimeout; | ||
| return !LocalAppContextSwitches.UseLegacyIdleTimeoutBehavior && | ||
| idleTimeout != TimeSpan.Zero && | ||
| DateTime.UtcNow > obj.IdleSinceUtc + idleTimeout; |
There was a problem hiding this comment.
Same comment about overflow here.
| <value>The minimum amount of time (in seconds) for this connection to live in the pool before being destroyed.</value> | ||
| </data> | ||
| <data name="DbConnectionString_IdleTimeout" xml:space="preserve"> | ||
| <value>The maximum amount of time (in seconds) a connection can sit unused (idle) in the pool before it is discarded. A value of 0 disables idle expiration.</value> |
There was a problem hiding this comment.
The last part isn't really true. When the switch is true, a value of 0 here is ignored for the old pool, and you get the legacy idle expiration.
There are docs and comments elsewhere in the review that are similarly misleading.
Maybe we don't need the switch at all:
- If the app is using the new connection pool:
- If idle timeout is omitted from the connection string, or it is explicitly set to 0, then idle discarding is disabled.
- Otherwise, idle discarding is enabled for whatever timeout is specified.
- If the app is using the old connection pool:
- If idle timeout is omitted from the connection string, or it is explicitly set to 0, then idle discarding uses the legacy behaviour.
- Otherwise, modern idle discarding is enabled for whatever timeout is specified.
Does that work?
There was a problem hiding this comment.
Some people may choose to use the new connection string property, but not the new pool. We want to give them a smooth upgrade path to the new pool in the future. Keeping behavior identical makes sure they don't have to change their connection string later when switching to the new pool.
Adds a configurable idle connection timeout for pooled
SqlConnectioninstances.Compatibility and behavior:
IdleTimeoutdefaults to300seconds.Switch.Microsoft.Data.SqlClient.UseLegacyIdleTimeoutBehavior, which defaults totrueso current behavior is preserved unless the switch is disabled.IdleTimeoutcontrols when pooled connections are considered idle and eligible for expiration.Additional changes: