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

Add HTTP/3 #1294

Merged
merged 1 commit into from
Jan 27, 2020
Merged

Add HTTP/3 #1294

merged 1 commit into from
Jan 27, 2020

Conversation

scalablecory
Copy link
Contributor

@scalablecory scalablecory commented Jan 4, 2020

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.

@scalablecory scalablecory added NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) area-System.Net.Http labels Jan 4, 2020
@scalablecory scalablecory requested review from a team January 4, 2020 13:26
@scalablecory scalablecory self-assigned this Jan 4, 2020
@@ -8,6 +8,11 @@ namespace System.Net.Http.HPack
{
internal static class IntegerEncoder
{
/// <summary>
Copy link
Contributor

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)?

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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

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.

Copy link
Contributor Author

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?

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

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

Choose a reason for hiding this comment

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

Suggested change
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.

Copy link
Member

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.

Copy link
Contributor Author

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.

}

QuicStream stream = await streamTask.ConfigureAwait(false);
_ = ProcessServerStreamAsync(stream);
Copy link
Contributor

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.

Copy link
Contributor Author

@scalablecory scalablecory Jan 5, 2020

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.

}
catch (HuffmanDecodingException ex)
{
throw new QPackDecodingException("TODO sync with corefx" /*CoreStrings.QPackHuffmanError, */, ex);
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
throw new QPackDecodingException("TODO sync with corefx" /*CoreStrings.QPackHuffmanError, */, ex);
throw new QPackDecodingException("TODO sync with runtime/libraries" /*CoreStrings.QPackHuffmanError, */, ex);

?

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor

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.

// 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;
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
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.

Copy link
Contributor Author

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.

: WaitThenSendAsync(waitTask, request, cancellationToken);
}

private async Task<HttpResponseMessage> WaitThenSendAsync(Task taskToWaitFor, HttpRequestMessage request, CancellationToken cancellationToken)
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Member

@stephentoub stephentoub Jan 17, 2020

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.

Copy link
Contributor Author

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

Choose a reason for hiding this comment

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

nit: scrap the goto 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

why?

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Member

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)

Copy link
Member

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

Copy link
Contributor Author

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

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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


while (!VariableLengthIntegerHelper.TryRead(buffer.AvailableSpan, out lastStreamId, out bytesRead))
{
buffer.EnsureAvailableSpace(1);
Copy link
Contributor

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.

Copy link
Member

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

Copy link
Contributor

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.

Copy link
Contributor Author

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

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?

Copy link
Contributor Author

@scalablecory scalablecory Jan 6, 2020

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.

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

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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

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.

Copy link
Contributor Author

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

Choose a reason for hiding this comment

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

Codeshare between ReadResponseContent/ReadResponseContentAsync

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Any ideas?

Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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?

Copy link
Contributor Author

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
Copy link
Contributor

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

@Tratcher Tratcher Jan 6, 2020

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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

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.

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

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.

Copy link
Contributor

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.

Copy link
Member

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.

Copy link
Contributor

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?

Copy link
Member

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 ;)

Copy link
Member

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.

{
internal static partial class Http3Frame
{
public static bool TryReadIntegerPair(ReadOnlySpan<byte> buffer, out long a, out long b, out int bytesRead)
Copy link
Member

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

Choose a reason for hiding this comment

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

Duplicated from VariableLengthIntegerHelper?

Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Member

@Tratcher Tratcher left a comment

Choose a reason for hiding this comment

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

Incremental review

/// The requested operation cannot be served over HTTP/3. The peer should retry over HTTP/1.1.
/// </summary>
VersionFallback = 0x110,
}
Copy link
Member

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" 😄

Copy link
Contributor Author

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

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

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.

Copy link
Contributor

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

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

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?

Copy link
Contributor Author

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].

Copy link
Member

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]?

Copy link
Contributor Author

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.

Copy link
Member

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?

Copy link
Member

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.

Copy link
Member

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.)

Copy link
Contributor Author

@scalablecory scalablecory Jan 19, 2020

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.

Copy link
Member

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.

Copy link
Contributor Author

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

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?

Copy link
Contributor

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

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?

Copy link
Contributor Author

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

@stephentoub stephentoub Jan 17, 2020

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.

Copy link
Contributor

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?

Copy link
Member

@gfoidl gfoidl Jan 17, 2020

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
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 an issue link for all of these TODOs?

Copy link
Contributor

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

Choose a reason for hiding this comment

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

s_instance

Copy link
Contributor Author

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

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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}.");
Copy link
Contributor

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.

Copy link
Contributor Author

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

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

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
Copy link
Contributor

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?

Copy link
Contributor Author

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.

@jkotalik
Copy link
Contributor

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.

Copy link
Member

@Tratcher Tratcher left a 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;

Copy link
Member

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

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

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);
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
maxAge = Math.Min(maxAge.GetValueOrDefault(), maxAgeTmp);
maxAge = Math.Min(maxAge.Value, maxAgeTmp);

You already checked for null. Default would never work here anyways.

Copy link
Contributor Author

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.


int lowerCase = ch | 0x20;

if ((uint)(lowerCase - 'a') <= 'f' - 'a') // lowerCase >= 'a' && lowerCase <= 'f'
Copy link
Member

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

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.

Copy link
Contributor Author

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

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

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.

  1. 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.
  2. 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.
  3. 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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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

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

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

@carlossanlop carlossanlop Jan 21, 2020

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

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

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?

@scalablecory
Copy link
Contributor Author

/azp list

@scalablecory
Copy link
Contributor Author

/azp run runtime-libraries outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@scalablecory
Copy link
Contributor Author

CI failures appear to be an unrelated mix of flakey CI and #2131

@scalablecory scalablecory merged commit 6f6c94d into dotnet:master Jan 27, 2020
@davidsh davidsh removed the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Feb 19, 2020
@davidsh davidsh added this to the 5.0 milestone Feb 19, 2020
@scalablecory scalablecory deleted the http3 branch July 29, 2020 16:05
@ghost ghost locked as resolved and limited conversation to collaborators Dec 11, 2020
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.