-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Add HTTP/3 #1294
Add HTTP/3 #1294
Conversation
src/libraries/Common/src/System/Net/Http/Http2/Hpack/HPackEncoder.cs
Outdated
Show resolved
Hide resolved
@@ -8,6 +8,11 @@ namespace System.Net.Http.HPack | |||
{ | |||
internal static class IntegerEncoder | |||
{ | |||
/// <summary> |
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.
Move IntegerEncoder out of Http2 (as it's used by Http3)?
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.
Need to figure out the sharing script for this. Maybe just merge the http/2 and http/3 into a single shared directory structure.
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 looks like this is the only type that's shared between Http2 and Http3? If so then having Http3 reference the Http2 class/file should be fine. I'd like to keep the other files separate for clarity.
How about this new dir structure? No need to duplicate the scripts.
- src/libraries/Common/src/System/Net/Http/AspShared
- /Http2
- /Http3
- Copy scripts, readme, etc.
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.
Yep, that's in line with what I was thinking.
@@ -0,0 +1,105 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. |
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.
Probably swap these copyrights to MIT as that's what is being used in corefx.
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 agree, but I'm not sure enough of the technicalities to know if it's legal for us to change it. Our strategy with HPACK was to keep the original license from ASP.NET. @terrajobst any opinions?
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.
cc: @richlander
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.
Go ahead ... make the license change.
{ | ||
internal static partial class Http3Frame | ||
{ | ||
public static bool TryReadIntegerPair(ReadOnlySpan<byte> buffer, out long a, out long b, out int bytesRead) |
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.
public static bool TryReadIntegerPair(ReadOnlySpan<byte> buffer, out long a, out long b, out int bytesRead) | |
public static bool TryReadVariableLengthIntegerPair(ReadOnlySpan<byte> buffer, out long a, out long b, out int bytesRead) |
as the out params are longs, either we should consistently say VariableLengthInteger or Long instead of Integer.
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.
TryReadVLIPair? 😁
VariableLengthInteger is the spec term, but it's a bit of a mouthful.
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 feel like the current name is fine, but if others express a strong opinion I'm not opposed to changes.
src/libraries/System.Net.Http/src/System/Net/Http/Headers/AltSvcHeaderParser.cs
Show resolved
Hide resolved
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ArrayBuffer.cs
Show resolved
Hide resolved
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs
Show resolved
Hide resolved
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs
Outdated
Show resolved
Hide resolved
} | ||
|
||
QuicStream stream = await streamTask.ConfigureAwait(false); | ||
_ = ProcessServerStreamAsync(stream); |
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'd keep this task around and await it in some cleanup code.
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 here; the process is cleaned up when the connection is disposed, and errors are only observed through SendAsync()
. Will add a comment though.
src/libraries/Common/src/System/Net/Http/Http3/Helpers/VariableLengthIntegerHelper.cs
Outdated
Show resolved
Hide resolved
src/libraries/Common/src/System/Net/Http/Http3/Helpers/VariableLengthIntegerHelper.cs
Show resolved
Hide resolved
} | ||
catch (HuffmanDecodingException ex) | ||
{ | ||
throw new QPackDecodingException("TODO sync with corefx" /*CoreStrings.QPackHuffmanError, */, ex); |
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.
throw new QPackDecodingException("TODO sync with corefx" /*CoreStrings.QPackHuffmanError, */, ex); | |
throw new QPackDecodingException("TODO sync with runtime/libraries" /*CoreStrings.QPackHuffmanError, */, ex); |
?
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.
Ultimately these strings need to be put into resources.
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 have shared resources mostly working in the Http2 code.
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.
Let's leave this as a follow up item.
src/libraries/Common/src/System/Net/Http/Http3/QPack/QPackEncoder.cs
Outdated
Show resolved
Hide resolved
src/libraries/Common/src/System/Net/Http/Http3/QPack/QPackEncoder.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Http/src/System/Net/Http/Headers/AltSvcHeaderParser.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Http/src/System/Net/Http/Headers/AltSvcHeaderParser.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs
Outdated
Show resolved
Hide resolved
// writes more bytes than was precomputed (this latter case is probably an error -- TODO). | ||
// In this case, each write results in a data frame. TODO: optimize this to avoid small frame sizes. | ||
|
||
byte[] dataFrame = _dataFrame; |
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.
byte[] dataFrame = _dataFrame; | |
byte[] dataFrame = _dataFrame ??= new byte[9]; |
Saves some C# code when the following if
is removed. More or less a matter of taste.
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'm fine with it now; will update if there are strong opinions.
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpAuthority.cs
Outdated
Show resolved
Hide resolved
: WaitThenSendAsync(waitTask, request, cancellationToken); | ||
} | ||
|
||
private async Task<HttpResponseMessage> WaitThenSendAsync(Task taskToWaitFor, HttpRequestMessage request, CancellationToken cancellationToken) |
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: ValueTask all the things?
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.
For now, only the things I expect to finish synchronously.
In the future, depending on the outcome of @stephentoub's ValueTask
caching patch, maybe ValueTask
all the things.
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 think it's reasonable to use ValueTask<T>
for any of these that are internal/private, as long as we're only ever awaiting them at the call sites. That will help us to better evaluate the ValueTask opt-in change I made.
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.
@stephentoub I'm open to it, but how will it help us evaluate things when there's no baseline to compare to?
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 comparison I'm referring to is when lots of stuff uses ValueTask<T>
, and we flip the switch to enable automatic pooling (if very little is using ValueTask<T>
in our code, then flipping that switch won't produce any meaningful results). If the method completes synchronously, then using ValueTask<T>
was a good idea, and if it completes asynchronously, then the overhead compared to the allocation/state management/etc. is minimal, so I'm not particularly concerned with a comparison about HTTP/3's implementation with ValueTask<T>
vs Task<T>
here.
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.
Thanks. I'll leave this for a post-merge update.
Abort(ex); | ||
} | ||
|
||
retryRequest: |
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: scrap the goto 😄
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?
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 more of a personal opinion. I think it generally makes code less readable and usually can be fixed via refactoring. We avoid them in AspNetCore, not sure if it varies in the runtime repo.
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'll take another look to see if they can reasonably be removed without a size/perf penalty.
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.
Does the goto mess up the lock statement? I'd rather not find out.
A local method or just inlining the code seems more appropriate here.
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.
Does the goto mess up the lock statement?
C# doesn't let you create irreducible control-flow graphs with goto; so no. (i.e. you can't jump into a different scope).
It outputs a leave.s
il instruction inside a lock, executing the finally first, undoing the lock
The leave.s instruction is similar to the br instruction, but it can be used to exit a try, filter, or catch block whereas the ordinary branch instructions can only be used in such a block to > transfer control within it. The leave.s instruction empties the evaluation stack and ensures that the appropriate surrounding finally blocks are executed.
tl;dr C# won't compile if your goto use is unsound (unlike other languages)
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.
Aside, the C# compiler on the other hand... does create irreducible control-flow graphs in il, in certain circumstances dotnet/roslyn#39814
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.
Leaving this as a post-merge TODO. I'm not worried about the goto specifically but I'm going to go over the entire locking strategy, which was made a bit coarse to start with, and goto might naturally go away as part of that -- either way, once I get to this method I'll see if it's reasonable to get rid of it.
_requestStreamsRemaining += (newMaximumStreamCount - _maximumRequestStreams); | ||
_maximumRequestStreams = newMaximumStreamCount; | ||
|
||
while (_requestStreamsRemaining != 0 && _waitingRequests.TryDequeue(out TaskCompletionSourceWithCancellation<bool> tcs)) |
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'm a bit confused by the _waitingRequests queue here. This looks like it is draining all pending calls to SendAsync. Why do we need to do this?
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 is to respond to new MAX_STREAMS events from QUIC. _waitingRequests
are any requests waiting for a new stream to open up.
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 think you need another place where you dequeue when a request is finished. Ex: max request stream count is 100, someone makes 101 requests, so one request is waiting. When a request finishes, it needs to call TryDequeue to start the next request, correct?
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.
MAX_STREAMS is a cumulative maximum, not a maximum concurrent streams:
Note that these frames (and the corresponding transport parameters) do not describe the number of streams that can be opened concurrently. The limit includes streams that have been closed as well as those that are open.
https://quicwg.org/base-drafts/draft-ietf-quic-transport.html#section-19.11-8
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs
Show resolved
Hide resolved
|
||
while (!VariableLengthIntegerHelper.TryRead(buffer.AvailableSpan, out lastStreamId, out bytesRead)) | ||
{ | ||
buffer.EnsureAvailableSpace(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.
This seems inefficient. This is only growing the buffer by one each time, which causes TryRead and ReadAsync to be called multiple times if we are reading a large integer.
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 looks like ArrayBuffer grows by doubling even if only 1 byte is needed
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.
Okay not as bad as I initially thought. Maybe something should be renamed to make it more clear that we are increasing the space by more than just 1 (at least double).
I still think this pattern of growing the buffer is prone to smaller reads though.
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.
Current strategy is to: read in just enough to parse headers in one I/O, and then let user control read pattern through Content.
Once we get implementation finished, we'll want to tune this. It's not clear what buffer size will be optimal just yet, but it will be trivial to change when we're ready.
while (!Http3Frame.TryReadIntegerPair(_buffer.ActiveSpan, out frameType, out payloadLength, out bytesRead)) | ||
{ | ||
_buffer.EnsureAvailableSpace(2); | ||
bytesRead = await _stream.ReadAsync(_buffer.AvailableMemory, cancellationToken).ConfigureAwait(false); |
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.
These small reads don't seem like they would be good for perf. Do you think it may be better to preemptively allocate some amount in the buffer s.t. future calls that use the payload length already have it?
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.
AvailableMemory
will typically be much larger here. 2
is just the minimum that we need for two integers.
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs
Outdated
Show resolved
Hide resolved
private int ReadResponseContent(HttpResponseMessage response, Span<byte> buffer) | ||
{ | ||
// This should only be called by our read stream, and our local _response should be nulled out by now. | ||
Debug.Assert(_response == 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.
I see this called from Read(), why does response need to be 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.
Once the response headers are read, HttpRequestStream._response
will be nulled out to avoid circular references. By the time we're reading content, this should already have happened. I'll see about improving the comment.
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 see, what circular references are you worried about?
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.
stream -> response -> content -> stream
while (buffer.Length != 0) | ||
{ | ||
// Sync over async here -- QUIC implementation does it per-I/O already; this is at least more coarse-grained. | ||
if (_dataPayloadRemaining <= 0 && !ReadNextDataFrameAsync(response, CancellationToken.None).GetAwaiter().GetResult()) |
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 will probably need to change eventually to no do sync over async.
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.
Yea, I had same thought -- leaving as a TODO until we firm up the behavior of QuicStream.
} | ||
} | ||
|
||
private async ValueTask<int> ReadResponseContentAsync(HttpResponseMessage response, Memory<byte> buffer, CancellationToken cancellationToken) |
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.
Codeshare between ReadResponseContent/ReadResponseContentAsync
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.
Any ideas?
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 I'm mistaken, but the only difference I see is that ReadNextDataFrameAsync is called sync in ReadResponseContent and async in ReadResponseContentAsync.
At a minimum, you can share the logic to calculate the totalBytesRead and update the buffers.
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.
Stream.Read
vs Stream.ReadAsync
as well. I can share the exception handling; I'll do that.
public void Trace(string message, [CallerMemberName] string memberName = null) => | ||
_connection.Trace(_stream.StreamId, message, memberName); | ||
|
||
private sealed class Http3ReadStream : HttpBaseStream |
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 Http3RequestStream derive from HttpBaseStream s.t. you can avoid allocating an extra object per request?
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.
Possibly, but it gets weird as the users can dispose the stream. I will leave this for future TODO to look into.
} | ||
} | ||
|
||
private sealed class Http3WriteStream : HttpBaseStream |
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.
Same here, can Http3WriteStream be removed and part of Http3RequestStream?
@@ -7,7 +7,7 @@ | |||
|
|||
namespace System.Net.Http.HPack | |||
{ | |||
internal static class StaticTable | |||
internal static class H2StaticTable |
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.
More accurately these are the HPack and QPack static tables, not the H2 and H2 tables. How about calling them HPack.StaticTable in the few places both namespaces are in scope?
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 easier to disambiguate them this way, without having to see what using
is in effect. I'd like them to have separate names.
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 rename to HPackStaticTable and QPackStaticTable?
@@ -8,6 +8,11 @@ namespace System.Net.Http.HPack | |||
{ | |||
internal static class IntegerEncoder | |||
{ | |||
/// <summary> |
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 looks like this is the only type that's shared between Http2 and Http3? If so then having Http3 reference the Http2 class/file should be fine. I'd like to keep the other files separate for clarity.
How about this new dir structure? No need to duplicate the scripts.
- src/libraries/Common/src/System/Net/Http/AspShared
- /Http2
- /Http3
- Copy scripts, readme, etc.
src/libraries/Common/src/System/Net/Http/Http2/IHttpHeadersHandler.cs
Outdated
Show resolved
Hide resolved
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
#if KESTREL | ||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 |
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.
We don't need this kestrel namespace, the internal shared types can be in System.Net.Http. Only IHttpHeadersHandler needed to preserve the namespace because it's public.
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.
Yeah go ahead and make the changes here, we will react in Kestrel to the namespace changes.
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.
Only IHttpHeadersHandler needed to preserve the namespace because it's public.
We're adding new methods to the interface. That's similarly breaking, no? I thought the purpose of the pubternals approach was that API changes were permitted.
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 think we can change IHttpHeadersHandler to be in the System.Net.Http namespace, but IIRC we use IHttpHeadersHandler in our benchmarks repo. cc @sebastienros we may update a namespace that we'd need to react to in benchmarks, that okay?
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.
Always ok if you apply the change in the benchmarks too ;)
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.
IHttpHeadersHandler is an internal type in System.Net.Http.dll, it doesn't matter what the namespace is there.
IHttpHeadersHandler is a public (pubternal) type in Kestrel. Yes we can make API changes to pubternal types, that's not the question here. We should not use the System.Net.Http namespace for public types in Kestrel.
src/libraries/Common/src/System/Net/Http/Http3/Frames/Http3ErrorCode.cs
Outdated
Show resolved
Hide resolved
{ | ||
internal static partial class Http3Frame | ||
{ | ||
public static bool TryReadIntegerPair(ReadOnlySpan<byte> buffer, out long a, out long b, out int bytesRead) |
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.
TryReadVLIPair? 😁
VariableLengthInteger is the spec term, but it's a bit of a mouthful.
return integerValue; | ||
} | ||
|
||
static bool TryDecodeHttpInteger(ReadOnlySpan<byte> buffer, out long value, out int bytesRead) |
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.
Duplicated from VariableLengthIntegerHelper?
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.
Intentional -- our loopback code does not share product code.
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.
eek. That seems like an odd decision to me. What are the main reasons for not sharing "product code". Can you do internals visible to?
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.
What are the main reasons for not sharing "product code".
Two implementations of something make it easier to find bugs, and the test implementation often sacrifices perf for flexibility or simplicity.
Can you do internals visible to?
If we did anything it would be to include the source directly; InternalsVisibleTo is a hard no per various discussions on its goodness.
src/libraries/Common/src/System/Net/Http/Http3/Helpers/VariableLengthIntegerHelper.cs
Show resolved
Hide resolved
src/libraries/Common/src/System/Net/Http/Http3/QPack/QPackEncoder.cs
Outdated
Show resolved
Hide resolved
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.
Incremental review
src/libraries/Common/src/System/Net/Http/Http3/Http3SettingType.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Http/src/System/Net/Http/Headers/AltSvcHeaderValue.cs
Show resolved
Hide resolved
src/libraries/System.Net.Http/src/System/Net/Http/Headers/AltSvcHeaderValue.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Http/src/System/Net/Http/Headers/AltSvcHeaderValue.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs
Show resolved
Hide resolved
/// The requested operation cannot be served over HTTP/3. The peer should retry over HTTP/1.1. | ||
/// </summary> | ||
VersionFallback = 0x110, | ||
} |
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'm disappointed there's no "EnhanceYourCalm" 😄
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.
Alas, it was renamed to H3_EXCESSIVE_LOAD. 😢
{ | ||
/// <summary> | ||
/// Variable length integer encoding and decoding methods. Based on https://tools.ietf.org/html/draft-ietf-quic-transport-24#section-16. | ||
/// Either will take up 1, 2, 4, or 8 bytes. |
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: "Either"?
public const uint OneByteLimit = 64; | ||
private const uint TwoByteLimit = 16383; | ||
private const uint FourByteLimit = 1073741823; | ||
private const long EightByteLimit = 4611686018427387903L; // 2^62-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.
Rather than writing out the number, could we instead write this as something like (long)((1UL << 62) - 1)
? Same goes for the others. The comment at that point isn't necessary, and it's clear how the value is derived rather than having a magic value.
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 the spec it's listed as the entire integer, so it was easier to copy like that. https://tools.ietf.org/html/draft-ietf-quic-transport-24#section-16
private const byte LengthOneByte = 0x00; | ||
private const byte LengthTwoByte = 0x40; | ||
private const byte LengthFourByte = 0x80; | ||
private const byte LengthEightByte = 0xC0; |
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: LengthTwoBytes, LengthFourBytes, etc.?
_ => 8 // LengthEightByte | ||
}; | ||
|
||
Span<byte> temp = (stackalloc byte[8])[..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.
Why do we need to slice it?
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.
(stackalloc byte[8])[..length]
has marginally better codegen than stackalloc byte[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.
I wasn't questioning that part, but rather why does it need to be perfectly sized at all. Why not just stackalloc byte[8]
?
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.
Oh. SequenceReader.TryCopyTo
reads in the full buffer or fails, so 8 would cause EOF scenario to fail.
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 don't understand. temp
here is the target of TryCopyTo, not the source. What am I missing?
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 about L121 with TryRead(temp, out ...)
What about it? That TryRead is taking a span, not a reader, and it's not using a reader internally.
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.
Ah, with the current implementation of TryRead
it will work with any large enough size. So you're correct, slicing to actual size isn't needed.
(I believe to remember there were a version of TryRead expecting the actual size to work -- but irrelevant right now.)
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.
Slice is required here because of reader.TryCopyTo
:
- Our stream has reach EOF and the reader has exactly 2 bytes left, containing a 2-byte integer.
reader.TryCopyTo
-- which succeeds only if it can fill the entire span -- fails because there are not 8 bytes available.- Upstream we see EOF but
VariableLengthIntegerHelper.TryRead
fails, so it throws something indicating an incomplete stream.
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.
which succeeds only if it can fill the entire buffer
Ugh. That is not an intuitive design. That's not how any other "CopyTo" I'm aware of behaves.
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 got @GrabYourPitchforks too. Somehow nobody caught it during API review.
} | ||
} | ||
|
||
public static long GetInteger(in ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined) |
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.
Does using in
for ReadOnlySequence<byte>
measurably help? On Linux, too?
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 would need to dig up the benchmark, but my general policy with in
is based on the size of the struct. ROS is a relatively large struct, so my policy has to pass it via in.
@benaadams do you happen to have concrete numbers on passing ROS via in
?
if (TryRead(ref reader, out long value)) | ||
{ | ||
consumed = examined = buffer.GetPosition(reader.Consumed); | ||
return (long)value; |
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.
value
is already declared as long
. Why are we casting to long
?
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.
Leftover from me trying to use proper unsigned types. Will remove.
|
||
if (longToEncode < OneByteLimit) | ||
{ | ||
if (!buffer.IsEmpty) |
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: in other places like https://github.com/dotnet/runtime/pull/1294/files#diff-fb1bdc75c84ead7a94a9c379a52f6230R46, we've used buffer.Length != 0
. It'd be nice to be consistent one way or the other. Personally I prefer the Length comparison in cases like this where we're writing out, simply because an "empty" check makes me think we're trying to read something from it, as we're checking to see if anything is there.
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.
@gfoidl IIRC you mentioned that checking buffer.Length != 0
is faster than buffer.IsEmpty
. Is that correct?
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 results in the same codegen -- so no perf-difference. My suggestion was about readability / expressiveness, and IMHO buffer.IsEmpty
does a good job therefore.
Edit: I'm also fine with @stephentoub rationale for the length-check for the "output side". We should be consistent accross the board. When the rule is "input -> IsEmpty, output -> length-check" then it's good and clear.
{ | ||
} | ||
|
||
// TODO |
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 an issue link for all of these TODOs?
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 wasn't sure which repo to file issues in (these are ported from aspnetcore). I think we can start filing issues in the runtime repo now :)
{ | ||
internal class H3StaticTable | ||
{ | ||
private static readonly H3StaticTable _instance = new H3StaticTable(); |
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.
s_instance
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 whole class needs to be made static. Going to leave a comment and do post-merge.
private HttpResponseMessage _response; | ||
|
||
// Header decoding. | ||
private QPackDecoder _headerDecoder; |
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.
We should probably sync about how the QPackDecoder references the DynamicTable. From what I can tell, the Dynamic table will be per connection, so it may be tricky to get that all correct.
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'm leaning towards removing DynamicTable
from H3 completely. It's complex and not MVP -- we can add later once everything else is working.
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.
If you can confirm we can remove it entirely on both the client and server, then let's do it.
} | ||
break; | ||
default: | ||
Debug.Fail($"Recieved unexpected frame type {frameType}."); |
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 don't think this should be a debug fail.
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.
ReadFrameEnvelopeAsync
does its own checks; the cases here are the only ones it should see.
} | ||
} | ||
|
||
private void BufferHeaders(HttpRequestMessage request) |
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: CommitHeaders? We use this terminology when we write the headers to a buffer and don't flush them.
} | ||
} | ||
|
||
private void BufferIndexedHeader(int index) |
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.
Same with these names: Buffer => Commit.
throw new Http3ConnectionException(Http3ErrorCode.ProtocolError); | ||
} | ||
|
||
int statusCode = staticIndex switch |
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 thought we had a table for this in H3StaticTable?
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 believe we had the reverse table, but I could be wrong.
Alright, I did a second round of review and this is making great progress. We should discuss what our timeline is for merging this. Ideally, I'd like to start reacting to this in Kestrel somewhat soon. |
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 PR has gotten too large to effectively review. At 7K lines it's literally lagging my browser. What can be done to reduce the scope so we can get the core reviewed and merged before further iterating? E.g. can we skip all of the QPack optimizations?
@@ -4,7 +4,6 @@ | |||
|
|||
#if KESTREL | |||
using System; | |||
|
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.
Revert
internal enum Http3ErrorCode : uint | ||
{ | ||
/// <summary> | ||
/// HTTP_NO_ERROR (0x100): |
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 draft 24 (https://tools.ietf.org/html/draft-ietf-quic-http-24#section-8.1) these were renamed to H3_NO_ERROR
public string Origin; | ||
public string AltSvc; | ||
|
||
public AltSvcFrame(string origin, string altSvc, int streamId) |
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.
Document the expected encoding state here, these must be respectively pre-encoded or it will affect the lengths.
else | ||
{ | ||
// RFC makes it unclear what to do if a duplicate parameter is found. For now, take the minimum. | ||
maxAge = Math.Min(maxAge.GetValueOrDefault(), maxAgeTmp); |
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.
maxAge = Math.Min(maxAge.GetValueOrDefault(), maxAgeTmp); | |
maxAge = Math.Min(maxAge.Value, maxAgeTmp); |
You already checked for null. Default would never work here anyways.
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 is an optimization. Value
is implemented as HasValue ? GetValueOrDefault() : throw
, and JIT isn't yet smart enough to remove the dead branch.
src/libraries/System.Net.Http/src/System/Net/Http/Headers/AltSvcHeaderParser.cs
Show resolved
Hide resolved
|
||
int lowerCase = ch | 0x20; | ||
|
||
if ((uint)(lowerCase - 'a') <= 'f' - 'a') // lowerCase >= 'a' && lowerCase <= 'f' |
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.
Upper case here only too.
private readonly Dictionary<long, Http3RequestStream> _activeRequests = new Dictionary<long, Http3RequestStream>(); | ||
|
||
// Set when GOAWAY is being processed, when aborting, or when disposing. | ||
private long _lastProcessedStreamId = -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.
Style: use long?
and HasValue rather than a sentinel value -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.
I'm fine with this as it's not a public API and saves some space. Our HTTP/2 code does the same.
Abort(ex); | ||
} | ||
|
||
retryRequest: |
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.
Does the goto mess up the lock statement? I'd rather not find out.
A local method or just inlining the code seems more appropriate here.
OnHeader(staticIndex: null, descriptor, staticValue: default, literalValue: value); | ||
} | ||
|
||
void IHttpHeadersHandler.OnStaticIndexedHeader(int index) |
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 is a useful optimization for known fields, but it's leaking H/Qpack implementation details into the request streams. I think there's a way to do this without leaking, and in a way that gives us more opportunities for optimizations.
- Define a KnownHeaderId enum for any header names we care about. This aligns closely with the KnownHeader(s) classes you already have. This doesn't need to be restricted to the ones defined in H/Qpack's static tables, we could also use it for dynamic compression optimizations later.
- Have H/Qpack decoders map static values to the KnownHeaderId enum. The decoders should also handle the known values mapping internally. This abstracts out the compression details early and allows for more unified code paths later in the flow.
- Replace IHttpHeadersHandler.OnStaticIndexedHeader(int index) and IHttpHeadersHandler.OnStaticIndexedHeader(int index, ReadOnlySpan value) with IHttpHeadersHandler.OnKnownHeader(KnownHeaderId knownHeader, ReadOnlySpan value). This implementation can share most code across H2 and H3 since it uses unified ids.
This means HeaderDescriptor doesn't have to know about H/QPack implementation details, only a KnownHeaderId enum value and a unified lookup table for KnownHeaders.
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.
Lets chat about this next week. I'm not as concerned about leaking some of QPack into H3 as they are tightly intertwined already, but I'd like to hear more about what other optimizations you feel we can get out of this abstraction.
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.
Initially I flagged this as a testing/maintenance issue. QPack can't be reviewed or tested in isolation because this chunk of it has been implemented directly inside the H3RequestStream.
Looking over this again I see a bigger problem. IHttpHeadersHandler is the boundary between the shared code (QPack) and the non-shared code (Http3RequestStream). All of the QPack logic you implement inside Http3RequestStream is code we have to duplicate in Kestrel. We should be able to share all of the QPack logic but to do so it needs to be factored appropriately.
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.
Follow-up on offline convo:
There's still concern that the abstraction as proposed will not provide real maintainability benefit and will be inefficient due to extra LUTs. We're going to give it a look post-merge to see if maintainability is improved.
There's a 2nd option we can keep in our back pocket of sharing type names between the two repos that would give the same level of abstraction without the added inefficiency.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
#if KESTREL | ||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 |
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.
IHttpHeadersHandler is an internal type in System.Net.Http.dll, it doesn't matter what the namespace is there.
IHttpHeadersHandler is a public (pubternal) type in Kestrel. Yes we can make API changes to pubternal types, that's not the question here. We should not use the System.Net.Http namespace for public types in Kestrel.
using System.Runtime.Serialization; | ||
|
||
#if KESTREL | ||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack |
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.
Reminder, please get rid of the Kestrel namespace for all internal types.
@@ -203,6 +203,7 @@ public static partial class HttpVersion | |||
public static readonly System.Version Version10; | |||
public static readonly System.Version Version11; | |||
public static readonly System.Version Version20; | |||
public static readonly System.Version Version30; |
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.
@scalablecory I noticed you're adding new APIs in the ref files. Please make sure to have all these new public API documented in the implementation (src files) with triple slash comments so that we can automatically port them in the future.
|
||
if (request.HasHeaders && request.Headers.Host != null) | ||
{ | ||
BufferLiteralHeaderWithStaticNameReference(H3StaticTable.Authority, request.Headers.Host); |
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.
Encoding?
@@ -17,6 +17,8 @@ namespace System.Net.Http | |||
#endif | |||
interface IHttpHeadersHandler | |||
{ | |||
void OnStaticIndexedHeader(int index); |
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.
Did @Tratcher chat with you about the issues with these added interface definitions?
/azp list |
/azp run runtime-libraries outerloop |
Azure Pipelines successfully started running 1 pipeline(s). |
CI failures appear to be an unrelated mix of flakey CI and #2131 |
This adds HTTP/3 (draft 24) support to SocketsHttpHandler.
Marked NO MERGE for now because it a) depends on some QUIC APIs that don't exist yet, and b) due to that hasn't been tested against a live server.
This adds the APIs in #1293.
There are some minor optimization opportunities (mostly in removing some async allocations) that will be investigated once things are in a state that we can benchmark.
Some of the initial H3 code from Kestrel is shared and thus included in this PR. @jkotalik this code has had some changes mostly to support
Span
, but also some optimizations.