Skip to content

Commit

Permalink
http2: propagates reset events to CodecEventCallbacks when sending RS…
Browse files Browse the repository at this point in the history
…T_STREAM (#37784)

We have found this patch useful in our testing.

The delta between the first and second commit demonstrates test coverage
for the new code.

The new behavior can be disabled with the runtime guard
`envoy.reloadable_features.http2_propagate_reset_events`.

Commit Message: propagates reset events to CodecEventCallbacks when
sending RST_STREAM
Additional Description:
Risk Level: low
Testing: ran unit and integration tests locally
Docs Changes:
Release Notes:
Platform Specific Features:
Runtime guard: `envoy.reloadable_features.http2_propagate_reset_events`

---------

Signed-off-by: Biren Roy <[email protected]>
  • Loading branch information
birenroy authored Jan 9, 2025
1 parent 3a60882 commit e294469
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 0 deletions.
4 changes: 4 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ bug_fixes:
change: |
Fixed a bug where the DNS refresh rate was the DNS TTL instead of the configured dns_refresh_rate/dns_failure_refresh_rate
when we failed to resolve the DNS query after a successful resolution.
- area: http2
change: |
Propagates codec reset events when sending HTTP/2 RST_STREAM frames. Can be temporarily reverted by setting
runtime guard ``envoy.reloadable_features.http2_propagate_reset_events`` to false.
removed_config_or_runtime:
# *Normally occurs at the end of the* :ref:`deprecation period <deprecated>`
Expand Down
7 changes: 7 additions & 0 deletions source/common/http/http2/codec_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1226,6 +1226,13 @@ int ConnectionImpl::onFrameSend(int32_t stream_id, size_t length, uint8_t type,
case OGHTTP2_RST_STREAM_FRAME_TYPE: {
ENVOY_CONN_LOG(debug, "sent reset code={}", connection_, error_code);
stats_.tx_reset_.inc();
if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http2_propagate_reset_events") &&
stream != nullptr && !stream->local_end_stream_sent_) {
// The RST_STREAM may preempt further DATA frames, and serves as the
// notification of the end of the stream.
stream->onResetEncoded(error_code);
stream->local_end_stream_sent_ = true;
}
break;
}

Expand Down
5 changes: 5 additions & 0 deletions source/common/http/http2/codec_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,11 @@ class ConnectionImpl : public virtual Connection,
codec_callbacks_->onCodecEncodeComplete();
}
}
void onResetEncoded(uint32_t error_code) {
if (codec_callbacks_ && error_code != 0) {
codec_callbacks_->onCodecLowLevelReset();
}
}

