diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index b53bfa4846fac..1e7a1b01e674e 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -18,6 +18,7 @@ Minor Behavior Changes * file: changed disk based files to truncate files which are not being appended to. This behavioral change can be temporarily reverted by setting runtime guard ``envoy.reloadable_features.append_or_truncate`` to false. * grpc: flip runtime guard ``envoy.reloadable_features.enable_grpc_async_client_cache`` to be default enabled. async grpc client created through getOrCreateRawAsyncClient will be cached by default. * http: avoiding delay-close for HTTP/1.0 responses framed by connection: close as well as HTTP/1.1 if the request is fully read. This means for responses to such requests, the FIN will be sent immediately after the response. This behavior can be temporarily reverted by setting ``envoy.reloadable_features.skip_delay_close`` to false. If clients are are seen to be receiving sporadic partial responses and flipping this flag fixes it, please notify the project immediately. +* http: lazy disable downstream connection reading in the HTTP/1 codec to reduce unnecessary system calls. This behavioral change can be temporarily reverted by setting runtime guard ``envoy.reloadable_features.http1_lazy_read_disable`` to false. * http: now the max concurrent streams of http2 connection can not only be adjusted down according to the SETTINGS frame but also can be adjusted up, of course, it can not exceed the configured upper bounds. This fix is guarded by ``envoy.reloadable_features.http2_allow_capacity_increase_by_settings``. * http: when writing custom filters, `injectEncodedDataToFilterChain` and `injectDecodedDataToFilterChain` now trigger sending of headers if they were not yet sent due to `StopIteration`. Previously, calling one of the inject functions in that state would trigger an assertion. See issue #19891 for more details. * perf: ssl contexts are now tracked without scan based garbage collection and greatly improved the performance on secret update. diff --git a/source/common/http/http1/codec_impl.cc b/source/common/http/http1/codec_impl.cc index 98e290b99259b..06a67909541ef 100644 --- a/source/common/http/http1/codec_impl.cc +++ b/source/common/http/http1/codec_impl.cc @@ -964,7 +964,9 @@ ServerConnectionImpl::ServerConnectionImpl( response_buffer_releasor_([this](const Buffer::OwnedBufferFragmentImpl* fragment) { releaseOutboundResponse(fragment); }), - headers_with_underscores_action_(headers_with_underscores_action) {} + headers_with_underscores_action_(headers_with_underscores_action), + runtime_lazy_read_disable_( + Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http1_lazy_read_disable")) {} uint32_t ServerConnectionImpl::getHeadersSize() { // Add in the size of the request URL if processing request headers. @@ -1143,13 +1145,39 @@ void ServerConnectionImpl::onBody(Buffer::Instance& data) { } } +Http::Status ServerConnectionImpl::dispatch(Buffer::Instance& data) { + if (runtime_lazy_read_disable_ && active_request_ != nullptr && + active_request_->remote_complete_) { + // Eagerly read disable the connection if the downstream is sending pipelined requests as we + // serially process them. Reading from the connection will be re-enabled after the active + // request is completed. + active_request_->response_encoder_.readDisable(true); + return okStatus(); + } + + Http::Status status = ConnectionImpl::dispatch(data); + + if (runtime_lazy_read_disable_ && active_request_ != nullptr && + active_request_->remote_complete_) { + // Read disable the connection if the downstream is sending additional data while we are working + // on an existing request. Reading from the connection will be re-enabled after the active + // request is completed. + if (data.length() > 0) { + active_request_->response_encoder_.readDisable(true); + } + } + return status; +} + ParserStatus ServerConnectionImpl::onMessageCompleteBase() { ASSERT(!handling_upgrade_); if (active_request_) { // The request_decoder should be non-null after we've called the newStream on callbacks. ASSERT(active_request_->request_decoder_); - active_request_->response_encoder_.readDisable(true); + if (!runtime_lazy_read_disable_) { + active_request_->response_encoder_.readDisable(true); + } active_request_->remote_complete_ = true; if (deferred_end_stream_headers_) { diff --git a/source/common/http/http1/codec_impl.h b/source/common/http/http1/codec_impl.h index e1e508747fa0d..53b342509f615 100644 --- a/source/common/http/http1/codec_impl.h +++ b/source/common/http/http1/codec_impl.h @@ -81,8 +81,6 @@ class StreamEncoderImpl : public virtual StreamEncoder, void setIsResponseToConnectRequest(bool value) { is_response_to_connect_request_ = value; } void setDetails(absl::string_view details) { details_ = details; } - void clearReadDisableCallsForTests() { read_disable_calls_ = 0; } - const StreamInfo::BytesMeterSharedPtr& bytesMeter() override { return bytes_meter_; } protected: @@ -477,6 +475,7 @@ class ServerConnectionImpl : public ServerConnection, public ConnectionImpl { Status onUrl(const char* data, size_t length) override; Status onStatus(const char*, size_t) override { return okStatus(); } // ConnectionImpl + Http::Status dispatch(Buffer::Instance& data) override; void onEncodeComplete() override; StreamInfo::BytesMeter& getBytesMeter() override { if (active_request_) { @@ -540,6 +539,8 @@ class ServerConnectionImpl : public ServerConnection, public ConnectionImpl { // The action to take when a request header name contains underscore characters. const envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction headers_with_underscores_action_; + + const bool runtime_lazy_read_disable_{}; }; /** diff --git a/source/common/runtime/runtime_features.cc b/source/common/runtime/runtime_features.cc index 7ae10128a1204..e589022fe5750 100644 --- a/source/common/runtime/runtime_features.cc +++ b/source/common/runtime/runtime_features.cc @@ -32,6 +32,7 @@ RUNTIME_GUARD(envoy_reloadable_features_do_not_await_headers_on_upstream_timeout RUNTIME_GUARD(envoy_reloadable_features_enable_grpc_async_client_cache); RUNTIME_GUARD(envoy_reloadable_features_fix_added_trailers); RUNTIME_GUARD(envoy_reloadable_features_handle_stream_reset_during_hcm_encoding); +RUNTIME_GUARD(envoy_reloadable_features_http1_lazy_read_disable); RUNTIME_GUARD(envoy_reloadable_features_http2_allow_capacity_increase_by_settings); RUNTIME_GUARD(envoy_reloadable_features_http2_new_codec_wrapper); RUNTIME_GUARD(envoy_reloadable_features_http_ext_authz_do_not_skip_direct_response_and_redirect); @@ -147,6 +148,7 @@ constexpr absl::Flag* runtime_features[] = { &FLAGS_envoy_reloadable_features_enable_grpc_async_client_cache, &FLAGS_envoy_reloadable_features_fix_added_trailers, &FLAGS_envoy_reloadable_features_handle_stream_reset_during_hcm_encoding, + &FLAGS_envoy_reloadable_features_http1_lazy_read_disable, &FLAGS_envoy_reloadable_features_http2_allow_capacity_increase_by_settings, &FLAGS_envoy_reloadable_features_http2_new_codec_wrapper, &FLAGS_envoy_reloadable_features_http_ext_authz_do_not_skip_direct_response_and_redirect, diff --git a/test/common/http/http1/codec_impl_test.cc b/test/common/http/http1/codec_impl_test.cc index 825b6431c335b..7435817f4efda 100644 --- a/test/common/http/http1/codec_impl_test.cc +++ b/test/common/http/http1/codec_impl_test.cc @@ -3006,6 +3006,159 @@ TEST_F(Http1ServerConnectionImplTest, ManyLargeRequestHeadersAccepted) { testRequestHeadersAccepted(createLargeHeaderFragment(64)); } +TEST_F(Http1ServerConnectionImplTest, RuntimeLazyReadDisableTest) { + TestScopedRuntime scoped_runtime; + Runtime::RuntimeFeaturesDefaults::get().restoreDefaults(); + + // No readDisable for normal non-piped HTTP request. + { + initialize(); + + NiceMock decoder; + Http::ResponseEncoder* response_encoder = nullptr; + EXPECT_CALL(callbacks_, newStream(_, _)) + .WillOnce(Invoke([&](ResponseEncoder& encoder, bool) -> RequestDecoder& { + response_encoder = &encoder; + return decoder; + })); + + EXPECT_CALL(decoder, decodeHeaders_(_, true)); + EXPECT_CALL(decoder, decodeData(_, _)).Times(0); + + EXPECT_CALL(connection_, readDisable(true)).Times(0); + + Buffer::OwnedImpl buffer("GET / HTTP/1.1\r\nhost: a.com\r\n\r\n"); + auto status = codec_->dispatch(buffer); + EXPECT_TRUE(status.ok()); + + std::string output; + ON_CALL(connection_, write(_, _)).WillByDefault(AddBufferToString(&output)); + TestResponseHeaderMapImpl headers{{":status", "200"}}; + response_encoder->encodeHeaders(headers, true); + EXPECT_EQ("HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n", output); + + EXPECT_CALL(connection_, readDisable(false)).Times(0); + // Delete active request. + connection_.dispatcher_.clearDeferredDeleteList(); + } + + Runtime::LoaderSingleton::getExisting()->mergeValues( + {{"envoy.reloadable_features.http1_lazy_read_disable", "false"}}); + + // Always call readDisable if lazy read disable flag is set to false. + { + initialize(); + + NiceMock decoder; + Http::ResponseEncoder* response_encoder = nullptr; + EXPECT_CALL(callbacks_, newStream(_, _)) + .WillOnce(Invoke([&](ResponseEncoder& encoder, bool) -> RequestDecoder& { + response_encoder = &encoder; + return decoder; + })); + + EXPECT_CALL(decoder, decodeHeaders_(_, true)); + EXPECT_CALL(decoder, decodeData(_, _)).Times(0); + + EXPECT_CALL(connection_, readDisable(true)); + + Buffer::OwnedImpl buffer("GET / HTTP/1.1\r\nhost: a.com\r\n\r\n"); + + auto status = codec_->dispatch(buffer); + EXPECT_TRUE(status.ok()); + + std::string output; + ON_CALL(connection_, write(_, _)).WillByDefault(AddBufferToString(&output)); + TestResponseHeaderMapImpl headers{{":status", "200"}}; + response_encoder->encodeHeaders(headers, true); + EXPECT_EQ("HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n", output); + + EXPECT_CALL(connection_, readDisable(false)); + // Delete active request. + connection_.dispatcher_.clearDeferredDeleteList(); + } +} + +// Tests the scenario where the client sends pipelined requests and the requests reach Envoy at the +// same time. +TEST_F(Http1ServerConnectionImplTest, PipedRequestWithSingleEvent) { + TestScopedRuntime scoped_runtime; + Runtime::RuntimeFeaturesDefaults::get().restoreDefaults(); + + initialize(); + + NiceMock decoder; + Http::ResponseEncoder* response_encoder = nullptr; + EXPECT_CALL(callbacks_, newStream(_, _)) + .WillOnce(Invoke([&](ResponseEncoder& encoder, bool) -> RequestDecoder& { + response_encoder = &encoder; + return decoder; + })); + + EXPECT_CALL(decoder, decodeHeaders_(_, true)); + EXPECT_CALL(decoder, decodeData(_, _)).Times(0); + + EXPECT_CALL(connection_, readDisable(true)); + + Buffer::OwnedImpl buffer( + "GET / HTTP/1.1\r\nhost: a.com\r\n\r\nGET / HTTP/1.1\r\nhost: b.com\r\n\r\n"); + auto status = codec_->dispatch(buffer); + EXPECT_TRUE(status.ok()); + + std::string output; + ON_CALL(connection_, write(_, _)).WillByDefault(AddBufferToString(&output)); + TestResponseHeaderMapImpl headers{{":status", "200"}}; + response_encoder->encodeHeaders(headers, true); + EXPECT_EQ("HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n", output); + + EXPECT_CALL(connection_, readDisable(false)); + // Delete active request to re-enable connection reading. + connection_.dispatcher_.clearDeferredDeleteList(); +} + +// Tests the scenario where the client sends pipelined requests. The second request reaches Envoy +// before the end of the first request. +TEST_F(Http1ServerConnectionImplTest, PipedRequestWithMutipleEvent) { + TestScopedRuntime scoped_runtime; + Runtime::RuntimeFeaturesDefaults::get().restoreDefaults(); + + initialize(); + + NiceMock decoder; + Http::ResponseEncoder* response_encoder = nullptr; + EXPECT_CALL(callbacks_, newStream(_, _)) + .WillOnce(Invoke([&](ResponseEncoder& encoder, bool) -> RequestDecoder& { + response_encoder = &encoder; + return decoder; + })); + + EXPECT_CALL(decoder, decodeHeaders_(_, true)); + EXPECT_CALL(decoder, decodeData(_, _)).Times(0); + + Buffer::OwnedImpl buffer("GET / HTTP/1.1\r\nhost: a.com\r\n\r\n"); + auto status = codec_->dispatch(buffer); + EXPECT_TRUE(status.ok()); + + Buffer::OwnedImpl second_buffer("GET / HTTP/1.1\r\nhost: a.com\r\n\r\n"); + + // Second request before first request complete will disable downstream connection reading. + EXPECT_CALL(connection_, readDisable(true)); + auto second_status = codec_->dispatch(second_buffer); + EXPECT_TRUE(second_status.ok()); + // The second request will no be consumed. + EXPECT_TRUE(second_buffer.length() != 0); + + std::string output; + ON_CALL(connection_, write(_, _)).WillByDefault(AddBufferToString(&output)); + TestResponseHeaderMapImpl headers{{":status", "200"}}; + response_encoder->encodeHeaders(headers, true); + EXPECT_EQ("HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n", output); + + EXPECT_CALL(connection_, readDisable(false)); + // Delete active request to re-enable connection reading. + connection_.dispatcher_.clearDeferredDeleteList(); +} + // Tests that incomplete response headers of 80 kB header value fails. TEST_F(Http1ClientConnectionImplTest, ResponseHeadersWithLargeValueRejected) { initialize(); diff --git a/test/integration/fake_upstream.cc b/test/integration/fake_upstream.cc index 0c7eb169adb62..9675e7b63e01d 100644 --- a/test/integration/fake_upstream.cc +++ b/test/integration/fake_upstream.cc @@ -298,29 +298,9 @@ void FakeStream::finishGrpcStream(Grpc::Status::GrpcStatus status) { {"grpc-status", std::to_string(static_cast(status))}}); } -// The TestHttp1ServerConnectionImpl outlives its underlying Network::Connection -// so must not access the Connection on teardown. To achieve this, clear the -// read disable calls to avoid checking / editing the Connection blocked state. class TestHttp1ServerConnectionImpl : public Http::Http1::ServerConnectionImpl { public: using Http::Http1::ServerConnectionImpl::ServerConnectionImpl; - - Http::Http1::ParserStatus onMessageCompleteBase() override { - auto rc = ServerConnectionImpl::onMessageCompleteBase(); - - if (activeRequest() && activeRequest()->request_decoder_) { - // Undo the read disable from the base class - we have many tests which - // waitForDisconnect after a full request has been read which will not - // receive the disconnect if reading is disabled. - activeRequest()->response_encoder_.readDisable(false); - } - return rc; - } - ~TestHttp1ServerConnectionImpl() override { - if (activeRequest()) { - activeRequest()->response_encoder_.clearReadDisableCallsForTests(); - } - } }; class TestHttp2ServerConnectionImpl : public Http::Http2::ServerConnectionImpl {