Skip to content

Commit 1f0e170

Browse files
committed
Avoid confusing double reservation release. Use params in callbacks to avoid closures.
1 parent 76784de commit 1f0e170

File tree

2 files changed

+49
-30
lines changed

2 files changed

+49
-30
lines changed

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

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -291,19 +291,24 @@ public bool TryGetConnection(
291291
return false;
292292
}
293293

294+
private struct CreateState
295+
{
296+
internal ChannelDbConnectionPool pool;
297+
internal DbConnection? owningConnection;
298+
internal DbConnectionOptions userOptions;
299+
}
300+
294301
/// <summary>
295302
/// Opens a new internal connection to the database.
296303
/// </summary>
297304
/// <param name="owningConnection">The owning connection.</param>
298305
/// <param name="userOptions">The options for the connection.</param>
299-
/// <param name="async">Whether to open the connection asynchronously.</param>
300306
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
301307
/// <returns>A task representing the asynchronous operation, with a result of the new internal connection.</returns>
302308
/// <throws>InvalidOperationException - when the newly created connection is invalid or already in the pool.</throws>
303309
private DbConnectionInternal? OpenNewInternalConnection(
304310
DbConnection? owningConnection,
305311
DbConnectionOptions userOptions,
306-
bool async,
307312
CancellationToken cancellationToken)
308313
{
309314
cancellationToken.ThrowIfCancellationRequested();
@@ -313,7 +318,7 @@ public bool TryGetConnection(
313318
// in case of an exception.
314319

315320
return _connectionSlots.Add(
316-
createCallback: () =>
321+
createCallback: static (state) =>
317322
{
318323
// https://github.com/dotnet/SqlClient/issues/3459
319324
// TODO: This blocks the thread for several network calls!
@@ -323,20 +328,25 @@ public bool TryGetConnection(
323328
// DbConnectionInternal doesn't support an async open. It's better to block this thread and keep
324329
// throughput high than to queue all of our opens onto a single worker thread. Add an async path
325330
// when this support is added to DbConnectionInternal.
326-
return ConnectionFactory.CreatePooledConnection(
327-
this,
328-
owningConnection,
329-
PoolGroup.ConnectionOptions,
330-
PoolGroup.PoolKey,
331-
userOptions);
331+
return state.pool.ConnectionFactory.CreatePooledConnection(
332+
state.pool,
333+
state.owningConnection,
334+
state.pool.PoolGroup.ConnectionOptions,
335+
state.pool.PoolGroup.PoolKey,
336+
state.userOptions);
332337
},
333-
cleanupCallback: (newConnection) =>
338+
cleanupCallback: static (newConnection, idleConnectionWriter) =>
334339
{
335-
if (newConnection is not null)
336-
{
337-
RemoveConnection(newConnection);
338-
}
339-
});
340+
idleConnectionWriter.TryWrite(null);
341+
newConnection?.Dispose();
342+
},
343+
new CreateState
344+
{
345+
pool = this,
346+
owningConnection = owningConnection,
347+
userOptions = userOptions
348+
},
349+
_idleConnectionWriter);
340350
}
341351