const StreamInfo::BytesMeterSharedPtr& bytesMeter() override { return bytes_meter_; }
ConnectionImpl& parent_;
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 @@ -53,6 +53,7 @@ RUNTIME_GUARD(envoy_reloadable_features_http1_balsa_disallow_lone_cr_in_chunk_ex
RUNTIME_GUARD(envoy_reloadable_features_http1_use_balsa_parser);
RUNTIME_GUARD(envoy_reloadable_features_http2_discard_host_header);
RUNTIME_GUARD(envoy_reloadable_features_http2_no_protocol_error_upon_clean_close);
RUNTIME_GUARD(envoy_reloadable_features_http2_propagate_reset_events);
RUNTIME_GUARD(envoy_reloadable_features_http3_happy_eyeballs);
RUNTIME_GUARD(envoy_reloadable_features_http3_remove_empty_trailers);
RUNTIME_GUARD(envoy_reloadable_features_http_filter_avoid_reentrant_local_reply);
Expand Down
41 changes: 41 additions & 0 deletions test/common/http/http2/codec_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -255,10 +255,13 @@ class Http2CodecImplTestFixture {
.WillRepeatedly(Invoke([&](ResponseEncoder& encoder, bool) -> RequestDecoder& {
response_encoder_ = &encoder;
encoder.getStream().addCallbacks(server_stream_callbacks_);
encoder.getStream().registerCodecEventCallbacks(&server_codec_event_callbacks_);
encoder.getStream().setFlushTimeout(std::chrono::milliseconds(30000));
request_decoder_.setResponseEncoder(response_encoder_);
return request_decoder_;
}));
EXPECT_CALL(server_codec_event_callbacks_, onCodecEncodeComplete())
.Times(::testing::AnyNumber());

ON_CALL(server_connection_.dispatcher_, trackedObjectStackIsEmpty())
.WillByDefault(Return(true));
Expand Down Expand Up @@ -436,6 +439,7 @@ class Http2CodecImplTestFixture {
MockRequestDecoderShimWithUhv request_decoder_;
ResponseEncoder* response_encoder_{};
MockStreamCallbacks server_stream_callbacks_;
MockCodecEventCallbacks server_codec_event_callbacks_;
// Corrupt a metadata frame payload.
bool corrupt_metadata_frame_ = false;
bool expect_buffered_data_on_teardown_ = false;
Expand Down Expand Up @@ -971,6 +975,11 @@ TEST_P(Http2CodecImplTest, RefusedStreamReset) {
EXPECT_CALL(server_stream_callbacks_,
onResetStream(StreamResetReason::LocalRefusedStreamReset, _));
EXPECT_CALL(callbacks, onResetStream(StreamResetReason::RemoteRefusedStreamReset, _));
if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http2_propagate_reset_events")) {
EXPECT_CALL(server_codec_event_callbacks_, onCodecLowLevelReset()).Times(2);
} else {
EXPECT_CALL(server_codec_event_callbacks_, onCodecLowLevelReset());
}
response_encoder_->getStream().resetStream(StreamResetReason::LocalRefusedStreamReset);
driveToCompletion();
}
Expand Down Expand Up @@ -1721,6 +1730,7 @@ TEST_P(Http2CodecImplDeferredResetTest, DeferredResetServerIfLocalEndStreamBefor
EXPECT_CALL(*flush_timer, enableTimer(std::chrono::milliseconds(30000), _));
response_encoder_->encodeData(body, true);
EXPECT_CALL(server_stream_callbacks_, onResetStream(StreamResetReason::LocalReset, _));
EXPECT_CALL(server_codec_event_callbacks_, onCodecLowLevelReset());
EXPECT_CALL(*flush_timer, disableTimer());
response_encoder_->getStream().resetStream(StreamResetReason::LocalReset);
}));
Expand Down Expand Up @@ -1760,6 +1770,7 @@ TEST_P(Http2CodecImplDeferredResetTest, LargeDataDeferredResetServerIfLocalEndSt
EXPECT_CALL(*flush_timer, enableTimer(std::chrono::milliseconds(30000), _));
response_encoder_->encodeData(body, true);
EXPECT_CALL(server_stream_callbacks_, onResetStream(StreamResetReason::LocalReset, _));
EXPECT_CALL(server_codec_event_callbacks_, onCodecLowLevelReset());
EXPECT_CALL(*flush_timer, disableTimer());
response_encoder_->getStream().resetStream(StreamResetReason::LocalReset);
}));
Expand Down Expand Up @@ -1796,6 +1807,7 @@ TEST_P(Http2CodecImplDeferredResetTest, NoDeferredResetServerIfResetBeforeLocalE
EXPECT_CALL(server_stream_callbacks_, onAboveWriteBufferHighWatermark()).Times(AnyNumber());
response_encoder_->encodeData(body, false);
EXPECT_CALL(server_stream_callbacks_, onResetStream(StreamResetReason::LocalReset, _));
EXPECT_CALL(server_codec_event_callbacks_, onCodecLowLevelReset());
response_encoder_->getStream().resetStream(StreamResetReason::LocalReset);
}));
EXPECT_TRUE(request_encoder_->encodeHeaders(request_headers, false).ok());
Expand Down Expand Up @@ -2003,6 +2015,11 @@ TEST_P(Http2CodecImplFlowControlTest, EarlyResetRestoresWindow) {
server_->onUnderlyingConnectionAboveWriteBufferHighWatermark();
server_->onUnderlyingConnectionBelowWriteBufferLowWatermark();
}));
if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http2_propagate_reset_events")) {
EXPECT_CALL(server_codec_event_callbacks_, onCodecLowLevelReset()).Times(2);
} else {
EXPECT_CALL(server_codec_event_callbacks_, onCodecLowLevelReset());
}
response_encoder_->getStream().resetStream(StreamResetReason::LocalRefusedStreamReset);
driveToCompletion();

