From 01b62a78e96abb41ca9d127002c1c235811c460f Mon Sep 17 00:00:00 2001 From: Yan Avlasov Date: Thu, 28 Sep 2023 16:11:58 +0000 Subject: [PATCH 1/5] Close HTTP connections that prematurely reset streams Signed-off-by: Yan Avlasov --- changelogs/current.yaml | 9 +++ source/common/http/conn_manager_config.h | 1 + source/common/http/conn_manager_impl.cc | 64 +++++++++++++++++ source/common/http/conn_manager_impl.h | 23 ++++++ source/common/runtime/runtime_features.cc | 1 + .../multiplexed_integration_test.cc | 72 +++++++++++++++++++ 6 files changed, 170 insertions(+) diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 9ecf0d6e48..97f517aa3b 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -2,6 +2,15 @@ date: Pending behavior_changes: # *Changes that are expected to cause an incompatibility if applicable; deployment changes are likely required* +- area: http + change: | + Close HTTP/2 and HTTP/3 connections that prematurely reset streams. The runtime key + ``overload.premature_reset_min_stream_lifetime_seconds`` determines the interval where received stream + reset is considered premature (with 1 second default). The runtime key ``overload.premature_reset_total_stream_count``, + with the default value of 500, determines the number of requests received from a connection before the check for premature + resets is applied. The connection is disconnected if more than 50% of resets are premature. + Setting the runtime key ``envoy.restart_features.send_goaway_for_premature_rst_streams`` to ``false`` completely disables + this check. minor_behavior_changes: # *Changes that may cause incompatibilities for some users, but should not for most* diff --git a/source/common/http/conn_manager_config.h b/source/common/http/conn_manager_config.h index 70cc200dda..e494ac2d46 100644 --- a/source/common/http/conn_manager_config.h +++ b/source/common/http/conn_manager_config.h @@ -63,6 +63,7 @@ namespace Http { COUNTER(downstream_rq_rejected_via_ip_detection) \ COUNTER(downstream_rq_response_before_rq_complete) \ COUNTER(downstream_rq_rx_reset) \ + COUNTER(downstream_rq_too_many_premature_resets) \ COUNTER(downstream_rq_timeout) \ COUNTER(downstream_rq_header_timeout) \ COUNTER(downstream_rq_too_large) \ diff --git a/source/common/http/conn_manager_impl.cc b/source/common/http/conn_manager_impl.cc index 15f4027b34..1f01b5af38 100644 --- a/source/common/http/conn_manager_impl.cc +++ b/source/common/http/conn_manager_impl.cc @@ -1,5 +1,6 @@ #include "source/common/http/conn_manager_impl.h" +#include #include #include #include @@ -54,6 +55,11 @@ namespace Envoy { namespace Http { +const absl::string_view ConnectionManagerImpl::PrematureResetTotalStreamCountKey = + "overload.premature_reset_total_stream_count"; +const absl::string_view ConnectionManagerImpl::PrematureResetMinStreamLifetimeSecondsKey = + "overload.premature_reset_min_stream_lifetime_seconds"; + bool requestWasConnect(const RequestHeaderMapPtr& headers, Protocol protocol) { if (!headers) { return false; @@ -262,6 +268,10 @@ void ConnectionManagerImpl::doEndStream(ActiveStream& stream, bool check_for_def } void ConnectionManagerImpl::doDeferredStreamDestroy(ActiveStream& stream) { + ++closed_non_internally_destroyed_requests_; + if (isPrematureRstStream(stream)) { + ++number_premature_stream_resets_; + } if (stream.max_stream_duration_timer_) { stream.max_stream_duration_timer_->disableTimer(); stream.max_stream_duration_timer_ = nullptr; @@ -294,6 +304,7 @@ void ConnectionManagerImpl::doDeferredStreamDestroy(ActiveStream& stream) { if (connection_idle_timer_ && streams_.empty()) { connection_idle_timer_->enableTimer(config_.idleTimeout().value()); } + maybeDrainDueToPrematureResets(); } RequestDecoder& ConnectionManagerImpl::newStream(ResponseEncoder& response_encoder, @@ -534,6 +545,59 @@ void ConnectionManagerImpl::doConnectionClose( } } +bool ConnectionManagerImpl::isPrematureRstStream(const ActiveStream& stream) const { + // Check if the request was prematurely reset, by comparing its lifetime to the configured + // threshold. + MonotonicTime current_time = time_source_.monotonicTime(); + MonotonicTime request_start_time = stream.filter_manager_.streamInfo().startTimeMonotonic(); + std::chrono::nanoseconds duration = + std::chrono::duration_cast(current_time - request_start_time); + + // Check if request lifetime is longer than the premature reset threshold. + if (duration.count() > 0) { + const uint64_t lifetime = std::chrono::duration_cast(duration).count(); + const uint64_t min_lifetime = runtime_.snapshot().getInteger( + ConnectionManagerImpl::PrematureResetMinStreamLifetimeSecondsKey, 1); + if (lifetime > min_lifetime) { + return false; + } + } + + // If request has completed before configured threshold, also check if the Envoy proxied the + // response from the upstream. Requests without the response status were reset. + // TODO(RyanTheOptimist): Possibly support half_closed_local instead. + return !stream.filter_manager_.streamInfo().responseCode(); +} + +// Sends a GOAWAY if too many streams have been reset prematurely on this +// connection. +void ConnectionManagerImpl::maybeDrainDueToPrematureResets() { + if (!Runtime::runtimeFeatureEnabled( + "envoy.restart_features.send_goaway_for_premature_rst_streams") || + closed_non_internally_destroyed_requests_ == 0) { + return; + } + + const uint64_t limit = + runtime_.snapshot().getInteger(ConnectionManagerImpl::PrematureResetTotalStreamCountKey, 500); + + if (closed_non_internally_destroyed_requests_ < limit) { + return; + } + + if (static_cast(number_premature_stream_resets_) / + closed_non_internally_destroyed_requests_ < + .5) { + return; + } + + if (drain_state_ == DrainState::NotDraining) { + stats_.named_.downstream_rq_too_many_premature_resets_.inc(); + doConnectionClose(Network::ConnectionCloseType::NoFlush, absl::nullopt, + "too_many_premature_resets"); + } +} + void ConnectionManagerImpl::onGoAway(GoAwayErrorCode) { // Currently we do nothing with remote go away frames. In the future we can decide to no longer // push resources if applicable. diff --git a/source/common/http/conn_manager_impl.h b/source/common/http/conn_manager_impl.h index 583bea311d..bbac5a8279 100644 --- a/source/common/http/conn_manager_impl.h +++ b/source/common/http/conn_manager_impl.h @@ -115,6 +115,14 @@ class ConnectionManagerImpl : Logger::Loggable, void setClearHopByHopResponseHeaders(bool value) { clear_hop_by_hop_response_headers_ = value; } bool clearHopByHopResponseHeaders() const { return clear_hop_by_hop_response_headers_; } + // This runtime key configures the number of streams which must be closed on a connection before + // envoy will potentially drain a connection due to excessive prematurely reset streams. + static const absl::string_view PrematureResetTotalStreamCountKey; + + // The minimum lifetime of a stream, in seconds, in order not to be considered + // prematurely closed. + static const absl::string_view PrematureResetMinStreamLifetimeSecondsKey; + private: struct ActiveStream; class MobileConnectionManagerImpl; @@ -440,6 +448,15 @@ class ConnectionManagerImpl : Logger::Loggable, void doConnectionClose(absl::optional close_type, absl::optional response_flag, absl::string_view details); + // Returns true if a RST_STREAM for the given stream is premature. Premature + // means the RST_STREAM arrived before response headers were sent and than + // the stream was alive for short period of time. This period is specified + // by the optional runtime value PrematureResetMinStreamLifetimeSecondsKey, + // or one second if that is not present. + bool isPrematureRstStream(const ActiveStream& stream) const; + // Sends a GOAWAY if both sufficient streams have been closed on a connection + // and at least half have been prematurely reset? + void maybeDrainDueToPrematureResets(); enum class DrainState { NotDraining, Draining, Closing }; @@ -478,6 +495,12 @@ class ConnectionManagerImpl : Logger::Loggable, bool clear_hop_by_hop_response_headers_{true}; // The number of requests accumulated on the current connection. uint64_t accumulated_requests_{}; + // The number of requests closed on the current connection which were + // not internally destroyed + uint64_t closed_non_internally_destroyed_requests_{}; + // The number of requests that received a premature RST_STREAM, according to + // the definition given in `isPrematureRstStream()`. + uint64_t number_premature_stream_resets_{0}; const std::string proxy_name_; // for Proxy-Status. }; diff --git a/source/common/runtime/runtime_features.cc b/source/common/runtime/runtime_features.cc index 893b0a9e32..a21ad3d900 100644 --- a/source/common/runtime/runtime_features.cc +++ b/source/common/runtime/runtime_features.cc @@ -85,6 +85,7 @@ RUNTIME_GUARD(envoy_reloadable_features_validate_connect); RUNTIME_GUARD(envoy_reloadable_features_validate_upstream_headers); RUNTIME_GUARD(envoy_restart_features_explicit_wildcard_resource); RUNTIME_GUARD(envoy_restart_features_remove_runtime_singleton); +RUNTIME_GUARD(envoy_restart_features_send_goaway_for_premature_rst_streams); RUNTIME_GUARD(envoy_restart_features_use_apple_api_for_dns_lookups); // Begin false flags. These should come with a TODO to flip true. diff --git a/test/integration/multiplexed_integration_test.cc b/test/integration/multiplexed_integration_test.cc index c64ee4a1fe..6fa5e86725 100644 --- a/test/integration/multiplexed_integration_test.cc +++ b/test/integration/multiplexed_integration_test.cc @@ -1,4 +1,5 @@ #include +#include #include #include @@ -24,6 +25,7 @@ #include "test/mocks/http/mocks.h" #include "test/test_common/network_utility.h" #include "test/test_common/printers.h" +#include "test/test_common/simulated_time_system.h" #include "test/test_common/utility.h" #include "gtest/gtest.h" @@ -51,6 +53,15 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, MultiplexedIntegrationTest, {Http::CodecType::HTTP1})), HttpProtocolIntegrationTest::protocolTestParamsToString); +class MultiplexedIntegrationTestWithSimulatedTime : public Event::TestUsingSimulatedTime, + public MultiplexedIntegrationTest {}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, MultiplexedIntegrationTestWithSimulatedTime, + testing::ValuesIn(HttpProtocolIntegrationTest::getProtocolTestParams( + {Http::CodecType::HTTP2, Http::CodecType::HTTP3}, + {Http::CodecType::HTTP1})), + HttpProtocolIntegrationTest::protocolTestParamsToString); + TEST_P(MultiplexedIntegrationTest, RouterRequestAndResponseWithBodyNoBuffer) { testRouterRequestAndResponseWithBody(1024, 512, false, false); } @@ -1039,6 +1050,67 @@ TEST_P(MultiplexedIntegrationTest, GoAway) { EXPECT_EQ("200", response->headers().getStatusValue()); } +// TODO(rch): Add a unit test which covers internal redirect handling. +TEST_P(MultiplexedIntegrationTestWithSimulatedTime, GoAwayAfterTooManyResets) { + EXCLUDE_DOWNSTREAM_HTTP3; // Need to wait for the server to reset the stream + // before opening new one. + config_helper_.addRuntimeOverride("envoy.restart_features.send_goaway_for_premature_rst_streams", + "true"); + const int total_streams = 100; + config_helper_.addRuntimeOverride("overload.premature_reset_total_stream_count", + absl::StrCat(total_streams)); + initialize(); + + Http::TestRequestHeaderMapImpl headers{ + {":method", "GET"}, {":path", "/healthcheck"}, {":scheme", "http"}, {":authority", "host"}}; + codec_client_ = makeHttpConnection(lookupPort("http")); + for (int i = 0; i < total_streams; ++i) { + auto encoder_decoder = codec_client_->startRequest(headers); + request_encoder_ = &encoder_decoder.first; + auto response = std::move(encoder_decoder.second); + codec_client_->sendReset(*request_encoder_); + ASSERT_TRUE(response->waitForReset()); + } + + // Envoy should disconnect client due to premature reset check + ASSERT_TRUE(codec_client_->waitForDisconnect()); + test_server_->waitForCounterEq("http.config_test.downstream_rq_rx_reset", total_streams); + test_server_->waitForCounterEq("http.config_test.downstream_rq_too_many_premature_resets", 1); +} + +TEST_P(MultiplexedIntegrationTestWithSimulatedTime, DontGoAwayAfterTooManyResetsForLongStreams) { + EXCLUDE_DOWNSTREAM_HTTP3; // Need to wait for the server to reset the stream + // before opening new one. + config_helper_.addRuntimeOverride("envoy.restart_features.send_goaway_for_premature_rst_streams", + "true"); + const int total_streams = 100; + const int stream_lifetime_seconds = 2; + config_helper_.addRuntimeOverride("overload.premature_reset_total_stream_count", + absl::StrCat(total_streams)); + + config_helper_.addRuntimeOverride("overload.premature_reset_min_stream_lifetime_seconds", + absl::StrCat(stream_lifetime_seconds)); + + initialize(); + + Http::TestRequestHeaderMapImpl headers{ + {":method", "GET"}, {":path", "/healthcheck"}, {":scheme", "http"}, {":authority", "host"}}; + codec_client_ = makeHttpConnection(lookupPort("http")); + + std::string request_counter = "http.config_test.downstream_rq_total"; + std::string reset_counter = "http.config_test.downstream_rq_rx_reset"; + for (int i = 0; i < total_streams * 2; ++i) { + auto encoder_decoder = codec_client_->startRequest(headers); + request_encoder_ = &encoder_decoder.first; + auto response = std::move(encoder_decoder.second); + test_server_->waitForCounterEq(request_counter, i + 1); + timeSystem().advanceTimeWait(std::chrono::seconds(2 * stream_lifetime_seconds)); + codec_client_->sendReset(*request_encoder_); + ASSERT_TRUE(response->waitForReset()); + test_server_->waitForCounterEq(reset_counter, i + 1); + } +} + TEST_P(MultiplexedIntegrationTest, Trailers) { testTrailers(1024, 2048, false, false); } TEST_P(MultiplexedIntegrationTest, TrailersGiantBody) { From b8f05c054c6d372a5c79593c4157719a4448c00c Mon Sep 17 00:00:00 2001 From: Yan Avlasov Date: Sat, 30 Sep 2023 13:58:46 +0000 Subject: [PATCH 2/5] Limit on the number of HTTP requests processed from a connection in an I/O cycle Signed-off-by: Yan Avlasov --- changelogs/current.yaml | 7 + source/common/http/conn_manager_impl.cc | 93 ++++++- source/common/http/conn_manager_impl.h | 24 +- test/common/http/conn_manager_impl_test_2.cc | 243 ++++++++++++++++++ .../http/conn_manager_impl_test_base.cc | 19 ++ .../common/http/conn_manager_impl_test_base.h | 2 + test/common/http/http2/http2_frame.cc | 12 +- test/common/http/http2/http2_frame.h | 14 +- .../multiplexed_integration_test.cc | 170 ++++++++++++ 9 files changed, 569 insertions(+), 15 deletions(-) diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 97f517aa3b..a072ac765f 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -11,6 +11,13 @@ behavior_changes: resets is applied. The connection is disconnected if more than 50% of resets are premature. Setting the runtime key ``envoy.restart_features.send_goaway_for_premature_rst_streams`` to ``false`` completely disables this check. +- area: http + change: | + Add runtime flag ``http.max_requests_per_io_cycle`` for setting the limit on the number of HTTP requests processed + from a single connection in a single I/O cycle. Requests over this limit are processed in subsequent I/O cycles. This + mitigates CPU starvation by connections that simultaneously send high number of requests by allowing requests from other + connections to make progress. This runtime value can be set to 1 in the presence of abusive HTTP/2 or HTTP/3 connections. + By default this limit is disabled. minor_behavior_changes: # *Changes that may cause incompatibilities for some users, but should not for most* diff --git a/source/common/http/conn_manager_impl.cc b/source/common/http/conn_manager_impl.cc index 1f01b5af38..02faaa932d 100644 --- a/source/common/http/conn_manager_impl.cc +++ b/source/common/http/conn_manager_impl.cc @@ -59,6 +59,10 @@ const absl::string_view ConnectionManagerImpl::PrematureResetTotalStreamCountKey "overload.premature_reset_total_stream_count"; const absl::string_view ConnectionManagerImpl::PrematureResetMinStreamLifetimeSecondsKey = "overload.premature_reset_min_stream_lifetime_seconds"; +// Runtime key for maximum number of requests that can be processed from a single connection per +// I/O cycle. Requests over this limit are deferred until the next I/O cycle. +const absl::string_view ConnectionManagerImpl::MaxRequestsPerIoCycle = + "http.max_requests_per_io_cycle"; bool requestWasConnect(const RequestHeaderMapPtr& headers, Protocol protocol) { if (!headers) { @@ -112,7 +116,9 @@ ConnectionManagerImpl::ConnectionManagerImpl(ConnectionManagerConfig& config, time_source_(time_source), proxy_name_(StreamInfo::ProxyStatusUtils::makeProxyName( /*node_id=*/local_info_.node().id(), /*server_name=*/config_.serverName(), - /*proxy_status_config=*/config_.proxyStatusConfig())) {} + /*proxy_status_config=*/config_.proxyStatusConfig())), + max_requests_during_dispatch_(runtime_.snapshot().getInteger( + ConnectionManagerImpl::MaxRequestsPerIoCycle, UINT32_MAX)) {} const ResponseHeaderMap& ConnectionManagerImpl::continueHeader() { static const auto headers = createHeaderMap( @@ -122,6 +128,12 @@ const ResponseHeaderMap& ConnectionManagerImpl::continueHeader() { void ConnectionManagerImpl::initializeReadFilterCallbacks(Network::ReadFilterCallbacks& callbacks) { read_callbacks_ = &callbacks; + if (max_requests_during_dispatch_ != UINT32_MAX) { + deferred_request_processing_callback_ = + callbacks.connection().dispatcher().createSchedulableCallback( + [this]() -> void { onDeferredRequestProcessing(); }); + } + stats_.named_.downstream_cx_total_.inc(); stats_.named_.downstream_cx_active_.inc(); if (read_callbacks_->connection().ssl()) { @@ -391,6 +403,7 @@ void ConnectionManagerImpl::createCodec(Buffer::Instance& data) { } Network::FilterStatus ConnectionManagerImpl::onData(Buffer::Instance& data, bool) { + requests_during_dispatch_count_ = 0; if (!codec_) { // Http3 codec should have been instantiated by now. createCodec(data); @@ -1215,7 +1228,12 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapPtr&& he traceRequest(); } - filter_manager_.decodeHeaders(*request_headers_, end_stream); + if (!connection_manager_.shouldDeferRequestProxyingToNextIoCycle()) { + filter_manager_.decodeHeaders(*request_headers_, end_stream); + } else { + state_.deferred_to_next_io_iteration_ = true; + state_.deferred_end_stream_ = end_stream; + } // Reset it here for both global and overridden cases. resetIdleTimer(); @@ -1283,8 +1301,15 @@ void ConnectionManagerImpl::ActiveStream::decodeData(Buffer::Instance& data, boo connection_manager_.read_callbacks_->connection().dispatcher()); maybeEndDecode(end_stream); filter_manager_.streamInfo().addBytesReceived(data.length()); - - filter_manager_.decodeData(data, end_stream); + if (!state_.deferred_to_next_io_iteration_) { + filter_manager_.decodeData(data, end_stream); + } else { + if (!deferred_data_) { + deferred_data_ = std::make_unique(); + } + deferred_data_->move(data); + state_.deferred_end_stream_ = end_stream; + } } void ConnectionManagerImpl::ActiveStream::decodeTrailers(RequestTrailerMapPtr&& trailers) { @@ -1295,7 +1320,9 @@ void ConnectionManagerImpl::ActiveStream::decodeTrailers(RequestTrailerMapPtr&& ASSERT(!request_trailers_); request_trailers_ = std::move(trailers); maybeEndDecode(true); - filter_manager_.decodeTrailers(*request_trailers_); + if (!state_.deferred_to_next_io_iteration_) { + filter_manager_.decodeTrailers(*request_trailers_); + } } void ConnectionManagerImpl::ActiveStream::decodeMetadata(MetadataMapPtr&& metadata_map) { @@ -1895,5 +1922,61 @@ void ConnectionManagerImpl::ActiveStream::resetStream(Http::StreamResetReason, a connection_manager_.doEndStream(*this); } +bool ConnectionManagerImpl::ActiveStream::onDeferredRequestProcessing() { + // TODO(yanavlasov): Merge this with the filter manager continueIteration() method + if (!state_.deferred_to_next_io_iteration_) { + return false; + } + state_.deferred_to_next_io_iteration_ = false; + bool end_stream = + state_.deferred_end_stream_ && deferred_data_ == nullptr && request_trailers_ == nullptr; + filter_manager_.decodeHeaders(*request_headers_, end_stream); + if (end_stream) { + return true; + } + if (deferred_data_ != nullptr) { + end_stream = state_.deferred_end_stream_ && request_trailers_ == nullptr; + filter_manager_.decodeData(*deferred_data_, end_stream); + } + if (request_trailers_ != nullptr) { + filter_manager_.decodeTrailers(*request_trailers_); + } + return true; +} + +bool ConnectionManagerImpl::shouldDeferRequestProxyingToNextIoCycle() { + // Do not defer this stream if stream deferral is disabled + if (deferred_request_processing_callback_ == nullptr) { + return false; + } + // Defer this stream if there are already deferred streams, so they are not + // processed out of order + if (deferred_request_processing_callback_->enabled()) { + return true; + } + ++requests_during_dispatch_count_; + bool defer = requests_during_dispatch_count_ > max_requests_during_dispatch_; + if (defer) { + deferred_request_processing_callback_->scheduleCallbackNextIteration(); + } + return defer; +} + +void ConnectionManagerImpl::onDeferredRequestProcessing() { + requests_during_dispatch_count_ = 1; // 1 stream is always let through + // Streams are inserted at the head of the list. As such process deferred + // streams at the back of the list first. + for (auto reverse_iter = streams_.rbegin(); reverse_iter != streams_.rend();) { + auto& stream_ptr = *reverse_iter; + // Move the iterator to the next item in case the `onDeferredRequestProcessing` call removes the + // stream from the list. + ++reverse_iter; + bool was_deferred = stream_ptr->onDeferredRequestProcessing(); + if (was_deferred && shouldDeferRequestProxyingToNextIoCycle()) { + break; + } + } +} + } // namespace Http } // namespace Envoy diff --git a/source/common/http/conn_manager_impl.h b/source/common/http/conn_manager_impl.h index bbac5a8279..ca78226368 100644 --- a/source/common/http/conn_manager_impl.h +++ b/source/common/http/conn_manager_impl.h @@ -122,6 +122,7 @@ class ConnectionManagerImpl : Logger::Loggable, // The minimum lifetime of a stream, in seconds, in order not to be considered // prematurely closed. static const absl::string_view PrematureResetMinStreamLifetimeSecondsKey; + static const absl::string_view MaxRequestsPerIoCycle; private: struct ActiveStream; @@ -322,7 +323,7 @@ class ConnectionManagerImpl : Logger::Loggable, State() : codec_saw_local_complete_(false), saw_connection_close_(false), successful_upgrade_(false), is_internally_created_(false), is_tunneling_(false), - decorated_propagate_(true) {} + decorated_propagate_(true), deferred_to_next_io_iteration_(false) {} bool codec_saw_local_complete_ : 1; // This indicates that local is complete as written all // the way through to the codec. @@ -338,6 +339,14 @@ class ConnectionManagerImpl : Logger::Loggable, bool is_tunneling_ : 1; bool decorated_propagate_ : 1; + + // Indicates that sending headers to the filter manager is deferred to the + // next I/O cycle. If data or trailers are received when this flag is set + // they are deferred too. + // TODO(yanavlasov): encapsulate the entire state of deferred streams into a separate + // structure, so it can be atomically created and cleared. + bool deferred_to_next_io_iteration_ : 1; + bool deferred_end_stream_ : 1; }; // Per-stream idle timeout callback. @@ -370,6 +379,11 @@ class ConnectionManagerImpl : Logger::Loggable, // If header map failed validation, it sends an error response and returns false. bool validateHeaders(); + // Dispatch deferred headers, body and trailers to the filter manager. + // Return true if this stream was deferred and dispatched pending headers, body and trailers (if + // present). Return false if this stream was not deferred. + bool onDeferredRequestProcessing(); + ConnectionManagerImpl& connection_manager_; // TODO(snowp): It might make sense to move this to the FilterManager to avoid storing it in // both locations, then refer to the FM when doing stream logs. @@ -414,6 +428,8 @@ class ConnectionManagerImpl : Logger::Loggable, Http::HeaderValidatorPtr header_validator_; friend FilterManager; + + std::unique_ptr deferred_data_; }; using ActiveStreamPtr = std::unique_ptr; @@ -458,6 +474,9 @@ class ConnectionManagerImpl : Logger::Loggable, // and at least half have been prematurely reset? void maybeDrainDueToPrematureResets(); + bool shouldDeferRequestProxyingToNextIoCycle(); + void onDeferredRequestProcessing(); + enum class DrainState { NotDraining, Draining, Closing }; ConnectionManagerConfig& config_; @@ -502,6 +521,9 @@ class ConnectionManagerImpl : Logger::Loggable, // the definition given in `isPrematureRstStream()`. uint64_t number_premature_stream_resets_{0}; const std::string proxy_name_; // for Proxy-Status. + uint32_t requests_during_dispatch_count_{0}; + const uint32_t max_requests_during_dispatch_{UINT32_MAX}; + Event::SchedulableCallbackPtr deferred_request_processing_callback_; }; } // namespace Http diff --git a/test/common/http/conn_manager_impl_test_2.cc b/test/common/http/conn_manager_impl_test_2.cc index 74e6daf74c..67704bc2f0 100644 --- a/test/common/http/conn_manager_impl_test_2.cc +++ b/test/common/http/conn_manager_impl_test_2.cc @@ -13,6 +13,7 @@ using testing::Mock; using testing::Property; using testing::Ref; using testing::Return; +using testing::ReturnArg; using testing::ReturnRef; namespace Envoy { @@ -3405,6 +3406,248 @@ TEST_F(HttpConnectionManagerImplTest, HeaderValidatorAccept) { } #endif // ENVOY_ENABLE_UHV +// Validate that deferred streams are processed with a variety of +// headers, data and trailer arriving in the same I/O cycle +TEST_F(HttpConnectionManagerImplTest, LimitWorkPerIOCycle) { + const int kRequestsSentPerIOCycle = 100; + EXPECT_CALL(runtime_.snapshot_, getInteger(_, _)).WillRepeatedly(ReturnArg<1>()); + // Process 1 request per I/O cycle + auto* deferred_request_callback = enableStreamsPerIoLimit(1); + setup(false, ""); + + // Store the basic request encoder during filter chain setup. + std::vector> encoder_filters; + int decode_headers_call_count = 0; + for (int i = 0; i < kRequestsSentPerIOCycle; ++i) { + std::shared_ptr filter(new NiceMock()); + + // Each 4th request is headers only + EXPECT_CALL(*filter, decodeHeaders(_, i % 4 == 0 ? true : false)) + .WillRepeatedly(Invoke([&](RequestHeaderMap&, bool) -> FilterHeadersStatus { + ++decode_headers_call_count; + return FilterHeadersStatus::StopIteration; + })); + + // Each 1st request is headers and data only + // Each 2nd request is headers, data and trailers + if (i % 4 == 1 || i % 4 == 2) { + EXPECT_CALL(*filter, decodeData(_, i % 4 == 1 ? true : false)) + .WillOnce(Return(FilterDataStatus::StopIterationNoBuffer)); + } + + // Each 3rd request is headers and trailers (no data) + if (i % 4 == 2 || i % 4 == 3) { + EXPECT_CALL(*filter, decodeTrailers(_)).WillOnce(Return(FilterTrailersStatus::StopIteration)); + } + + EXPECT_CALL(*filter, setDecoderFilterCallbacks(_)); + encoder_filters.push_back(std::move(filter)); + } + + uint64_t random_value = 0; + EXPECT_CALL(random_, random()).WillRepeatedly(Invoke([&random_value]() { + return random_value++; + })); + + EXPECT_CALL(filter_factory_, createFilterChain(_)) + .Times(kRequestsSentPerIOCycle) + .WillRepeatedly(Invoke([&encoder_filters](FilterChainManager& manager) -> bool { + static int index = 0; + int i = index++; + FilterFactoryCb factory([&encoder_filters, i](FilterChainFactoryCallbacks& callbacks) { + callbacks.addStreamDecoderFilter(encoder_filters[i]); + }); + manager.applyFilterFactoryCb({}, factory); + return true; + })); + + EXPECT_CALL(filter_callbacks_.connection_.dispatcher_, deferredDelete_(_)) + .Times(kRequestsSentPerIOCycle); + + std::vector> response_encoders(kRequestsSentPerIOCycle); + for (auto& encoder : response_encoders) { + EXPECT_CALL(encoder, getStream()).WillRepeatedly(ReturnRef(encoder.stream_)); + } + + EXPECT_CALL(*codec_, dispatch(_)) + .WillRepeatedly(Invoke([&](Buffer::Instance& data) -> Http::Status { + for (int i = 0; i < kRequestsSentPerIOCycle; ++i) { + decoder_ = &conn_manager_->newStream(response_encoders[i]); + + RequestHeaderMapPtr headers{new TestRequestHeaderMapImpl{ + {":authority", "host"}, {":path", "/"}, {":method", "GET"}}}; + + RequestTrailerMapPtr trailers{ + new TestRequestTrailerMapImpl{{"key1", "value1"}, {"key2", "value2"}}}; + + Buffer::OwnedImpl data("data"); + + switch (i % 4) { + case 0: + decoder_->decodeHeaders(std::move(headers), true); + break; + case 1: + decoder_->decodeHeaders(std::move(headers), false); + decoder_->decodeData(data, true); + break; + case 2: + decoder_->decodeHeaders(std::move(headers), false); + decoder_->decodeData(data, false); + decoder_->decodeTrailers(std::move(trailers)); + break; + case 3: + decoder_->decodeHeaders(std::move(headers), false); + decoder_->decodeTrailers(std::move(trailers)); + break; + } + } + + data.drain(4); + return Http::okStatus(); + })); + + // Kick off the incoming data. + Buffer::OwnedImpl fake_input("1234"); + conn_manager_->onData(fake_input, false); + + EXPECT_TRUE(deferred_request_callback->enabled_); + // Only one request should go through the filter chain + ASSERT_EQ(decode_headers_call_count, 1); + + // Let other requests to go through the filter chain. Call expectations will fail + // if this is not the case. + int deferred_request_count = 0; + while (deferred_request_callback->enabled_) { + deferred_request_callback->invokeCallback(); + ++deferred_request_count; + } + + ASSERT_EQ(deferred_request_count, kRequestsSentPerIOCycle); + + for (auto& filter : encoder_filters) { + ResponseHeaderMapPtr response_headers{new TestResponseHeaderMapImpl{{":status", "200"}}}; + filter->callbacks_->streamInfo().setResponseCodeDetails(""); + filter->callbacks_->encodeHeaders(std::move(response_headers), true, "details"); + } + + EXPECT_EQ(kRequestsSentPerIOCycle, stats_.named_.downstream_rq_2xx_.value()); + EXPECT_EQ(kRequestsSentPerIOCycle, listener_stats_.downstream_rq_2xx_.value()); + EXPECT_EQ(kRequestsSentPerIOCycle, stats_.named_.downstream_rq_completed_.value()); + EXPECT_EQ(kRequestsSentPerIOCycle, listener_stats_.downstream_rq_completed_.value()); +} + +TEST_F(HttpConnectionManagerImplTest, StreamDeferralPreservesOrder) { + EXPECT_CALL(runtime_.snapshot_, getInteger(_, _)).WillRepeatedly(ReturnArg<1>()); + // Process 1 request per I/O cycle + auto* deferred_request_callback = enableStreamsPerIoLimit(1); + setup(false, ""); + + std::vector> encoder_filters; + int expected_request_id = 0; + const Http::LowerCaseString request_id_header(absl::string_view("request-id")); + // Two requests are processed in 2 I/O reads + const int TotalRequests = 2 * 2; + for (int i = 0; i < TotalRequests; ++i) { + std::shared_ptr filter(new NiceMock()); + + EXPECT_CALL(*filter, decodeHeaders(_, true)) + .WillRepeatedly(Invoke([&](RequestHeaderMap& headers, bool) -> FilterHeadersStatus { + // Check that requests are decoded in expected order + int request_id = 0; + ASSERT(absl::SimpleAtoi(headers.get(request_id_header)[0]->value().getStringView(), + &request_id)); + ASSERT(request_id == expected_request_id); + ++expected_request_id; + return FilterHeadersStatus::StopIteration; + })); + + EXPECT_CALL(*filter, setDecoderFilterCallbacks(_)); + encoder_filters.push_back(std::move(filter)); + } + + uint64_t random_value = 0; + EXPECT_CALL(random_, random()).WillRepeatedly(Invoke([&random_value]() { + return random_value++; + })); + + EXPECT_CALL(filter_factory_, createFilterChain(_)) + .Times(TotalRequests) + .WillRepeatedly(Invoke([&encoder_filters](FilterChainManager& manager) -> bool { + static int index = 0; + int i = index++; + FilterFactoryCb factory([&encoder_filters, i](FilterChainFactoryCallbacks& callbacks) { + callbacks.addStreamDecoderFilter(encoder_filters[i]); + }); + manager.applyFilterFactoryCb({}, factory); + return true; + })); + + EXPECT_CALL(filter_callbacks_.connection_.dispatcher_, deferredDelete_(_)).Times(TotalRequests); + + std::vector> response_encoders(TotalRequests); + for (auto& encoder : response_encoders) { + EXPECT_CALL(encoder, getStream()).WillRepeatedly(ReturnRef(encoder.stream_)); + } + auto response_encoders_iter = response_encoders.begin(); + + int request_id = 0; + EXPECT_CALL(*codec_, dispatch(_)) + .WillRepeatedly(Invoke([&](Buffer::Instance& data) -> Http::Status { + // The second request should be deferred + for (int i = 0; i < 2; ++i) { + decoder_ = &conn_manager_->newStream(*response_encoders_iter); + ++response_encoders_iter; + + RequestHeaderMapPtr headers{ + new TestRequestHeaderMapImpl{{":authority", "host"}, + {":path", "/"}, + {":method", "GET"}, + {"request-id", absl::StrCat(request_id)}}}; + + ++request_id; + decoder_->decodeHeaders(std::move(headers), true); + } + + data.drain(4); + return Http::okStatus(); + })); + + // Kick off the incoming data. + Buffer::OwnedImpl fake_input("1234"); + conn_manager_->onData(fake_input, false); + + EXPECT_TRUE(deferred_request_callback->enabled_); + // Only one request should go through the filter chain + ASSERT_EQ(expected_request_id, 1); + + // Test arrival of another request. New request is read from the socket before deferred callbacks. + Buffer::OwnedImpl fake_input2("1234"); + conn_manager_->onData(fake_input2, false); + + // No requests from the second read should go through as there are deferred stream present + ASSERT_EQ(expected_request_id, 1); + + // Let other requests to go through the filter chain. Call expectations will fail + // if this is not the case. + int deferred_request_count = 0; + while (deferred_request_callback->enabled_) { + deferred_request_callback->invokeCallback(); + ++deferred_request_count; + } + + ASSERT_EQ(deferred_request_count, TotalRequests); + + for (auto& filter : encoder_filters) { + ResponseHeaderMapPtr response_headers{new TestResponseHeaderMapImpl{{":status", "200"}}}; + filter->callbacks_->streamInfo().setResponseCodeDetails(""); + filter->callbacks_->encodeHeaders(std::move(response_headers), true, "details"); + } + + EXPECT_EQ(TotalRequests, stats_.named_.downstream_rq_2xx_.value()); + EXPECT_EQ(TotalRequests, listener_stats_.downstream_rq_2xx_.value()); + EXPECT_EQ(TotalRequests, stats_.named_.downstream_rq_completed_.value()); + EXPECT_EQ(TotalRequests, listener_stats_.downstream_rq_completed_.value()); +} } // namespace Http } // namespace Envoy diff --git a/test/common/http/conn_manager_impl_test_base.cc b/test/common/http/conn_manager_impl_test_base.cc index 1048162bd1..48d733bf05 100644 --- a/test/common/http/conn_manager_impl_test_base.cc +++ b/test/common/http/conn_manager_impl_test_base.cc @@ -77,6 +77,7 @@ void HttpConnectionManagerImplTest::setup(bool ssl, const std::string& server_na conn_manager_ = std::make_unique( *this, drain_close_, random_, http_context_, runtime_, local_info_, cluster_manager_, overload_manager_, test_time_.timeSystem()); + conn_manager_->initializeReadFilterCallbacks(filter_callbacks_); if (tracing) { @@ -304,5 +305,23 @@ void HttpConnectionManagerImplTest::testPathNormalization( conn_manager_->onData(fake_input, false); } +Event::MockSchedulableCallback* +HttpConnectionManagerImplTest::enableStreamsPerIoLimit(uint32_t limit) { + EXPECT_CALL(runtime_.snapshot_, getInteger("http.max_requests_per_io_cycle", _)) + .WillOnce(Return(limit)); + + // Expect HCM to create and set schedulable callback + auto* deferred_request_callback = + new Event::MockSchedulableCallback(&filter_callbacks_.connection_.dispatcher_); + EXPECT_CALL(*deferred_request_callback, enabled()) + .WillRepeatedly( + Invoke([deferred_request_callback]() { return deferred_request_callback->enabled_; })); + EXPECT_CALL(*deferred_request_callback, scheduleCallbackNextIteration()) + .WillRepeatedly( + Invoke([deferred_request_callback]() { deferred_request_callback->enabled_ = true; })); + + return deferred_request_callback; +} + } // namespace Http } // namespace Envoy diff --git a/test/common/http/conn_manager_impl_test_base.h b/test/common/http/conn_manager_impl_test_base.h index d7428b4d4c..badbc8398e 100644 --- a/test/common/http/conn_manager_impl_test_base.h +++ b/test/common/http/conn_manager_impl_test_base.h @@ -174,6 +174,8 @@ class HttpConnectionManagerImplTest : public testing::Test, public ConnectionMan }; } + Event::MockSchedulableCallback* enableStreamsPerIoLimit(uint32_t limit); + Envoy::Event::SimulatedTimeSystem test_time_; NiceMock route_config_provider_; std::shared_ptr route_config_{new NiceMock()}; diff --git a/test/common/http/http2/http2_frame.cc b/test/common/http/http2/http2_frame.cc index 68569c0daf..f7f2ad117d 100644 --- a/test/common/http/http2/http2_frame.cc +++ b/test/common/http/http2/http2_frame.cc @@ -339,7 +339,11 @@ Http2Frame Http2Frame::makeRequest(uint32_t stream_index, absl::string_view host makeNetworkOrderStreamId(stream_index)); frame.appendStaticHeader(StaticHeaderIndex::MethodGet); frame.appendStaticHeader(StaticHeaderIndex::SchemeHttps); - frame.appendHeaderWithoutIndexing(StaticHeaderIndex::Path, path); + if (path.empty() || path == "/") { + frame.appendStaticHeader(StaticHeaderIndex::Path); + } else { + frame.appendHeaderWithoutIndexing(StaticHeaderIndex::Path, path); + } frame.appendHeaderWithoutIndexing(StaticHeaderIndex::Authority, host); frame.adjustPayloadSize(); return frame; @@ -363,7 +367,11 @@ Http2Frame Http2Frame::makePostRequest(uint32_t stream_index, absl::string_view makeNetworkOrderStreamId(stream_index)); frame.appendStaticHeader(StaticHeaderIndex::MethodPost); frame.appendStaticHeader(StaticHeaderIndex::SchemeHttps); - frame.appendHeaderWithoutIndexing(StaticHeaderIndex::Path, path); + if (path.empty() || path == "/") { + frame.appendStaticHeader(StaticHeaderIndex::Path); + } else { + frame.appendHeaderWithoutIndexing(StaticHeaderIndex::Path, path); + } frame.appendHeaderWithoutIndexing(StaticHeaderIndex::Authority, host); frame.adjustPayloadSize(); return frame; diff --git a/test/common/http/http2/http2_frame.h b/test/common/http/http2/http2_frame.h index dca17c4804..1b2ba0fea5 100644 --- a/test/common/http/http2/http2_frame.h +++ b/test/common/http/http2/http2_frame.h @@ -209,6 +209,13 @@ class Http2Frame { ConstIterator end() const { return data_.end(); } bool empty() const { return data_.empty(); } + void appendHeaderWithoutIndexing(const Header& header); + // This method updates payload length in the HTTP2 header based on the size of the data_ + void adjustPayloadSize() { + ASSERT(size() >= HeaderSize); + setPayloadSize(size() - HeaderSize); + } + private: void buildHeader(Type type, uint32_t payload_size = 0, uint8_t flags = 0, uint32_t stream_id = 0); void setPayloadSize(uint32_t size); @@ -228,15 +235,8 @@ class Http2Frame { // Headers are directly encoded void appendStaticHeader(StaticHeaderIndex index); void appendHeaderWithoutIndexing(StaticHeaderIndex index, absl::string_view value); - void appendHeaderWithoutIndexing(const Header& header); void appendEmptyHeader(); - // This method updates payload length in the HTTP2 header based on the size of the data_ - void adjustPayloadSize() { - ASSERT(size() >= HeaderSize); - setPayloadSize(size() - HeaderSize); - } - DataContainer data_; }; diff --git a/test/integration/multiplexed_integration_test.cc b/test/integration/multiplexed_integration_test.cc index 6fa5e86725..69a75e3dfd 100644 --- a/test/integration/multiplexed_integration_test.cc +++ b/test/integration/multiplexed_integration_test.cc @@ -1987,6 +1987,176 @@ TEST_P(Http2FrameIntegrationTest, UpstreamWindowUpdateAfterGoAway) { tcp_client_->close(); } +TEST_P(Http2FrameIntegrationTest, MultipleHeaderOnlyRequests) { + const int kRequestsSentPerIOCycle = 20; + autonomous_upstream_ = true; + config_helper_.addRuntimeOverride("http.max_requests_per_io_cycle", "1"); + beginSession(); + + std::string buffer; + for (int i = 0; i < kRequestsSentPerIOCycle; ++i) { + auto request = Http2Frame::makeRequest(Http2Frame::makeClientStreamId(i), "a", "/", + {{"response_data_blocks", "0"}, {"no_trailers", "1"}}); + absl::StrAppend(&buffer, std::string(request)); + } + + ASSERT_TRUE(tcp_client_->write(buffer, false, false)); + + for (int i = 0; i < kRequestsSentPerIOCycle; ++i) { + auto frame = readFrame(); + EXPECT_EQ(Http2Frame::Type::Headers, frame.type()); + EXPECT_EQ(Http2Frame::ResponseStatus::Ok, frame.responseStatus()); + } + tcp_client_->close(); +} + +TEST_P(Http2FrameIntegrationTest, MultipleRequests) { + const int kRequestsSentPerIOCycle = 20; + autonomous_upstream_ = true; + config_helper_.addRuntimeOverride("http.max_requests_per_io_cycle", "1"); + beginSession(); + + std::string buffer; + for (int i = 0; i < kRequestsSentPerIOCycle; ++i) { + auto request = + Http2Frame::makePostRequest(Http2Frame::makeClientStreamId(i), "a", "/", + {{"response_data_blocks", "0"}, {"no_trailers", "1"}}); + absl::StrAppend(&buffer, std::string(request)); + } + + for (int i = 0; i < kRequestsSentPerIOCycle; ++i) { + auto data = Http2Frame::makeDataFrame(Http2Frame::makeClientStreamId(i), "a", + Http2Frame::DataFlags::EndStream); + absl::StrAppend(&buffer, std::string(data)); + } + + ASSERT_TRUE(tcp_client_->write(buffer, false, false)); + + for (int i = 0; i < kRequestsSentPerIOCycle; ++i) { + auto frame = readFrame(); + EXPECT_EQ(Http2Frame::Type::Headers, frame.type()); + EXPECT_EQ(Http2Frame::ResponseStatus::Ok, frame.responseStatus()); + } + tcp_client_->close(); +} + +TEST_P(Http2FrameIntegrationTest, MultipleRequestsWithTrailers) { + const int kRequestsSentPerIOCycle = 20; + autonomous_upstream_ = true; + config_helper_.addRuntimeOverride("http.max_requests_per_io_cycle", "1"); + beginSession(); + + std::string buffer; + for (int i = 0; i < kRequestsSentPerIOCycle; ++i) { + auto request = + Http2Frame::makePostRequest(Http2Frame::makeClientStreamId(i), "a", "/", + {{"response_data_blocks", "0"}, {"no_trailers", "1"}}); + absl::StrAppend(&buffer, std::string(request)); + } + + for (int i = 0; i < kRequestsSentPerIOCycle; ++i) { + auto data = Http2Frame::makeDataFrame(Http2Frame::makeClientStreamId(i), "a"); + absl::StrAppend(&buffer, std::string(data)); + } + + for (int i = 0; i < kRequestsSentPerIOCycle; ++i) { + auto trailers = Http2Frame::makeEmptyHeadersFrame( + Http2Frame::makeClientStreamId(i), + static_cast(Http::Http2::orFlags( + Http2Frame::HeadersFlags::EndStream, Http2Frame::HeadersFlags::EndHeaders))); + trailers.appendHeaderWithoutIndexing({"k", "v"}); + trailers.adjustPayloadSize(); + absl::StrAppend(&buffer, std::string(trailers)); + } + + ASSERT_TRUE(tcp_client_->write(buffer, false, false)); + + for (int i = 0; i < kRequestsSentPerIOCycle; ++i) { + auto frame = readFrame(); + EXPECT_EQ(Http2Frame::Type::Headers, frame.type()); + EXPECT_EQ(Http2Frame::ResponseStatus::Ok, frame.responseStatus()); + } + tcp_client_->close(); +} + +TEST_P(Http2FrameIntegrationTest, MultipleHeaderOnlyRequestsFollowedByReset) { + // This number of requests stays below premature reset detection. + const int kRequestsSentPerIOCycle = 20; + config_helper_.addRuntimeOverride("http.max_requests_per_io_cycle", "1"); + beginSession(); + + std::string buffer; + for (int i = 0; i < kRequestsSentPerIOCycle; ++i) { + auto request = Http2Frame::makeRequest(Http2Frame::makeClientStreamId(i), "a", "/", + {{"response_data_blocks", "0"}, {"no_trailers", "1"}}); + absl::StrAppend(&buffer, std::string(request)); + } + + for (int i = 0; i < kRequestsSentPerIOCycle; ++i) { + auto reset = Http2Frame::makeResetStreamFrame(Http2Frame::makeClientStreamId(i), + Http2Frame::ErrorCode::Cancel); + absl::StrAppend(&buffer, std::string(reset)); + } + + ASSERT_TRUE(tcp_client_->write(buffer, false, false)); + test_server_->waitForCounterEq("http.config_test.downstream_rq_rx_reset", + kRequestsSentPerIOCycle); + // Client should remain connected + ASSERT_TRUE(tcp_client_->connected()); + tcp_client_->close(); +} + +TEST_P(Http2FrameIntegrationTest, ResettingDeferredRequestsTriggersPrematureResetCheck) { + const int kRequestsSentPerIOCycle = 20; + // Set premature stream count to the number of streams we are about to send + config_helper_.addRuntimeOverride("overload.premature_reset_total_stream_count", "20"); + config_helper_.addRuntimeOverride("http.max_requests_per_io_cycle", "1"); + beginSession(); + + std::string buffer; + for (int i = 0; i < kRequestsSentPerIOCycle; ++i) { + auto request = Http2Frame::makeRequest(Http2Frame::makeClientStreamId(i), "a", "/", + {{"response_data_blocks", "0"}, {"no_trailers", "1"}}); + absl::StrAppend(&buffer, std::string(request)); + } + + for (int i = 0; i < kRequestsSentPerIOCycle; ++i) { + auto reset = Http2Frame::makeResetStreamFrame(Http2Frame::makeClientStreamId(i), + Http2Frame::ErrorCode::Cancel); + absl::StrAppend(&buffer, std::string(reset)); + } + + ASSERT_TRUE(tcp_client_->write(buffer, false, false)); + // Envoy should close the client connection due to too many premature resets + tcp_client_->waitForDisconnect(); + test_server_->waitForCounterEq("http.config_test.downstream_rq_too_many_premature_resets", 1); +} + +TEST_P(Http2FrameIntegrationTest, CloseConnectionWithDeferredStreams) { + // Use large number of requests to ensure close is detected while there are + // still some deferred streams. + const int kRequestsSentPerIOCycle = 1000; + config_helper_.addRuntimeOverride("http.max_requests_per_io_cycle", "1"); + // Ensure premature reset detection does not get in the way + config_helper_.addRuntimeOverride("overload.premature_reset_total_stream_count", "1001"); + beginSession(); + + std::string buffer; + for (int i = 0; i < kRequestsSentPerIOCycle; ++i) { + auto request = Http2Frame::makeRequest(Http2Frame::makeClientStreamId(i), "a", "/", + {{"response_data_blocks", "0"}, {"no_trailers", "1"}}); + absl::StrAppend(&buffer, std::string(request)); + } + + ASSERT_TRUE(tcp_client_->write(buffer, false, false)); + ASSERT_TRUE(tcp_client_->connected()); + // Drop the downstream connection + tcp_client_->close(); + // Test that Envoy can clean-up deferred streams + test_server_->waitForCounterEq("http.config_test.downstream_rq_rx_reset", + kRequestsSentPerIOCycle); +} + INSTANTIATE_TEST_SUITE_P(IpVersions, Http2FrameIntegrationTest, testing::ValuesIn(Http2FrameIntegrationTest::testParams()), frameIntegrationTestParamToString); From 53b89ece6ad2cb00d5a096ef3eee7989550419bc Mon Sep 17 00:00:00 2001 From: Ryan Northey Date: Tue, 10 Oct 2023 13:41:13 +0100 Subject: [PATCH 3/5] repo: Disable old website publishing Signed-off-by: Ryan Northey --- .azure-pipelines/pipelines.yml | 7 ------- docs/publish.sh | 33 --------------------------------- 2 files changed, 40 deletions(-) diff --git a/.azure-pipelines/pipelines.yml b/.azure-pipelines/pipelines.yml index ef7d1d4f97..6c03ec2016 100644 --- a/.azure-pipelines/pipelines.yml +++ b/.azure-pipelines/pipelines.yml @@ -413,13 +413,6 @@ stages: GCS_ARTIFACT_BUCKET: $(GcsArtifactBucket) condition: eq(variables['isMain'], 'true') - - task: InstallSSHKey@0 - inputs: - hostName: "github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=" - sshPublicKey: "$(DocsPublicKey)" - sshPassphrase: "$(SshDeployKeyPassphrase)" - sshKeySecureFile: "$(DocsPrivateKey)" - - script: docs/publish.sh displayName: "Publish to GitHub" workingDirectory: $(Build.SourcesDirectory) diff --git a/docs/publish.sh b/docs/publish.sh index c371ccc78d..4ff0f9c68c 100755 --- a/docs/publish.sh +++ b/docs/publish.sh @@ -29,37 +29,4 @@ if [[ "$VERSION" =~ $DEV_VERSION_REGEX ]]; then else echo "Ignoring docs push" fi - exit 0 -else - PUBLISH_DIR="${CHECKOUT_DIR}/docs/envoy/v${VERSION}" fi - -if [[ "${AZP_BRANCH}" != "${MAIN_BRANCH}" ]] && ! [[ "${AZP_BRANCH}" =~ ${RELEASE_BRANCH_REGEX} ]]; then - # Most likely a tag, do nothing. - echo 'Ignoring non-release branch for docs push.' - exit 0 -fi - -DOCS_MAIN_BRANCH="main" - -echo 'cloning' -git clone git@github.com:envoyproxy/envoy-website "${CHECKOUT_DIR}" -b "${DOCS_MAIN_BRANCH}" --depth 1 - -if [[ -e "$PUBLISH_DIR" ]]; then - # Defense against the unexpected. - echo 'Docs version already exists, not continuing!.' - exit 1 -fi - -mkdir -p "$PUBLISH_DIR" -cp -r "$DOCS_DIR"/* "$PUBLISH_DIR" -cd "${CHECKOUT_DIR}" - -git config user.name "envoy-docs(Azure Pipelines)" -git config user.email envoy-docs@users.noreply.github.com - -set -x - -git add . -git commit -m "docs envoy@$BUILD_SHA" -git push origin "${DOCS_MAIN_BRANCH}" From 119f376ce7822496cd9fe98ea36d83be3d751ef5 Mon Sep 17 00:00:00 2001 From: Ryan Northey Date: Tue, 10 Oct 2023 13:56:10 +0100 Subject: [PATCH 4/5] repo: Fix format issue Signed-off-by: Ryan Northey --- docs/publish.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/publish.sh b/docs/publish.sh index 4ff0f9c68c..99e7fe5b2f 100755 --- a/docs/publish.sh +++ b/docs/publish.sh @@ -11,13 +11,10 @@ set -e -DOCS_DIR=generated/docs -CHECKOUT_DIR=envoy-docs BUILD_SHA=$(git rev-parse HEAD) VERSION="$(cat VERSION.txt)" MAIN_BRANCH="refs/heads/main" -RELEASE_BRANCH_REGEX="^refs/heads/release/v.*" DEV_VERSION_REGEX="-dev$" if [[ "$VERSION" =~ $DEV_VERSION_REGEX ]]; then From be9fe65b97b9a66f8ecd333f7815c9b4c60ab017 Mon Sep 17 00:00:00 2001 From: Ryan Northey Date: Tue, 10 Oct 2023 12:17:05 +0000 Subject: [PATCH 5/5] repo: Release v1.24.11 Summary of changes: - Resolve CVE-2023-44487 - Update Docker images to resolve glibc vulnerabilites Full changelog v1.24.10...v1.24.11 Docker images: https://hub.docker.com/r/envoyproxy/envoy/tags?page=1&name=v1.24.11 Docs: https://www.envoyproxy.io/docs/envoy/v1.24.11/ Release notes: https://www.envoyproxy.io/docs/envoy/v1.24.11/version_history/v1.24/v1.24.11 Signed-off-by: Ryan Northey --- VERSION.txt | 2 +- changelogs/current.yaml | 16 +--------------- docs/inventories/v1.24/objects.inv | Bin 141703 -> 141751 bytes docs/versions.yaml | 2 +- 4 files changed, 3 insertions(+), 17 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index 07987d1eb6..d6c68ad2d0 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -1.24.11-dev +1.24.11 diff --git a/changelogs/current.yaml b/changelogs/current.yaml index a072ac765f..c5c5e55329 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -1,7 +1,6 @@ -date: Pending +date: October 10, 2023 behavior_changes: -# *Changes that are expected to cause an incompatibility if applicable; deployment changes are likely required* - area: http change: | Close HTTP/2 and HTTP/3 connections that prematurely reset streams. The runtime key @@ -18,16 +17,3 @@ behavior_changes: mitigates CPU starvation by connections that simultaneously send high number of requests by allowing requests from other connections to make progress. This runtime value can be set to 1 in the presence of abusive HTTP/2 or HTTP/3 connections. By default this limit is disabled. - -minor_behavior_changes: -# *Changes that may cause incompatibilities for some users, but should not for most* - -bug_fixes: -# *Changes expected to improve the state of the world and are unlikely to have negative effects* - -removed_config_or_runtime: -# *Normally occurs at the end of the* :ref:`deprecation period ` - -new_features: - -deprecated: diff --git a/docs/inventories/v1.24/objects.inv b/docs/inventories/v1.24/objects.inv index 8fe2b0773b37583012baa2b5e6c4147a8401f219..986bfcc604afd3819b4c175b4f0a1c79f516b25e 100644 GIT binary patch delta 23701 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�}$|vyEaSLr2 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 diff --git a/docs/versions.yaml b/docs/versions.yaml index 757e25d8b1..15899ad7e7 100644 --- a/docs/versions.yaml +++ b/docs/versions.yaml @@ -17,4 +17,4 @@ "1.21": 1.21.6 "1.22": 1.22.11 "1.23": 1.23.12 -"1.24": 1.24.9 +"1.24": 1.24.10