342352
/// <summary>
@@ -367,7 +377,7 @@ private void RemoveConnection(DbConnectionInternal connection)
367377
{
368378
_connectionSlots.TryRemove(connection);
369379

370-
// Closing a connection opens a free spot in the pool.
380+
// Removing a connection from the pool opens a free slot.
371381
// Write a null to the idle connection channel to wake up a waiter, who can now open a new
372382
// connection. Statement order is important since we have synchronous completions on the channel.
373383
_idleConnectionWriter.TryWrite(null);
@@ -433,7 +443,6 @@ private async Task<DbConnectionInternal> GetInternalConnection(
433443
connection ??= OpenNewInternalConnection(
434444
owningConnection,
435445
userOptions,
436-
async,
437446
cancellationToken);
438447

439448
// If we're at max capacity and couldn't open a connection. Block on the idle channel with a

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

Lines changed: 23 additions & 13 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.Channels;
89
using System.Threading.Tasks;
910
using Microsoft.Data.ProviderBase;
1011

@@ -45,15 +46,17 @@ namespace Microsoft.Data.SqlClient.ConnectionPool
4546
internal sealed class ConnectionPoolSlots
4647
{
4748

48-
private sealed class Reservation : IDisposable
49+
private sealed class Reservation<T> : IDisposable
4950
{
50-
private readonly ConnectionPoolSlots _slots;
51+
private Action<T> cleanupCallback;
52+
private T state;
5153
private bool _retain = false;
5254
private bool _disposed = false;
5355

54-
internal Reservation(ConnectionPoolSlots slots)
56+
internal Reservation(T state, Action<T> cleanupCallback)
5557
{
56-
_slots = slots;
58+
this.state = state;
59+
this.cleanupCallback = cleanupCallback;
5760
}
5861

5962
public void Dispose()
@@ -65,19 +68,20 @@ public void Dispose()
6568

6669
if (!_retain)
6770
{
68-
_slots.ReleaseReservation();
69-
_disposed = true;
71+
cleanupCallback(state);
7072
}
73+
74+
_disposed = true;
7175
}
7276

7377
internal void Keep()
7478
{
7579
_retain = true;
7680
}
77-
7881
}
7982

80-
internal delegate void CleanupCallback(DbConnectionInternal? connection);
83+
internal delegate T CreateCallback<T, S>(S state);
84+
internal delegate void CleanupCallback<T>(DbConnectionInternal? connection, T state);
8185

8286
private readonly DbConnectionInternal?[] _connections;
8387
private readonly uint _capacity;
@@ -104,11 +108,17 @@ internal ConnectionPoolSlots(uint fixedCapacity)
104108
/// </summary>
105109
/// <param name="createCallback">The connection to add to the collection.</param>
106110
/// <param name="cleanupCallback">Callback to clean up resources if an exception occurs.</param>
111+
/// <param name="createState">State made available to the create callback.</param>
112+
/// <param name="cleanupState">State made available to the cleanup callback.</param>
107113
/// <exception cref="InvalidOperationException">
108114
/// Thrown when unable to find an empty slot.
109115
/// This can occur if a reservation is not taken before adding a connection.
110116
/// </exception>
111-
internal DbConnectionInternal? Add(Func<DbConnectionInternal?> createCallback, CleanupCallback cleanupCallback)
117+
internal DbConnectionInternal? Add<T, S>(
118+
CreateCallback<DbConnectionInternal?, T> createCallback,
119+
CleanupCallback<S> cleanupCallback,
120+
T createState,
121+
S cleanupState)
112122
{
113123
DbConnectionInternal? connection = null;
114124
try
@@ -119,7 +129,7 @@ internal ConnectionPoolSlots(uint fixedCapacity)
119129
return null;
120130
}
121131

122-
connection = createCallback();
132+
connection = createCallback(createState);
123133

124134
if (connection is null)
125135
{
@@ -139,7 +149,7 @@ internal ConnectionPoolSlots(uint fixedCapacity)
139149
}
140150
catch (Exception e)
141151
{
142-
cleanupCallback(connection);
152+
cleanupCallback(connection, cleanupState);
143153
throw new Exception("Failed to create or add connection", e);
144154
}
145155
}
@@ -177,7 +187,7 @@ internal bool TryRemove(DbConnectionInternal connection)
177187
/// Attempts to reserve a spot in the collection.
178188
/// </summary>
179189
/// <returns>True if a reservation was successfully obtained.</returns>
180-
private Reservation? TryReserve()
190+
private Reservation<ConnectionPoolSlots>? TryReserve()
181191
{
182192
for (var expected = _reservations; expected < _capacity; expected = _reservations)
183193
{
@@ -191,7 +201,7 @@ internal bool TryRemove(DbConnectionInternal connection)
191201
continue;
192202
}
193203

194-
return new Reservation(this);
204+
return new Reservation<ConnectionPoolSlots>(this, (slots) => slots.ReleaseReservation());
195205
}
196206
return null;
197207
}

0 commit comments

Comments
 (0)