Skip to content

Commit ac757fa

Browse files
authored
Cleanup | Split out TransactedConnectionPool (#3746)
* Move TransactedConnectionPool to its own class. * Enable nullable. * Clean up properties. * Add doc comments. * Minor cleanup. Add regions. * Sync connection pool unit test namespace and folder structure. * Add unit tests. * Address copilot comments. * Address edwardneal's comments. * Address paul's comments. * Clean up test variable names. Use ConcurrentBag in tests to avoid concurrency issue.
1 parent d6f1b19 commit ac757fa

File tree

7 files changed

+1097
-260
lines changed

7 files changed

+1097
-260
lines changed

src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@
150150
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\ConnectionPool\SqlConnectionPoolProviderInfo.cs">
151151
<Link>Microsoft\Data\SqlClient\ConnectionPool\SqlConnectionPoolProviderInfo.cs</Link>
152152
</Compile>
153+
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\ConnectionPool\TransactedConnectionPool.cs">
154+
<Link>Microsoft\Data\SqlClient\ConnectionPool\TransactedConnectionPool.cs</Link>
155+
</Compile>
153156
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\ConnectionPool\WaitHandleDbConnectionPool.cs">
154157
<Link>Microsoft\Data\SqlClient\ConnectionPool\WaitHandleDbConnectionPool.cs</Link>
155158
</Compile>

src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,9 @@
341341
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\ConnectionPool\SqlConnectionPoolProviderInfo.cs">
342342
<Link>Microsoft\Data\SqlClient\ConnectionPool\SqlConnectionPoolProviderInfo.cs</Link>
343343
</Compile>
344+
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\ConnectionPool\TransactedConnectionPool.cs">
345+
<Link>Microsoft\Data\SqlClient\ConnectionPool\TransactedConnectionPool.cs</Link>
346+
</Compile>
344347
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\ConnectionPool\WaitHandleDbConnectionPool.cs">
345348
<Link>Microsoft\Data\SqlClient\ConnectionPool\WaitHandleDbConnectionPool.cs</Link>
346349
</Compile>

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

Lines changed: 353 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 0 additions & 260 deletions
Original file line numberDiff line numberDiff line change
@@ -59,22 +59,6 @@ internal sealed class WaitHandleDbConnectionPool : IDbConnectionPool
5959
// This class is a way to stash our cloned Tx key for later disposal when it's no longer needed.
6060
// We can't get at the key in the dictionary without enumerating entries, so we stash an extra
6161
// copy as part of the value.
62-
private sealed class TransactedConnectionList : List<DbConnectionInternal>
63-
{
64-
private Transaction _transaction;
65-
internal TransactedConnectionList(int initialAllocation, Transaction tx) : base(initialAllocation)
66-
{
67-
_transaction = tx;
68-
}
69-
70-
internal void Dispose()
71-
{
72-
if (_transaction != null)
73-
{
74-
_transaction.Dispose();
75-
}
76-
}
77-
}
7862

7963
private sealed class PendingGetConnection
8064
{
@@ -91,250 +75,6 @@ public PendingGetConnection(long dueTime, DbConnection owner, TaskCompletionSour
9175
public DbConnectionOptions UserOptions { get; private set; }
9276
}
9377

94-
private sealed class TransactedConnectionPool
95-
{
96-
Dictionary<Transaction, TransactedConnectionList> _transactedCxns;
97-
98-
IDbConnectionPool _pool;
99-
100-
private static int _objectTypeCount; // EventSource Counter
101-
internal readonly int _objectID = System.Threading.Interlocked.Increment(ref _objectTypeCount);
102-
103-
internal TransactedConnectionPool(IDbConnectionPool pool)
104-
{
105-
Debug.Assert(pool != null, "null pool?");
106-
107-
_pool = pool;
108-
_transactedCxns = new Dictionary<Transaction, TransactedConnectionList>();
109-
SqlClientEventSource.Log.TryPoolerTraceEvent("<prov.DbConnectionPool.TransactedConnectionPool.TransactedConnectionPool|RES|CPOOL> {0}, Constructed for connection pool {1}", ObjectID, _pool.Id);
110-
}
111-
112-
internal int ObjectID
113-
{
114-
get
115-
{
116-
return _objectID;
117-
}
118-
}
119-
120-
internal IDbConnectionPool Pool
121-
{
122-
get
123-
{
124-
return _pool;
125-
}
126-
}
127-
128-
internal DbConnectionInternal GetTransactedObject(Transaction transaction)
129-
{
130-
Debug.Assert(transaction != null, "null transaction?");
131-
132-
DbConnectionInternal transactedObject = null;
133-
134-
TransactedConnectionList connections;
135-
bool txnFound = false;
136-
137-
lock (_transactedCxns)
138-
{
139-
txnFound = _transactedCxns.TryGetValue(transaction, out connections);
140-
}
141-
142-
// NOTE: GetTransactedObject is only used when AutoEnlist = True and the ambient transaction
143-
// (Sys.Txns.Txn.Current) is still valid/non-null. This, in turn, means that we don't need
144-
// to worry about a pending asynchronous TransactionCompletedEvent to trigger processing in
145-
// TransactionEnded below and potentially wipe out the connections list underneath us. It
146-
// is similarly alright if a pending addition to the connections list in PutTransactedObject
147-
// below is not completed prior to the lock on the connections object here...getting a new
148-
// connection is probably better than unnecessarily locking
149-
if (txnFound)
150-
{
151-
Debug.Assert(connections != null);
152-
153-
// synchronize multi-threaded access with PutTransactedObject (TransactionEnded should
154-
// not be a concern, see comments above)
155-
lock (connections)
156-
{
157-
int i = connections.Count - 1;
158-
if (0 <= i)
159-
{
160-
transactedObject = connections[i];
161-
connections.RemoveAt(i);
162-
}
163-
}
164-
}
165-
166-
if (transactedObject != null)
167-
{
168-
SqlClientEventSource.Log.TryPoolerTraceEvent("<prov.DbConnectionPool.TransactedConnectionPool.GetTransactedObject|RES|CPOOL> {0}, Transaction {1}, Connection {2}, Popped.", ObjectID, transaction.GetHashCode(), transactedObject.ObjectID);
169-
}
170-
return transactedObject;
171-
}
172-
173-
internal void PutTransactedObject(Transaction transaction, DbConnectionInternal transactedObject)
174-
{
175-
Debug.Assert(transaction != null, "null transaction?");
176-
Debug.Assert(transactedObject != null, "null transactedObject?");
177-
178-
TransactedConnectionList connections;
179-
bool txnFound = false;
180-
181-
// NOTE: because TransactionEnded is an asynchronous notification, there's no guarantee
182-
// around the order in which PutTransactionObject and TransactionEnded are called.
183-
184-
lock (_transactedCxns)
185-
{
186-
// Check if a transacted pool has been created for this transaction
187-
if (txnFound = _transactedCxns.TryGetValue(transaction, out connections))
188-
{
189-
Debug.Assert(connections != null);
190-
191-
// synchronize multi-threaded access with GetTransactedObject
192-
lock (connections)
193-
{
194-
Debug.Assert(0 > connections.IndexOf(transactedObject), "adding to pool a second time?");
195-
SqlClientEventSource.Log.TryPoolerTraceEvent("<prov.DbConnectionPool.TransactedConnectionPool.PutTransactedObject|RES|CPOOL> {0}, Transaction {1}, Connection {2}, Pushing.", ObjectID, transaction.GetHashCode(), transactedObject.ObjectID);
196-
connections.Add(transactedObject);
197-
}
198-
}
199-
}
200-
201-
// CONSIDER: the following code is more complicated than it needs to be to avoid cloning the
202-
// transaction and allocating memory within a lock. Is that complexity really necessary?
203-
if (!txnFound)
204-
{
205-
// create the transacted pool, making sure to clone the associated transaction
206-
// for use as a key in our internal dictionary of transactions and connections
207-
Transaction transactionClone = null;
208-
TransactedConnectionList newConnections = null;
209-
210-
try
211-
{
212-
transactionClone = transaction.Clone();
213-
newConnections = new TransactedConnectionList(2, transactionClone); // start with only two connections in the list; most times we won't need that many.
214-
215-
lock (_transactedCxns)
216-
{
217-
// NOTE: in the interim between the locks on the transacted pool (this) during
218-
// execution of this method, another thread (threadB) may have attempted to
219-
// add a different connection to the transacted pool under the same
220-
// transaction. As a result, threadB may have completed creating the
221-
// transacted pool while threadA was processing the above instructions.
222-
if (txnFound = _transactedCxns.TryGetValue(transaction, out connections))
223-
{
224-
Debug.Assert(connections != null);
225-
226-
// synchronize multi-threaded access with GetTransactedObject
227-
lock (connections)
228-
{
229-
Debug.Assert(0 > connections.IndexOf(transactedObject), "adding to pool a second time?");
230-
SqlClientEventSource.Log.TryPoolerTraceEvent("<prov.DbConnectionPool.TransactedConnectionPool.PutTransactedObject|RES|CPOOL> {0}, Transaction {1}, Connection {2}, Pushing.", ObjectID, transaction.GetHashCode(), transactedObject.ObjectID);
231-
connections.Add(transactedObject);
232-
}
233-
}
234-
else
235-
{
236-
SqlClientEventSource.Log.TryPoolerTraceEvent("<prov.DbConnectionPool.TransactedConnectionPool.PutTransactedObject|RES|CPOOL> {0}, Transaction {1}, Connection {2}, Adding List to transacted pool.", ObjectID, transaction.GetHashCode(), transactedObject.ObjectID);
237-
238-
// add the connection/transacted object to the list
239-
newConnections.Add(transactedObject);
240-
241-
_transactedCxns.Add(transactionClone, newConnections);
242-
transactionClone = null; // we've used it -- don't throw it or the TransactedConnectionList that references it away.
243-
}
244-
}
245-
}
246-
finally
247-
{
248-
if (transactionClone != null)
249-
{
250-
if (newConnections != null)
251-
{
252-
// another thread created the transaction pool and thus the new
253-
// TransactedConnectionList was not used, so dispose of it and
254-
// the transaction clone that it incorporates.
255-
newConnections.Dispose();
256-
}
257-
else
258-
{
259-
// memory allocation for newConnections failed...clean up unused transactionClone
260-
transactionClone.Dispose();
261-
}
262-
}
263-
}
264-
SqlClientEventSource.Log.TryPoolerTraceEvent("<prov.DbConnectionPool.TransactedConnectionPool.PutTransactedObject|RES|CPOOL> {0}, Transaction {1}, Connection {2}, Added.", ObjectID, transaction.GetHashCode(), transactedObject.ObjectID);
265-
}
266-
267-
SqlClientEventSource.Metrics.EnterFreeConnection();
268-
}
269-
270-
internal void TransactionEnded(Transaction transaction, DbConnectionInternal transactedObject)
271-
{
272-
SqlClientEventSource.Log.TryPoolerTraceEvent("<prov.DbConnectionPool.TransactedConnectionPool.TransactionEnded|RES|CPOOL> {0}, Transaction {1}, Connection {2}, Transaction Completed", ObjectID, transaction.GetHashCode(), transactedObject.ObjectID);
273-
TransactedConnectionList connections;
274-
int entry = -1;
275-
276-
// NOTE: because TransactionEnded is an asynchronous notification, there's no guarantee
277-
// around the order in which PutTransactionObject and TransactionEnded are called. As
278-
// such, it is possible that the transaction does not yet have a pool created.
279-
280-
// TODO: is this a plausible and/or likely scenario? Do we need to have a mechanism to ensure
281-
// TODO: that the pending creation of a transacted pool for this transaction is aborted when
282-
// TODO: PutTransactedObject finally gets some CPU time?
283-
284-
lock (_transactedCxns)
285-
{
286-
if (_transactedCxns.TryGetValue(transaction, out connections))
287-
{
288-
Debug.Assert(connections != null);
289-
290-
bool shouldDisposeConnections = false;
291-
292-
// Lock connections to avoid conflict with GetTransactionObject
293-
lock (connections)
294-
{
295-
entry = connections.IndexOf(transactedObject);
296-
297-
if (entry >= 0)
298-
{
299-
connections.RemoveAt(entry);
300-
}
301-
302-
// Once we've completed all the ended notifications, we can
303-
// safely remove the list from the transacted pool.
304-
if (0 >= connections.Count)
305-
{
306-
SqlClientEventSource.Log.TryPoolerTraceEvent("<prov.DbConnectionPool.TransactedConnectionPool.TransactionEnded|RES|CPOOL> {0}, Transaction {1}, Removing List from transacted pool.", ObjectID, transaction.GetHashCode());
307-
_transactedCxns.Remove(transaction);
308-
309-
// we really need to dispose our connection list; it may have
310-
// native resources via the tx and GC may not happen soon enough.
311-
shouldDisposeConnections = true;
312-
}
313-
}
314-
315-
if (shouldDisposeConnections)
316-
{
317-
connections.Dispose();
318-
}
319-
}
320-
else
321-
{
322-
SqlClientEventSource.Log.TryPoolerTraceEvent("<prov.DbConnectionPool.TransactedConnectionPool.TransactionEnded|RES|CPOOL> {0}, Transaction {1}, Connection {2}, Transacted pool not yet created prior to transaction completing. Connection may be leaked.", ObjectID, transaction.GetHashCode(), transactedObject.ObjectID);
323-
}
324-
}
325-
326-
// If (and only if) we found the connection in the list of
327-
// connections, we'll put it back...
328-
if (0 <= entry)
329-
{
330-
331-
SqlClientEventSource.Metrics.ExitFreeConnection();
332-
Pool.PutObjectFromTransactedPool(transactedObject);
333-
}
334-
}
335-
336-
}
337-
33878
private sealed class PoolWaitHandles
33979
{
34080
private readonly Semaphore _poolSemaphore;

0 commit comments

Comments
 (0)