Skip to content

Commit 6a8ab66

Browse files
Merge branch 'feat/sqlclientx' into cheena/tdsparser-tokens
2 parents 24adedd + 295867b commit 6a8ab66

File tree

6 files changed

+184
-57
lines changed

6 files changed

+184
-57
lines changed

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClientX/PoolingDataSource.cs

Lines changed: 100 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ internal sealed class PoolingDataSource : SqlDataSource
5353
/// </summary>
5454
private readonly ChannelReader<SqlConnector?> _idleConnectorReader;
5555
private readonly ChannelWriter<SqlConnector?> _idleConnectorWriter;
56+
57+
private ValueTask _warmupTask;
58+
private CancellationTokenSource _warmupCTS;
59+
private readonly SemaphoreSlim _warmupLock;
5660
#endregion
5761

5862
// Counts the total number of open connectors tracked by the pool.
@@ -65,11 +69,12 @@ internal sealed class PoolingDataSource : SqlDataSource
6569
/// Initializes a new PoolingDataSource.
6670
/// </summary>
6771
//TODO: support auth contexts and provider info
68-
internal PoolingDataSource(SqlConnectionStringBuilder connectionStringBuilder,
72+
internal PoolingDataSource(
73+
SqlConnectionString connectionString,
6974
SqlCredential credential,
7075
DbConnectionPoolGroupOptions options,
7176
RateLimiterBase connectionRateLimiter)
72-
: base(connectionStringBuilder, credential)
77+
: base(connectionString, credential)
7378
{
7479
_connectionPoolGroupOptions = options;
7580
_connectionRateLimiter = connectionRateLimiter;
@@ -83,6 +88,10 @@ internal PoolingDataSource(SqlConnectionStringBuilder connectionStringBuilder,
8388
_idleConnectorWriter = idleChannel.Writer;
8489

8590
//TODO: initiate idle lifetime and pruning fields
91+
92+
_warmupTask = ValueTask.CompletedTask;
93+
_warmupCTS = new CancellationTokenSource();
94+
_warmupLock = new SemaphoreSlim(1);
8695
}
8796

8897
#region properties
@@ -192,7 +201,7 @@ internal override async ValueTask<SqlConnector> GetInternalConnection(SqlConnect
192201
return connector;
193202
}
194203
}
195-
}
204+
}
196205
finally
197206
{
198207
//TODO: log error
@@ -273,7 +282,7 @@ private void CloseConnector(SqlConnector connector)
273282
}
274283

275284

276-
int i;
285+
int i;
277286
for (i = 0; i < MaxPoolSize; i++)
278287
{
279288
if (Interlocked.CompareExchange(ref _connectors[i], null, connector) == connector)
@@ -300,40 +309,44 @@ private void CloseConnector(SqlConnector connector)
300309

301310
// Only turn off the timer one time, when it was this Close that brought Open back to _min.
302311
//TODO: pruning
312+
313+
// Ensure that we return to min pool size if closing this connector brought us below min pool size.
314+
_ = WarmUp();
303315
}
304316

305317
/// <summary>
306318
/// A state object used to pass context to the rate limited connector creation operation.
307319
/// </summary>
308320
internal readonly struct OpenInternalConnectionState
309321
{
310-
internal readonly SqlConnectionX _owningConnection;
311-
internal readonly TimeSpan _timeout;
312-
313-
internal OpenInternalConnectionState(SqlConnectionX owningConnection, TimeSpan timeout)
314-
{
315-
_owningConnection = owningConnection;
316-
_timeout = timeout;
317-
}
322+
internal PoolingDataSource Pool { get; init; }
323+
internal SqlConnectionX? OwningConnection { get; init; }
324+
internal TimeSpan Timeout { get; init; }
318325
}
319326

