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

[HTTP/3] Support for HTTP/3 multiple connections #101531

Merged
merged 28 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9602872
QuicConnection StreamsAvailable properties and event handler
ManickaP Apr 11, 2024
366c740
EnableHttp3MultipleConnections property
ManickaP Apr 11, 2024
c1959d4
H3 connection pool
ManickaP Apr 12, 2024
79d5cdb
Fixed unused methods.
ManickaP Apr 25, 2024
6537793
Update src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpH…
ManickaP Apr 25, 2024
81bdce1
Fix release reserved stream count once it's opened.
ManickaP Apr 25, 2024
7c6744f
Available streams count is thread safe
ManickaP Apr 26, 2024
c597fa5
Move StreamsAvailable callback into connection options, remove count …
ManickaP May 15, 2024
977e0bc
HTTP adjustments to StreamsAvailable callback in options
ManickaP May 16, 2024
a8e8e76
Comments about multiple connections, including mention of being again…
ManickaP May 16, 2024
3ef524b
Miha's Feedback: H/3 authority evaluation, user callback try-catch
ManickaP May 20, 2024
119b483
Fix releasing stream
ManickaP May 23, 2024
e967690
Added logging for stream counts
ManickaP May 23, 2024
19cac0b
Fix stream counting in case of open stream failure
ManickaP May 28, 2024
366730a
Feedback
ManickaP May 28, 2024
a316b89
Renamed API according to review feedback
ManickaP Jun 17, 2024
2c3a47d
Renamed API according to review feedback
ManickaP Jun 17, 2024
0dff95b
More renames; Stored Action callback feedback
ManickaP Jun 17, 2024
ff47d4b
Fixed borked platform guard
ManickaP Jun 17, 2024
e830ecb
Use the cached delegate :facepalm:
ManickaP Jun 17, 2024
2721f0f
Easy feedback
ManickaP Jun 18, 2024
114e89a
Generated ref source
ManickaP Jun 18, 2024
01e75dc
SupportedOS ordering, because I cannot stand it different
ManickaP Jun 19, 2024
fd285fe
Idle connection back where it was
ManickaP Jun 19, 2024
7f6e3c4
Assert
ManickaP Jun 19, 2024
2710d18
Fix handling negative stream capacity and 0 reporting STREAMS_AVAILABLE
ManickaP Jun 19, 2024
346a278
Handling cummulative increments, canceled streams and more tests.
ManickaP Jun 25, 2024
ede8407
Removed logging
ManickaP Jun 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/libraries/System.Net.Http/ref/System.Net.Http.cs
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ public SocketsHttpHandler() { }
public System.Net.ICredentials? Credentials { get { throw null; } set { } }
public System.Net.ICredentials? DefaultProxyCredentials { get { throw null; } set { } }
public bool EnableMultipleHttp2Connections { get { throw null; } set { } }
public bool EnableMultipleHttp3Connections { get { throw null; } set { } }
public System.TimeSpan Expect100ContinueTimeout { get { throw null; } set { } }
public int InitialHttp2StreamWindowSize { get { throw null; } set { } }
[System.Runtime.Versioning.UnsupportedOSPlatformGuardAttribute("browser")]
Expand Down
3 changes: 3 additions & 0 deletions src/libraries/System.Net.Http/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,9 @@
<data name="net_http_invalid_header_name" xml:space="preserve">
<value>Received an invalid header name: '{0}'.</value>
</data>
<data name="net_http_http3_connection_not_established" xml:space="preserve">
<value>An HTTP/3 connection could not be established because the server did not complete the HTTP/3 handshake.</value>
</data>
<data name="net_http_http3_connection_error" xml:space="preserve">
<value>The HTTP/3 server sent invalid data on the connection. HTTP/3 error code '{0}' (0x{1}).</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,12 @@ public bool EnableMultipleHttp2Connections
set => throw new PlatformNotSupportedException();
}

public bool EnableMultipleHttp3Connections
{
get => throw new PlatformNotSupportedException();
set => throw new PlatformNotSupportedException();
}

