Skip to content

Prevent the receive completion operation queued if unnecessary #4900

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 20 commits into
base: main
Choose a base branch
from

Conversation

masa-koz
Copy link
Contributor

@masa-koz masa-koz commented Mar 8, 2025

Description

If the receive complete API is called on a user thread before QUIC_STREAM_EVENT_RECEIVE finished, MsQuic will call QuicStreamReceiveComplete immediately even though a completion operation is queued. And a completion operation will triger QuicStreamReceiveComplete even though RecvCompletionLength is zero.

So, this pull request introduces a new flag to track if the receive complete API was called inline and updates related logic in the MsQuic library. The most important changes include adding the new flag RecvCompletionInlineCalled, modifying the condition checks, and updating comments to reflect the new logic.

Testing

NA

Documentation

NA

@masa-koz masa-koz requested a review from a team as a code owner March 8, 2025 09:01
@nibanks nibanks added external Proposed by non-MSFT Area: Core Related to the shared, core protocol logic labels Mar 8, 2025
@masa-koz
Copy link
Contributor Author

@nibanks I modified this pr according to your suggestion.

@masa-koz masa-koz changed the title Add inline completion flag for receive API in QUIC stream Prevent the receive completion operation queued if unnecessary Mar 10, 2025
@nibanks
Copy link
Member

nibanks commented Mar 10, 2025

The more I think about this, the more I am wondering what the real problem is with the original behavior:

If the receive complete API is called on a user thread before QUIC_STREAM_EVENT_RECEIVE finished, MsQuic will call QuicStreamReceiveComplete immediately even though a completion operation is queued. And a completion operation will triger QuicStreamReceiveComplete even though RecvCompletionLength is zero.

What is the downside of calling the queued completion with a zero length?

@nibanks
Copy link
Member

nibanks commented Mar 10, 2025

Is the problem that the zero drain results in a paused receive path?

@masa-koz
Copy link
Contributor Author

Yes. I'm afraid that I forgot the detail, but recv path paused even though the data arrived.

@nibanks
Copy link
Member

nibanks commented Mar 11, 2025

Yes. I'm afraid that I forgot the detail, but recv path paused even though the data arrived.

Thanks. Let me think on this problem a bit more to see what might be the simplest way forward.

@nibanks
Copy link
Member

nibanks commented Apr 16, 2025

@guhetier can you please add this PR to your to-do list to help drive to completion? We need to get this fixed before the next release.

@guhetier guhetier self-assigned this Apr 16, 2025
@guhetier
Copy link
Contributor

Hi @masa-koz, are you still interested in working on this PR?
If yes, I'm happy to help you get to a lock-free solution, otherwise, I can take over to fix this issue.

After looking at the problem again, I think we can avoid the lock by storing both ReceiveCallActive and RecvCompletionLength in the same uint64_t and updating them in one atomic operation:

  1. The top bit of RecvCompletionLength is used for ReceiveCallActive
  2. In QuicStreamRecvFlush, Stream->Flags.ReceiveCallActive = TRUE; can become something like:
    InterlockedExchangeAdd64((int64_t*)&Stream->RecvCompletionLength, 0x8000000000000000);
  1. ... and after the notification returns, we can both get the number of bytes to complete and clear the flag with:
    BufferLength = InterlockedExchangeAdd64((int64_t*)&Stream->RecvCompletionLength, -0x8000000000000000);
    BufferLength &= ~0x8000000000000000;
    InterlockedExchangeAdd64((int64_t*)&Stream->RecvCompletionLength, BufferLength);
  1. In MsQuicStreamReceiveComplete, we start by always using InterlockedExchangeAdd64 to add the number of completed bytes to Stream->RecvCompletionLength, and we check the return value top bit to know if a receive call is pending, or if we need to queue a work item.

I think this should work. There is still a possibility of a race in "Multiple Receive" mode, but in that mode, a receive completion work item with 0 bytes completed is not a problem, we don't stop indicating receives.

