Skip to content

Get/Return pooled connections #3404

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 41 commits into
base: main
Choose a base branch
from

Conversation

mdaigle
Copy link
Contributor

@mdaigle mdaigle commented Jun 9, 2025

Description

This PR adds Get and Return functionality to the new ChannelDbConnectionPool class.
A channel and corresponding channel reader and writer are added to underpin these operations.
An array is added to hold references to all connections managed by the pool.
Logic is included to respect max pool size in all cases.

Not included: pool warm up (including respecting min pool size), pool pruning, transactions, tracing/logs. These will come in follow-up PRs.

Also includes extensive unit test coverage for the implemented functionality. Integration testing is reserved for a later state when more of the pool functionality is available.

Issues

#3356

Testing

Tests focus on the Get and Return flows with special focus on the following:

  • async behavior
  • max pool size respected
  • connections successfully reused
  • requests successfully queued and queue order respected
  • stress testing with many parallel operations

@mdaigle mdaigle force-pushed the dev/mdaigle/get-return-pooled-connections branch from 9d95355 to 73a379b Compare June 9, 2025 20:45
@mdaigle mdaigle changed the title Dev/mdaigle/get return pooled connections Get/Return pooled connections Jun 9, 2025
@mdaigle mdaigle marked this pull request as ready for review June 9, 2025 23:25
@mdaigle mdaigle requested a review from a team as a code owner June 9, 2025 23:25
@mdaigle
Copy link
Contributor Author

mdaigle commented Jun 10, 2025

@roji tagging you here if you have any time to review. Thanks!

Copy link
Contributor

@benrr101 benrr101 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, it looks good. A bunch of things I'd like addressed, but you know I'll accept valid arguments against fixing them :)

Copy link
Contributor

@paulmedynski paulmedynski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have reviewed the ChannelDbConnectionPool changes. I will look at the tests once we have converged on an implementation. It might be a good idea to host another meeting to walk through the commentary here.


return Task.FromResult<DbConnectionInternal?>(newConnection);
}
catch
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Catch only what is documented to be thrown.

/// </summary>
private static int _instanceCount;

private readonly int _instanceID = Interlocked.Increment(ref _instanceCount);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interlocked.Increment(ref _instanceCount);

does it matter if it becomes negative ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it starts at zero and only ever goes up

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Counters are naturally unsigned, so can we use uint or ulong instead? I'd be proud if those ever wrapped :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, should this be _instanceId ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no Interlocked.Increment() overload for uint. It doesn't matter if it goes negative. It's only used for identification of unique pools in trace messages.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is for .NET:

https://learn.microsoft.com/en-us/dotnet/api/system.threading.interlocked.increment?view=net-9.0#system-threading-interlocked-increment(system-uint32@)

Framework is stuck with signed only.

Signed Increment() doesn't throw on overflow, so there's no programming problem here, just perhaps an unexpected logging issue with negative IDs. Not worth worrying about IMO.

@@ -20,52 +25,136 @@ namespace Microsoft.Data.SqlClient.ConnectionPool
/// </summary>
internal sealed class ChannelDbConnectionPool : IDbConnectionPool

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ChannelDbConnectionPool

it might be good to add some documentation on how this class works and how this is different than the WaitHandleDbConnectionPool.

async: true,
timeout
).ConfigureAwait(false);
taskCompletionSource.SetResult(connection);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any particular reason to not use TrySetResult and TrySetException to avoid unnecessary exceptions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this idea, done!

// We're potentially on a new thread, so we need to properly set the ambient transaction.
// We rely on the caller to capture the ambient transaction in the TaskCompletionSource's AsyncState
// so that we can access it here. Read: area for improvement.
ADP.SetCurrentTransaction(taskCompletionSource.Task.AsyncState as Transaction);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the current source code of WaitHandleDbConnectionPool, it seems like everything between ADP.SetCurrentTransaction and taskCompletionSource.SetResult is synchronous, unlike here, where GetInternalConnection can go async. I wonder whether it's to make the default TransactionScope work (you have to pass a parameter to support async) with OpenAsync, although it's kinda weird to do so anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is an important point. I haven't included transaction support in the scope of work for this PR and included this line prematurely. This line is incorrect for the reason you stated. When we go down the async path, we may end up creating/retrieving a connection in a continuation and that continuation would have no reference to the ambient transaction.

This does make me wonder whether we need any additional hygiene measures to make sure we reset ambient transaction state on threads. We wouldn't want a managed thread to hold onto the ambient transaction state and incorrectly enlist other connections.

… connection disposal when opening new connection.
Copy link
Contributor

@paulmedynski paulmedynski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're getting closer!

{
cancellationToken.ThrowIfCancellationRequested();
Debug.Assert(finalToken.IsCancellationRequested);
// We didn't find an idle connection, try to open a new one.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These ??= comments are a bit misleading. At line 466 we don't know whether or not we got an idle connection. We only know that if we hit the await part. Maybe this would be clearer as If we didn't find ..., then try to ....

benrr101
benrr101 previously approved these changes Jul 8, 2025
Copy link

codecov bot commented Jul 16, 2025

Codecov Report

Attention: Patch coverage is 89.32806% with 27 lines in your changes missing coverage. Please review.

Project coverage is 63.10%. Comparing base (8eb9f32) to head (531215c).
Report is 15 commits behind head on main.

Files with missing lines Patch % Lines
...qlClient/ConnectionPool/ChannelDbConnectionPool.cs 89.28% 21 Missing ⚠️
...Microsoft/Data/ProviderBase/DbConnectionFactory.cs 60.00% 2 Missing ⚠️
...icrosoft/Data/ProviderBase/DbConnectionInternal.cs 33.33% 2 Missing ⚠️
...ta/SqlClient/ConnectionPool/ConnectionPoolSlots.cs 95.91% 2 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (8eb9f32) and HEAD (531215c). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (8eb9f32) HEAD (531215c)
addons 1 0
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3404      +/-   ##
==========================================
- Coverage   68.86%   63.10%   -5.76%     
==========================================
  Files         280      272       -8     
  Lines       62417    62093     -324     
==========================================
- Hits        42982    39184    -3798     
- Misses      19435    22909    +3474     
Flag Coverage Δ
addons ?
netcore 67.27% <89.32%> (-5.47%) ⬇️
netfx 62.57% <89.32%> (-5.47%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

benrr101
benrr101 previously approved these changes Jul 17, 2025
Copy link
Contributor

@paulmedynski paulmedynski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still looking for some changes from previous reviews. Good changes in these last 4 commits though.

Copy link
Contributor

@paulmedynski paulmedynski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks good. Will look at tests shortly. Going to loop through older comments first.

cleanupCallback(state);
}

_disposed = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this go on line 42 in case cleanupCallback() throws? We don't want to try disposal again.

/// This can occur if a reservation is not taken before adding a connection.
/// <param name="createCallback">Callback that provides the connection to add to the collection. This callback
/// *must not* call any other ConnectionPoolSlots methods.</param>
/// <param name="cleanupCallback">Callback to clean up resources if an exception occurs. This callback *must
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This must also not throw, since you call it during disposal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants