Skip to content
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

Merged
merged 6 commits into from
Jul 13, 2021
Merged

QUIC read pipeline changes #55505

merged 6 commits into from
Jul 13, 2021

Conversation

CarnaViire
Copy link
Member

This brings changes to read states and behavior done initially in #52929 with my fixes to it to make all tests work

@ghost
Copy link

ghost commented Jul 12, 2021

Tagging subscribers to this area: @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

Issue Details

This brings changes to read states and behavior done initially in #52929 with my fixes to it to make all tests work

Author: CarnaViire
Assignees: -
Labels:

area-System.Net.Quic

Milestone: -

@CarnaViire
Copy link
Member Author

I've run stress tests on this. It fixes "The response ended prematurely" exceptions (and does not bring any new ones 😁).
More than that, I found out that GET Aborted scenario actually lacked proper checking for HTTP/3 exceptions, so if we treat

HttpRequestException->IOException->HttpRequestException->QuicStreamAbortedException("Stream aborted by peer (258).")
HttpRequestException->QuicStreamAbortedException("Stream aborted by peer (258).")

as expected exceptions (258 = H3_INTERNAL_ERROR (0x102)), then all GET Aborted are passing so far 🥳

client_1  | HttpStress Run Final Report
client_1  |
client_1  | [07/12/2021 17:48:50] Total: 16,218 Runtime: 00:11:00
client_1  |      0: GET                       Success: 1,071    Canceled: 81    Fail: 0
client_1  |      1: GET Partial               Success: 1,082    Canceled: 69    Fail: 0
client_1  |      2: GET Headers               Success: 1,054    Canceled: 97    Fail: 0
client_1  |      3: GET Parameters            Success: 1,067    Canceled: 85    Fail: 0
client_1  |      4: GET Aborted               Success: 1,075    Canceled: 77    Fail: 0
client_1  |      5: POST                      Success: 1,074    Canceled: 79    Fail: 0
client_1  |      6: POST Multipart Data       Success: 1,079    Canceled: 75    Fail: 0
client_1  |      7: POST Duplex               Success: 1,076    Canceled: 79    Fail: 0
client_1  |      8: POST Duplex Slow          Success: 1,069    Canceled: 85    Fail: 0
client_1  |      9: POST Duplex Dispose       Success: 1,080    Canceled: 75    Fail: 0
client_1  |     10: POST ExpectContinue       Success: 0        Canceled: 77    Fail: 0
client_1  |     11: HEAD                      Success: 1,082    Canceled: 73    Fail: 0
client_1  |     12: PUT                       Success: 1,082    Canceled: 71    Fail: 0
client_1  |     13: PUT Slow                  Success: 1,062    Canceled: 90    Fail: 0
client_1  |     14: GET Slow                  Success: 1,077    Canceled: 75    Fail: 0
client_1  |         TOTAL                     Success: 15,030   Canceled: 1,188 Fail: 0

Copy link
Member

@wfurt wfurt left a 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>();
Copy link
Member

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.

Copy link
Member Author

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


and adding on every RECV event
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.

Copy link
Member

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);
Copy link
Member

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?

Copy link
Member Author

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)
Copy link
Member

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?

Copy link
Member Author

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.
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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?

Copy link
Member

@ManickaP ManickaP left a 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.
Copy link
Member

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:

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>();
Copy link
Member

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;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
long abortError = -1;
long abortError;

readState = _state.ReadState;
abortError = _state.ReadErrorCode;

if (readState != ReadState.PendingRead && cancellationToken.IsCancellationRequested)
Copy link
Member

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();
Copy link
Member

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.

Copy link
Member

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.

Comment on lines +446 to +451
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;
Copy link
Member

Choose a reason for hiding this comment

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

NIT:

Suggested change
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.
Copy link
Member

Choose a reason for hiding this comment

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

Where?

Copy link
Member Author

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

Copy link
Member

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:
Copy link
Member

Choose a reason for hiding this comment

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

Awesome comment! 🥳

@CarnaViire
Copy link
Member Author

I will create a follow-up PR with cosmetic changes

@CarnaViire CarnaViire merged commit 430d87f into dotnet:main Jul 13, 2021
CarnaViire added a commit that referenced this pull request Jul 14, 2021
Follow-up for NITs and cosmetic changes from #55505
@karelz karelz added this to the 6.0.0 milestone Jul 15, 2021
@wfurt wfurt mentioned this pull request Jul 18, 2021
@ghost ghost locked as resolved and limited conversation to collaborators Aug 14, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants