Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
126f16a
Add configurable idle connection timeout (ADO #39970)
priyankatiwari08 May 19, 2026
2a94926
Address Copilot review feedback on #4295
priyankatiwari08 May 20, 2026
93ab7ee
Address PR #4295 inline review feedback
priyankatiwari08 May 21, 2026
dd30ce7
Address PR review: default IdleTimeout=300, remove synonym, drop tran…
priyankatiwari08 May 26, 2026
b3a7b12
Address PR #4295 review feedback on idle timeout behavior
priyankatiwari08 May 27, 2026
a6a69c5
Remove Pool Idle Timeout synonym; keep only canonical Connection Idle…
priyankatiwari08 May 27, 2026
a37358d
Address review feedback for idle connection timeout
priyankatiwari08 May 29, 2026
f32d6cd
Address Paul's review feedback: rename and doc cleanup
priyankatiwari08 Jun 4, 2026
0f0d018
Apply review feedback: drop guard, overflow-safe expiry, Yoda swap, 3…
priyankatiwari08 Jun 4, 2026
911b83a
Re-add legacy-switch guard around ReturnedToPool stamp
priyankatiwari08 Jun 4, 2026
228fe6f
Merge branch 'main' into feature/idle-connection-timeout-pr1
priyankatiwari08 Jun 4, 2026
80da656
Fix merge conflict damage in LocalAppContextSwitchesHelper
priyankatiwari08 Jun 4, 2026
7611570
Fix merge conflict damage in LocalAppContextSwitches
priyankatiwari08 Jun 4, 2026
abfa301
Pass idleTimeout to DbConnectionPoolGroupOptions in budget test
priyankatiwari08 Jun 5, 2026
5b1bbe9
Add TimeoutTimer arg to TryGetConnection / idleTimeout to DbConnectio…
priyankatiwari08 Jun 5, 2026
0cf4d4a
Address PR #4295 Copilot review: document UseLegacyIdleTimeoutBehavio…
priyankatiwari08 Jun 5, 2026
a6942f4
Address PR 4295 review feedback: cleanup cadence, idle-eviction gate,…
priyankatiwari08 Jun 10, 2026
182b596
Merge origin/main; fix new ChannelDbConnectionPoolPruningTest to pass…
priyankatiwari08 Jun 10, 2026
f78fac0
Fix ChannelDbConnectionPoolPruningTest: pass idleTimeout: 0 to DbConn…
priyankatiwari08 Jun 10, 2026
e999310
Address PR 4295 review: gate WaitHandleDbConnectionPool destroy loop …
priyankatiwari08 Jun 10, 2026
b52495f
Potential fix for pull request finding
priyankatiwari08 Jun 11, 2026
1e37541
Address PR 4295 open reviews: revert CleanupCallback IsIdleExpired ga…
priyankatiwari08 Jun 18, 2026
72d241d
Clarify IdleTimeout doc: spell out the half-timeout early-eviction case
priyankatiwari08 Jun 21, 2026
3680ad3
Address Copilot review on 72d241d7: trace reason, IsIdleExpired XML d…
priyankatiwari08 Jun 21, 2026
5b47aff
Address Paul review on 3680ad36: capture IsIdleExpired once, plain da…
priyankatiwari08 Jun 22, 2026
1435958
Add failing regression test for return-path idle eviction
priyankatiwari08 Jun 23, 2026
1dd68e6
Stamp ReturnedTime before IsLiveConnection in ChannelDbConnectionPool…
priyankatiwari08 Jun 23, 2026
6324ed6
Address Copilot review feedback: spelling and IdleTimeout doc
priyankatiwari08 Jun 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,31 @@ 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. The default is 300 (5 minutes).
</summary>
<value>
The idle timeout for pooled connections, in seconds.
</value>
<remarks>
<para>
This property corresponds to the "Connection Idle Timeout" key within the connection string.
</para>
<para>
In versions where the AppContext switch <c>Switch.Microsoft.Data.SqlClient.UseLegacyIdleTimeoutBehavior</c> is enabled (the default), the driver preserves historical pooling behavior and does not enforce this setting. Set the switch to <see langword="false" /> to enable idle-timeout enforcement.
</para>
<para>
The driver makes a best effort to discard 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.
</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 discarded.
</para>
Comment thread
priyankatiwari08 marked this conversation as resolved.
Comment thread
priyankatiwari08 marked this conversation as resolved.
</remarks>
</IdleTimeout>
<MaxPoolSize>
<summary>
Gets or sets the maximum number of connections allowed in the connection pool for this specific connection string.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1368,6 +1368,10 @@ public SqlConnectionStringBuilder(string connectionString) { }
[System.ComponentModel.DisplayNameAttribute("Load Balance Timeout")]
[System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)]
public int LoadBalanceTimeout { get { throw null; } set { } }
/// <include file='../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml' path='docs/members[@name="SqlConnectionStringBuilder"]/IdleTimeout/*'/>
[System.ComponentModel.DisplayNameAttribute("Connection Idle Timeout")]
[System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)]
public int IdleTimeout { get { throw null; } set { } }
/// <include file='../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml' path='docs/members[@name="SqlConnectionStringBuilder"]/MaxPoolSize/*'/>
[System.ComponentModel.DisplayNameAttribute("Max Pool Size")]
[System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ internal static class DbConnectionStringDefaults
internal const bool IntegratedSecurity = false;
internal const SqlConnectionIPAddressPreference IpAddressPreference = SqlConnectionIPAddressPreference.IPv4First;
internal const int LoadBalanceTimeout = 0; // default of 0 means don't use
// Default configured idle timeout is 5 minutes. Connection pool behavior is gated by
// LocalAppContextSwitches.UseLegacyIdleTimeoutBehavior for compatibility.
internal const int IdleTimeout = 300;
internal const int MaxPoolSize = 100;
Comment thread
priyankatiwari08 marked this conversation as resolved.
internal const int MinPoolSize = 0;
internal const bool MultipleActiveResultSets = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Comment thread
paulmedynski marked this conversation as resolved.
internal const string MaxPoolSize = "Max Pool Size";
internal const string MinPoolSize = "Min Pool Size";
internal const string MultipleActiveResultSets = "Multiple Active Result Sets";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ internal DbConnectionInternal(ConnectionState state, bool hidePassword, bool all
ShouldHidePassword = hidePassword;
State = state;
CreateTime = DateTime.UtcNow;
// Initialize the returned-to-pool stamp to creation time so that a freshly built connection is treated
// as "just used" by the pool's idle-expiry checks until the pool's return path stamps it again on first return.
// Without this initialization, ReturnedTime would default to DateTime.MinValue, which would cause
// IsLiveConnection to immediately evict every new connection whenever IdleTimeout is configured.
ReturnedTime = CreateTime;
}

#region Properties
Expand All @@ -91,6 +96,15 @@ internal DbConnectionInternal(ConnectionState state, bool hidePassword, bool all
/// </summary>
internal DateTime CreateTime { get; }

/// <summary>
/// UTC timestamp of when this connection was last returned to the pool.
/// Stamped by <see cref="SetReturnedTime"/>. Initialized to <see cref="CreateTime"/> in the constructor
/// so a freshly built connection is treated as "just used" until its first return.
/// Internal setter exists to support deterministic unit tests without reflection.
/// The pool reads this value to decide whether the connection has sat idle longer than the configured idle timeout.
/// </summary>
internal DateTime ReturnedTime { get; set; }
Comment thread
priyankatiwari08 marked this conversation as resolved.

/// <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.
Expand Down Expand Up @@ -726,6 +740,17 @@ internal virtual void PrepareForReplaceConnection()
// By default, there is no preparation required
}

/// <summary>
/// Stamps <see cref="ReturnedTime"/> with the current UTC time. The pool calls this from its
/// return-to-pool path only when it intends the idle-timeout machinery to act on the value;
/// the connection owns the mechanism (recording the time) while the pool owns the policy
/// (deciding when a stamp is meaningful).
/// </summary>
internal void SetReturnedTime()
{
ReturnedTime = DateTime.UtcNow;
}

internal void PrePush(DbConnection expectedOwner)
{
// Called by IDbConnectionPool when we're about to be put into it's pool, we take this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,18 @@ public void ReturnInternalConnection(DbConnectionInternal connection, DbConnecti
{
ValidateOwnershipAndSetPoolingState(connection, owningObject);

// Stamp the return time before IsLiveConnection runs so the idle-expiry gate inside it
// measures time-in-pool, not time-since-last-return. Without this, a connection whose
// checkout exceeded IdleTimeout (e.g. a long-running query) would be wrongly evicted on
// return even though it was actively in use on the wire. The same gating conditions are
// applied here as in IsLiveConnection so we avoid the per-return DateTime.UtcNow when
// idle expiry is disabled or the legacy idle-timeout behavior is in effect.
if (!LocalAppContextSwitches.UseLegacyIdleTimeoutBehavior &&
PoolGroupOptions.IdleTimeout != TimeSpan.Zero)
{
connection.SetReturnedTime();
}

if (!IsLiveConnection(connection))
{
RemoveConnection(connection);
Expand Down Expand Up @@ -453,6 +465,22 @@ 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.
// ReturnedTime 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.
// Use subtraction rather than addition so the comparison cannot throw if ReturnedTime is
// ever close to DateTime.MaxValue. A clock skew that leaves ReturnedTime in the future
// produces a negative TimeSpan, which falls through as not-expired (fail safe).
TimeSpan idleTimeout = PoolGroupOptions.IdleTimeout;
Comment thread
mdaigle marked this conversation as resolved.
if (!LocalAppContextSwitches.UseLegacyIdleTimeoutBehavior &&
idleTimeout != TimeSpan.Zero &&
DateTime.UtcNow - connection.ReturnedTime > idleTimeout)
{
return false;
}

// Broken physical connection
if (!connection.IsConnectionAlive())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -22,7 +23,8 @@ public DbConnectionPoolGroupOptions(
int maxPoolSize,
int creationTimeout,
int loadBalanceTimeout,
bool hasTransactionAffinity
bool hasTransactionAffinity,
int idleTimeout
)
{
_poolByIdentity = poolByIdentity;
Expand All @@ -36,6 +38,16 @@ bool hasTransactionAffinity
_useLoadBalancing = true;
}

if (idleTimeout < 0)
Comment thread
priyankatiwari08 marked this conversation as resolved.
{
throw new ArgumentOutOfRangeException(nameof(idleTimeout), idleTimeout, "Idle timeout cannot be negative.");
}

if (idleTimeout != 0)
{
_idleTimeout = TimeSpan.FromSeconds(idleTimeout);
Comment thread
paulmedynski marked this conversation as resolved.
}

_hasTransactionAffinity = hasTransactionAffinity;
}

Expand All @@ -54,6 +66,20 @@ public TimeSpan LoadBalanceTimeout
{
get { return _loadBalanceTimeout; }
}
/// <summary>
/// The maximum time a pooled connection can sit unused (idle) in the pool before it becomes
/// eligible for eviction. Eviction is best-effort: a connection that has been idle longer
/// than this value is discarded either on the next retrieval attempt or during a periodic
/// pool maintenance pass (in pool implementations that perform periodic maintenance),
/// whichever happens first. Implementations may check this threshold opportunistically, so
/// eviction may occur somewhat before or after the exact timeout depending on the pool
/// implementation and maintenance cadence.
/// <see cref="TimeSpan.Zero"/> disables idle expiration.
/// </summary>
public TimeSpan IdleTimeout
{
get { return _idleTimeout; }
}
public int MaxPoolSize
{
get { return _maxPoolSize; }
Expand Down
Loading
Loading