-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
QUIC read pipeline changes #55505
QUIC read pipeline changes #55505
Conversation
Tagging subscribers to this area: @dotnet/ncl Issue DetailsThis brings changes to read states and behavior done initially in #52929 with my fixes to it to make all tests work
|
I've run stress tests on this. It fixes "The response ended prematurely" exceptions (and does not bring any new ones 😁).
as expected exceptions (258 = H3_INTERNAL_ERROR (0x102)), then all
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it generally looks ok to me. But the state transitions are not easy to verify.
It would be great if also @stephentoub can take a look at the cancellation and pinning.
// Resettable completions to be used for multiple calls to receive. | ||
public readonly ResettableCompletionSource<uint> ReceiveResettableCompletionSource = new ResettableCompletionSource<uint>(); | ||
// filled when ReadState.BuffersAvailable: | ||
public QuicBuffer[] ReceiveQuicBuffers = Array.Empty<QuicBuffer>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you change List to array? It seems like that would be more difficult to manage in general.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was Cory's decision. Before, we were clearing List on every read
runtime/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicStream.cs
Line 436 in 102fc35
_state.ReceiveQuicBuffers.Clear(); |
and adding on every RECV event
runtime/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicStream.cs
Line 782 in 102fc35
state.ReceiveQuicBuffers.Add(receiveEvent.Buffers[i]); |
I guess array way is more performant, so I decided to take in this change.
I may return the list back if you wish.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The array seems fine to me, I'd keep it.
new Span<byte>(nativeBuffer.Buffer, takeLength).CopyTo(destinationBuffer); | ||
destinationBuffer = destinationBuffer.Slice(takeLength); | ||
} | ||
while (destinationBuffer.Length != 0 && ++i < sourceBuffers.Length); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do we handle partial data? e.g. We have more data than destinationBuffer buffer. So we consume some but how do we know where to start next time?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Below we return amount we've consumed, and then we call ReceiveComplete(taken);
to tell msquic that amount. Then we call EnableReceive();
which will instruct msquic to produce new RECV event with remaining data.
We've done the same thing before, it was just moved around.
The only actual addition is, if we've consumed everything right inside RECV event callback, we tell mquic that right away, without additional EnableReceive()
|
||
lock (_state) | ||
switch (readState) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With exception of pre-cancellation the readState
seems never updated. Are we getting here only in failed cases?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
readState
is a copy of an initial value of _state.ReadState
. So it is not intended to change, with exception of pre-cancellation, when there's nothing left to do except throw, and throwing is done in one place here. _state.ReadState
is changed depending on whether there is data available (IndividualReadComplete->None) or not available (None->PendingRead) or upon cancellation (any->Aborted)
Are we getting here only in failed cases?
Yes (Well, almost, as ReadsCompleted
, which is EOS, is a success case). In success case where we have data already, above we return new ValueTask<int>(taken);
, in success case where we wait for data, return _state.ReceiveResettableCompletionSource.GetValueTask();
QuicBuffer[] oldReceiveBuffers = state.ReceiveQuicBuffers; | ||
state.ReceiveQuicBuffers = ArrayPool<QuicBuffer>.Shared.Rent((int)receiveEvent.BufferCount); | ||
|
||
if (oldReceiveBuffers.Length != 0) // don't return Array.Empty. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we assert somehow the old buffers were fully consumed?
There was a problem hiding this comment.
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 do that. Unconsumed data arrives again from the point we've stopped. New RECV event wouldn't come until we call EnableReceive()
, and we call it only after we've consumed as much as we could and said so to msquic in ReceiveComplete(taken)
, so new event will have all the remaining data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a comment wouldn't hurt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some nits, and suggestion about the code organization, but otherwise looks good and if it works then great 👍
// set when ReadState.PendingRead: | ||
public Memory<byte> ReceiveUserBuffer; | ||
public CancellationTokenRegistration ReceiveCancellationRegistration; | ||
public MsQuicStream? RootedReceiveStream; // roots the stream in the pinned state to prevent GC during an async read I/O. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be called just Stream
or we should rename the equivalent in MsQuicConnection
:
runtime/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicConnection.cs
Line 49 in 594901a
public MsQuicConnection? Connection; |
Also, position-wise, it should be higher, next to the handles, to follow the similarity with connection. It's a NIT, but my "consistency" radar is really unhappy about it 😄
public Memory<byte> ReceiveUserBuffer; | ||
public CancellationTokenRegistration ReceiveCancellationRegistration; | ||
public MsQuicStream? RootedReceiveStream; // roots the stream in the pinned state to prevent GC during an async read I/O. | ||
public readonly ResettableCompletionSource<int> ReceiveResettableCompletionSource = new ResettableCompletionSource<int>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason why this property lost its comment?
if (NetEventSource.Log.IsEnabled()) | ||
{ | ||
NetEventSource.Info(_state, $"[Stream#{_state.GetHashCode()}] reading into Memory of '{destination.Length}' bytes."); | ||
} | ||
|
||
ReadState readState; | ||
long abortError = -1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
long abortError = -1; | |
long abortError; |
readState = _state.ReadState; | ||
abortError = _state.ReadErrorCode; | ||
|
||
if (readState != ReadState.PendingRead && cancellationToken.IsCancellationRequested) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not in PendingRead
?
Do we somehow guard against parallel reads? Does this PR contain something that prevents that?
EDIT: I see it in the switch after.
{ | ||
state.ReceiveResettableCompletionSource.CompleteException( | ||
ExceptionDispatchInfo.SetCurrentStackTrace(new OperationCanceledException("Read was canceled", token))); | ||
return _state.ReceiveResettableCompletionSource.GetValueTask(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The combination of if-else if-else if
with return
at the end of some of the branches is super confusing to me. For instance, the first if (readState ...)
will continue after the whole block, but the two else if-else if
both end with return
, ending the flow there. Could we reshuffle this? Maybe put the two else-if which return as first and make them just ifs.
What is your opinion on the code organization of this? Do you find it easily readable/comprehensible? Maybe I'm just not familiar enough with this style.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As we just talked offline, feel free to keep this as is, even for a follow up. My "feelings" about readability are not sound enough reason for so much work.
ex = | ||
canceledSynchronously ? new OperationCanceledException(cancellationToken) : // aborted by token being canceled before the async op started. | ||
abortError == -1 ? new QuicOperationAbortedException() : // aborted by user via some other operation. | ||
new QuicStreamAbortedException(abortError); // aborted by peer. | ||
|
||
break; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT:
ex = | |
canceledSynchronously ? new OperationCanceledException(cancellationToken) : // aborted by token being canceled before the async op started. | |
abortError == -1 ? new QuicOperationAbortedException() : // aborted by user via some other operation. | |
new QuicStreamAbortedException(abortError); // aborted by peer. | |
break; | |
ex = canceledSynchronously ? new OperationCanceledException(cancellationToken) : // aborted by token being canceled before the async op started. | |
ThrowHelper.GetStreamAbortedException(abortError); // aborted by peer. | |
break; |
{ | ||
state.ReceiveQuicBuffers.Add(receiveEvent.Buffers[i]); | ||
// This is a 0-length receive that happens once reads are finished (via abort or otherwise). | ||
// State changes for this are handled elsewhere. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In PEER_SEND_SHUTDOWN / PEER_SEND_ABORT / SHUTDOWN_COMPLETE event handlers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we put it in that comment instead of "elsewhere"? 😄
@@ -1306,6 +1411,16 @@ private static uint HandleEventConnectionClose(State state) | |||
private static Exception GetConnectionAbortedException(State state) => | |||
ThrowHelper.GetConnectionAbortedException(state.ConnectionState.AbortErrorCode); | |||
|
|||
// Read state transitions: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome comment! 🥳
I will create a follow-up PR with cosmetic changes |
Follow-up for NITs and cosmetic changes from #55505
This brings changes to read states and behavior done initially in #52929 with my fixes to it to make all tests work