Skip to content
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

WIP: Feature | Introduce "Pool Idle Timeout" connection property #348

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 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 @@ -585,13 +585,24 @@ False
<format type="text/markdown"><![CDATA[

## Remarks
When connection pooling is enabled and a timeout error or other login error occurs, an exception will be thrown and subsequent connection attempts will fail for the next five seconds, the "blocking period". If the application attempts to connect within the blocking period, the first exception will be thrown again. Subsequent failures after a blocking period ends will result in a new blocking period that is twice as long as the previous blocking period, up to a maximum of one minute.
When connection pooling is enabled and a timeout error or other login error occurs, an exception will be thrown and subsequent connection attempts will fail for the next five seconds, the "blocking period". If the application attempts to connect within the blocking period, the first exception will be thrown again. Subsequent failures after a blocking period ends will result in a new blocking period that is twice as long as the previous blocking period, up to a maximum of one minute.

Attempting to connect to Azure SQL databases can fail with transient errors which are typically recovered within a few seconds. However, with the connection pool blocking period behavior, you may not be able to reach your database for extensive periods even though the database is available. This is especially problematic for apps that need to render fast. The **PoolBlockingPeriod** enables you to select the blocking period best suited for your app. See the <xref:Microsoft.Data.SqlClient.PoolBlockingPeriod> enumeration for available settings.

]]></format>
</remarks>
</PoolBlockingPeriod>
<PoolIdleTimeout>
<summary>Gets or sets the maximum length of time (in seconds) a connection can be idle in the pool before it is automatically closed.</summary>
<value>The value of the <see cref="P:Microsoft.Data.SqlClient.SqlConnectionStringBuilder.PoolIdleTimeout" /> property, or randomly generated value between 4-8 minutes if no value has been supplied. Acceptable value range: 0 to <see cref="P:System.Int32.MaxValue" /></value>
<remarks>
<format type="text/markdown"><![CDATA[

## Remarks
This property corresponds to the "Pool Idle Timeout" or "pool idle timeout" keys within the connection string.
]]></format>
</remarks>
</PoolIdleTimeout>
<Pooling>
<summary>Gets or sets a Boolean value that indicates whether the connection will be pooled or explicitly opened every time that the connection is requested.</summary>
<value>The value of the <see cref="P:Microsoft.Data.SqlClient.SqlConnectionStringBuilder.Pooling" /> property, or <see langword="true" /> if none has been supplied.</value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,10 @@ public SqlConnectionStringBuilder(string connectionString) { }
[System.ComponentModel.DisplayNameAttribute("Min Pool Size")]
[System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)]
public int MinPoolSize { get { throw null; } set { } }
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml' path='docs/members[@name="SqlConnectionStringBuilder"]/PoolIdleTimeout/*'/>
[System.ComponentModel.DisplayNameAttribute("Pool Idle Timeout")]
[System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)]
public int PoolIdleTimeout { get { throw null; } set { } }
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml' path='docs/members[@name="SqlConnectionStringBuilder"]/MultipleActiveResultSets/*'/>
[System.ComponentModel.DisplayNameAttribute("MultipleActiveResultSets")]
[System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ internal static partial class ADP

internal const CompareOptions DefaultCompareOptions = CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth | CompareOptions.IgnoreCase;
internal const int DefaultConnectionTimeout = DbConnectionStringDefaults.ConnectTimeout;

internal const int DefaultPoolIdleTimeout = DbConnectionStringDefaults.PoolIdleTimeout;

static partial void TraceException(string trace, Exception e);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@ internal static partial class DbConnectionStringDefaults
internal const bool MultiSubnetFailover = false;
internal const int MaxPoolSize = 100;
internal const int MinPoolSize = 0;
internal const int PoolIdleTimeout = -1;
internal const int PacketSize = 8000;
internal const string Password = "";
internal const bool PersistSecurityInfo = false;
Expand Down Expand Up @@ -698,6 +699,7 @@ internal static partial class DbConnectionStringKeywords
internal const string MaxPoolSize = "Max Pool Size";
internal const string Pooling = "Pooling";
internal const string MinPoolSize = "Min Pool Size";
internal const string PoolIdleTimeout = "Pool Idle Timeout";
#if netcoreapp
internal const string PoolBlockingPeriod = "PoolBlockingPeriod";
#endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ internal WaitHandle[] GetHandles(bool withCreate)
private static readonly Random s_random = new Random(5101977); // Value obtained from Dave Driver

private readonly int _cleanupWait;
private readonly int _poolIdleTimeout;
private readonly DbConnectionPoolIdentity _identity;

private readonly DbConnectionFactory _connectionFactory;
Expand Down Expand Up @@ -399,17 +400,27 @@ internal DbConnectionPool(

_state = State.Initializing;

lock (s_random)
{ // Random.Next is not thread-safe
_cleanupWait = s_random.Next(12, 24) * 10 * 1000; // 2-4 minutes in 10 sec intervals
}

_connectionFactory = connectionFactory;
_connectionPoolGroup = connectionPoolGroup;
_connectionPoolGroupOptions = connectionPoolGroup.PoolGroupOptions;
_connectionPoolProviderInfo = connectionPoolProviderInfo;
_identity = identity;

_poolIdleTimeout = _connectionPoolGroupOptions.PoolIdleTimeout;

if (_poolIdleTimeout > -1)
{
// Use PoolIdleTimout (in seconds) to create _cleanupWait timer
_cleanupWait = _poolIdleTimeout * 1000;
}
else
{
lock (s_random)
{ // Random.Next is not thread-safe
_cleanupWait = s_random.Next(12, 24) * 10 * 1000; // 2-4 minutes in 10 sec intervals
}
}

_waitHandles = new PoolWaitHandles();

_errorWait = ERROR_WAIT_DEFAULT;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ internal sealed class DbConnectionPoolGroupOptions
private readonly bool _poolByIdentity;
private readonly int _minPoolSize;
private readonly int _maxPoolSize;
private readonly int _poolIdleTimeout;
private readonly int _creationTimeout;
private readonly TimeSpan _loadBalanceTimeout;
private readonly bool _hasTransactionAffinity;
Expand All @@ -20,6 +21,7 @@ public DbConnectionPoolGroupOptions(
bool poolByIdentity,
int minPoolSize,
int maxPoolSize,
int poolIdleTimeout,
int creationTimeout,
int loadBalanceTimeout,
bool hasTransactionAffinity
Expand All @@ -28,6 +30,7 @@ bool hasTransactionAffinity
_poolByIdentity = poolByIdentity;
_minPoolSize = minPoolSize;
_maxPoolSize = maxPoolSize;
_poolIdleTimeout = poolIdleTimeout;
_creationTimeout = creationTimeout;

if (0 != loadBalanceTimeout)
Expand Down Expand Up @@ -59,6 +62,10 @@ public int MinPoolSize
{
get { return _minPoolSize; }
}
public int PoolIdleTimeout
{
get { return _poolIdleTimeout; }
}
public bool PoolByIdentity
{
get { return _poolByIdentity; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ override protected DbConnectionPoolGroupOptions CreateConnectionPoolGroupOptions
opt.IntegratedSecurity,
opt.MinPoolSize,
opt.MaxPoolSize,
opt.PoolIdleTimeout,
connectionTimeout,
opt.LoadBalanceTimeout,
opt.Enlist);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ internal static partial class DEFAULT
internal const bool MARS = false;
internal const int Max_Pool_Size = 100;
internal const int Min_Pool_Size = 0;
internal const int Pool_Idle_Timeout = ADP.DefaultPoolIdleTimeout;
internal const bool MultiSubnetFailover = DbConnectionStringDefaults.MultiSubnetFailover;
internal const int Packet_Size = 8000;
internal const string Password = "";
Expand Down Expand Up @@ -81,6 +82,7 @@ internal static class KEY
internal const string MARS = "multipleactiveresultsets";
internal const string Max_Pool_Size = "max pool size";
internal const string Min_Pool_Size = "min pool size";
internal const string Pool_Idle_Timeout = "pool idle timeout";
internal const string MultiSubnetFailover = "multisubnetfailover";
internal const string Network_Library = "network library";
internal const string Packet_Size = "packet size";
Expand Down Expand Up @@ -196,6 +198,7 @@ internal static class TRANSACTIONBINDING
private readonly int _loadBalanceTimeout;
private readonly int _maxPoolSize;
private readonly int _minPoolSize;
private readonly int _poolIdleTimeout;
private readonly int _packetSize;
private readonly int _connectRetryCount;
private readonly int _connectRetryInterval;
Expand Down Expand Up @@ -251,6 +254,7 @@ internal SqlConnectionString(string connectionString) : base(connectionString, G
_loadBalanceTimeout = ConvertValueToInt32(KEY.Load_Balance_Timeout, DEFAULT.Load_Balance_Timeout);
_maxPoolSize = ConvertValueToInt32(KEY.Max_Pool_Size, DEFAULT.Max_Pool_Size);
_minPoolSize = ConvertValueToInt32(KEY.Min_Pool_Size, DEFAULT.Min_Pool_Size);
_poolIdleTimeout = ConvertValueToInt32(KEY.Pool_Idle_Timeout, DEFAULT.Pool_Idle_Timeout);
_packetSize = ConvertValueToInt32(KEY.Packet_Size, DEFAULT.Packet_Size);
_connectRetryCount = ConvertValueToInt32(KEY.Connect_Retry_Count, DEFAULT.Connect_Retry_Count);
_connectRetryInterval = ConvertValueToInt32(KEY.Connect_Retry_Interval, DEFAULT.Connect_Retry_Interval);
Expand Down Expand Up @@ -302,13 +306,16 @@ internal SqlConnectionString(string connectionString) : base(connectionString, G
{
throw ADP.InvalidMinMaxPoolSizeValues();
}
if (_poolIdleTimeout < -1)
{
throw ADP.InvalidConnectionOptionValue(KEY.Pool_Idle_Timeout);
}

if ((_packetSize < TdsEnums.MIN_PACKET_SIZE) || (TdsEnums.MAX_PACKET_SIZE < _packetSize))
{
throw SQL.InvalidPacketSizeValue();
}


ValidateValueLength(_applicationName, TdsEnums.MAXLEN_APPNAME, KEY.Application_Name);
ValidateValueLength(_currentLanguage, TdsEnums.MAXLEN_LANGUAGE, KEY.Current_Language);
ValidateValueLength(_dataSource, TdsEnums.MAXLEN_SERVERNAME, KEY.Data_Source);
Expand Down Expand Up @@ -479,6 +486,7 @@ internal SqlConnectionString(SqlConnectionString connectionOptions, string dataS
#endif
_maxPoolSize = connectionOptions._maxPoolSize;
_minPoolSize = connectionOptions._minPoolSize;
_poolIdleTimeout = connectionOptions._poolIdleTimeout;
_multiSubnetFailover = connectionOptions._multiSubnetFailover;
_packetSize = connectionOptions._packetSize;
_applicationName = connectionOptions._applicationName;
Expand Down Expand Up @@ -531,6 +539,7 @@ internal SqlConnectionString(SqlConnectionString connectionOptions, string dataS
internal int LoadBalanceTimeout { get { return _loadBalanceTimeout; } }
internal int MaxPoolSize { get { return _maxPoolSize; } }
internal int MinPoolSize { get { return _minPoolSize; } }
internal int PoolIdleTimeout { get { return _poolIdleTimeout; } }
internal int PacketSize { get { return _packetSize; } }
internal int ConnectRetryCount { get { return _connectRetryCount; } }
internal int ConnectRetryInterval { get { return _connectRetryInterval; } }
Expand Down Expand Up @@ -566,7 +575,7 @@ protected internal override string Expand()
{
if (null != _expandedAttachDBFilename)
{
return ExpandAttachDbFileName(_expandedAttachDBFilename);
return ExpandAttachDbFileName(_expandedAttachDBFilename);
}
else
{
Expand Down Expand Up @@ -629,6 +638,7 @@ internal static Dictionary<string, string> GetParseSynonyms()
{ KEY.MARS, KEY.MARS },
{ KEY.Max_Pool_Size, KEY.Max_Pool_Size },
{ KEY.Min_Pool_Size, KEY.Min_Pool_Size },
{ KEY.Pool_Idle_Timeout, KEY.Pool_Idle_Timeout },
{ KEY.MultiSubnetFailover, KEY.MultiSubnetFailover },
{ KEY.Network_Library, KEY.Network_Library },
{ KEY.Packet_Size, KEY.Packet_Size },
Expand Down Expand Up @@ -843,7 +853,7 @@ internal SqlConnectionAttestationProtocol ConvertValueToAttestationProtocol()
}
catch (FormatException e)
{
throw ADP.InvalidConnectionOptionValue(KEY.AttestationProtocol, e);
throw ADP.InvalidConnectionOptionValue(KEY.AttestationProtocol, e);
}
catch (OverflowException e)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ private enum Keywords
Pooling,
MinPoolSize,
MaxPoolSize,
PoolIdleTimeout,
#if netcoreapp
PoolBlockingPeriod,
#endif
Expand Down Expand Up @@ -97,6 +98,7 @@ private enum Keywords
private int _loadBalanceTimeout = DbConnectionStringDefaults.LoadBalanceTimeout;
private int _maxPoolSize = DbConnectionStringDefaults.MaxPoolSize;
private int _minPoolSize = DbConnectionStringDefaults.MinPoolSize;
private int _poolIdleTimeout = DbConnectionStringDefaults.PoolIdleTimeout;
private int _packetSize = DbConnectionStringDefaults.PacketSize;
private int _connectRetryCount = DbConnectionStringDefaults.ConnectRetryCount;
private int _connectRetryInterval = DbConnectionStringDefaults.ConnectRetryInterval;
Expand Down Expand Up @@ -136,6 +138,7 @@ private static string[] CreateValidKeywords()
validKeywords[(int)Keywords.LoadBalanceTimeout] = DbConnectionStringKeywords.LoadBalanceTimeout;
validKeywords[(int)Keywords.MaxPoolSize] = DbConnectionStringKeywords.MaxPoolSize;
validKeywords[(int)Keywords.MinPoolSize] = DbConnectionStringKeywords.MinPoolSize;
validKeywords[(int)Keywords.PoolIdleTimeout] = DbConnectionStringKeywords.PoolIdleTimeout;
validKeywords[(int)Keywords.MultipleActiveResultSets] = DbConnectionStringKeywords.MultipleActiveResultSets;
validKeywords[(int)Keywords.MultiSubnetFailover] = DbConnectionStringKeywords.MultiSubnetFailover;
// validKeywords[(int)Keywords.NamedConnection] = DbConnectionStringKeywords.NamedConnection;
Expand Down Expand Up @@ -180,6 +183,7 @@ private static Dictionary<string, Keywords> CreateKeywordsDictionary()
hash.Add(DbConnectionStringKeywords.MultipleActiveResultSets, Keywords.MultipleActiveResultSets);
hash.Add(DbConnectionStringKeywords.MaxPoolSize, Keywords.MaxPoolSize);
hash.Add(DbConnectionStringKeywords.MinPoolSize, Keywords.MinPoolSize);
hash.Add(DbConnectionStringKeywords.PoolIdleTimeout, Keywords.PoolIdleTimeout);
hash.Add(DbConnectionStringKeywords.MultiSubnetFailover, Keywords.MultiSubnetFailover);
// hash.Add(DbConnectionStringKeywords.NamedConnection, Keywords.NamedConnection);
hash.Add(DbConnectionStringKeywords.PacketSize, Keywords.PacketSize);
Expand Down Expand Up @@ -288,7 +292,6 @@ public override object this[string keyword]
case Keywords.WorkstationID:
WorkstationID = ConvertToString(value);
break;

case Keywords.ConnectTimeout:
ConnectTimeout = ConvertToInt32(value);
break;
Expand All @@ -301,6 +304,9 @@ public override object this[string keyword]
case Keywords.MinPoolSize:
MinPoolSize = ConvertToInt32(value);
break;
case Keywords.PoolIdleTimeout:
PoolIdleTimeout = ConvertToInt32(value);
break;
case Keywords.PacketSize:
PacketSize = ConvertToInt32(value);
break;
Expand Down Expand Up @@ -646,6 +652,21 @@ public int MinPoolSize
}
}

/// <include file='..\..\..\..\..\..\..\doc\snippets\Microsoft.Data.SqlClient\SqlConnectionStringBuilder.xml' path='docs/members[@name="SqlConnectionStringBuilder"]/PoolIdleTimeout/*' />
public int PoolIdleTimeout
{
get { return _poolIdleTimeout; }
set
{
if (value < 0)
Copy link
Contributor

Choose a reason for hiding this comment

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

This check for <0 will mean that users can't set it to the default value of -1 to return to the default behaviour which seems counter intuitive, both removing and setting to default value should normally yield the same result I think.

Copy link
Member Author

Choose a reason for hiding this comment

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

We're only accepting values 0 to Int32.Max as documented here.

This customized behaviour is disabled by default (hence -1) so setting to anything other than the range is essentially not allowed.

Same is with Connection Timeout although it's default is 15 secs. We not really obstructing default value, but anything beyond range. I was also thinking of whether I should also check for Max value here, but didn't see that at other places, but maybe we should check everywhere?

Copy link
Contributor

Choose a reason for hiding this comment

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

This customized behaviour is disabled by default (hence -1) so setting to anything other than the range is essentially not allowed.

Sure, but if you enable it how do you then disable it without starting again with a new connection string builder? Or is that not a suggested use case?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes you're right. I will change it to < -1.

Copy link
Contributor

Choose a reason for hiding this comment

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

Is 0 a useful value to allow? I would expect that to instantly dispose of pools which is the same thing as just not using pooling.

Copy link
Member Author

Choose a reason for hiding this comment

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

The behavior is same, yes. Allowing 0 or not, doesn't seem a problem to me.

@David-Engel if you could confirm the range as well, I can update code accordingly.

{
throw ADP.InvalidConnectionOptionValue(DbConnectionStringKeywords.PoolIdleTimeout);
}
SetValue(DbConnectionStringKeywords.PoolIdleTimeout, value);
_poolIdleTimeout = value;
}
}

/// <include file='..\..\..\..\..\..\..\doc\snippets\Microsoft.Data.SqlClient\SqlConnectionStringBuilder.xml' path='docs/members[@name="SqlConnectionStringBuilder"]/MultipleActiveResultSets/*' />
public bool MultipleActiveResultSets
{
Expand Down Expand Up @@ -920,6 +941,8 @@ private object GetAt(Keywords index)
return MaxPoolSize;
case Keywords.MinPoolSize:
return MinPoolSize;
case Keywords.PoolIdleTimeout:
return PoolIdleTimeout;
case Keywords.MultiSubnetFailover:
return MultiSubnetFailover;
// case Keywords.NamedConnection: return NamedConnection;
Expand Down Expand Up @@ -1048,6 +1071,9 @@ private void Reset(Keywords index)
case Keywords.MinPoolSize:
_minPoolSize = DbConnectionStringDefaults.MinPoolSize;
break;
case Keywords.PoolIdleTimeout:
_poolIdleTimeout = DbConnectionStringDefaults.PoolIdleTimeout;
break;
case Keywords.MultiSubnetFailover:
_multiSubnetFailover = DbConnectionStringDefaults.MultiSubnetFailover;
break;
Expand Down
Loading