-
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
[retry only] Additional HTTP/2 connections created when active streams limit is reached #39439
[retry only] Additional HTTP/2 connections created when active streams limit is reached #39439
Conversation
- Exception string put in .resx
Tagging subscribers to this area: @dotnet/ncl |
Note regarding the This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change. |
…after-stream-count-limit
- Debug logging for the other
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
Outdated
Show resolved
Hide resolved
|
||
private Http2Connection PickRandomConnection(HttpRequestMessage request, Http2Connection[] currentHttp2Connections) | ||
{ | ||
// All connections are busy so pick a random one to wait on 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.
Why did we decide this is ok? This is one of the main concerns I raised previously.
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.
Due to a high complexity of the other approach. @geoffkizer raised the concern that other solution is so complex that it will hinder other HTTP/2 optimization efforts. We still can go back to a more advanced version later, I won't delete that another branch.
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: @karelz
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 we explicitly decided it was ok. I agree, it's an issue.
The problem is, given this approach, it's hard to implement this the correct way. We don't have a notification from the Http2Connection that a new stream has become available. We could add this, of course, and we could add some queuing logic (similar to HTTP/1.1) to queue requests when all connections are busy, etc, etc.
That said, I'm not sure it's worth the trouble. The key scenario here is to support multiple HTTP2 connections. My suggestion is that we remove the connection limit here. Meaning, when you enable multiple HTTP2 connections, the number of connections is unbounded. This is what WinHTTP does as well.
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.
That notification and queueing are implemented in my other PR #38748, but it added noticeable complexity as you can see. It still might be there is a way to achieve the same with some simpler mechanism, but I currently don't see how.
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.
- Connection notifies about an available stream via StreamSlotAvailable
- Requests are queued while waiting for a free stream thanks to built-in support in SemaphoreSlim
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/CreditManager.cs
Show resolved
Hide resolved
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs
Outdated
Show resolved
Hide resolved
Http2Connection[]? localHttp2Connections = _http2Connections; | ||
int newCollectionSize = localHttp2Connections == null ? 1 : localHttp2Connections.Length + 1; | ||
Http2Connection[] newHttp2Connections = new Http2Connection[newCollectionSize]; | ||
newHttp2Connections[0] = newConnection; |
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 add the connection at the beginning of the list? I thought we were going to add it to the end.
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.
To have the search from the newest to the oldest in GetHttp2ConnectionAcceptingNewStreams
. If we decide to change the order, we will change it 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.
I guess what I'm asking is, why did we decide to search from newest to oldest instead of oldest to newest?
I don't have a strong opinion here, but I'd like to understand the rationale.
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 assumption is that older connections tend to have longer running requests than new ones because the lack of available streams on the old connections is the only driver for creating new ones. Thus, it looks more efficiently to firstly check the newest for available streams.
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.
Ok.
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs
Outdated
Show resolved
Hide resolved
Maximum connections limit removed. |
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs
Outdated
Show resolved
Hide resolved
@@ -648,6 +647,53 @@ public byte[] Http2AltSvcOriginUri | |||
return await GetHttpConnectionAsync(request, async, cancellationToken).ConfigureAwait(false); | |||
} | |||
|
|||
private Http2Connection? GetExistingHttp2Connection() | |||
{ | |||
Http2Connection[]? localConnections = _http2Connections; |
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 probably worth a comment here to explain that the array itself is never mutated, though the array reference may change.
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 explained above _http2Connections
declaration.
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs
Show resolved
Hide resolved
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/SocketsHttpHandler.cs
Outdated
Show resolved
Hide resolved
- Expiration test fixed
…g expired HTTP/2 connections - Expiration test fixed
@@ -1461,7 +1473,7 @@ private void Abort(Exception abortException) | |||
|
|||
// Check idle timeout when there are not pending requests for a while. | |||
if ((connectionIdleTimeout != Timeout.InfiniteTimeSpan) && | |||
(_httpStreams.Count == 0) && | |||
(_httpStreams.Count == 0) && (_idleSinceTickCount > 0) && |
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 is this change necessary?
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.
On a new Http2Connection _idleSinceTickCount
is set to 0 until the first request is sent. On the other hand, setting PooledConnectionIdleTimeout
to a specific value starts a timer calling this CleanCacheAndDisposeIfUnused
method every second or less, so this check gets done before the first request goes through connection while _idleSinceTickCount
is still 0 which is always less thanEnvironment.TickCount64
therefore a brand new connection can get almost immediately expired.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this issue existed previously? I'm glad you caught it!
That said, maybe it would be better to initialize _idleSinceTickCount
to the current tick in the Http2Connection constructor? I'm not sure where we check this value, but it seems better to have it be correct always than to special case 0.
Adding @wfurt, I think he wrote this code... thoughts?
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.
Yes, it is an existing issue. Currently _idleSinceTickCount
is read only in this method and assigned in RemoveStream
. We could initialize it in the constructor, but it seems the intention was to update it when a request completes and stream gets closed, thus setting a value in the ctor will break that invariant.
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 intent is to track the last time the connection might have been idle (if there were no other active streams). As such initializing it to the current tick count in the constructor seems to be more inline with the intent, to me.
I don't know if there's ever a case where we can get into a situation where an Http2Connection is created but never actually used -- maybe if cancellation happens at exactly the right time? If that ever happens, we will still want to idle the connection out eventually.
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.
Agree, makes sense. I will initialize it in the constructor.
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.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.
Overall looks good to me. One small issue and one question above.
…s limit is reached (dotnet#39439) HTTP/2 standard commands clients to not open more than one HTTP/2 connection to the same server. At the same time, server has right to limit the maximum number of active streams per that HTTP/2 connection. These two directives combined impose limit on the number of requests concurrently send to the server. This limitation is justified in client to server scenarios, but become a bottleneck in server to server cases like gRPC. This PR introduces a new SocketsHttpHandler API enabling establishing additional HTTP/2 connections to the same server when the maximum stream limit is reached on the existing ones. **Note**. This algorithm version uses only retries to make request choose another connection when all stream slots are occupied. It does not implement stream credit management in `HttpConnectionPool` and therefore exhibit a sub-optimal request scheduling behavior in "request burst" and "infinite requests" scenarios. Fixes dotnet#35088
HTTP/2 standard commands clients to not open more than one HTTP/2 connection to the same server. At the same time, server has right to limit the maximum number of active streams per that HTTP/2 connection. These two directive combined impose limit on the number of requests concurrently send to the server. This limitation is justified in client to server scenarios, but become a bottleneck in server to server cases like gRPC. This PR introduces a new SocketsHttpHandler API enabling establishing additional HTTP/2 connections to the same server when the maximum stream limit is reached on the existing ones.
Note. This algorithm version uses only retries to make request choose another connection when all stream slots are occupied. It does not implement stream credit management in
HttpConnectionPool
and therefore exhibit a sub-optimal request scheduling behavior in "request burst" and "infinite requests" scenarios.Fixes #35088