This is similar to what Nick suggested in a comment, but slightly simpler.

@masa-koz
Copy link
Contributor Author

I'm still interested. But I have no time now. Could you mind waiting for a few days?

@guhetier
Copy link
Contributor

It can wait a few days, we want to make sure we have this done for the next release (which doesn't have a specific date yet). Thanks for your contributions!

@nibanks
Copy link
Member

nibanks commented Apr 22, 2025

There are also merge conflicts preventing the CI from being run.

Copy link

codecov bot commented Apr 24, 2025

Codecov Report

Attention: Patch coverage is 76.92308% with 3 lines in your changes missing coverage. Please review.

Project coverage is 86.95%. Comparing base (186accc) to head (dabf3e5).
Report is 12 commits behind head on main.

Files with missing lines Patch % Lines
src/core/api.c 50.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4900      +/-   ##
==========================================
+ Coverage   86.28%   86.95%   +0.67%     
==========================================
  Files          59       59              
  Lines       18011    18013       +2     
==========================================
+ Hits        15541    15664     +123     
+ Misses       2470     2349     -121     

☔ 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.

Copy link
Member

@nibanks nibanks left a comment

Choose a reason for hiding this comment

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

Here's a slight redesign that makes it cleaner and more efficient (I think).

@masa-koz
Copy link
Contributor Author

@nibanks
I've changed the code to follow your suggestion.

@nibanks
Copy link
Member

nibanks commented Apr 25, 2025

Could you replace merge/rebase main to get a few recent CI fixes? Then I think we'll be good to go. Thanks so much for working with us on this PR!

(int64_t)BufferLength);
if ((BufferLength & QUIC_STREAM_RECV_COMPLETION_LENGTH_CANARY_BIT) != 0 &&
(RecvCompletionLength & QUIC_STREAM_RECV_COMPLETION_LENGTH_CANARY_BIT) != 0) {
QuicTraceEvent(
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not convinced this is a good way to handle the error...
If we simply goto Exit with a log, we silently break our contract with the app: the completed some bytes but we won't restart receive indications (or in multi-receive mode, they are now out of sync).

IMO, this should be a fatal error for the connection.

@nibanks, opinions?

(I thought I commented on this earlier but can't find it anymore...)

Copy link
Member

Choose a reason for hiding this comment

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

For one, you have to be careful about aborting the connection on the app thread here. I think we may do something special above in one of the other functions. But you're probably right that we should abort the connection if this happens.

Copy link
Member

Choose a reason for hiding this comment

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

See this code in StreamSend above:

            //
            // We can't fail the send at this point, because we're already queued
            // the send above. So instead, we're just going to abort the whole
            // connection.
            //
            if (InterlockedCompareExchange16(
                    (short*)&Connection->BackUpOperUsed, 1, 0) != 0) {
                goto Exit; // It's already started the shutdown.
            }
            Oper = &Connection->BackUpOper;
            Oper->FreeAfterProcess = FALSE;
            Oper->Type = QUIC_OPER_TYPE_API_CALL;
            Oper->API_CALL.Context = &Connection->BackupApiContext;
            Oper->API_CALL.Context->Type = QUIC_API_TYPE_CONN_SHUTDOWN;
            Oper->API_CALL.Context->CONN_SHUTDOWN.Flags = QUIC_CONNECTION_SHUTDOWN_FLAG_SILENT;
            Oper->API_CALL.Context->CONN_SHUTDOWN.ErrorCode = (QUIC_VAR_INT)QUIC_STATUS_OUT_OF_MEMORY;
            Oper->API_CALL.Context->CONN_SHUTDOWN.RegistrationShutdown = FALSE;
            Oper->API_CALL.Context->CONN_SHUTDOWN.TransportShutdown = TRUE;
            QuicConnQueueHighestPriorityOper(Connection, Oper);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area: Core Related to the shared, core protocol logic external Proposed by non-MSFT
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants