Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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,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 on its next retrieval.
</summary>
<value>
The value of the <see cref="P:Microsoft.Data.SqlClient.SqlConnectionStringBuilder.IdleTimeout" /> property, or 0 if none has been supplied.
</value>
<remarks>
<para>
This property corresponds to the "Connection Idle Timeout" key (synonym: "Pool Idle Timeout") within the connection string.
</para>
<para>
When a caller retrieves a connection from the pool, the driver checks how long the connection has been sitting idle. If the idle duration exceeds the value of <c>Connection Idle Timeout</c>, the connection is discarded and a different valid or newly-created connection is returned instead. 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 recycled.
</para>
</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,7 @@ 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
internal const int IdleTimeout = 0; // default of 0 means don't expire idle connections
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's default this to 300, like npgsql. We'll later use this value to determine how often we prune. Today we prune every 4-8 minutes. 300 seconds will default us to 5 mins which falls in that range.
https://www.npgsql.org/doc/connection-string-parameters.html

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also use this value to set the _cleanupWait value in WaitHandleDbConnectionPool

internal const int MaxPoolSize = 100;
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";
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 @@ -32,6 +32,7 @@ internal static class DbConnectionStringSynonyms
internal const string PacketSize = "packetsize";
internal const string PersistSecurityInfo = "persistsecurityinfo";
internal const string PoolBlockingPeriod = "poolblockingperiod";
internal const string PoolIdleTimeout = "pool idle timeout";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need to add a synonym

internal const string Pwd = "pwd";
internal const string Server = "server";
internal const string ServerCertificate = "servercertificate";
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 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
Expand All @@ -91,6 +96,13 @@ 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.
/// Used by the pool to discard connections that have sat unused longer than the configured idle timeout.
/// </summary>
internal DateTime IdleSinceUtc { get; private set; }

/// <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 @@ -734,6 +746,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()
{
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,14 @@ 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 (the default) to avoid the per-return
// DateTime.UtcNow on the hot return path.
if (PoolGroupOptions.IdleTimeout != TimeSpan.Zero)
{
connection.MarkPooledIdle();
}
var written = _idleChannel.TryWrite(connection);
Debug.Assert(written, "Failed to write returning connection to the idle channel.");
}
Expand Down Expand Up @@ -436,6 +444,15 @@ private bool IsLiveConnection(DbConnectionInternal connection)
return false;
}

// Connection has been sitting idle longer than the configured idle timeout.
// 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;
if (idleTimeout != TimeSpan.Zero && DateTime.UtcNow > connection.IdleSinceUtc + idleTimeout)
Comment on lines +447 to +451
{
return false;
}

// Connection was created before the last Clear, so it's stale.
if (connection.ClearGeneration != _clearGeneration)
{
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 = 0
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make this required and have the default value handled by DbConnectionStringDefaults.

)
{
_poolByIdentity = poolByIdentity;
Expand All @@ -36,6 +38,11 @@ bool hasTransactionAffinity
_useLoadBalancing = true;
}

if (0 != idleTimeout)
{
_idleTimeout = new TimeSpan(0, 0, idleTimeout);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_idleTimeout = new TimeSpan(0, 0, idleTimeout);
_idleTimeout = TimeSpan.FromSeconds(idleTimeout);

}

_hasTransactionAffinity = hasTransactionAffinity;
}

Expand All @@ -54,6 +61,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; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,14 @@ 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.");
// Stamp the idle-since timestamp before parking the connection in the transacted
// pool so the next retrieval measures idle time from when it left the active set,
// not from create-time or the previous general-pool return. Skip when idle expiry
// is disabled to avoid an unnecessary DateTime.UtcNow on the hot return path.
if (PoolGroupOptions.IdleTimeout != TimeSpan.Zero)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. Transacting connections don't need to get an idle stamp because they won't be proactively closed. You can add a comment explaining it.

{
obj.MarkPooledIdle();
}
_transactedConnectionPool.PutTransactedObject(transaction, obj);
rootTxn = true;
}
Expand Down Expand Up @@ -1028,7 +1036,7 @@ private bool TryGetConnection(DbConnection owningObject, uint waitForMultipleObj
Interlocked.Decrement(ref _waitCount);
obj = GetFromGeneralPool();

if ((obj != null) && (!obj.IsConnectionAlive()))
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);
Expand Down Expand Up @@ -1207,7 +1215,7 @@ private DbConnectionInternal GetFromTransactedPool(out Transaction transaction)
throw;
}
}
else if (!obj.IsConnectionAlive())
else if (!obj.IsConnectionAlive() || IsIdleExpired(obj))
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 93ab7ee. DeactivateObject now calls obj.MarkPooledIdle() (guarded by the idle-timeout-enabled check) immediately before _transactedConnectionPool.PutTransactedObject(transaction, obj), so idle duration measures time parked in the transacted pool rather than time since create-time / last general-pool return.

