-
Notifications
You must be signed in to change notification settings - Fork 10.5k
HTTP/3: Fix per-request context allocation, QuicStream.DisposeAsync #43011
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
Conversation
c298fb3 to
6ff4b42
Compare
| Debug.Assert(streamDirectionFeature != null); | ||
| Debug.Assert(streamIdFeature != null); | ||
|
|
||
| var context = CreateHttpStreamContext(streamContext); |
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's not generally clear why the changes in this file matter. Are they primarily about avoiding CreateHttpStreamContext(...) in the else portion of CreateHttp3Stream<TContext>(...)❔ (I'm somewhat unsure whether I'm missing another branch where CreateHttpStreamContext(...) is no longer called.)
Separately context and _context are both used below. Could we use slightly more distinct names❔
| && !_serverAborted | ||
| && _shutdownReadReason == null | ||
| && _shutdownWriteReason == null; | ||
| } |
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.
Null out _stream inside the lock, I assume DisposeCore is safe outside of the lock.
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.
I moved it because DisposeAsync can't happen inside a lock.
The stream shouldn't be used anymore because _processingTask is awaited. The one place it could be used, and _stream is accessed in a lock, is Abort.
I've changed Abort so that it takes a local copy of _stream and then checks it for null.
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.
So basically, we need to be able to call _stream.Abort() while _stream.DisposeAsync() is running? And anything else would be invalid since middleware has already completed.
The only problem is that Abort() could in theory still be in the middle of executing while we return the context to the pool because it doesn't do any synchronization. This could be bad if Abort() does something like setting _serverAborted after the QuicStreamContext has already been pushed and poped from the pool and "reset". I don't think Abort() can be thread safe without acquiring the _shutdownLock.
The other thing to make sure of is that we null _stream out before it enters the pool. DisposeAsync() is always awaited by Http3Stream before it calls Dispose() which may pool this QuicStreamContext, so we're good. Maybe putting it at the start of Dispose() before the if (!_connection.TryReturnStream(this)) would make this more obvious.
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.
I've changed:
DisposeAsyncto callstream.DisposeAsyncbefore the lock, and moved setting_streamto null inside the lock again. I think I like it more now, but I don't think this makes a huge difference as draining will already have happened by other code awaitingWaitForWritesClosedAsync. Because draining has already happened,stream.DisposeAsyncshould complete immediately.Abortnow checks for null_streamand uses lock when determining whether it should run.
| _shutdownReason = abortReason; | ||
|
|
||
| var resolvedErrorCode = _error ?? 0; | ||
| QuicLog.StreamAbort(_log, this, resolvedErrorCode, abortReason.Message); |
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 produce a different log (or no log?) when _stream is null which would imply the stream had already stopped?
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.
I've made Abort exit immediately if _stream is null. There is nothing for it to do: no stream to abort and pipes are completed in dispose.
| && !_serverAborted | ||
| && _shutdownReadReason == null | ||
| && _shutdownWriteReason == null; | ||
| } |
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.
So basically, we need to be able to call _stream.Abort() while _stream.DisposeAsync() is running? And anything else would be invalid since middleware has already completed.
The only problem is that Abort() could in theory still be in the middle of executing while we return the context to the pool because it doesn't do any synchronization. This could be bad if Abort() does something like setting _serverAborted after the QuicStreamContext has already been pushed and poped from the pool and "reset". I don't think Abort() can be thread safe without acquiring the _shutdownLock.
The other thing to make sure of is that we null _stream out before it enters the pool. DisposeAsync() is always awaited by Http3Stream before it calls Dispose() which may pool this QuicStreamContext, so we're good. Maybe putting it at the start of Dispose() before the if (!_connection.TryReturnStream(this)) would make this more obvious.
WebTransport changes started allocating context for every request.
QuicStream.Dispose just calls DisposeAsync but blocks. Call DisposeAsync directly.