-
Notifications
You must be signed in to change notification settings - Fork 5.5k
quic: fixing the disconnect between quiche stream count and Envoy #18694
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 4 commits
d4a4f82
7390eef
b48d119
c38fc9a
98b584b
c6d5bc4
ed2b450
4fca90c
8d2e36f
2c508a9
2986fa1
b03f111
edd45cf
764a701
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 |
|---|---|---|
|
|
@@ -28,6 +28,19 @@ ActiveClient::ActiveClient(Envoy::Http::HttpConnPoolImplBase& parent, | |
| Upstream::Host::CreateConnectionData& data) | ||
| : MultiplexedActiveClientBase(parent, getMaxStreams(parent.host()->cluster()), | ||
| parent.host()->cluster().stats().upstream_cx_http3_total_, data) { | ||
| auto& connection = dynamic_cast<ActiveClient*>(this)->codec_client_->connection(); | ||
|
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. Is the dyanmic_cast actually needed here? It looks like it's casting from ActiveClient to ActiveClient?
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. Yeah but due to Envoy "name everything the same thing" this is actually casting from the base class Active Client to the HTTP/3 active client :-P |
||
| Quic::EnvoyQuicClientSession* session = const_cast<Quic::EnvoyQuicClientSession*>( | ||
| dynamic_cast<const Quic::EnvoyQuicClientSession*>(connection.get())); | ||
|
RyanTheOptimist marked this conversation as resolved.
Outdated
|
||
| ASSERT(session != nullptr); | ||
| notifier_ = std::make_unique<Quic::ScopedStreamNotifier>( | ||
| [this](int32_t s) -> void { onMaxStreamsChanged(s); }, *session); | ||
| } | ||
|
|
||
| void ActiveClient::onMaxStreamsChanged(uint32_t num_streams) { | ||
| updateCapacity(num_streams); | ||
| if (state() == ActiveClient::State::BUSY && currentUnusedCapacity() != 0) { | ||
| parent_.transitionActiveClientState(*this, ActiveClient::State::READY); | ||
| } | ||
| } | ||
|
|
||
| void Http3ConnPoolImpl::setQuicConfigFromClusterConfig(const Upstream::ClusterInfo& cluster, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,57 @@ class ActiveClient : public MultiplexedActiveClientBase { | |
| public: | ||
| ActiveClient(Envoy::Http::HttpConnPoolImplBase& parent, | ||
| Upstream::Host::CreateConnectionData& data); | ||
|
|
||
| // Update quiche_capacity_ when a MAX_STREAMS frame arrives. | ||
| void onMaxStreamsChanged(uint32_t num_streams); | ||
|
|
||
| RequestEncoder& newStreamEncoder(ResponseDecoder& response_decoder) override { | ||
| ASSERT(quiche_capacity_ != 0); | ||
| // Each time a quic stream is allocated the quic capacity needs to get | ||
| // decremented. See comments by quiche_capacity_. | ||
| updateCapacity(quiche_capacity_ - 1); | ||
| return MultiplexedActiveClientBase::newStreamEncoder(response_decoder); | ||
| } | ||
|
|
||
| // Overload the default capacity calculations to return the quic capacity | ||
| // (modified by any stream limits in Envoy config) | ||
| int64_t currentUnusedCapacity() const override { | ||
| return std::min<int64_t>(quiche_capacity_, effectiveConcurrentStreamLimit()); | ||
| } | ||
|
|
||
| void updateCapacity(uint64_t new_quiche_capacity) { | ||
| // Each time we update the capacity make sure to reflect the update in the | ||
| // connection pool. | ||
| // | ||
| // Due to interplay between the max number of concurrent streams Envoy will | ||
| // allow and the max number of streams per connection this is not as simple | ||
| // as just updating based on the delta between quiche_capacity_ and | ||
| // new_quiche_capacity, so we use the delta between the actual calculated | ||
| // capacity before and after the update. | ||
|
RyanTheOptimist marked this conversation as resolved.
|
||
| uint64_t old_capacity = currentUnusedCapacity(); | ||
| quiche_capacity_ = new_quiche_capacity; | ||
| uint64_t new_capacity = currentUnusedCapacity(); | ||
|
|
||
| if (new_capacity < old_capacity) { | ||
| parent_.decrClusterStreamCapacity(old_capacity - new_capacity); | ||
| } else if (old_capacity < new_capacity) { | ||
| parent_.incrClusterStreamCapacity(new_capacity - old_capacity); | ||
| } | ||
| } | ||
|
|
||
| std::unique_ptr<Quic::ScopedStreamNotifier> notifier_; | ||
| // Unlike HTTP/2 and HTTP/1, rather than having a cap on the number of active | ||
| // streams, QUIC has a fixed number of streams available which is updated via | ||
| // the MAX_STREAMS frame. | ||
|
RyanTheOptimist marked this conversation as resolved.
|
||
| // | ||
| // As such each time we create a new stream for QUIC, the capacity goes down | ||
| // by one, but unlike the other two codecs it is _not_ restored on stream | ||
| // closure. | ||
| // | ||
| // We track the QUIC capacity here, and overload currentUnusedCapacity so the | ||
| // connection pool can accurately keep track of when it is safe to create new | ||
| // streams. | ||
| uint64_t quiche_capacity_ = 100; | ||
|
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'm not sure that this initialization to 100 is quite right. At the QUIC protocol layer, this starts at 0 until the
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. we need to be at 100 or we'll prefetch a connection for each incoming stream: commented
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. Hm. I definitely appreciate the point about the connection prefetch issue. That makes sense to me. We have to be careful that we don't actually send streams on the wire that are above the peers MAX_STREAMS limit or we will commit a protocol violation and the connection will be closed. In quiche, we do this via ShouldCreateOutgoingBidirectionalStream() returning false until we have stream capacity, but with #18614 that will return true. With 0-RTT in particular, we could send a bunch of requests in the first flight and trigger this. But maybe we already have logic which is queueing requests somewhere to avoid this?
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. Added a test to future-proof that we update this before we raise the connected event.
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.
But do we send 0-RTT request before updating this? initial_max_streams_bidi in transport parameters will update this initially. And it seems reasonable to me that 0-RTT can be sent before receiving transport param.
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. we don't support 0-rtt right now. When we do, we'll want to set this based on the cached params. |
||
| }; | ||
|
|
||
| // Http3 subclass of FixedHttpConnPoolImpl which exists to store quic data. | ||
|
|
@@ -45,6 +96,7 @@ class Http3ConnPoolImpl : public FixedHttpConnPoolImpl { | |
| quic::QuicConfig& quic_config); | ||
|
|
||
| Quic::PersistentQuicInfoImpl& quicInfo() { return *quic_info_; } | ||
| bool quic() override { return true; } | ||
|
|
||
| private: | ||
| // Store quic helpers which can be shared between connections and must live | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,9 +4,31 @@ | |
|
|
||
| #include "quic_filter_manager_connection_impl.h" | ||
|
|
||
| namespace quic { | ||
| namespace test { | ||
|
|
||
| // TODO(alyssawilk) add the necessary accessors to quiche and remove this. | ||
| class QuicSessionPeer { | ||
| public: | ||
| static quic::QuicStreamIdManager& getStream(Envoy::Quic::EnvoyQuicClientSession* session) { | ||
|
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. nit: getStream() => getStreamManager()? I'm slightly curious if this peer will end up violating the C++ One Definition Rule. Maybe it's never in the same build as the test peer so maybe this is fine? Anyway, it's short lived in any case.
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'll see your getStreamManager and raise you a getStreamIdManager :-) |
||
| return session->ietf_streamid_manager_.bidirectional_stream_id_manager_; | ||
| } | ||
| }; | ||
|
|
||
| } // namespace test | ||
| } // namespace quic | ||
|
|
||
| namespace Envoy { | ||
| namespace Quic { | ||
|
|
||
| ScopedStreamNotifier::ScopedStreamNotifier(std::function<void(uint32_t)> notify, | ||
| EnvoyQuicClientSession& session) | ||
| : on_can_create_streams_(notify), session_(session) { | ||
| session.setNotifier(*this); | ||
| } | ||
|
|
||
| ScopedStreamNotifier::~ScopedStreamNotifier() { session_.clearNotifier(); } | ||
|
|
||
| EnvoyQuicClientSession::EnvoyQuicClientSession( | ||
| const quic::QuicConfig& config, const quic::ParsedQuicVersionVector& supported_versions, | ||
| std::unique_ptr<EnvoyQuicClientConnection> connection, const quic::QuicServerId& server_id, | ||
|
|
@@ -87,6 +109,14 @@ void EnvoyQuicClientSession::SetDefaultEncryptionLevel(quic::EncryptionLevel lev | |
| } | ||
| } | ||
|
|
||
| void EnvoyQuicClientSession::OnCanCreateNewOutgoingStream(bool) { | ||
|
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. Oh! I just realized we should probably pay attention to this param. It's at the top of this method.
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. Yep - I fixed that in one of the later pushes so if you look at all changes, it's a no-op for unidirectional streams :-)
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'm so bad at github UI. |
||
| if (notifier_.has_value()) { | ||
| quic::QuicStreamIdManager& manager = quic::test::QuicSessionPeer::getStream(this); | ||
| uint32_t streams_available = manager.outgoing_max_streams() - manager.outgoing_stream_count(); | ||
|
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. Might be nice to
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. ack
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 think we want
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. yeah I started with that, but unfortunately it doesn't pan out i [2021-10-20 20:45:07.866][402][critical][assert] [source/common/quic/envoy_quic_client_session.cc:109] assert failure: manager.outgoing_max_streams() > manager.outgoing_stream_count().
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. Yes, I think you're right that this is a quiche bug. In particular, I think QuicSession::StreamDraining() is doing the wrong thing and needs to wrap the call to OnCanCreateNewOutgoingStream(unidirectional) inside of So the assert you have sounds fine for now and I'll look into fixing that in quiche, if that SGTY?
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. SGTM :-) |
||
| notifier_->notify(streams_available); | ||
| } | ||
| } | ||
|
|
||
| std::unique_ptr<quic::QuicSpdyClientStream> EnvoyQuicClientSession::CreateClientStream() { | ||
| ASSERT(codec_stats_.has_value() && http3_options_.has_value()); | ||
| return std::make_unique<EnvoyQuicClientStream>(GetNextOutgoingBidirectionalStreamId(), 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.
I'm confused by this check. How do we get here if capacity == 0? Wouldn't we not be attaching to this client if we don't have any capacity? Should this by capacity == 1 with an ASSERT that capacity != 0? I'm probably missing something.
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, it can't be 0, I was just trying to be as consistent as possible with the prior >=. I'll update to == 1 since it's confusing.