-
Notifications
You must be signed in to change notification settings - Fork 330
Add configurable idle connection timeout (ADO #39970) #4295
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
126f16a
2a94926
93ab7ee
dd30ce7
b3a7b12
a6a69c5
a37358d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -979,6 +979,28 @@ The following example converts an existing connection string from using SQL Serv | |
| </para> | ||
| </remarks> | ||
| </LoadBalanceTimeout> | ||
| <IdleTimeout> | ||
| <summary> | ||
| 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. | ||
| </value> | ||
| <remarks> | ||
| <para> | ||
| 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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. close -> discard |
||
| </para> | ||
| <para> | ||
| 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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. recycled -> discarded |
||
| </para> | ||
| </remarks> | ||
| </IdleTimeout> | ||
| <MaxPoolSize> | ||
| <summary> | ||
| Gets or sets the maximum number of connections allowed in the connection pool for this specific connection string. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,6 +31,7 @@ internal static class DbConnectionStringKeywords | |
| 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"; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 const string MaxPoolSize = "Max Pool Size"; | ||
| internal const string MinPoolSize = "Min Pool Size"; | ||
| internal const string MultipleActiveResultSets = "Multiple Active Result Sets"; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -82,6 +82,11 @@ internal DbConnectionInternal(ConnectionState state, bool hidePassword, bool all | |
| ShouldHidePassword = hidePassword; | ||
| State = state; | ||
| CreateTime = DateTime.UtcNow; | ||
| // Initialize the idle-since stamp to creation time so that a freshly built connection is treated | ||
| // as "just used" by idle-expiry checks until the pool's return path stamps it again on first return. | ||
| // Without this initialization, IdleSinceUtc would default to DateTime.MinValue, which would cause | ||
| // IsLiveConnection to immediately evict every new connection whenever IdleTimeout is configured. | ||
| IdleSinceUtc = CreateTime; | ||
| } | ||
|
|
||
| #region Properties | ||
|
|
@@ -91,6 +96,14 @@ internal DbConnectionInternal(ConnectionState state, bool hidePassword, bool all | |
| /// </summary> | ||
| internal DateTime CreateTime { get; } | ||
|
|
||
| /// <summary> | ||
| /// UTC timestamp of when this connection was last placed into the pool's idle state. | ||
| /// Stamped by <see cref="MarkPooledIdle"/> from the pool's return-to-pool path. | ||
| /// 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; } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure we should have "Utc" in this property name. If you change how 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. |
||
|
|
||
| /// <summary> | ||
| /// The pool generation at the time this connection was created or added to the pool. | ||
| /// Used by <see cref="ChannelDbConnectionPool"/> to detect stale connections after a pool clear. | ||
|
|
@@ -734,6 +747,16 @@ internal virtual void PrepareForReplaceConnection() | |
| // By default, there is no preparation required | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Stamps <see cref="IdleSinceUtc"/> with the current UTC time. Called by the pool's return-to-pool path | ||
| /// (after the connection has been deactivated and is about to enter the idle pool) so that the pool can later | ||
| /// decide whether the connection has sat idle for too long and should be discarded. | ||
| /// </summary> | ||
| internal void MarkPooledIdle() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmmm, we're confusing our concerns here. Is the pool responsible for managing the idleness of connections, or should the connection be responsible for its idleness based on knowing when it was returned into the pool? If it's the former, then why not have the pool set If it's the former, then this should be called I think some separation of policy and mechanism is warranted here:
|
||
| { | ||
| IdleSinceUtc = DateTime.UtcNow; | ||
| } | ||
|
|
||
| internal void PrePush(DbConnection expectedOwner) | ||
| { | ||
| // Called by IDbConnectionPool when we're about to be put into it's pool, we take this | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -254,6 +254,15 @@ public void ReturnInternalConnection(DbConnectionInternal connection, DbConnecti | |
| } | ||
| else | ||
| { | ||
| // Stamp the idle-since timestamp immediately before putting the connection back in the | ||
| // 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 && | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand the optimization here, but it is imposing a correctness and maintenance burden as well. Have we measured the difference between using these conditions, and always calling If we do keep these conditions, let's wrap them in a helper since they must always be checked together.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 |
||
| PoolGroupOptions.IdleTimeout != TimeSpan.Zero) | ||
| { | ||
| connection.MarkPooledIdle(); | ||
| } | ||
| var written = _idleChannel.TryWrite(connection); | ||
| Debug.Assert(written, "Failed to write returning connection to the idle channel."); | ||
| } | ||
|
|
@@ -424,6 +433,19 @@ public bool TryGetConnection( | |
| /// <returns>Returns true if the connection is live and unexpired, otherwise returns false.</returns> | ||
| private bool IsLiveConnection(DbConnectionInternal connection) | ||
| { | ||
| // Connection has been sitting idle longer than the configured idle timeout. | ||
| // Checked before the (potentially expensive) liveness probe so an idle-expired | ||
| // connection is discarded without an SNI round-trip. | ||
| // IdleSinceUtc is initialized to CreateTime so a freshly minted connection never trips this | ||
| // check on first retrieval, and is then stamped by ReturnInternalConnection on every return. | ||
| TimeSpan idleTimeout = PoolGroupOptions.IdleTimeout; | ||
|
mdaigle marked this conversation as resolved.
|
||
| if (!LocalAppContextSwitches.UseLegacyIdleTimeoutBehavior && | ||
| idleTimeout != TimeSpan.Zero && | ||
| DateTime.UtcNow > connection.IdleSinceUtc + idleTimeout) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comparison is susceptible to overflow (throws). Re-arrange it like this to avoid: You may also want to explicitly check for underflow (i.e. if |
||
| { | ||
| return false; | ||
| } | ||
|
|
||
| // Broken physical connection | ||
| if (!connection.IsConnectionAlive()) | ||
| { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ internal sealed class DbConnectionPoolGroupOptions | |
| private readonly int _maxPoolSize; | ||
| private readonly int _creationTimeout; | ||
| private readonly TimeSpan _loadBalanceTimeout; | ||
| private readonly TimeSpan _idleTimeout; | ||
| private readonly bool _hasTransactionAffinity; | ||
| private readonly bool _useLoadBalancing; | ||
|
|
||
|
|
@@ -22,7 +23,8 @@ public DbConnectionPoolGroupOptions( | |
| int maxPoolSize, | ||
| int creationTimeout, | ||
| int loadBalanceTimeout, | ||
| bool hasTransactionAffinity | ||
| bool hasTransactionAffinity, | ||
| int idleTimeout | ||
| ) | ||
| { | ||
| _poolByIdentity = poolByIdentity; | ||
|
|
@@ -36,6 +38,16 @@ bool hasTransactionAffinity | |
| _useLoadBalancing = true; | ||
| } | ||
|
|
||
| if (idleTimeout < 0) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not use |
||
| { | ||
| throw new ArgumentOutOfRangeException(nameof(idleTimeout), idleTimeout, "Idle timeout cannot be negative."); | ||
| } | ||
|
|
||
| if (0 != idleTimeout) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Swap backwards operands: Also, do we have an upper bound? Does max int really make sense? |
||
| { | ||
| _idleTimeout = TimeSpan.FromSeconds(idleTimeout); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this throw? If yes, did we document it? |
||
| } | ||
|
|
||
| _hasTransactionAffinity = hasTransactionAffinity; | ||
| } | ||
|
|
||
|
|
@@ -54,6 +66,14 @@ public TimeSpan LoadBalanceTimeout | |
| { | ||
| get { return _loadBalanceTimeout; } | ||
| } | ||
| /// <summary> | ||
| /// The maximum time a pooled connection can sit unused (idle) in the pool before it is discarded | ||
| /// on the next retrieval attempt. <see cref="TimeSpan.Zero"/> disables idle expiration. | ||
| /// </summary> | ||
| public TimeSpan IdleTimeout | ||
| { | ||
| get { return _idleTimeout; } | ||
| } | ||
| public int MaxPoolSize | ||
| { | ||
| get { return _maxPoolSize; } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -221,8 +221,20 @@ internal WaitHandleDbConnectionPool( | |
|
|
||
| lock (s_random) | ||
| { | ||
| // 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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This condition is wrong - we will incorrectly apply the legacy behaviour if the switch is We have 3 states:
The last 2 are new behaviour we are introducing here. |
||
| { | ||
| // The WaitHandle pool takes two pruning cycles to remove an idle connection | ||
| // (new->old generation, then old->closed), so halve the configured timeout | ||
| // to approximate the requested idle lifetime. | ||
| long cleanupWaitMilliseconds = (long)idleTimeout.TotalMilliseconds / 2; | ||
| _cleanupWait = cleanupWaitMilliseconds >= int.MaxValue ? int.MaxValue : (int)cleanupWaitMilliseconds; | ||
| } | ||
| else | ||
| { | ||
| // Preserve the historical 2-4 minute random cleanup window. | ||
| _cleanupWait = s_random.Next(12, 24) * 10 * 1000; // 2-4 minutes in 10 sec intervals, WebData 103603 | ||
| } | ||
| } | ||
|
|
||
| _connectionFactory = connectionFactory; | ||
|
|
@@ -670,6 +682,10 @@ private void DeactivateObject(DbConnectionInternal obj) | |
| // DelegatedTransactionEnded event will clean up the | ||
| // connection appropriately regardless of the pool state. | ||
| Debug.Assert(_transactedConnectionPool != null, "Transacted connection pool was not expected to be null."); | ||
| // Transacting connections are held in their own store and are never | ||
| // proactively closed (doing so would abort the transaction, which can be | ||
| // distributed). Idle-timeout enforcement does not apply here, so we do | ||
| // not stamp IdleSinceUtc when parking the connection in the transacted pool. | ||
| _transactedConnectionPool.PutTransactedObject(transaction, obj); | ||
| rootTxn = true; | ||
| } | ||
|
|
@@ -1028,7 +1044,7 @@ private bool TryGetConnection(DbConnection owningObject, uint waitForMultipleObj | |
| Interlocked.Decrement(ref _waitCount); | ||
| obj = GetFromGeneralPool(); | ||
|
|
||
| if ((obj != null) && (!obj.IsConnectionAlive())) | ||
| if ((obj != null) && (IsIdleExpired(obj) || !obj.IsConnectionAlive())) | ||
| { | ||
| SqlClientEventSource.Log.TryPoolerTraceEvent("<prov.DbConnectionPool.GetConnection|RES|CPOOL> {0}, Connection {1}, found dead and removed.", Id, obj.ObjectID); | ||
| DestroyObject(obj); | ||
|
|
@@ -1209,6 +1225,8 @@ private DbConnectionInternal GetFromTransactedPool(out Transaction transaction) | |
| } | ||
| else if (!obj.IsConnectionAlive()) | ||
| { | ||
| // Transacting connections are exempt from idle-timeout eviction (closing them | ||
| // would abort the transaction, possibly distributed). Only liveness is checked here. | ||
| SqlClientEventSource.Log.TryPoolerTraceEvent("<prov.DbConnectionPool.GetFromTransactedPool|RES|CPOOL> {0}, Connection {1}, found dead and removed.", Id, obj.ObjectID); | ||
| DestroyObject(obj); | ||
| obj = null; | ||
|
|
@@ -1329,13 +1347,35 @@ private void PutNewObject(DbConnectionInternal obj) | |
|
|
||
| SqlClientEventSource.Log.TryPoolerTraceEvent("<prov.DbConnectionPool.PutNewObject|RES|CPOOL> {0}, Connection {1}, Pushing to general pool.", Id, obj.ObjectID); | ||
|
|
||
| // Stamp the idle-since timestamp immediately before placing the connection on the idle stack | ||
| // so that idle-expiry checks on later retrieval can decide whether it has sat unused too long. | ||
| // Skip the stamp when idle expiry is disabled (the default) to avoid the per-return | ||
| // DateTime.UtcNow on the hot return path. | ||
| if (!LocalAppContextSwitches.UseLegacyIdleTimeoutBehavior && | ||
| PoolGroupOptions.IdleTimeout != TimeSpan.Zero) | ||
| { | ||
| obj.MarkPooledIdle(); | ||
| } | ||
| _stackNew.Push(obj); | ||
| _waitHandles.PoolSemaphore.Release(1); | ||
|
|
||
| SqlClientDiagnostics.Metrics.EnterFreeConnection(); | ||
|
|
||
| } | ||
|
|
||
| /// <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; | ||
|
mdaigle marked this conversation as resolved.
|
||
| return !LocalAppContextSwitches.UseLegacyIdleTimeoutBehavior && | ||
| idleTimeout != TimeSpan.Zero && | ||
| DateTime.UtcNow > obj.IdleSinceUtc + idleTimeout; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment about overflow here. |
||
| } | ||
|
|
||
| public void ReturnInternalConnection(DbConnectionInternal obj, DbConnection owningObject) | ||
| { | ||
| Debug.Assert(obj != null, "null obj?"); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.