Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
32 changes: 30 additions & 2 deletions source/common/http/http1/codec_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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_) {
Expand Down
5 changes: 3 additions & 2 deletions source/common/http/http1/codec_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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_) {
Expand Down Expand Up @@ -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_{};
};

/**
Expand Down
2 changes: 2 additions & 0 deletions source/common/runtime/runtime_features.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -147,6 +148,7 @@ constexpr absl::Flag<bool>* 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,
Expand Down
153 changes: 153 additions & 0 deletions test/common/http/http1/codec_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<MockRequestDecoder> 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<MockRequestDecoder> 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<MockRequestDecoder> 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<MockRequestDecoder> 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();
Expand Down
20 changes: 0 additions & 20 deletions test/integration/fake_upstream.cc
Original file line number Diff line number Diff line change
Expand Up @@ -298,29 +298,9 @@ void FakeStream::finishGrpcStream(Grpc::Status::GrpcStatus status) {
{"grpc-status", std::to_string(static_cast<uint32_t>(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 {
Expand Down