Skip to content

Commit ebde504

Browse files
Fix pooled connection re-use on access token expiry (#635)
1 parent 16f5f32 commit ebde504

File tree

9 files changed

+122
-62
lines changed

9 files changed

+122
-62
lines changed

src/Microsoft.Data.SqlClient/netcore/src/Common/src/Microsoft/Data/Common/AdapterUtil.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ internal static partial class ADP
2929
internal static Task<bool> FalseTask => _falseTask ?? (_falseTask = Task.FromResult(false));
3030

3131
internal const CompareOptions DefaultCompareOptions = CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth | CompareOptions.IgnoreCase;
32+
3233
internal const int DefaultConnectionTimeout = DbConnectionStringDefaults.ConnectTimeout;
34+
internal const int InfiniteConnectionTimeout = 0; // infinite connection timeout identifier in seconds
35+
internal const int MaxBufferAccessTokenExpiry = 600; // max duration for buffer in seconds
3336

3437
static private void TraceException(string trace, Exception e)
3538
{

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/ProviderBase/DbConnectionInternal.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ virtual protected bool ReadyToPrepareTransaction
203203
}
204204
}
205205

206+
internal virtual bool IsAccessTokenExpired => false;
207+
206208
abstract protected void Activate(Transaction transaction);
207209

208210
internal void ActivateConnection(Transaction transaction)

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/ProviderBase/DbConnectionPool.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,6 +1285,13 @@ private bool TryGetConnection(DbConnection owningObject, uint waitForMultipleObj
12851285
_waitHandles.CreationSemaphore.Release(1);
12861286
}
12871287
}
1288+
1289+
// Do not use this pooled connection if access token is about to expire soon before we can connect.
1290+
if(null != obj && obj.IsAccessTokenExpired)
1291+
{
1292+
DestroyObject(obj);
1293+
obj = null;
1294+
}
12881295
} while (null == obj);
12891296
}
12901297

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ override protected DbConnectionInternal CreateConnection(DbConnectionOptions opt
3737
{
3838
SqlConnectionString opt = (SqlConnectionString)options;
3939
SqlConnectionPoolKey key = (SqlConnectionPoolKey)poolKey;
40-
SqlInternalConnection result = null;
4140
SessionData recoverySessionData = null;
4241

4342
SqlConnection sqlOwningConnection = (SqlConnection)owningConnection;
@@ -131,8 +130,7 @@ override protected DbConnectionInternal CreateConnection(DbConnectionOptions opt
131130
opt = new SqlConnectionString(opt, instanceName, userInstance: false, setEnlistValue: null);
132131
poolGroupProviderInfo = null; // null so we do not pass to constructor below...
133132
}
134-
result = new SqlInternalConnectionTds(identity, opt, key.Credential, poolGroupProviderInfo, "", null, redirectedUserInstance, userOpt, recoverySessionData, applyTransientFaultHandling: applyTransientFaultHandling, key.AccessToken, pool);
135-
return result;
133+
return new SqlInternalConnectionTds(identity, opt, key.Credential, poolGroupProviderInfo, "", null, redirectedUserInstance, userOpt, recoverySessionData, applyTransientFaultHandling: applyTransientFaultHandling, key.AccessToken, pool);
136134
}
137135

138136
protected override DbConnectionOptions CreateConnectionOptions(string connectionString, DbConnectionOptions previous)

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs

Lines changed: 58 additions & 37 deletions
Large diffs are not rendered by default.

src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Common/AdapterUtil.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2180,6 +2180,8 @@ static internal Exception InvalidArgumentValue(string methodName)
21802180
internal const int DecimalMaxPrecision28 = 28; // there are some cases in Odbc where we need that ...
21812181
internal const int DefaultCommandTimeout = 30;
21822182
internal const int DefaultConnectionTimeout = DbConnectionStringDefaults.ConnectTimeout;
2183+
internal const int InfiniteConnectionTimeout = 0; // infinite connection timeout identifier in seconds
2184+
internal const int MaxBufferAccessTokenExpiry = 600; // max duration for buffer in seconds
21832185
internal const float FailoverTimeoutStep = 0.08F; // fraction of timeout to use for fast failover connections
21842186
internal const float FailoverTimeoutStepForTnir = 0.125F; // Fraction of timeout to use in case of Transparent Network IP resolution.
21852187
internal const int MinimumTimeoutForTnirMs = 500; // The first login attempt in Transparent network IP Resolution

src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/ProviderBase/DbConnectionInternal.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,8 @@ public ConnectionState State
382382
}
383383
}
384384