public Func<SocketsHttpConnectionContext, CancellationToken, ValueTask<Stream>>? ConnectCallback
{
get => throw new PlatformNotSupportedException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,10 @@ public static async ValueTask<SslStream> EstablishSslConnectionAsync(SslClientAu
return sslStream;
}

[SupportedOSPlatform("windows")]
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
public static async ValueTask<QuicConnection> ConnectQuicAsync(HttpRequestMessage request, DnsEndPoint endPoint, TimeSpan idleTimeout, SslClientAuthenticationOptions clientAuthenticationOptions, CancellationToken cancellationToken)
[SupportedOSPlatform("windows")]
public static async ValueTask<QuicConnection> ConnectQuicAsync(HttpRequestMessage request, DnsEndPoint endPoint, TimeSpan idleTimeout, SslClientAuthenticationOptions clientAuthenticationOptions, Action<QuicConnection, QuicStreamCapacityChangedArgs> streamCapacityCallback, CancellationToken cancellationToken)
CarnaViire marked this conversation as resolved.
Show resolved Hide resolved
{
clientAuthenticationOptions = SetUpRemoteCertificateValidationCallback(clientAuthenticationOptions, request);

Expand All @@ -126,7 +126,8 @@ public static async ValueTask<QuicConnection> ConnectQuicAsync(HttpRequestMessag
DefaultStreamErrorCode = (long)Http3ErrorCode.RequestCancelled,
DefaultCloseErrorCode = (long)Http3ErrorCode.NoError,
RemoteEndPoint = endPoint,
ClientAuthenticationOptions = clientAuthenticationOptions
ClientAuthenticationOptions = clientAuthenticationOptions,
StreamCapacityCallback = streamCapacityCallback,
}, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex) when (ex is not OperationCanceledException)
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK
{
_http2RequestQueue = new RequestQueue<Http2Connection?>();
}
if (IsHttp3Supported() && _http3Enabled)
{
_http3RequestQueue = new RequestQueue<Http3Connection?>();
}

if (_proxyUri != null && HttpUtilities.IsSupportedSecureScheme(_proxyUri.Scheme))
{
Expand Down Expand Up @@ -881,11 +885,12 @@ public void Dispose()
_availableHttp2Connections.Clear();
}

if (_http3Connection is not null)
if (IsHttp3Supported() && _availableHttp3Connections is not null)
{
toDispose ??= new();
toDispose.Add(_http3Connection);
_http3Connection = null;
toDispose.AddRange(_availableHttp3Connections);
_associatedHttp3ConnectionCount -= _availableHttp3Connections.Count;
_availableHttp3Connections.Clear();
}

if (_authorityExpireTimer != null)
Expand Down Expand Up @@ -956,6 +961,14 @@ public bool CleanCacheAndDisposeIfUnused()
// Note: Http11 connections will decrement the _associatedHttp11ConnectionCount when disposed.
// Http2 connections will not, hence the difference in handing _associatedHttp2ConnectionCount.
}
if (IsHttp3Supported() && _availableHttp3Connections is not null)
{
int removed = ScavengeHttp3ConnectionList(_availableHttp3Connections, ref toDispose, nowTicks, pooledConnectionLifetime, pooledConnectionIdleTimeout);
_associatedHttp3ConnectionCount -= removed;

// Note: Http11 connections will decrement the _associatedHttp11ConnectionCount when disposed.
// Http3 connections will not, hence the difference in handing _associatedHttp3ConnectionCount.
}
}

// Dispose the stale connections outside the pool lock, to avoid holding the lock too long.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ internal sealed partial class Http2Connection : HttpConnectionBase

private TaskCompletionSourceWithCancellation<bool>? _initialSettingsReceived;

private readonly HttpConnectionPool _pool;
private readonly Stream _stream;

