Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 3 additions & 1 deletion api/envoy/config/core/v3/protocol.proto
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,9 @@ message KeepaliveSettings {
google.protobuf.Duration interval = 1 [(validate.rules).duration = {gte {nanos: 1000000}}];

// How long to wait for a response to a keepalive PING. If a response is not received within this
// time period, the connection will be aborted.
// time period, the connection will be aborted. Note that in order to prevent the influence of
// Head-of-line (HOL) blocking the timeout period is extended when *any* frame is received on
// the connection, under the assumption that if a frame is received the connection is healthy.
google.protobuf.Duration timeout = 2 [(validate.rules).duration = {
required: true
gte {nanos: 1000000}
Expand Down
6 changes: 6 additions & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ Minor Behavior Changes
----------------------
*Changes that may cause incompatibilities for some users, but should not for most*

* http: the behavior of the :ref:`timeout <envoy_v3_api_field_config.core.v3.KeepaliveSettings.timeout>`
field has been modified to extend the timeout when *any* frame is received on the owning HTTP/2
connection. This negates the effect of head-of-line (HOL) blocking for slow connections. If
any frame is received the assumption is that the connection is working. This behavior change
can be reverted by setting the ``envoy.reloadable_features.http2_delay_keepalive_timeout`` runtime
flag to false.
* thrift: add validate_clusters in :ref:`RouteConfiguration <envoy_v3_api_msg_extensions.filters.network.thrift_proxy.v3.RouteConfiguration>` to override the default behavior of cluster validation.
* tls: if both :ref:`match_subject_alt_names <envoy_v3_api_field_extensions.transport_sockets.tls.v3.CertificateValidationContext.match_subject_alt_names>` and :ref:`match_typed_subject_alt_names <envoy_v3_api_field_extensions.transport_sockets.tls.v3.CertificateValidationContext.match_typed_subject_alt_names>` are specified, the former (deprecated) field is ignored. Previously, setting both fields would result in an error.
* tls: removed SHA-1 cipher suites from the server-side defaults.
Expand Down
15 changes: 14 additions & 1 deletion source/common/http/http2/codec_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -907,7 +907,10 @@ ConnectionImpl::ConnectionImpl(Network::Connection& connection, CodecStats& stat
protocol_constraints_(stats, http2_options),
skip_dispatching_frames_for_closed_connection_(Runtime::runtimeFeatureEnabled(
"envoy.reloadable_features.skip_dispatching_frames_for_closed_connection")),
dispatching_(false), raised_goaway_(false), random_(random_generator),
dispatching_(false), raised_goaway_(false),
delay_keepalive_timeout_(Runtime::runtimeFeatureEnabled(
"envoy.reloadable_features.http2_delay_keepalive_timeout")),
random_(random_generator),
last_received_data_time_(connection_.dispatcher().timeSource().monotonicTime()) {
// This library can only be used with the wrapper API enabled.
ASSERT(!use_oghttp2_library_ || use_new_codec_wrapper_);
Expand Down Expand Up @@ -1181,6 +1184,16 @@ Status ConnectionImpl::onFrameReceived(const nghttp2_frame* frame) {
return okStatus();
}

// In slow networks, HOL blocking can prevent the ping response from coming in a reasonable
// amount of time. To avoid HOL blocking influence, if we receive *any* frame extend the
// timeout for another timeout period. This will still timeout the connection if there is no
// activity, but if there is frame activity we assume the connection is still healthy and the
// PING ACK may be delayed behind other frames.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great comment!

if (delay_keepalive_timeout_ && keepalive_timeout_timer_ != nullptr &&
keepalive_timeout_timer_->enabled()) {
keepalive_timeout_timer_->enableTimer(keepalive_timeout_);
}

if (frame->hd.type == NGHTTP2_DATA) {
RETURN_IF_ERROR(trackInboundFrames(&frame->hd, frame->data.padlen));
}
Expand Down
1 change: 1 addition & 0 deletions source/common/http/http2/codec_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,7 @@ class ConnectionImpl : public virtual Connection,
std::map<int32_t, StreamImpl*> pending_deferred_reset_streams_;
bool dispatching_ : 1;
bool raised_goaway_ : 1;
const bool delay_keepalive_timeout_ : 1;
Event::SchedulableCallbackPtr protocol_constraint_violation_callback_;
Random::RandomGenerator& random_;
MonotonicTime last_received_data_time_{};
Expand Down
1 change: 1 addition & 0 deletions source/common/runtime/runtime_features.cc
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ RUNTIME_GUARD(envoy_reloadable_features_enable_grpc_async_client_cache);
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_delay_keepalive_timeout);
RUNTIME_GUARD(envoy_reloadable_features_http2_new_codec_wrapper);
RUNTIME_GUARD(envoy_reloadable_features_http_100_continue_case_insensitive);
RUNTIME_GUARD(envoy_reloadable_features_http_reject_path_with_fragment);
Expand Down
79 changes: 79 additions & 0 deletions test/common/http/http2/codec_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1184,6 +1184,85 @@ TEST_P(Http2CodecImplTest, ConnectionKeepalive) {
timeout_timer->invokeCallback();
}

// Verify that extending the timeout is performed when a frame is received.
TEST_P(Http2CodecImplTest, KeepaliveTimeoutDelay) {
constexpr uint32_t interval_ms = 100;
constexpr uint32_t timeout_ms = 200;
client_http2_options_.mutable_connection_keepalive()->mutable_interval()->set_nanos(interval_ms *
1000 * 1000);
client_http2_options_.mutable_connection_keepalive()->mutable_timeout()->set_nanos(timeout_ms *
1000 * 1000);
client_http2_options_.mutable_connection_keepalive()->mutable_interval_jitter()->set_value(0);
auto timeout_timer = new NiceMock<Event::MockTimer>(&client_connection_.dispatcher_);
auto send_timer = new NiceMock<Event::MockTimer>(&client_connection_.dispatcher_);
EXPECT_CALL(*timeout_timer, disableTimer());
EXPECT_CALL(*send_timer, enableTimer(std::chrono::milliseconds(interval_ms), _));
initialize();
InSequence s;

// Initiate a request.
TestRequestHeaderMapImpl request_headers;
HttpTestUtility::addDefaultHeaders(request_headers);
EXPECT_CALL(request_decoder_, decodeHeaders_(_, true));
EXPECT_TRUE(request_encoder_->encodeHeaders(request_headers, true).ok());
driveToCompletion();

// Now send a ping.
EXPECT_CALL(*timeout_timer, enableTimer(std::chrono::milliseconds(timeout_ms), _));
send_timer->invokeCallback();

// Send the response and make sure the keepalive timeout is extended. After the response is
// received the ACK will come in and reset.
EXPECT_CALL(*timeout_timer, enableTimer(std::chrono::milliseconds(timeout_ms), _));
EXPECT_CALL(response_decoder_, decodeHeaders_(_, true));
EXPECT_CALL(*timeout_timer, disableTimer()); // This indicates that an ACK was received.
EXPECT_CALL(*send_timer, enableTimer(std::chrono::milliseconds(interval_ms), _));
TestResponseHeaderMapImpl response_headers{{":status", "200"}};
response_encoder_->encodeHeaders(response_headers, true);
driveToCompletion();
}

// Verify that extending the timeout is not performed when a frame is received and the feature
// flag is disabled.
TEST_P(Http2CodecImplTest, KeepaliveTimeoutDelayRuntimeFalse) {
scoped_runtime_.mergeValues(
{{"envoy.reloadable_features.http2_delay_keepalive_timeout", "false"}});

constexpr uint32_t interval_ms = 100;
constexpr uint32_t timeout_ms = 200;
client_http2_options_.mutable_connection_keepalive()->mutable_interval()->set_nanos(interval_ms *
1000 * 1000);
client_http2_options_.mutable_connection_keepalive()->mutable_timeout()->set_nanos(timeout_ms *
1000 * 1000);
client_http2_options_.mutable_connection_keepalive()->mutable_interval_jitter()->set_value(0);
auto timeout_timer = new NiceMock<Event::MockTimer>(&client_connection_.dispatcher_);
auto send_timer = new NiceMock<Event::MockTimer>(&client_connection_.dispatcher_);
EXPECT_CALL(*timeout_timer, disableTimer());
EXPECT_CALL(*send_timer, enableTimer(std::chrono::milliseconds(interval_ms), _));
initialize();
InSequence s;

// Initiate a request.
TestRequestHeaderMapImpl request_headers;
HttpTestUtility::addDefaultHeaders(request_headers);
EXPECT_CALL(request_decoder_, decodeHeaders_(_, true));
EXPECT_TRUE(request_encoder_->encodeHeaders(request_headers, true).ok());
driveToCompletion();

// Now send a ping.
EXPECT_CALL(*timeout_timer, enableTimer(std::chrono::milliseconds(timeout_ms), _));
send_timer->invokeCallback();

// Send the response and make sure the keepalive timeout is not extended. After the response is
// received the ACK will come in and reset.
EXPECT_CALL(response_decoder_, decodeHeaders_(_, true));
EXPECT_CALL(*timeout_timer, disableTimer()); // This indicates that an ACK was received.
EXPECT_CALL(*send_timer, enableTimer(std::chrono::milliseconds(interval_ms), _));
TestResponseHeaderMapImpl response_headers{{":status", "200"}};
response_encoder_->encodeHeaders(response_headers, true);
driveToCompletion();
}

// Validate that jitter is added as expected based on configuration.
TEST_P(Http2CodecImplTest, ConnectionKeepaliveJitter) {
client_http2_options_.mutable_connection_keepalive()->mutable_interval()->set_seconds(1);
Expand Down