@@ -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
0 commit comments