// NOTE: These are mutable structs; do not make these readonly.
Expand Down Expand Up @@ -132,7 +131,6 @@ internal enum KeepAliveState
public Http2Connection(HttpConnectionPool pool, Stream stream, IPEndPoint? remoteEndPoint)
: base(pool, remoteEndPoint)
{
_pool = pool;
_stream = stream;

_incomingBuffer = new ArrayBuffer(initialSize: 0, usePool: true);
Expand Down Expand Up @@ -1794,18 +1792,6 @@ private bool ForceSendConnectionWindowUpdate()
return true;
}

public override long GetIdleTicks(long nowTicks)
{
// The pool is holding the lock as part of its scavenging logic.
// We must not lock on Http2Connection.SyncObj here as that could lead to lock ordering problems.
Debug.Assert(_pool.HasSyncObjLock);

// There is a race condition here where the connection pool may see this connection as idle right before
// we start processing a new request and start its disposal. This is okay as we will either
// return false from TryReserveStream, or process pending requests before tearing down the transport.
return _streamsInUse == 0 ? base.GetIdleTicks(nowTicks) : 0;
}

/// <summary>Abort all streams and cause further processing to fail.</summary>
/// <param name="abortException">Exception causing Abort to be called.</param>
private void Abort(Exception abortException)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@

namespace System.Net.Http
{
[SupportedOSPlatform("windows")]
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
[SupportedOSPlatform("windows")]
internal sealed class Http3Connection : HttpConnectionBase
{
private readonly HttpConnectionPool _pool;
private readonly HttpAuthority _authority;
private readonly byte[]? _altUsedEncodedHeader;
private QuicConnection? _connection;
Expand All @@ -33,7 +32,7 @@ internal sealed class Http3Connection : HttpConnectionBase

// Our control stream.
private QuicStream? _clientControl;
private Task _sendSettingsTask;
private Task? _sendSettingsTask;

// Server-advertised SETTINGS_MAX_FIELD_SECTION_SIZE
// https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4.1-2.2.1
Expand All @@ -54,6 +53,9 @@ internal sealed class Http3Connection : HttpConnectionBase
public Exception? AbortException => Volatile.Read(ref _abortException);
private object SyncObj => _activeRequests;

private int _availableRequestStreamsCount;
private TaskCompletionSource<bool>? _availableStreamsWaiter;

/// <summary>
/// If true, we've received GOAWAY, are aborting due to a connection-level error, or are disposing due to pool limits.
/// </summary>
Expand All @@ -66,12 +68,10 @@ private bool ShuttingDown
}
}

