Skip to content

Conversation

cheenamalhotra
Copy link
Member

@cheenamalhotra cheenamalhotra commented Sep 23, 2025

Fixes connection pool concurrency issue caused due to waiting on multiple semaphore handles, instead of only creation handle during pool create request.

Issue was first introduced in PR #2390 (https://github.com/dotnet/SqlClient/pull/2390/files#diff-d7c1dbddf2a7e205d7a85cbafbef036899799672dd5d1c486a64a05979e92230R1627) and has affected MDS v6 onwards.
Needs to be backported to v6.0 and v6.1 branches as well.

WIP: Working on adding tests - running CI first.

…le semaphore handles, instead of only creation handle during pool create request.
@cheenamalhotra cheenamalhotra requested a review from a team as a code owner September 23, 2025 22:04
@Copilot Copilot AI review requested due to automatic review settings September 23, 2025 22:04
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR fixes a concurrency issue in the connection pool by changing how semaphore handles are waited on during pool creation requests. Instead of waiting on multiple semaphore handles simultaneously, it now waits only on the creation handle to prevent potential race conditions.

  • Replaces WaitHandle.WaitAny() with direct WaitOne() on the creation semaphore
  • Simplifies the wait result handling logic by removing the intermediate wait result case
  • Removes unnecessary error tracing for non-timeout wait failures

Copy link

codecov bot commented Sep 23, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 66.71%. Comparing base (37d8a3f) to head (1c671db).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3632      +/-   ##
==========================================
+ Coverage   66.13%   66.71%   +0.58%     
==========================================
  Files         276      276              
  Lines       60765    60764       -1     
==========================================
+ Hits        40184    40541     +357     
+ Misses      20581    20223     -358     
Flag Coverage Δ
addons 90.82% <ø> (ø)
netcore 69.91% <100.00%> (+1.83%) ⬆️
netfx 68.86% <100.00%> (-1.11%) ⬇️

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.

@cheenamalhotra cheenamalhotra added this to the 7.0-preview2 milestone Sep 23, 2025
@paulmedynski paulmedynski self-assigned this Sep 24, 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.

Asking to simplify/clarify the semaphore block.

Also, is there a way to test this?

// and we must have the wait result
waitResult = WaitHandle.WaitAny(_waitHandles.GetHandles(withCreate: true), CreationTimeout);
bool creationHandleObtained = _waitHandles.CreationSemaphore.WaitOne(CreationTimeout);
waitResult = creationHandleObtained ? CREATION_HANDLE : WaitHandle.WaitTimeout;
Copy link
Contributor

Choose a reason for hiding this comment

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

There's no need to assign WaitHandle.WaitTimeout here. waitResult is only compared to CREATION_HANDLE for the remainder of its scope. You can simplify these 2 lines to:

if (_waitHandles.CreationSemaphore.WaitOne(CreationTimeout))
{
    waitResult = CREATION_HANDLE;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Looking closer, I don't think we need waitResult at all. It's being used as a stand-in for bool obtainedSemaphore. I think this would be clearer:

bool obtainedSemaphore = false;

try
{
    obtainedSemaphore = _waitHandles.CreationSemaphore.WaitOne(CreationTimeout);

    if (obtainedSemaphore)
    {
        ...
    }

    ...
}
finally
{
    if (obtainedSemaphore)
    {
        _waitHandles.CreationSemaphore.Release(1);
    }
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Bonus points if you want to create a SemaphoreScope RAII helper that obtains the semaphore on construction, and releases it on disposal. That would let the compiler write the finally block for you, and would keep the semaphore scoped inside the try block.

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed, the wait result pattern is only really meaningful when doing a WaitAny to see which semaphore you obtained.

Copy link
Member Author

Choose a reason for hiding this comment

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

I started with bare minimum changes to make it work :P

@edwardneal
Copy link
Contributor

This might also need to be backported to v5.1. #2390 ported the change from netcore, which has exactly the same behaviour:

waitResult = WaitHandle.WaitAny(_waitHandles.GetHandles(withCreate: true), CreationTimeout);

v6.0 is the first point that it impacted netfx, but this has been the netcore behaviour from the outset of the driver (even when going back to the PR which added System.Data.SqlClient to its first version of netcore.)

@samsharma2700 samsharma2700 self-assigned this Sep 26, 2025
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.

5 participants