320327
/// <inheritdoc/>
321-
internal override ValueTask<SqlConnector?> OpenNewInternalConnection(SqlConnectionX owningConnection, TimeSpan timeout, bool async, CancellationToken cancellationToken)
328+
internal override ValueTask<SqlConnector?> OpenNewInternalConnection(SqlConnectionX? owningConnection, TimeSpan timeout, bool async, CancellationToken cancellationToken)
322329
{
323330
return _connectionRateLimiter.Execute(
324331
RateLimitedOpen,
325-
new OpenInternalConnectionState(owningConnection, timeout),
332+
new OpenInternalConnectionState
333+
{
334+
Pool = this,
335+
OwningConnection = owningConnection,
336+
Timeout = timeout
337+
},
326338
async,
327339
cancellationToken
328340
);
329341

330-
async ValueTask<SqlConnector?> RateLimitedOpen(OpenInternalConnectionState state, bool async, CancellationToken cancellationToken)
342+
343+
static async ValueTask<SqlConnector?> RateLimitedOpen(OpenInternalConnectionState state, bool async, CancellationToken cancellationToken)
331344
{
332345
// As long as we're under max capacity, attempt to increase the connector count and open a new connection.
333-
for (var numConnectors = _numConnectors; numConnectors < MaxPoolSize; numConnectors = _numConnectors)
346+
for (var numConnectors = state.Pool._numConnectors; numConnectors < state.Pool.MaxPoolSize; numConnectors = state.Pool._numConnectors)
334347
{
335348
// Note that we purposefully don't use SpinWait for this: https://github.com/dotnet/coreclr/pull/21437
336-
if (Interlocked.CompareExchange(ref _numConnectors, numConnectors + 1, numConnectors) != numConnectors)
349+
if (Interlocked.CompareExchange(ref state.Pool._numConnectors, numConnectors + 1, numConnectors) != numConnectors)
337350
{
338351
continue;
339352
}
@@ -342,25 +355,25 @@ internal OpenInternalConnectionState(SqlConnectionX owningConnection, TimeSpan t
342355
{
343356
// We've managed to increase the open counter, open a physical connection.
344357
var startTime = Stopwatch.GetTimestamp();
345-
SqlConnector? connector = new SqlConnector(state._owningConnection, this);
358+
SqlConnector? connector = new SqlConnector(state.OwningConnection, state.Pool);
346359
//TODO: set clear counter on connector
347360

348-
await connector.Open(timeout, async, cancellationToken).ConfigureAwait(false);
361+
await connector.Open(state.Timeout, async, cancellationToken).ConfigureAwait(false);
349362

350363
int i;
351-
for (i = 0; i < MaxPoolSize; i++)
364+
for (i = 0; i < state.Pool.MaxPoolSize; i++)
352365
{
353-
if (Interlocked.CompareExchange(ref _connectors[i], connector, null) == null)
366+
if (Interlocked.CompareExchange(ref state.Pool._connectors[i], connector, null) == null)
354367
{
355368
break;
356369
}
357370
}
358371

359-
Debug.Assert(i < MaxPoolSize, $"Could not find free slot in {_connectors} when opening.");
360-
if (i == MaxPoolSize)
372+
Debug.Assert(i < state.Pool.MaxPoolSize, $"Could not find free slot in {state.Pool._connectors} when opening.");
373+
if (i == state.Pool.MaxPoolSize)
361374
{
362375
//TODO: generic exception?
363-
throw new Exception($"Could not find free slot in {_connectors} when opening. Please report a bug.");
376+
throw new Exception($"Could not find free slot in {state.Pool._connectors} when opening. Please report a bug.");
364377
}
365378

366379
// Only start pruning if we've incremented open count past _min.
@@ -375,12 +388,12 @@ internal OpenInternalConnectionState(SqlConnectionX owningConnection, TimeSpan t
375388
catch
376389
{
377390
// Physical open failed, decrement the open and busy counter back down.
378-
Interlocked.Decrement(ref _numConnectors);
391+
Interlocked.Decrement(ref state.Pool._numConnectors);
379392

380393
// In case there's a waiting attempt on the channel, we write a null to the idle connector channel
381394
// to wake it up, so it will try opening (and probably throw immediately)
382395
// Statement order is important since we have synchronous completions on the channel.
383-
_idleConnectorWriter.TryWrite(null);
396+
state.Pool._idleConnectorWriter.TryWrite(null);
384397

385398
// Just in case we always call UpdatePruningTimer for failed physical open
386399
//TODO: UpdatePruningTimer();
@@ -425,12 +438,67 @@ internal void PruneIdleConnections()
425438
}
426439

427440
/// <summary>
428-
/// Warms up the pool to bring it up to min pool size.
441+
/// Warms up the pool by bringing it up to min pool size.
429442
/// </summary>
430-
/// <exception cref="NotImplementedException"></exception>
431-
internal void WarmUp()
443+
/// <returns>A ValueTask containing a ValueTask that represents the warmup process.</returns>
444+
internal async ValueTask<ValueTask> WarmUp()
432445
{
433-
throw new NotImplementedException();
446+
// Avoid semaphore wait if task is still running
447+
if (!_warmupTask.IsCompleted)
448+
{
449+
return _warmupTask;
450+
}
451+
452+
// Prevent multiple threads from modifying the warmup task
453+
await _warmupLock.WaitAsync();
454+
455+
try
456+
{
457+
// The task may have been started by another thread while we were
458+
// waiting on the semaphore
459+
if (_warmupTask.IsCompleted)
460+
{
461+
_warmupTask = _WarmUp(_warmupCTS.Token);
462+
}
463+
}
464+
finally
465+
{
466+
_warmupLock.Release();
467+
}
468+
469+
return _warmupTask;
470+
471+
async ValueTask _WarmUp(CancellationToken ct)
472+
{
473+
// Best effort, we may over or under create due to race conditions.
474+
// Open new connections slowly. If many connections are needed immediately
475+
// upon pool creation they can always be created via user-initiated requests as fast
476+
// as a parallel, pool-initiated approach could.
477+
while (_numConnectors < MinPoolSize)
478+
{
479+
ct.ThrowIfCancellationRequested();
480+
481+
// Obey the same rate limit as user-initiated opens.
482+
// Ensures that pool-initiated opens are queued properly alongside user requests.
483+
SqlConnector? connector = await OpenNewInternalConnection(
484+
null,
485+
TimeSpan.FromSeconds(Settings.ConnectTimeout),
486+
true,
487+
ct)
488+
.ConfigureAwait(false);
489+
490+
// If connector is null, then we hit the max pool size and can stop
491+
// warming up the pool.
492+
if (connector == null)
493+
{
494+
return;
495+
}
496+
497+
// The connector has never been used, so it's safe to immediately return it to the
498+
// pool without resetting it.
499+
ReturnInternalConnection(connector);
500+
}
501+
}
434502
}
435503

436504
/// <summary>
@@ -439,6 +507,8 @@ internal void WarmUp()
439507
internal void Shutdown()
440508
{
441509
SqlClientEventSource.Log.TryPoolerTraceEvent("<prov.DbConnectionPool.Shutdown|RES|INFO|CPOOL> {0}", ObjectID);
510+
_warmupCTS.Dispose();
511+
_warmupLock.Dispose();
442512
_connectionRateLimiter?.Dispose();
443513
}
444514

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClientX/SqlConnector.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ internal sealed class SqlConnector
2828
private readonly TdsParserX _parser;
2929
private readonly ConnectionHandlerContext _connectionHandlerContext;
3030

31-
internal SqlConnector(SqlConnectionX owningConnection, SqlConnectionString connectionOptions, SqlDataSource dataSource)
31+
internal SqlConnector(SqlConnectionX? owningConnection, SqlConnectionString connectionOptions, SqlDataSource dataSource)
3232
{
3333
OwningConnection = owningConnection;
3434
ConnectionOptions = connectionOptions;

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClientX/SqlDataSource.cs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
using System;
99
using System.Data.Common;
10-
using System.Security.AccessControl;
1110
using System.Threading;
1211
using System.Threading.Tasks;
1312
using Microsoft.Data.SqlClient;
@@ -22,32 +21,43 @@ namespace Microsoft.Data.SqlClientX
2221
/// </summary>
2322
internal abstract class SqlDataSource : DbDataSource
2423
{
25-
#region private
26-
private readonly SqlConnectionStringBuilder _connectionStringBuilder;
2724
private protected volatile int _isDisposed;
28-
#endregion
2925

3026
#region constructors
3127
/// <summary>
3228
/// Initializes a new instance of SqlDataSource.
3329
/// </summary>
34-
/// <param name="connectionStringBuilder">The connection string that connections produced by this data source should use.</param>
30+
/// <param name="connectionString">The connection string that connections produced by this data source should use.</param>
3531
/// <param name="credential">The credentials that connections produced by this data source should use.</param>
3632
internal SqlDataSource(
37-
SqlConnectionStringBuilder connectionStringBuilder,
33+
SqlConnectionString connectionString,
3834
SqlCredential credential)
3935
{
40-
_connectionStringBuilder = connectionStringBuilder;
36+
Settings = connectionString;
37+
var hidePassword = !connectionString.PersistSecurityInfo;
38+
ConnectionString = connectionString.UsersConnectionString(hidePassword);
4139
Credential = credential;
4240
}
4341
#endregion
4442

4543
#region properties
4644
/// <inheritdoc />
47-
public override string ConnectionString => _connectionStringBuilder.ConnectionString;
45+
public override string ConnectionString { get; }
46+
47+
/// <summary>
48+
/// Stores settings for the data source.
49+
/// Do not expose publicly.
50+
/// </summary>
51+
internal SqlConnectionString Settings { get; }
4852

53+
/// <summary>
54+
/// Credentials to use for new connections created by this data source.
55+
/// </summary>
4956
internal SqlCredential Credential { get; }
5057

58+
/// <summary>
59+
/// Gives pool statistics for total, idle, and busy connections.
60+
/// </summary>
5161
internal abstract (int Total, int Idle, int Busy) Statistics { get; }
5262
#endregion
5363

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClientX/SqlDataSourceBuilder.cs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ internal sealed class SqlDataSourceBuilder
2323
/// <summary>
2424
/// A connection string builder that can be used to configure the connection string on the builder.
2525
/// </summary>
26-
public SqlConnectionStringBuilder ConnectionStringBuilder { get; }
26+
public SqlConnectionString ConnectionString { get; }
2727

2828
// TODO: how does it interact with credentials specified in ConnectionStringBuilder?
2929
public SqlCredential Credential { get; set; }
@@ -44,7 +44,7 @@ public SqlDataSourceBuilder(string connectionString = null, SqlCredential creden
4444
/// </summary>
4545
public SqlDataSourceBuilder(SqlConnectionStringBuilder connectionStringBuilder, SqlCredential sqlCredential = null)
4646
{
47-
ConnectionStringBuilder = connectionStringBuilder;
47+
ConnectionString = new SqlConnectionString(connectionStringBuilder.ConnectionString);
4848
Credential = sqlCredential;
4949
}
5050

@@ -53,44 +53,44 @@ public SqlDataSourceBuilder(SqlConnectionStringBuilder connectionStringBuilder,
5353
/// </summary>
5454
public SqlDataSource Build()
5555
{
56-
if (ConnectionStringBuilder.Pooling)
56+
if (ConnectionString.Pooling)
5757
{
5858
//TODO: pool group layer
5959

6060
DbConnectionPoolGroupOptions poolGroupOptions = new DbConnectionPoolGroupOptions(
61-
ConnectionStringBuilder.IntegratedSecurity,
62-
ConnectionStringBuilder.MinPoolSize,
63-
ConnectionStringBuilder.MaxPoolSize,
61+
ConnectionString.IntegratedSecurity,
62+
ConnectionString.MinPoolSize,
63+
ConnectionString.MaxPoolSize,
6464
//TODO: carry over connect timeout conversion logic from SqlConnectionFactory? if not, don't need an extra allocation for this object, just use connection string builder
65-
ConnectionStringBuilder.ConnectTimeout,
66-
ConnectionStringBuilder.LoadBalanceTimeout,
67-
ConnectionStringBuilder.Enlist);
65+
ConnectionString.ConnectTimeout,
66+
ConnectionString.LoadBalanceTimeout,
67+
ConnectionString.Enlist);
6868

6969
//TODO: evaluate app context switch for concurrency limit
7070
RateLimiterBase rateLimiter = IsBlockingPeriodEnabled() ? new BlockingPeriodRateLimiter() : new PassthroughRateLimiter();
7171

72-
return new PoolingDataSource(ConnectionStringBuilder,
72+
return new PoolingDataSource(ConnectionString,
7373
Credential,
7474
poolGroupOptions,
7575
rateLimiter);
7676
}
7777
else
7878
{
7979
return new UnpooledDataSource(
80-
ConnectionStringBuilder,
80+
ConnectionString,
8181
Credential);
8282
}
8383
}
8484

8585
private bool IsBlockingPeriodEnabled()
8686
{
87-
var policy = ConnectionStringBuilder.PoolBlockingPeriod;
87+
var policy = ConnectionString.PoolBlockingPeriod;
8888

8989
switch (policy)
9090
{
9191
case PoolBlockingPeriod.Auto:
9292
{
93-
if (ADP.IsAzureSqlServerEndpoint(ConnectionStringBuilder.DataSource))
93+
if (ADP.IsAzureSqlServerEndpoint(ConnectionString.DataSource))
9494
{
9595
return false; // in Azure it will be Disabled
9696
}

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClientX/UnpooledDataSource.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ internal sealed class UnpooledDataSource : SqlDataSource
2323
/// <summary>
2424
/// Initializes a new instance of UnpooledDataSource.
2525
/// </summary>
26-
/// <param name="connectionStringBuilder"></param>
26+
/// <param name="connectionString"></param>
2727
/// <param name="credential"></param>
28-
internal UnpooledDataSource(SqlConnectionStringBuilder connectionStringBuilder, SqlCredential credential) :
29-
base(connectionStringBuilder, credential)
28+
internal UnpooledDataSource(SqlConnectionString connectionString, SqlCredential credential) :
29+
base(connectionString, credential)
3030
{
3131
}
3232

0 commit comments

Comments
 (0)