-
Notifications
You must be signed in to change notification settings - Fork 5.3k
quic: supporting connections with zero initial available streams #18775
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -101,9 +101,7 @@ void EnvoyQuicClientSession::OnCanCreateNewOutgoingStream(bool unidirectional) { | |
| return; | ||
| } | ||
| uint32_t streams_available = streamsAvailable(); | ||
| if (streams_available > 0) { | ||
| http_connection_callbacks_->onMaxStreamsChanged(streams_available); | ||
| } | ||
| http_connection_callbacks_->onMaxStreamsChanged(streams_available); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I should have thought about this in the previous PR, but I think we might want to tweak the name/documentation for this method. (I'd be happy to do this in a followup if that works for you). The argument to this method is the number of currently available streams. But the "max streams" value in HTTP/3 is the largest stream id that can be opened (well, divided by four, but whatever :>). In other words, the terminology here is slightly misaligned. I think I would be inclined to renamed this onStreamsAvailableChanged() or some such. WDYT?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with the suggested name change. Also, be aware that there are two existing limits on pools: max concurrent requests, and max requests over the lifetime of the connection. For any of these cases, probably best to use a verbose name so it's clear which limit is being referenced.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wary of this name. think if we change it to onStreamsAvailableChanged it weakens the clarity where the current APIs are named after the protocol frames. We have Edit: would be happy to make the protocol more clear. onHttp3MaxStreamsChanged? |
||
| } | ||
|
|
||
| std::unique_ptr<quic::QuicSpdyClientStream> EnvoyQuicClientSession::CreateClientStream() { | ||
|
|
@@ -144,12 +142,11 @@ uint64_t EnvoyQuicClientSession::streamsAvailable() { | |
| void EnvoyQuicClientSession::OnTlsHandshakeComplete() { | ||
| quic::QuicSpdyClientSession::OnTlsHandshakeComplete(); | ||
|
|
||
| // TODO(alyssawilk) support the case where a connection starts with 0 max streams. | ||
| ASSERT(streamsAvailable()); | ||
| if (streamsAvailable() > 0) { | ||
| OnCanCreateNewOutgoingStream(false); | ||
| raiseConnectionEvent(Network::ConnectionEvent::Connected); | ||
| } | ||
| // Fake this to make sure we set the connection pool stream limit correctly | ||
| // before use. This may result in OnCanCreateNewOutgoingStream with zero | ||
| // available streams. | ||
| OnCanCreateNewOutgoingStream(false); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I'm reading EnvoyQuicClientSession::OnCanCreateNewOutgoingStream() correctly, if streamsAvailable() is 0 it will be a no-op. Are we sure we need to call it here when there are no streams available?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is it a no-op?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Man, I hate the github UI. I missed that this method is changed in this PR specifically to make it not a no-op. Lordy. Major reviewer fail on my part! |
||
| raiseConnectionEvent(Network::ConnectionEvent::Connected); | ||
| } | ||
|
|
||
| std::unique_ptr<quic::QuicCryptoClientStreamBase> EnvoyQuicClientSession::CreateQuicCryptoStream() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,8 +35,15 @@ class TestActiveClient : public ActiveClient { | |
| ASSERT_TRUE(testClient != nullptr); | ||
| testClient->active_streams_++; | ||
| } | ||
| int64_t currentUnusedCapacity() const override { | ||
| if (capacity_override_.has_value()) { | ||
| return capacity_override_.value(); | ||
| } | ||
| return ActiveClient::currentUnusedCapacity(); | ||
| } | ||
|
|
||
| uint32_t active_streams_{}; | ||
| absl::optional<uint64_t> capacity_override_; | ||
| }; | ||
|
|
||
| class TestPendingStream : public PendingStream { | ||
|
|
@@ -417,6 +424,26 @@ TEST_F(ConnPoolImplDispatcherBaseTest, MaxConnectionDurationCallbackWhileConnect | |
| pool_.destructAllConnections(); | ||
| } | ||
|
|
||
| // Test the behavior of a client created with 0 zero streams available. | ||
| TEST_F(ConnPoolImplDispatcherBaseTest, NoAvailableStreams) { | ||
| // Start with a concurrent stream limit of 0. | ||
| stream_limit_ = 1; | ||
| newConnectingClient(); | ||
| clients_.back()->capacity_override_ = 0; | ||
| pool_.decrClusterStreamCapacity(stream_limit_); | ||
|
|
||
| // Make sure that when the connected event is raised, there is no call to | ||
| // onPoolReady, and the client is marked as busy. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If a connection is setup with an initial capacity of zero, do we assume it will become non-zero soon? If so, is there any timer/timeout currently watching for this to take too long? I assume the connect_timeout is done already, because we're connected. If not, should the pool create another connection? If we don't, won't the request be stuck here with no way to make forward progress?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wary of creating new connections in this case - I'd assuming new connections would also start with capacity 0.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When in this state, is the downstream-request-timeout is still in place, correct? So if nothing happens, eventually we'll give up (assuming there is a configured timeout)? Another thought on this: should we not count the connection as established, from the pool's perspective, until it has non-zero capacity? Maybe the existing connect_timeout should cover this condition, because it's logically not fully established yet.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think if we change this to time out, we'd want to also change the way we time out HTTP/1 and HTTP/2 streams when we hit the connection + stream limits, and all would be outside the scope of this pr.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think I'm missing a lot of context here, but AIUI for H3 we can have a connected connection which cannot accept any streams? Is that correct? To me this seems fundamentally different from H1 and H2 in which a new connection should be able to accept at least 1 stream? Without thinking about it too much I tend to agree with @ggreenway that a connection that doesn't ever have at least 1 available stream isn't really connected and should timeout? I think this is different from H1 or H2? Either way I agree this can be done as a separate PR whatever we decide.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we handle the above corner case correctly for H2? If it's possible there I agree it's the same. Either way I suggest we file an issue to track this and then follow up later?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. looking, I think we'll back hole the traffic, like we would for HTTP/3 before this series of fixes
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it is different for h2, because in h2 it is allowed to start sending frames (new stream, etc) before receiving a SETTINGS frame from the peer, which is what could set the max-streams to zero. Also, if we get a SETTINGS frame with a stream limit of zero, we move the connection to draining and never use it again.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, this function doesn't make sense to me. It seems like we should use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you're correct, that's why I filed a tracking bug for it :-) |
||
| EXPECT_CALL(pool_, onPoolReady).Times(0); | ||
| clients_.back()->onEvent(Network::ConnectionEvent::Connected); | ||
| EXPECT_EQ(ActiveClient::State::BUSY, clients_.back()->state()); | ||
|
|
||
| // Clean up. | ||
| EXPECT_CALL(pool_, instantiateActiveClient); | ||
| EXPECT_CALL(pool_, onPoolFailure); | ||
| pool_.destructAllConnections(); | ||
| } | ||
|
|
||
| // Remote close simulates the peer closing the connection. | ||
| TEST_F(ConnPoolImplBaseTest, PoolIdleCallbackTriggeredRemoteClose) { | ||
| EXPECT_CALL(dispatcher_, createTimer_(_)).Times(AnyNumber()); | ||
|
|
||
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.
With HTTP/3, MAX_STREAMS can only increase. I'm not sure this is possible. Is this code reached in an integration tests?
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 wait, looking at how this is called, is the num_streams argument the new MAX_STREAMS value (which must always increase) or is it the current stream capacity? I think maybe the latter. Mind adding a comment this this method? (You'd think I could remember from the previous PR, but apparently not)
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 for today, but didn't you say this can happen for a rejected 0-rtt handshake?
If we start out assuming some number of streams and create some number, then the 0-rtt handshake is rejected and we fail over to real handshake with fewer streams, I assume we can lower the functional capacity?
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, hah! Yes, I did say that... and subsequently forgot it. You're totally right. Would it be worth adding a comment that says something like:
// With HTTP/3 this can only happen during a rejected 0-RTT handshake.