public Http3Connection(HttpConnectionPool pool, HttpAuthority authority, QuicConnection connection, bool includeAltUsedHeader)
: base(pool, connection.RemoteEndPoint)
public Http3Connection(HttpConnectionPool pool, HttpAuthority authority, bool includeAltUsedHeader)
: base(pool)
{
_pool = pool;
_authority = authority;
_connection = connection;

if (includeAltUsedHeader)
{
Expand All @@ -87,6 +87,13 @@ public Http3Connection(HttpConnectionPool pool, HttpAuthority authority, QuicCon
// Use this as an initial value before we receive the SETTINGS frame.
_maxHeaderListSize = maxHeaderListSize;
}
}

public void InitQuicConnection(QuicConnection connection)
{
MarkConnectionAsEstablished(connection.RemoteEndPoint);

_connection = connection;

// Errors are observed via Abort().
_sendSettingsTask = SendSettingsAsync();
Expand Down Expand Up @@ -128,6 +135,9 @@ private void CheckForShutdown()
{
// Close the QuicConnection in the background.

_availableStreamsWaiter?.SetResult(false);
_availableStreamsWaiter = null;

_connectionClosedTask ??= _connection.CloseAsync((long)Http3ErrorCode.NoError).AsTask();

QuicConnection connection = _connection;
Expand All @@ -151,7 +161,7 @@ private void CheckForShutdown()

if (_clientControl != null)
{
await _sendSettingsTask.ConfigureAwait(false);
await _sendSettingsTask!.ConfigureAwait(false);
await _clientControl.DisposeAsync().ConfigureAwait(false);
_clientControl = null;
}
Expand All @@ -162,6 +172,75 @@ private void CheckForShutdown()
}
}

public bool TryReserveStream()
{
lock (SyncObj)
{
Debug.Assert(_availableRequestStreamsCount >= 0);

if (NetEventSource.Log.IsEnabled()) Trace($"_availableRequestStreamsCount = {_availableRequestStreamsCount}");

if (_availableRequestStreamsCount == 0)
{
return false;
}

--_availableRequestStreamsCount;
return true;
}
}

public void ReleaseStream()
{
lock (SyncObj)
{
Debug.Assert(_availableRequestStreamsCount >= 0);

if (NetEventSource.Log.IsEnabled()) Trace($"_availableRequestStreamsCount = {_availableRequestStreamsCount}");
++_availableRequestStreamsCount;
ManickaP marked this conversation as resolved.
Show resolved Hide resolved

_availableStreamsWaiter?.SetResult(!ShuttingDown);
_availableStreamsWaiter = null;
}
}

public void StreamCapacityCallback(QuicConnection connection, QuicStreamCapacityChangedArgs args)
ManickaP marked this conversation as resolved.
Show resolved Hide resolved
{
Debug.Assert(_connection is null || connection == _connection);

lock (SyncObj)
{
Debug.Assert(_availableRequestStreamsCount >= 0);

if (NetEventSource.Log.IsEnabled()) Trace($"_availableRequestStreamsCount = {_availableRequestStreamsCount} + bidirectionalStreamsCountIncrement = {args.BidirectionalIncrement}");

_availableRequestStreamsCount += args.BidirectionalIncrement;
_availableStreamsWaiter?.SetResult(!ShuttingDown);
_availableStreamsWaiter = null;
}
}

public Task<bool> WaitForAvailableStreamsAsync()
{
lock (SyncObj)
{
Debug.Assert(_availableRequestStreamsCount >= 0);

if (ShuttingDown)
{
return Task.FromResult(false);
}
if (_availableRequestStreamsCount > 0)
{
return Task.FromResult(true);
}

Debug.Assert(_availableStreamsWaiter is null);
_availableStreamsWaiter = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
ManickaP marked this conversation as resolved.
Show resolved Hide resolved
return _availableStreamsWaiter.Task;
}
}

public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, long queueStartingTimestamp, CancellationToken cancellationToken)
{
// Allocate an active request
Expand All @@ -184,7 +263,6 @@ public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, lon
{
MarkConnectionAsNotIdle();
}

_activeRequests.Add(quicStream, requestStream);
}
}
Expand Down Expand Up @@ -363,10 +441,8 @@ public void RemoveStream(QuicStream stream)
}
}

public override long GetIdleTicks(long nowTicks) => throw new NotImplementedException("We aren't scavenging HTTP3 connections yet");

public override void Trace(string message, [CallerMemberName] string? memberName = null) =>
Trace(0, message, memberName);
Trace(0, _connection is not null ? $"{_connection} {message}" : message, memberName);

internal void Trace(long streamId, string message, [CallerMemberName] string? memberName = null) =>
NetEventSource.Log.HandlerMessage(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@

namespace System.Net.Http
{
[SupportedOSPlatform("windows")]
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
[SupportedOSPlatform("windows")]
internal sealed class Http3RequestStream : IHttpStreamHeadersHandler, IAsyncDisposable, IDisposable
{
private readonly HttpRequestMessage _request;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ internal sealed partial class HttpConnection : HttpConnectionBase
private static readonly ulong s_http10Bytes = BitConverter.ToUInt64("HTTP/1.0"u8);
private static readonly ulong s_http11Bytes = BitConverter.ToUInt64("HTTP/1.1"u8);

private readonly HttpConnectionPool _pool;
internal readonly Stream _stream;
private readonly TransportContext? _transportContext;

Expand Down Expand Up @@ -77,10 +76,8 @@ public HttpConnection(
IPEndPoint? remoteEndPoint)
: base(pool, remoteEndPoint)
{
Debug.Assert(pool != null);
Debug.Assert(stream != null);

_pool = pool;
_stream = stream;

_transportContext = transportContext;
Expand Down
Loading
Loading