385+
internal virtual bool IsAccessTokenExpired => false;
386+
385387
abstract protected void Activate(SysTx.Transaction transaction);
386388

387389
internal void ActivateConnection(SysTx.Transaction transaction)

src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/ProviderBase/DbConnectionPool.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1528,6 +1528,13 @@ private bool TryGetConnection(DbConnection owningObject, uint waitForMultipleObj
15281528
{
15291529
Marshal.ThrowExceptionForHR(releaseSemaphoreResult); // will only throw if (hresult < 0)
15301530
}
1531+
1532+
// Do not use this pooled connection if access token is about to expire soon before we can connect.
1533+
if (null != obj && obj.IsAccessTokenExpired)
1534+
{
1535+
DestroyObject(obj);
1536+
obj = null;
1537+
}
15311538
} while (null == obj);
15321539
}
15331540

src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ sealed internal class SqlInternalConnectionTds : SqlInternalConnection, IDisposa
119119
// Connection Resiliency
120120
private bool _sessionRecoveryRequested;
121121
internal bool _sessionRecoveryAcknowledged;
122-
internal SessionData _currentSessionData; // internal for use from TdsParser only, otehr should use CurrentSessionData property that will fix database and language
122+
internal SessionData _currentSessionData; // internal for use from TdsParser only, other should use CurrentSessionData property that will fix database and language
123123
private SessionData _recoverySessionData;
124124

125125
// Federated Authentication
@@ -131,20 +131,33 @@ sealed internal class SqlInternalConnectionTds : SqlInternalConnection, IDisposa
131131
internal bool _federatedAuthenticationInfoRequested; // Keep this distinct from _federatedAuthenticationRequested, since some fedauth library types may not need more info
132132
internal bool _federatedAuthenticationInfoReceived;
133133

134+
// The Federated Authentication returned by TryGetFedAuthTokenLocked or GetFedAuthToken.
135+
SqlFedAuthToken _fedAuthToken = null;
134136
internal byte[] _accessTokenInBytes;
135137

136138
private readonly ActiveDirectoryAuthenticationTimeoutRetryHelper _activeDirectoryAuthTimeoutRetryHelper;
137139
private readonly SqlAuthenticationProviderManager _sqlAuthenticationProviderManager;
138140

139141
// Certificate auth calbacks.
140-
//
141142
ServerCertificateValidationCallback _serverCallback;
142143
ClientCertificateRetrievalCallback _clientCallback;
143144
SqlClientOriginalNetworkAddressInfo _originalNetworkAddressInfo;
144145

145146
internal bool _cleanSQLDNSCaching = false;
146147
private bool _serverSupportsDNSCaching = false;
147148

149+
/// <summary>
150+
/// Returns buffer time allowed before access token expiry to continue using the access token.
151+
/// </summary>
152+
private int accessTokenExpirationBufferTime
153+
{
154+
get
155+
{
156+
return (ConnectionOptions.ConnectTimeout == ADP.InfiniteConnectionTimeout || ConnectionOptions.ConnectTimeout >= ADP.MaxBufferAccessTokenExpiry)
157+
? ADP.MaxBufferAccessTokenExpiry : ConnectionOptions.ConnectTimeout;
158+
}
159+
}
160+
148161
/// <summary>
149162
/// Get or set if SQLDNSCaching FeatureExtAck is supported by the server.
150163
/// </summary>
@@ -807,6 +820,10 @@ protected override bool UnbindOnTransactionCompletion
807820
}
808821
}
809822

