Skip to content

Add configurable idle connection timeout (ADO #39970)#4295

Open
priyankatiwari08 wants to merge 3 commits into
dotnet:mainfrom
priyankatiwari08:feature/idle-connection-timeout-pr1
Open

Add configurable idle connection timeout (ADO #39970)#4295
priyankatiwari08 wants to merge 3 commits into
dotnet:mainfrom
priyankatiwari08:feature/idle-connection-timeout-pr1

Conversation

@priyankatiwari08
Copy link
Copy Markdown
Contributor

@priyankatiwari08 priyankatiwari08 commented May 19, 2026

This pull request adds a new Connection Idle Timeout connection-string keyword to Microsoft.Data.SqlClient, allowing users to control how long a pooled connection may remain idle before it is discarded on the next retrieval. This addresses #343, where firewalls or load balancers silently kill idle connections and applications then pull dead connections from the pool. Tracks ADO #39970.

The implementation is based on the Idle Connection Timeout spec.

Configuration surface:

  • New connection-string keyword Connection Idle Timeout (in seconds), with Pool Idle Timeout as a synonym.
  • New IdleTimeout property on SqlConnectionStringBuilder (int, seconds, default 0). Negative values throw at parse time. A value of 0 disables idle expiration.

Behavior:

  • Tracks an "idle since" timestamp on each pooled connection, stamped on return to the pool.
  • On retrieval, any connection that has been idle longer than the configured timeout is discarded and a fresh connection is returned.
  • Applies to both pool implementations (ChannelDbConnectionPool and WaitHandleDbConnectionPool).
  • Lazy retrieval check ignores MinPoolSize: a caller always receives a working connection.

Design decisions taken:

  • Keyword nameConnection Idle Timeout is the canonical keyword; Pool Idle Timeout is registered as a synonym.
  • No SqlConnection.IdleTimeout property — the keyword is exposed only on SqlConnectionStringBuilder.
  • Default value0 (idle expiration disabled).

Out of scope / deferred to a follow-up PR:

  • Proactive timer sweep and the MinPoolSize floor on proactive removal. The spec requires this to share a timer with the pruning feature (#37338), which is not yet landed; ChannelDbConnectionPool also has no periodic timer today. Deferred to a follow-up that builds on the pruning work.

Tests:

  • 6 unit tests in ChannelDbConnectionPoolTest.cs covering zero-disables, expiry on retrieval, recently-returned-not-expired, stamping on return, initial value, and "checked-out connections aren't affected".
  • 5 functional tests in SqlConnectionStringBuilderTest.cs covering default value, canonical keyword parsing, synonym parsing, round-trip through the builder, and negative-value rejection. Mirrors the existing LoadBalanceTimeout / MaxPoolSize / MinPoolSize test pattern.
  • No integration tests added — consistent with how the recent ChannelDbConnectionPool work itself was shipped.
  • All 11 tests pass on net9.0; clean build.

Links:

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).
Copilot AI review requested due to automatic review settings May 19, 2026 10:11
@github-project-automation github-project-automation Bot moved this to To triage in SqlClient Board May 19, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 Timeout parsing + SqlConnectionStringBuilder.IdleTimeout property (default 0 disables).
  • 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 IdleSinceUtc is the default (DateTime.MinValue) the idle-timeout check is a no-op and indicates the connection has never been pooled. In this PR IdleSinceUtc is initialized to CreateTime in DbConnectionInternal, so it will not be DateTime.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 (new IsIdleExpired check + MarkPooledIdle stamping), but there doesn’t appear to be any unit test coverage exercising this path (existing idle-timeout unit tests are only for ChannelDbConnectionPool). Please add at least one focused test validating that an idle-expired connection is discarded and replaced in WaitHandleDbConnectionPool, 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
Copy link
Copy Markdown

codecov Bot commented May 19, 2026

Codecov Report

❌ Patch coverage is 96.07843% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.89%. Comparing base (b700269) to head (126f16a).
⚠️ Report is 15 commits behind head on main.

Files with missing lines Patch % Lines
...c/Microsoft/Data/SqlClient/SqlConnectionOptions.cs 75.00% 2 Missing ⚠️
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     
Flag Coverage Δ
CI-SqlClient ?
PR-SqlClient-Project 65.89% <96.07%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

- 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).
@priyankatiwari08 priyankatiwari08 marked this pull request as ready for review May 21, 2026 11:20
@priyankatiwari08 priyankatiwari08 requested a review from a team as a code owner May 21, 2026 11:20
Copilot AI review requested due to automatic review settings May 21, 2026 11:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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

}
}
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.

@priyankatiwari08 priyankatiwari08 added this to the 7.1.0-preview2 milestone May 21, 2026
- 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.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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;

Comment on lines +447 to +451
// 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)
@paulmedynski paulmedynski self-assigned this May 21, 2026
@paulmedynski paulmedynski added the Area\Connection Pooling Use this label to tag issues that apply to problems with connection pool. label May 21, 2026
@paulmedynski paulmedynski moved this from To triage to In review in SqlClient Board May 21, 2026
@mdaigle mdaigle self-assigned this May 21, 2026
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

}
}
else if (!obj.IsConnectionAlive())
else if (!obj.IsConnectionAlive() || IsIdleExpired(obj))
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.

// 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.


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);

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.

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

@github-project-automation github-project-automation Bot moved this from In review to Waiting for customer in SqlClient Board May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area\Connection Pooling Use this label to tag issues that apply to problems with connection pool.

Projects

Status: Waiting for customer

Development

Successfully merging this pull request may close these issues.

4 participants