Expand Down Expand Up @@ -2197,6 +2214,7 @@ TEST_P(Http2CodecImplFlowControlTest, TrailingHeadersLargeServerBodyFlushTimeout
// Invoke a stream flush timeout. Make sure we don't get a reset locally for higher layers but
// we do get a reset on the client.
EXPECT_CALL(server_stream_callbacks_, onResetStream(_, _)).Times(0);
EXPECT_CALL(server_codec_event_callbacks_, onCodecLowLevelReset());
EXPECT_CALL(client_stream_callbacks, onResetStream(StreamResetReason::RemoteReset, _));
flush_timer->invokeCallback();
driveToCompletion();
Expand Down Expand Up @@ -2237,6 +2255,7 @@ TEST_P(Http2CodecImplFlowControlTest, LargeServerBodyFlushTimeout) {
// Invoke a stream flush timeout. Make sure we don't get a reset locally for higher layers but
// we do get a reset on the client.
EXPECT_CALL(server_stream_callbacks_, onResetStream(_, _)).Times(0);
EXPECT_CALL(server_codec_event_callbacks_, onCodecLowLevelReset());
EXPECT_CALL(client_stream_callbacks, onResetStream(StreamResetReason::RemoteReset, _));
flush_timer->invokeCallback();
driveToCompletion();
Expand Down Expand Up @@ -2405,6 +2424,7 @@ TEST_P(Http2CodecImplFlowControlTest, RstStreamOnPendingFlushTimeoutFlood) {

EXPECT_FALSE(violation_callback->enabled_);
EXPECT_CALL(server_stream_callbacks_, onResetStream(_, _));
EXPECT_CALL(server_codec_event_callbacks_, onCodecLowLevelReset());

// Pending flush timeout causes RST_STREAM to be sent and overflow the outbound frame queue.
flush_timer->invokeCallback();
Expand Down Expand Up @@ -2751,6 +2771,11 @@ TEST_P(Http2CodecImplTest, LargeRequestHeadersInvokeResetStream) {
std::string long_string = std::string(63 * 1024, 'q');
request_headers.addCopy("big", long_string);
EXPECT_CALL(server_stream_callbacks_, onResetStream(_, _));
if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http2_propagate_reset_events")) {
EXPECT_CALL(server_codec_event_callbacks_, onCodecLowLevelReset());
} else {
EXPECT_CALL(server_codec_event_callbacks_, onCodecLowLevelReset()).Times(0);
}
EXPECT_TRUE(request_encoder_->encodeHeaders(request_headers, false).ok());
driveToCompletion();
}
Expand Down Expand Up @@ -2798,6 +2823,11 @@ TEST_P(Http2CodecImplTest, HeaderNameWithUnderscoreAreRejected) {
request_headers.addCopy("bad_header", "something");

EXPECT_CALL(server_stream_callbacks_, onResetStream(_, _));
if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http2_propagate_reset_events")) {
EXPECT_CALL(server_codec_event_callbacks_, onCodecLowLevelReset());
} else {
EXPECT_CALL(server_codec_event_callbacks_, onCodecLowLevelReset()).Times(0);
}
EXPECT_TRUE(request_encoder_->encodeHeaders(request_headers, false).ok());
driveToCompletion();
EXPECT_EQ(
Expand Down Expand Up @@ -2852,6 +2882,11 @@ TEST_P(Http2CodecImplTest, ManyRequestHeadersInvokeResetStream) {
request_headers.addCopy(std::to_string(i), "");
}
EXPECT_CALL(server_stream_callbacks_, onResetStream(_, _));
if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http2_propagate_reset_events")) {
EXPECT_CALL(server_codec_event_callbacks_, onCodecLowLevelReset());
} else {
EXPECT_CALL(server_codec_event_callbacks_, onCodecLowLevelReset()).Times(0);
}
EXPECT_TRUE(request_encoder_->encodeHeaders(request_headers, false).ok());
driveToCompletion();
}
Expand Down Expand Up @@ -3668,6 +3703,7 @@ TEST_P(Http2CodecImplTest, ResetStreamCausesOutboundFlood) {

EXPECT_FALSE(violation_callback->enabled_);
EXPECT_CALL(server_stream_callbacks_, onResetStream(StreamResetReason::RemoteReset, _));
EXPECT_CALL(server_codec_event_callbacks_, onCodecLowLevelReset());

server_->getStream(1)->resetStream(StreamResetReason::RemoteReset);

Expand Down Expand Up @@ -3704,6 +3740,11 @@ TEST_P(Http2CodecImplTest, ConnectTest) {

EXPECT_CALL(callbacks, onResetStream(StreamResetReason::ConnectError, _));
EXPECT_CALL(server_stream_callbacks_, onResetStream(StreamResetReason::ConnectError, _));
if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http2_propagate_reset_events")) {
EXPECT_CALL(server_codec_event_callbacks_, onCodecLowLevelReset()).Times(2);
} else {
EXPECT_CALL(server_codec_event_callbacks_, onCodecLowLevelReset());
}
response_encoder_->getStream().resetStream(StreamResetReason::ConnectError);
driveToCompletion();
}
Expand Down
3 changes: 3 additions & 0 deletions test/mocks/http/mocks.cc
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ MockFilterManagerCallbacks::~MockFilterManagerCallbacks() = default;
MockStreamCallbacks::MockStreamCallbacks() = default;
MockStreamCallbacks::~MockStreamCallbacks() = default;

MockCodecEventCallbacks::MockCodecEventCallbacks() = default;
MockCodecEventCallbacks::~MockCodecEventCallbacks() = default;

MockServerConnection::MockServerConnection() {
ON_CALL(*this, protocol()).WillByDefault(Invoke([this]() { return protocol_; }));
}
Expand Down
9 changes: 9 additions & 0 deletions test/mocks/http/mocks.h
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,15 @@ class MockStreamCallbacks : public StreamCallbacks {
MOCK_METHOD(void, onBelowWriteBufferLowWatermark, ());
};

class MockCodecEventCallbacks : public CodecEventCallbacks {
public:
MockCodecEventCallbacks();
~MockCodecEventCallbacks();

MOCK_METHOD(void, onCodecEncodeComplete, ());
MOCK_METHOD(void, onCodecLowLevelReset, ());
};

class MockServerConnection : public ServerConnection {
public:
MockServerConnection();
Expand Down

0 comments on commit e294469

Please sign in to comment.