823+
/// <summary>
824+
/// Validates if federated authentication is used, Access Token used by this connection is active for the value of 'accessTokenExpirationBufferTime'.
825+
/// </summary>
826+
internal override bool IsAccessTokenExpired => _federatedAuthenticationInfoRequested && DateTime.FromFileTimeUtc(_fedAuthToken.expirationFileTime) < DateTime.UtcNow.AddSeconds(accessTokenExpirationBufferTime);
810827

811828
////////////////////////////////////////////////////////////////////////////////////////
812829
// GENERAL METHODS
@@ -1320,10 +1337,10 @@ internal void ExecuteTransactionYukon(
13201337
ThreadHasParserLockForClose = false;
13211338
_parserLock.Release();
13221339
releaseConnectionLock = false;
1323-
}, 0);
1340+
}, ADP.InfiniteConnectionTimeout);
13241341
if (reconnectTask != null)
13251342
{
1326-
AsyncHelper.WaitForCompletion(reconnectTask, 0); // there is no specific timeout for BeginTransaction, uses ConnectTimeout
1343+
AsyncHelper.WaitForCompletion(reconnectTask, ADP.InfiniteConnectionTimeout); // there is no specific timeout for BeginTransaction, uses ConnectTimeout
13271344
internalTransaction.ConnectionHasBeenRestored = true;
13281345
return;
13291346
}
@@ -2537,9 +2554,6 @@ internal void OnFedAuthInfo(SqlFedAuthInfo fedAuthInfo)
25372554
// We want to refresh the token, if taking the lock on the authentication context is successful.
25382555
bool attemptRefreshTokenLocked = false;
25392556

2540-
// The Federated Authentication returned by TryGetFedAuthTokenLocked or GetFedAuthToken.
2541-
SqlFedAuthToken fedAuthToken = null;
2542-
25432557
if (_dbConnectionPool != null)
25442558
{
25452559
Debug.Assert(_dbConnectionPool.AuthenticationContexts != null);
@@ -2574,7 +2588,7 @@ internal void OnFedAuthInfo(SqlFedAuthInfo fedAuthInfo)
25742588
}
25752589
else if (_forceExpiryLocked)
25762590
{
2577-
attemptRefreshTokenLocked = TryGetFedAuthTokenLocked(fedAuthInfo, dbConnectionPoolAuthenticationContext, out fedAuthToken);
2591+
attemptRefreshTokenLocked = TryGetFedAuthTokenLocked(fedAuthInfo, dbConnectionPoolAuthenticationContext, out _fedAuthToken);
25782592
}
25792593
#endif
25802594

@@ -2588,11 +2602,11 @@ internal void OnFedAuthInfo(SqlFedAuthInfo fedAuthInfo)
25882602

25892603
// Call the function which tries to acquire a lock over the authentication context before trying to update.
25902604
// If the lock could not be obtained, it will return false, without attempting to fetch a new token.
2591-
attemptRefreshTokenLocked = TryGetFedAuthTokenLocked(fedAuthInfo, dbConnectionPoolAuthenticationContext, out fedAuthToken);
2605+
attemptRefreshTokenLocked = TryGetFedAuthTokenLocked(fedAuthInfo, dbConnectionPoolAuthenticationContext, out _fedAuthToken);
25922606

2593-
// If TryGetFedAuthTokenLocked returns true, it means lock was obtained and fedAuthToken should not be null.
2607+
// If TryGetFedAuthTokenLocked returns true, it means lock was obtained and _fedAuthToken should not be null.
25942608
// If there was an exception in retrieving the new token, TryGetFedAuthTokenLocked should have thrown, so we won't be here.
2595-
Debug.Assert(!attemptRefreshTokenLocked || fedAuthToken != null, "Either Lock should not have been obtained or fedAuthToken should not be null.");
2609+
Debug.Assert(!attemptRefreshTokenLocked || _fedAuthToken != null, "Either Lock should not have been obtained or _fedAuthToken should not be null.");
25962610
Debug.Assert(!attemptRefreshTokenLocked || _newDbConnectionPoolAuthenticationContext != null, "Either Lock should not have been obtained or _newDbConnectionPoolAuthenticationContext should not be null.");
25972611

