Skip to content

Commit 76784de

Browse files
committed
Merge branch 'dev/mdaigle/disposable-reservations' into dev/mdaigle/get-return-pooled-connections
2 parents 2c639ec + 339dfc0 commit 76784de

File tree

5 files changed

+107
-65
lines changed

5 files changed

+107
-65
lines changed

src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionFactory.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,20 @@ internal DbConnectionInternal CreatePooledConnection(IDbConnectionPool pool, DbC
133133

134134
newConnection.MakePooledConnection(pool);
135135
}
136+
137+
if (newConnection == null)
138+
{
139+
throw ADP.InternalError(ADP.InternalErrorCode.CreateObjectReturnedNull); // CreateObject succeeded, but null object
140+
}
141+
if (!newConnection.CanBePooled)
142+
{
143+
throw ADP.InternalError(ADP.InternalErrorCode.NewObjectCannotBePooled); // CreateObject succeeded, but non-poolable object
144+
}
145+
136146
SqlClientEventSource.Log.TryTraceEvent("<prov.DbConnectionFactory.CreatePooledConnection|RES|CPOOL> {0}, Pooled database connection created.", ObjectID);
147+
148+
newConnection.PrePush(null);
149+
137150
return newConnection;
138151
}
139152

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPool.cs

Lines changed: 14 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -300,22 +300,21 @@ public bool TryGetConnection(
300300
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
301301
/// <returns>A task representing the asynchronous operation, with a result of the new internal connection.</returns>
302302
/// <throws>InvalidOperationException - when the newly created connection is invalid or already in the pool.</throws>
303-
private Task<DbConnectionInternal?> OpenNewInternalConnection(
303+
private DbConnectionInternal? OpenNewInternalConnection(
304304
DbConnection? owningConnection,
305305
DbConnectionOptions userOptions,
306-
bool async,
306+
bool async,
307307
CancellationToken cancellationToken)
308308
{
309+
cancellationToken.ThrowIfCancellationRequested();
310+
309311
// Opening a connection can be a slow operation and we don't want to hold a lock for the duration.
310312
// Instead, we reserve a connection slot prior to attempting to open a new connection and release the slot
311-
// in case of an exception.
312-
if (_connectionSlots.TryReserve())
313-
{
314-
DbConnectionInternal? newConnection = null;
315-
try
316-
{
317-
var startTime = Stopwatch.GetTimestamp();
313+
// in case of an exception.
318314

315+
return _connectionSlots.Add(
316+
createCallback: () =>
317+
{
319318
// https://github.com/dotnet/SqlClient/issues/3459
320319
// TODO: This blocks the thread for several network calls!
321320
// When running async, the blocked thread is one allocated from the managed thread pool (due to
@@ -324,44 +323,20 @@ public bool TryGetConnection(
324323
// DbConnectionInternal doesn't support an async open. It's better to block this thread and keep
325324
// throughput high than to queue all of our opens onto a single worker thread. Add an async path
326325
// when this support is added to DbConnectionInternal.
327-
newConnection = ConnectionFactory.CreatePooledConnection(
326+
return ConnectionFactory.CreatePooledConnection(
328327
this,
329328
owningConnection,
330329
PoolGroup.ConnectionOptions,
331330
PoolGroup.PoolKey,
332331
userOptions);
333-
334-
// We don't expect these conditions to happen, but we need to check them to be thorough.
335-
if (newConnection == null)
336-
{
337-
throw ADP.InternalError(ADP.InternalErrorCode.CreateObjectReturnedNull);
338-
}
339-
if (!newConnection.CanBePooled)
340-
{
341-
throw ADP.InternalError(ADP.InternalErrorCode.NewObjectCannotBePooled);
342-
}
343-
344-
ValidateOwnershipAndSetPoolingState(newConnection, null);
345-
346-
_connectionSlots.Add(newConnection);
347-
348-
return Task.FromResult<DbConnectionInternal?>(newConnection);
349-
}
350-
catch
332+
},
333+
cleanupCallback: (newConnection) =>
351334
{
352-
// If we failed to open a new connection, we need to reset any state we modified.
353-
// Clear the reservation we made, dispose of the connection if it was created, and wake up any waiting
354-
// attempts on the idle connection channel.
355335
if (newConnection is not null)
356336
{
357337
RemoveConnection(newConnection);
358338
}
359-
360-
throw;
361-
}
362-
}
363-
364-
return Task.FromResult<DbConnectionInternal?>(null);
339+
});
365340
}
366341

367342
/// <summary>
@@ -392,7 +367,6 @@ private void RemoveConnection(DbConnectionInternal connection)
392367
{
393368
_connectionSlots.TryRemove(connection);
394369

395-
_connectionSlots.ReleaseReservation();
396370
// Closing a connection opens a free spot in the pool.
397371
// Write a null to the idle connection channel to wake up a waiter, who can now open a new
398372
// connection. Statement order is important since we have synchronous completions on the channel.
@@ -456,11 +430,11 @@ private async Task<DbConnectionInternal> GetInternalConnection(
456430

457431

458432
// If we didn't find an idle connection, try to open a new one.
459-
connection ??= await OpenNewInternalConnection(
433+
connection ??= OpenNewInternalConnection(
460434
owningConnection,
461435
userOptions,
462436
async,
463-
cancellationToken).ConfigureAwait(false);
437+
cancellationToken);
464438

465439
// If we're at max capacity and couldn't open a connection. Block on the idle channel with a
466440
// timeout. Note that Channels guarantee fair FIFO behavior to callers of ReadAsync

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/ConnectionPoolSlots.cs

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System;
66
using System.Diagnostics;
77
using System.Threading;
8+
using System.Threading.Tasks;
89
using Microsoft.Data.ProviderBase;
910

1011
#nullable enable
@@ -43,6 +44,41 @@ namespace Microsoft.Data.SqlClient.ConnectionPool
4344
/// </example>
4445
internal sealed class ConnectionPoolSlots
4546
{
47+
48+
private sealed class Reservation : IDisposable
49+
{
50+
private readonly ConnectionPoolSlots _slots;
51+
private bool _retain = false;
52+
private bool _disposed = false;
53+
54+
internal Reservation(ConnectionPoolSlots slots)
55+
{
56+
_slots = slots;
57+
}
58+
59+
public void Dispose()
60+
{
61+
if (_disposed)
62+
{
63+
return;
64+
}
65+
66+
if (!_retain)
67+
{
68+
_slots.ReleaseReservation();
69+
_disposed = true;
70+
}
71+
}
72+
73+
internal void Keep()
74+
{
75+
_retain = true;
76+
}
77+
78+
}
79+
80+
internal delegate void CleanupCallback(DbConnectionInternal? connection);
81+
4682
private readonly DbConnectionInternal?[] _connections;
4783
private readonly uint _capacity;
4884
private volatile int _reservations;
@@ -66,30 +102,53 @@ internal ConnectionPoolSlots(uint fixedCapacity)
66102
/// <summary>
67103
/// Adds a connection to the collection. Can only be called after a reservation has been made.
68104
/// </summary>
69-
/// <param name="connection">The connection to add to the collection.</param>
105+
/// <param name="createCallback">The connection to add to the collection.</param>
106+
/// <param name="cleanupCallback">Callback to clean up resources if an exception occurs.</param>
70107
/// <exception cref="InvalidOperationException">
71108
/// Thrown when unable to find an empty slot.
72109
/// This can occur if a reservation is not taken before adding a connection.
73110
/// </exception>
74-
internal void Add(DbConnectionInternal connection)
111+
internal DbConnectionInternal? Add(Func<DbConnectionInternal?> createCallback, CleanupCallback cleanupCallback)
75112
{
76-
int i;
77-
for (i = 0; i < _capacity; i++)
113+
DbConnectionInternal? connection = null;
114+
try
78115
{
79-
if (Interlocked.CompareExchange(ref _connections[i], connection, null) == null)
116+
using var reservation = TryReserve();
117+
if (reservation is null)
80118
{
81-
return;
119+
return null;
82120
}
83-
}
84121

85-
throw new InvalidOperationException("Couldn't find an empty slot.");
122+
connection = createCallback();
123+
124+
if (connection is null)
125+
{
126+
return null;
127+
}
128+
129+
for (int i = 0; i < _capacity; i++)
130+
{
131+
if (Interlocked.CompareExchange(ref _connections[i], connection, null) == null)
132+
{
133+
reservation.Keep();
134+
return connection;
135+
}
136+
}
137+
138+
throw new InvalidOperationException("Couldn't find an empty slot.");
139+
}
140+
catch (Exception e)
141+
{
142+
cleanupCallback(connection);
143+
throw new Exception("Failed to create or add connection", e);
144+
}
86145
}
87146

88147
/// <summary>
89148
/// Releases a reservation that was previously obtained.
90149
/// Must be called after removing a connection from the collection or if an exception occurs.
91150
/// </summary>
92-
internal void ReleaseReservation()
151+
private void ReleaseReservation()
93152
{
94153
Interlocked.Decrement(ref _reservations);
95154
Debug.Assert(_reservations >= 0, "Released a reservation that wasn't held");
@@ -106,6 +165,7 @@ internal bool TryRemove(DbConnectionInternal connection)
106165
{
107166
if (Interlocked.CompareExchange(ref _connections[i], null, connection) == connection)
108167
{
168+
ReleaseReservation();
109169
return true;
110170
}
111171
}
@@ -117,7 +177,7 @@ internal bool TryRemove(DbConnectionInternal connection)
117177
/// Attempts to reserve a spot in the collection.
118178
/// </summary>
119179
/// <returns>True if a reservation was successfully obtained.</returns>
120-
internal bool TryReserve()
180+
private Reservation? TryReserve()
121181
{
122182
for (var expected = _reservations; expected < _capacity; expected = _reservations)
123183
{
@@ -131,9 +191,9 @@ internal bool TryReserve()
131191
continue;
132192
}
133193

134-
return true;
194+
return new Reservation(this);
135195
}
136-
return false;
196+
return null;
137197
}
138198
}
139199
}

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -745,15 +745,6 @@ private DbConnectionInternal CreateObject(DbConnection owningObject, DbConnectio
745745
try
746746
{
747747
newObj = _connectionFactory.CreatePooledConnection(this, owningObject, _connectionPoolGroup.ConnectionOptions, _connectionPoolGroup.PoolKey, userOptions);
748-
if (newObj == null)
749-
{
750-
throw ADP.InternalError(ADP.InternalErrorCode.CreateObjectReturnedNull); // CreateObject succeeded, but null object
751-
}
752-
if (!newObj.CanBePooled)
753-
{
754-
throw ADP.InternalError(ADP.InternalErrorCode.NewObjectCannotBePooled); // CreateObject succeeded, but non-poolable object
755-
}
756-
newObj.PrePush(null);
757748

758749
lock (_objectList)
759750
{

src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPoolTest.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ public void GetConnectionTimeout_ShouldThrowTimeoutException()
477477
DbConnectionInternal internalConnection = null;
478478

479479
// Act & Assert
480-
var ex = Assert.Throws<InvalidOperationException>(() =>
480+
var ex = Assert.Throws<Exception>(() =>
481481
{
482482
var completed = pool.TryGetConnection(
483483
new SqlConnection(),
@@ -487,7 +487,9 @@ out internalConnection
487487
);
488488
});
489489

490-
Assert.Equal("Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.", ex.Message);
490+
Assert.IsType<InvalidOperationException>(ex.InnerException);
491+
492+
Assert.Equal("Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.", ex.InnerException.Message);
491493
}
492494

493495
[Fact]
@@ -499,7 +501,7 @@ public async Task GetConnectionAsyncTimeout_ShouldThrowTimeoutException()
499501
TaskCompletionSource<DbConnectionInternal> taskCompletionSource = new TaskCompletionSource<DbConnectionInternal>();
500502

501503
// Act & Assert
502-
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
504+
var ex = await Assert.ThrowsAsync<Exception>(async () =>
503505
{
504506
var completed = pool.TryGetConnection(
505507
new SqlConnection(),
@@ -511,7 +513,9 @@ out internalConnection
511513
await taskCompletionSource.Task;
512514
});
513515

514-
Assert.Equal("Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.", ex.Message);
516+
Assert.IsType<InvalidOperationException>(ex.InnerException);
517+
518+
Assert.Equal("Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.", ex.InnerException.Message);
515519
}
516520

517521
[Fact]

0 commit comments

Comments
 (0)