Added a regression test IdleTimeout_TransactedPool_StampsOnReturn in WaitHandleDbConnectionPoolIdleTimeoutTest.cs that opens a TransactionScope, backdates IdleSinceUtc before return, and asserts the value is refreshed by the return path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot is almost right, but the real issue is that transacting connections need to be treated specially. We never proactively close a transacting connection because that will abort the transaction (if it's distributed). We put transacting connections off to the side in their own storage specifically because we don't want them to be cleaned up before the transaction finishes. We should not consider idle timeout at this spot.

{
SqlClientEventSource.Log.TryPoolerTraceEvent("<prov.DbConnectionPool.GetFromTransactedPool|RES|CPOOL> {0}, Connection {1}, found dead and removed.", Id, obj.ObjectID);
DestroyObject(obj);
Expand Down Expand Up @@ -1329,13 +1337,32 @@ 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 (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;
return idleTimeout != TimeSpan.Zero && DateTime.UtcNow > obj.IdleSinceUtc + idleTimeout;
}

public void ReturnInternalConnection(DbConnectionInternal obj, DbConnection owningObject)
{
Debug.Assert(obj != null, "null obj?");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,8 @@ private static DbConnectionPoolGroupOptions CreateConnectionPoolGroupOptions(Sql
opt.MaxPoolSize,
connectionTimeout,
opt.LoadBalanceTimeout,
opt.Enlist);
opt.Enlist,
opt.IdleTimeout);
}
return poolingOptions;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ internal static class TRANSACTIONBINDING
private readonly int _commandTimeout;
private readonly int _connectTimeout;
private readonly int _loadBalanceTimeout;
private readonly int _idleTimeout;
private readonly int _maxPoolSize;
private readonly int _minPoolSize;
private readonly int _packetSize;
Expand Down Expand Up @@ -195,6 +196,8 @@ static SqlConnectionOptions()
DbConnectionStringSynonyms.IpAddressPreference);
AddKeywordToMap(DbConnectionStringKeywords.LoadBalanceTimeout,
DbConnectionStringSynonyms.ConnectionLifetime);
AddKeywordToMap(DbConnectionStringKeywords.IdleTimeout,
DbConnectionStringSynonyms.PoolIdleTimeout);
AddKeywordToMap(DbConnectionStringKeywords.MultipleActiveResultSets,
DbConnectionStringSynonyms.MultipleActiveResultSets);
AddKeywordToMap(DbConnectionStringKeywords.MaxPoolSize);
Expand Down Expand Up @@ -274,6 +277,7 @@ internal SqlConnectionOptions(string connectionString)
_commandTimeout = ConvertValueToInt32(DbConnectionStringKeywords.CommandTimeout, DbConnectionStringDefaults.CommandTimeout);
_connectTimeout = ConvertValueToInt32(DbConnectionStringKeywords.ConnectTimeout, DbConnectionStringDefaults.ConnectTimeout);
_loadBalanceTimeout = ConvertValueToInt32(DbConnectionStringKeywords.LoadBalanceTimeout, DbConnectionStringDefaults.LoadBalanceTimeout);
_idleTimeout = ConvertValueToInt32(DbConnectionStringKeywords.IdleTimeout, DbConnectionStringDefaults.IdleTimeout);
_maxPoolSize = ConvertValueToInt32(DbConnectionStringKeywords.MaxPoolSize, DbConnectionStringDefaults.MaxPoolSize);
_minPoolSize = ConvertValueToInt32(DbConnectionStringKeywords.MinPoolSize, DbConnectionStringDefaults.MinPoolSize);
_packetSize = ConvertValueToInt32(DbConnectionStringKeywords.PacketSize, DbConnectionStringDefaults.PacketSize);
Expand Down Expand Up @@ -318,6 +322,11 @@ internal SqlConnectionOptions(string connectionString)
throw ADP.InvalidConnectionOptionValue(DbConnectionStringKeywords.LoadBalanceTimeout);
}

if (_idleTimeout < 0)
{
throw ADP.InvalidConnectionOptionValue(DbConnectionStringKeywords.IdleTimeout);
}

if (_connectTimeout < 0)
{
throw ADP.InvalidConnectionOptionValue(DbConnectionStringKeywords.ConnectTimeout);
Expand Down Expand Up @@ -579,6 +588,7 @@ internal SqlConnectionOptions(SqlConnectionOptions connectionOptions, string dat
_commandTimeout = connectionOptions._commandTimeout;
_connectTimeout = connectionOptions._connectTimeout;
_loadBalanceTimeout = connectionOptions._loadBalanceTimeout;
_idleTimeout = connectionOptions._idleTimeout;
_poolBlockingPeriod = connectionOptions._poolBlockingPeriod;
_maxPoolSize = connectionOptions._maxPoolSize;
_minPoolSize = connectionOptions._minPoolSize;
Expand Down Expand Up @@ -650,6 +660,9 @@ internal SqlConnectionOptions(SqlConnectionOptions connectionOptions, string dat
internal int CommandTimeout => _commandTimeout;
internal int ConnectTimeout => _connectTimeout;
internal int LoadBalanceTimeout => _loadBalanceTimeout;
// Maximum time (in seconds) a connection can sit idle in the pool before it is discarded
// on the next retrieval attempt. 0 disables idle expiration.
internal int IdleTimeout => _idleTimeout;
internal int MaxPoolSize => _maxPoolSize;
internal int MinPoolSize => _minPoolSize;
internal int PacketSize => _packetSize;
Expand Down
Loading
Loading