25982612
// Indicate in Bid Trace that we are successful with the update.
@@ -2609,8 +2623,8 @@ internal void OnFedAuthInfo(SqlFedAuthInfo fedAuthInfo)
26092623
if (dbConnectionPoolAuthenticationContext == null || attemptRefreshTokenUnLocked)
26102624
{
26112625
// Get the Federated Authentication Token.
2612-
fedAuthToken = GetFedAuthToken(fedAuthInfo);
2613-
Debug.Assert(fedAuthToken != null, "fedAuthToken should not be null.");
2626+
_fedAuthToken = GetFedAuthToken(fedAuthInfo);
2627+
Debug.Assert(_fedAuthToken != null, "_fedAuthToken should not be null.");
26142628

26152629
if (_dbConnectionPool != null)
26162630
{
@@ -2621,18 +2635,19 @@ internal void OnFedAuthInfo(SqlFedAuthInfo fedAuthInfo)
26212635
else if (!attemptRefreshTokenLocked)
26222636
{
26232637
Debug.Assert(dbConnectionPoolAuthenticationContext != null, "dbConnectionPoolAuthenticationContext should not be null.");
2624-
Debug.Assert(fedAuthToken == null, "fedAuthToken should be null in this case.");
2638+
Debug.Assert(_fedAuthToken == null, "_fedAuthToken should be null in this case.");
26252639
Debug.Assert(_newDbConnectionPoolAuthenticationContext == null, "_newDbConnectionPoolAuthenticationContext should be null.");
26262640

2627-
fedAuthToken = new SqlFedAuthToken();
2641+
_fedAuthToken = new SqlFedAuthToken();
26282642

26292643
// If the code flow is here, then we are re-using the context from the cache for this connection attempt and not
26302644
// generating a new access token on this thread.
2631-
fedAuthToken.accessToken = dbConnectionPoolAuthenticationContext.AccessToken;
2645+
_fedAuthToken.accessToken = dbConnectionPoolAuthenticationContext.AccessToken;
2646+
_fedAuthToken.expirationFileTime = dbConnectionPoolAuthenticationContext.ExpirationTime.ToFileTime();
26322647
}
26332648

2634-
Debug.Assert(fedAuthToken != null && fedAuthToken.accessToken != null, "fedAuthToken and fedAuthToken.accessToken cannot be null.");
2635-
_parser.SendFedAuthToken(fedAuthToken);
2649+
Debug.Assert(_fedAuthToken != null && _fedAuthToken.accessToken != null, "_fedAuthToken and _fedAuthToken.accessToken cannot be null.");
2650+
_parser.SendFedAuthToken(_fedAuthToken);
26362651
}
26372652

26382653
/// <summary>
@@ -2872,7 +2887,8 @@ internal void OnFeatureExtAck(int featureId, byte[] data)
28722887
{
28732888
if (_routingInfo != null)
28742889
{
2875-
if (TdsEnums.FEATUREEXT_SQLDNSCACHING != featureId) {
2890+
if (TdsEnums.FEATUREEXT_SQLDNSCACHING != featureId)
2891+
{
28762892
return;
28772893
}
28782894
}
@@ -3100,16 +3116,18 @@ internal void OnFeatureExtAck(int featureId, byte[] data)
31003116
throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream);
31013117
}
31023118

3103-
if (1 == data[0]) {
3119+
if (1 == data[0])
3120+
{
31043121
IsSQLDNSCachingSupported = true;
31053122
_cleanSQLDNSCaching = false;
3106-
3123+
31073124
if (_routingInfo != null)
31083125
{
31093126
IsDNSCachingBeforeRedirectSupported = true;
31103127
}
31113128
}
3112-
else {
3129+
else
3130+
{
31133131
// we receive the IsSupported whose value is 0
31143132
IsSQLDNSCachingSupported = false;
31153133
_cleanSQLDNSCaching = true;

0 commit comments

Comments
 (0)