Skip to content
Merged
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 @@ -160,11 +160,14 @@ internal ChannelDbConnectionPool(
_errorState = new BlockingPeriodErrorState(_instanceId, timeProvider: timeProvider);
}

// Pruning is only useful when the pool can grow beyond MinPoolSize.
// If min >= max, the pool is fixed-size and pruning would never activate.
if (MinPoolSize < MaxPoolSize)
{
Pruner = new PoolPruner(this, PoolGroupOptions.LoadBalanceTimeout);
// Pruning is only useful when the pool can grow beyond MinPoolSize and idle
// connections are subject to reclamation. If min >= max the pool is fixed-size so
// pruning would never activate; if Connection Idle Timeout is zero, idle connections
// are never reclaimed and there is nothing to prune. The pruning window (and thus how
// many samples are collected) is derived from IdleTimeout.
if (MinPoolSize < MaxPoolSize && PoolGroupOptions.IdleTimeout != TimeSpan.Zero)
{
Pruner = new PoolPruner(this, PoolGroupOptions.IdleTimeout);
}

State = Running;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Threading;
using Microsoft.Data.Common;
using Microsoft.Data.SqlClient.Internal;
Comment thread
apoorvdeshmukh marked this conversation as resolved.

#nullable enable

Expand All @@ -14,29 +15,41 @@ namespace Microsoft.Data.SqlClient.ConnectionPool
/// Periodically samples the idle connection count and, once enough samples are collected,
/// computes the median and prunes that many idle connections from the pool.
/// <para>
/// This type is only instantiated when the pool can shrink (MinPoolSize &lt; MaxPoolSize).
/// When the pool is fixed-size, the pool holds no <see cref="PoolPruner"/> instance.
/// This type is only instantiated when the pool can shrink (MinPoolSize &lt; MaxPoolSize) and
/// idle-timeout based reclamation is enabled (Connection Idle Timeout &gt; 0). When the pool is
/// fixed-size or idle reclamation is disabled, the pool holds no <see cref="PoolPruner"/> instance.
/// </para>
/// </summary>
internal sealed class PoolPruner : IDisposable
{
/// <summary>
/// Maximum allowed sample buffer size to prevent excessive memory allocation
/// from very large lifetime values. With a 10-second interval this supports
/// idle lifetimes up to 3000 seconds (50 minutes).
/// Maximum number of idle-count samples collected per pruning window. Bounds the sample
/// buffer size and the per-window sort cost regardless of how large the configured idle
/// timeout is. Once a window would need more than this many samples at
/// <see cref="MinIntervalSeconds"/>, the sampling interval is stretched instead of growing
/// the buffer, keeping the sample count bounded (the interval itself is separately capped by
/// <see cref="MaxIntervalSeconds"/>, so for very large idle timeouts the effective window can
/// be shorter than the configured timeout).
/// At the <see cref="MinIntervalSeconds"/> floor this supports idle timeouts up to
/// <c>MaxSampleSize * MinIntervalSeconds</c> = 3000 seconds (50 minutes) before stretching.
/// </summary>
internal const int MaxSampleSize = 300;

/// <summary>
/// Default sampling interval.
/// Minimum (default) sampling interval, in seconds. Acts as a floor so the pruning timer
/// never fires more often than this and preserves the Story 1 cadence for short/typical
/// idle timeouts (e.g. the default 300s idle timeout yields a 10s interval and 30 samples).
/// </summary>
private static readonly TimeSpan DefaultSamplingInterval = TimeSpan.FromSeconds(10);
internal const int MinIntervalSeconds = 10;

/// <summary>
/// Default lifetime window used for sample size calculation when
/// LoadBalanceTimeout (Connection Lifetime) is zero.
/// Maximum sampling interval, in seconds (1 day). Purely a defensive guard so the derived
/// interval handed to <see cref="Timer.Change(TimeSpan, TimeSpan)"/> stays well under its
/// ~49.7-day limit for pathological idle timeouts (the <c>Connection Idle Timeout</c> keyword
/// accepts up to <see cref="int.MaxValue"/> seconds). It only engages above ~300-day idle
/// timeouts, so it never affects any realistic configuration.
/// </summary>
private static readonly TimeSpan DefaultLifetimeWindow = TimeSpan.FromSeconds(300);
internal const int MaxIntervalSeconds = 24 * 60 * 60;

/// <summary>
/// The owning connection pool whose idle connections are pruned.
Expand All @@ -56,7 +69,8 @@ internal sealed class PoolPruner : IDisposable

/// <summary>
/// Number of idle count samples to collect before computing the median and pruning.
/// Equals lifetime / interval (rounded up), clamped to <see cref="MaxSampleSize"/>.
/// Equals <c>ceil(idleTimeout / samplingInterval)</c>, bounded by <see cref="MaxSampleSize"/>
/// (the interval is stretched rather than exceeding the sample cap for large idle timeouts).
/// </summary>
private readonly int _sampleSize;

Expand Down Expand Up @@ -88,25 +102,46 @@ internal sealed class PoolPruner : IDisposable
/// Creates a new pruner for the given pool.
/// </summary>
/// <param name="pool">The owning connection pool.</param>
/// <param name="lifetimeWindow">
/// The connection lifetime window used to compute sample count.
/// When zero or negative, <see cref="DefaultLifetimeWindow"/> is used.
/// <param name="idleTimeout">
/// The configured Connection Idle Timeout, used as the pruning window. The sampling interval
/// and sample count are derived from it so the window covers the idle timeout for typical
/// values, while the interval stays bounded by <see cref="MaxIntervalSeconds"/> and the sample
/// count by <see cref="MaxSampleSize"/> (see the constructor body for the exact formula). For
/// very large idle timeouts both are capped, so the effective window can be shorter than the
/// configured timeout. The pool only constructs a pruner when idle-timeout based reclamation is
/// enabled (idleTimeout &gt; 0); a defensive floor keeps the sizing math valid.
/// </param>
internal PoolPruner(ChannelDbConnectionPool pool, TimeSpan lifetimeWindow)
internal PoolPruner(ChannelDbConnectionPool pool, TimeSpan idleTimeout)
{
_pool = pool;
_samplingInterval = DefaultSamplingInterval;

int lifetimeSeconds = (int)lifetimeWindow.TotalSeconds;
if (lifetimeSeconds <= 0)
// The pool only constructs a pruner when IdleTimeout > 0, but floor at one second so
// the sizing math below always sees a positive window even under unexpected inputs.
int idleTimeoutSeconds = Math.Max(1, (int)idleTimeout.TotalSeconds);

// Derive the interval so the window covers the idle timeout with at most MaxSampleSize
// samples. Short/typical timeouts keep the MinIntervalSeconds cadence (e.g. 300s -> 10s,
// 30 samples); large timeouts stretch the interval and pin the count at MaxSampleSize. The
// MaxIntervalSeconds cap is a defensive Timer.Change overflow guard (engages >~300 days).
// Math.Clamp is unavailable on net462, so clamp explicitly with Min/Max.
int intervalForMaxSamples = DivideRoundingUp(idleTimeoutSeconds, MaxSampleSize);
int flooredInterval = Math.Max(intervalForMaxSamples, MinIntervalSeconds);
int intervalSeconds = Math.Min(flooredInterval, MaxIntervalSeconds);

int sampleSize = Math.Min(MaxSampleSize, DivideRoundingUp(idleTimeoutSeconds, intervalSeconds));

if (intervalSeconds != MinIntervalSeconds)
{
lifetimeSeconds = (int)DefaultLifetimeWindow.TotalSeconds;
// The interval was stretched (or clamped) away from the default. Surface the derived
// cadence so operators can tell why a very large Connection Idle Timeout samples less
// frequently than the default 10-second interval.
SqlClientEventSource.Log.TryPoolerTraceEvent(
"<prov.PoolPruner|RES|INFO|CPOOL> Idle timeout {0}s derived a pruning interval of {1}s with {2} samples (max {3}).",
idleTimeoutSeconds, intervalSeconds, sampleSize, MaxSampleSize);
}

int intervalSeconds = (int)_samplingInterval.TotalSeconds;
_sampleSize = Math.Min(
DivideRoundingUp(lifetimeSeconds, intervalSeconds),
MaxSampleSize);
_samplingInterval = TimeSpan.FromSeconds(intervalSeconds);
_sampleSize = sampleSize;

// Subtract 1 to convert from length to 0-based index. Safe because
// DivideRoundingUp always returns >= 1, so _sampleSize >= 1.
Expand All @@ -127,6 +162,9 @@ internal PoolPruner(ChannelDbConnectionPool pool, TimeSpan lifetimeWindow)
/// <summary>Total number of samples collected per pruning window. Exposed for unit tests.</summary>
internal int SampleSize => _sampleSize;

/// <summary>The derived interval between pruning samples/evaluations. Exposed for unit tests.</summary>
internal TimeSpan SamplingInterval => _samplingInterval;

/// <summary>Read-only view of the sample buffer contents. Exposed for unit tests.</summary>
internal ReadOnlySpan<int> Samples => _samples;

Expand Down Expand Up @@ -162,7 +200,7 @@ internal void UpdateTimer()
else if (numConnections <= _pool.PoolGroupOptions.MinPoolSize && _timerEnabled)
{
// Pool shrunk back to min — stop pruning, reset sample buffer.
_timer.Change(Timeout.Infinite, Timeout.Infinite);
_timer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
_sampleIndex = 0;
_timerEnabled = false;
}
Expand Down
Loading
Loading