Fix Http 1.1's read-ahead task management#103257
Merged
MihaZupan merged 2 commits intodotnet:mainfrom Jun 22, 2024
Merged
Conversation
Contributor
|
Tagging subscribers to this area: @dotnet/ncl |
Member
|
Not my area but I want to recognize that the change description here is really great 😀. |
This was referenced Jun 11, 2024
Closed
Member
Author
|
/azp run runtime-libraries-coreclr outerloop |
|
Azure Pipelines successfully started running 1 pipeline(s). |
bf4ef8f to
8c21819
Compare
wfurt
approved these changes
Jun 20, 2024
| @@ -133,7 +137,9 @@ public bool PrepareForReuse(bool async) | |||
| // If the read-ahead task is completed, then we've received either EOF or erroneous data the connection, so it's not usable. | |||
| if (ReadAheadTaskHasStarted) | |||
Member
There was a problem hiding this comment.
could still this be race condition? e.g. It did not start it but it will before we get to the next statement.
Member
Author
There was a problem hiding this comment.
By the time you get to PrepareForReuse, the caller is the only thing with a reference to this connection.
It's possible that the read was already started and is transitioning to Completed right under you, but it can't transition from NotStarted => something else, or from something to => NotStarted.
MihaZupan
commented
Jun 21, 2024
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs
Outdated
Show resolved
Hide resolved
Member
Author
rzikm
pushed a commit
to rzikm/dotnet-runtime
that referenced
this pull request
Jun 24, 2024
* Fix Http 1.1's read-ahead task management * Fix comment typo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #100616
Every HTTP/1.1 connection has a
ValueTask<int> _readAheadTaskfield that gets set either:PrepareForReuseright before the call toSendAsync, orAs a result of the latter, if a connection is added to the connection pool at any point (the shared
_http11Connectionsstack), we must assume that the read-ahead may have been set.When "consuming" connections, we carefully manage who may consume the read-ahead task to ensure that we don't leak unobserved task exceptions.
To achieve this, we're using an
int _readAheadTaskStatusflag to signal which thread will observe the result / whether the exception should be suppressed.The problem with the current state (before this PR) is that a connection may be briefly added to the connection pool and taken out right after to handle a pending request from the request queue.
Because we know that the connection was freshly added and therefore still good, we don't bother calling into
PrepareForReuse, which means we weren't updating the_readAheadTaskStatusas expected. This results in hitting a debug assert inHttpConnection.SendAsyncthat checks that someone claimed ownership of the read-ahead completion before sending the request (#100616).Why don't we always call
PrepareForReusethen?PrepareForReuseexpects its caller to immediately call intoSendAsyncif the check is successful. This allows it to store thestream.ReadAsyncinto_readAheadTaskdirectly, without any extra wrapping logic.runtime/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs
Line 166 in 0686ce6
But at this point in the connection management, we're not 100% certain that we will have a request for the connection yet - we have waiters that may get canceled at any point.
If we did call
PrepareForReuseand then had no waiter to signal, we'd have to throw away the connection.PrepareForReusealso uses different logic based on abool asyncargument which we don't yet have at this point.Why don't we give the connection to the waiter and have the consuming code call
PrepareForReuse?While not super likely, if the connection was already bad, it would mean that we now took a request from the head of the queue and pushed it back to the tail to retry.
It would also potentially mean needing an extra flag to remember if this is the first request on the connection to match how we currently handle new connections that are immediately bad. Without that, we could end up doing more connect retries to a faulty server than we currently do (maybe that'd be fine, but it's not an obvious consequence).
It should be a possible approach, but I found the following simpler to follow:
This PR:
I changed how we handle read-ahead completion ownership, allowing a thread to return its completion ownership:
If
TryOwnScavengingTaskCompletionfails, the read-ahead already completed and any potential exceptions got ignored => the connection is not usable and we try again.If
TryReturnScavengingTaskCompletionOwnershipfails, the read-ahead completed between us taking ownership and trying to return it => the connection is not usable and we try again.