Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow multiplexed upstream servers to half close the stream before the downstream #34461

Merged
merged 37 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
7263c53
Allow multiplexed upstream servers to half close the stream before th…
yanavlasov May 31, 2024
fd17760
Address comments
yanavlasov Jun 21, 2024
723ed65
Merge branch 'main' into allow-upstream-half-close
yanavlasov Jun 21, 2024
1d8172c
Address comments
yanavlasov Jun 22, 2024
3ba7cc4
Clarify comment
yanavlasov Jun 24, 2024
6133efc
Reuse isHalfCloseEnabled callback
yanavlasov Jun 24, 2024
fcf2a76
Add stopDecoding
yanavlasov Jun 24, 2024
f24bc34
Merge branch 'main' into allow-upstream-half-close
yanavlasov Jun 25, 2024
0e881dc
Merge branch 'main' into allow-upstream-half-close
yanavlasov Jun 28, 2024
b7b746f
Fixing tests
yanavlasov Jul 26, 2024
af60f0b
Address post merge test failures
yanavlasov Jul 30, 2024
01ae68b
Merge branch 'main' into allow-upstream-half-close
yanavlasov Jul 30, 2024
1f269a4
Post merge fix
yanavlasov Jul 30, 2024
1b961df
Fix gcc build error
yanavlasov Jul 30, 2024
8ae88a0
WiP
yanavlasov Jul 30, 2024
d8aabba
Merge branch 'main' into allow-upstream-half-close
yanavlasov Aug 2, 2024
2166fcb
Post merge fixes
yanavlasov Aug 2, 2024
e2deca2
WiP
yanavlasov Aug 8, 2024
9e9428f
Make ending filters in the middle of the filter chain work. p1
yanavlasov Aug 9, 2024
71ce1f0
Make encoding filters in the middle of the filter chain work. p2
yanavlasov Aug 9, 2024
68c807e
Update comments
yanavlasov Aug 12, 2024
9c308ba
Merge branch 'main' into allow-upstream-half-close
yanavlasov Aug 12, 2024
2d8cb15
post merge fixes
yanavlasov Aug 12, 2024
e1c00d0
Update comment
yanavlasov Aug 12, 2024
018a3e4
Update comments
yanavlasov Aug 12, 2024
9a692f3
Address comments
yanavlasov Aug 15, 2024
bd30f81
Merge branch 'main' into allow-upstream-half-close
yanavlasov Aug 15, 2024
b9948ab
Post merge fix
yanavlasov Aug 15, 2024
e224738
Update comment
yanavlasov Aug 15, 2024
e4ae6b4
Merge branch 'main' into allow-upstream-half-close
yanavlasov Aug 16, 2024
d5320da
Fix docs
yanavlasov Aug 16, 2024
b804cb5
Disable upstream filter tests
yanavlasov Aug 16, 2024
4b305d9
Add coverage
yanavlasov Aug 16, 2024
9155e49
Address comments
yanavlasov Aug 21, 2024
49b6162
Merge branch 'main' into allow-upstream-half-close
yanavlasov Aug 23, 2024
c02ac47
Add asserts
yanavlasov Aug 23, 2024
d965ff6
Merge branch 'main' into allow-upstream-half-close
yanavlasov Aug 26, 2024
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
7 changes: 7 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ behavior_changes:
:ref:`TlvsMetadata type <envoy_v3_api_msg_data.core.v3.TlvsMetadata>`.
This change can be temporarily disabled by setting the runtime flag
``envoy.reloadable_features.use_typed_metadata_in_proxy_protocol_listener`` to ``false``.
- area: http
change: |
Allow HTTP/2 (and HTTP/3) upstream servers to half close the stream before the downstream. This enables bidirectional
gRPC streams where server completes streaming before the client. Behavior of HTTP/1 or TCP proxy upstream servers is
unchanged and the stream is reset if the upstream server completes response before the downstream. The stream is also
reset if the upstream server responds with an error status before the downstream. This behavior is disabled by default
and can be enabled by setting the ``envoy.reloadable_features.allow_multiplexed_upstream_half_close`` runtime key to true.

minor_behavior_changes:
# *Changes that may cause incompatibilities for some users, but should not for most*
Expand Down
4 changes: 3 additions & 1 deletion envoy/http/stream_reset_handler.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ enum class StreamResetReason {
// Received payload did not conform to HTTP protocol.
ProtocolError,
// If the stream was locally reset by the Overload Manager.
OverloadManager
OverloadManager,
// If stream was locally reset due to HTTP/1 upstream half closing before downstream.
Http1PrematureUpstreamHalfClose,
KBaichoo marked this conversation as resolved.
Show resolved Hide resolved
};

/**
Expand Down
17 changes: 10 additions & 7 deletions source/common/http/async_client_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,16 @@ void AsyncStreamImpl::encodeHeaders(ResponseHeaderMapPtr&& headers, bool end_str
encoded_response_headers_ = true;
stream_callbacks_.onHeaders(std::move(headers), end_stream);
closeRemote(end_stream);
// At present, the router cleans up stream state as soon as the remote is closed, making a
// half-open local stream unsupported and dangerous. Ensure we close locally to trigger completion
// and keep things consistent. Another option would be to issue a stream reset here if local isn't
// yet closed, triggering cleanup along a more standardized path. However, this would require
// additional logic to handle the response completion and subsequent reset, and run the risk of
// being interpreted as a failure, when in fact no error has necessarily occurred. Gracefully
// closing seems most in-line with behavior elsewhere in Envoy for now.
// At present, the AsyncStream is always fully closed when the server half closes the stream.
// This is the case even when allow_multiplexed_upstream_half_close runtime flag is set, as there
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: remove flag name so we don't have to remember to update the comment when we remove the flag.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

// are currently no known use cases where early server half close needs to be supported.
//
// Always ensure we close locally to trigger completion. Another option would be to issue a stream
// reset here if local isn't yet closed, triggering cleanup along a more standardized path.
// However, this would require additional logic to handle the response completion and subsequent
// reset, and run the risk of being interpreted as a failure, when in fact no error has
// necessarily occurred. Gracefully closing seems most in-line with behavior elsewhere in Envoy
// for now.
closeLocal(end_stream);
}

Expand Down
4 changes: 3 additions & 1 deletion source/common/http/conn_manager_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,9 @@ class ConnectionManagerImpl : Logger::Loggable<Logger::Id::http>,
OptRef<const Tracing::Config> tracingConfig() const override;
const ScopeTrackedObject& scope() override;
OptRef<DownstreamStreamFilterCallbacks> downstreamCallbacks() override { return *this; }
bool isHalfCloseEnabled() override { return false; }
bool isHalfCloseEnabled() override {
return filter_manager_.allowUpstreamHalfClose() ? true : false;
Copy link
Contributor

Choose a reason for hiding this comment

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

return filter_manager_.allowUpstreamHalfClose()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

}

// DownstreamStreamFilterCallbacks
void setRoute(Router::RouteConstSharedPtr route) override;
Expand Down
1 change: 1 addition & 0 deletions source/common/http/conn_pool_base.cc
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ void MultiplexedActiveClientBase::onStreamReset(Http::StreamResetReason reason)
case StreamResetReason::RemoteRefusedStreamReset:
case StreamResetReason::Overflow:
case StreamResetReason::ConnectError:
case StreamResetReason::Http1PrematureUpstreamHalfClose:
break;
}
}
Expand Down
62 changes: 56 additions & 6 deletions source/common/http/filter_manager.cc
Original file line number Diff line number Diff line change
Expand Up @@ -903,16 +903,28 @@ FilterManager::commonEncodePrefix(ActiveStreamEncoderFilter* filter, bool end_st
FilterIterationStartState filter_iteration_start_state) {
// Only do base state setting on the initial call. Subsequent calls for filtering do not touch
// the base state.
ENVOY_STREAM_LOG(trace, "commonEncodePrefix end_stream: {}, isHalfCloseEnabled: {}", *this,
end_stream, filter_manager_callbacks_.isHalfCloseEnabled());
ENVOY_STREAM_LOG(trace,
"commonEncodePrefix end_stream: {}, isHalfCloseEnabled: {}, force_close: {}",
*this, end_stream, filter_manager_callbacks_.isHalfCloseEnabled(),
static_cast<bool>(state_.force_close_stream_));
Copy link
Contributor

Choose a reason for hiding this comment

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

add to dumpstate instead?

Copy link

Choose a reason for hiding this comment

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

I don't have a good understanding of dumpstate but just quickly read #7300. if the dumpstate need to be invoked by human or at crash automatically, I hope we keep this log for stream level debugging (while agreed that this information should be added on the other place too, if needed). It will be super helpful to users who debug streams with trace log, why their stream got reset or why not.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why not both? Added to both places.

if (filter == nullptr) {
// half close is enabled in case tcp proxying is done with http1 encoder. In this case, we
// should not set the local_complete_ flag to true when end_stream is true.
// setting local_complete_ to true will cause any data sent in the upstream direction to be
// dropped.
if (end_stream && !filter_manager_callbacks_.isHalfCloseEnabled()) {
ASSERT(!state_.local_complete_);
state_.local_complete_ = true;
if (allow_upstream_half_close_) {
if (end_stream) {
state_.encoder_end_stream_ = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

how is this different from remote_decode_complete? it's set when we see end stream. I think it's just badly named.

Copy link
Contributor

Choose a reason for hiding this comment

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

can we explicitly comment how this differs from local_complete as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is a bit messy. I have added more comments to local_complete and encoder_end_stream and left a TODO to always use encoder_end_stream for tracking end_stream state in encoder filer iteration.

I will do this refactor when the runtime flag is removed.

if (!filter_manager_callbacks_.isHalfCloseEnabled() || state_.force_close_stream_) {
ASSERT(!state_.local_complete_);
state_.local_complete_ = true;
}
}
} else {
if (end_stream && !filter_manager_callbacks_.isHalfCloseEnabled()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like we should be able to share code between these two

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My goal here was to make it clear that the old behavior is fully preserved when allow_upstream_half_close_ == false. Refactor can save a couple of lines of code but will make it harder to see whether behavior has changed or not.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm concerned that we have allow_upstream_half_close and isHalfCloseEnabled.
if filter_manager_callbacks_.isHalfCloseEnabled isn't working as intended can we just fix?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have changed FM to always use filter_manager_callbacks_.isHalfCloseEnabled

I will refactor FM to just always support independent half closes when runtime flag is removed.

ASSERT(!state_.local_complete_);
state_.local_complete_ = true;
}
}
return encoder_filters_.begin();
}
Expand Down Expand Up @@ -960,6 +972,8 @@ void DownstreamFilterManager::sendLocalReply(
ASSERT(!state_.under_on_local_reply_);
const bool is_head_request = state_.is_head_request_;
const bool is_grpc_request = state_.is_grpc_request_;
// Local reply closes the stream even if downstream is not half closed.
state_.force_close_stream_ = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

instead of setting a boolean here (which adds to the booleans we need to keep track of in all places, WDYT of adding a stopSending function which we call both on local reply and on receipt of error response, which stops downstream processing?

I think that'd then be reuseable for the "stream reset preceding data" bug as we could convert no_error stream resets into stop sending calls.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense. I have reworked this a bit and added the stopDecoding method.


// Stop filter chain iteration if local reply was sent while filter decoding or encoding callbacks
// are running.
Expand Down Expand Up @@ -1284,6 +1298,14 @@ void FilterManager::encodeHeaders(ActiveStreamEncoderFilter* filter, ResponseHea

const bool modified_end_stream = (end_stream && continue_data_entry == encoder_filters_.end());
state_.non_100_response_headers_encoded_ = true;
if (allow_upstream_half_close_) {
const uint64_t response_status = Http::Utility::getResponseStatus(headers);
if (!(Http::CodeUtility::is2xx(response_status) || Http::CodeUtility::is1xx(response_status))) {
// Even if upstream half close is enabled the stream is closed on error responses from the
// server.
state_.force_close_stream_ = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

this seems like a really weird place to put this. why not on all local resets and or local replies?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I could be overthinking here. My thought was to allow independent half close only when server response is 2xx. If it is error (ore redirect) response Envoy fully closes the stream. This bit of code treats upstream error response as a sendLocalReply.

I was thinking about case where upstream response with error to a POST with large body. Presently Envoy 'helps' gracefully stop body upload. With independent half closes the upstream will have to follow up an error response with a reset, otherwise upload will continue.

I'm not sure if Envoy needs to do this. We could get rid of this case and make it so independent half close is supported regardless of the upstream response status. And the perhaps add a flag if Envoy needs to support this corner case. Let me know what you think.

}
}
filter_manager_callbacks_.encodeHeaders(headers, modified_end_stream);
if (state_.saw_downstream_reset_) {
return;
Expand Down Expand Up @@ -1489,6 +1511,27 @@ void FilterManager::maybeEndEncode(bool end_stream) {
if (end_stream) {
ASSERT(!state_.remote_encode_complete_);
state_.remote_encode_complete_ = true;
if (allow_upstream_half_close_) {
maybeCloseStream();
Copy link
Contributor

Choose a reason for hiding this comment

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

should this be half close? closeUpstream?

should this logic all be in maybeClose?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It closes downstream and upstream streams. I have renamed it checkAndCloseStreamIfFullyClosed to hopefully make it clearer.

} else {
state_.stream_closed_ = true;
filter_manager_callbacks_.endStream();
}
}
}

void FilterManager::maybeCloseStream() {
ASSERT(allow_upstream_half_close_);
if (state_.stream_closed_) {
return;
}

// If upstream half close is enabled then close the stream either when force close
// is set (i.e local reply) or when both server and client half closed.
if (state_.remote_encode_complete_ &&
(state_.remote_decode_complete_ || state_.force_close_stream_)) {
state_.stream_closed_ = true;
ENVOY_STREAM_LOG(trace, "closing stream", *this);
filter_manager_callbacks_.endStream();
}
}
Expand Down Expand Up @@ -1671,7 +1714,14 @@ Buffer::InstancePtr ActiveStreamEncoderFilter::createBuffer() {
Buffer::InstancePtr& ActiveStreamEncoderFilter::bufferedData() {
return parent_.buffered_response_data_;
}
bool ActiveStreamEncoderFilter::complete() { return parent_.state_.local_complete_; }
bool ActiveStreamEncoderFilter::complete() {
// This is used for determining end_stream flag when iterating encoder filter chain.
// When the upstream half close is enabled the local complete may not be set when
// end stream is observed on the encode path in case upstream half closes before the
// downstream.
return parent_.allow_upstream_half_close_ ? parent_.state_.encoder_end_stream_
: parent_.state_.local_complete_;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we may need a rename / comment PR before we land this because I think this would be more clear if local_complete were more clear in the first place.

Copy link

Choose a reason for hiding this comment

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

or after? (when we consider the urgency of this issue)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have updated comments for local_complete_ and encoder_end_stream_ flags

}
bool ActiveStreamEncoderFilter::has1xxHeaders() {
return parent_.state_.has_1xx_headers_ && !continued_1xx_headers_;
}
Expand Down
40 changes: 32 additions & 8 deletions source/common/http/filter_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,9 @@ class FilterManager : public ScopeTrackedObject,
proxy_100_continue_(proxy_100_continue), buffer_limit_(buffer_limit),
filter_chain_factory_(filter_chain_factory),
no_downgrade_to_canonical_name_(Runtime::runtimeFeatureEnabled(
"envoy.reloadable_features.no_downgrade_to_canonical_name")) {}
"envoy.reloadable_features.no_downgrade_to_canonical_name")),
allow_upstream_half_close_(Runtime::runtimeFeatureEnabled(
"envoy.reloadable_features.allow_multiplexed_upstream_half_close")) {}
~FilterManager() override {
ASSERT(state_.destroyed_);
ASSERT(state_.filter_call_state_ == 0);
Expand Down Expand Up @@ -748,6 +750,9 @@ class FilterManager : public ScopeTrackedObject,
void decodeHeaders(RequestHeaderMap& headers, bool end_stream) {
state_.remote_decode_complete_ = end_stream;
decodeHeaders(nullptr, headers, end_stream);
if (allow_upstream_half_close_) {
Copy link
Contributor

Choose a reason for hiding this comment

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

can this be cone in maybeendDecode so we don't have to do it in all headers/body/data cases? if maybeEndStream did checks on allow_upstream_half_close_ I think it'd simplify things?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved the flag check to checkAndCloseStreamIfFullyClosed

maybeCloseStream();
}
}

/**
Expand All @@ -758,6 +763,9 @@ class FilterManager : public ScopeTrackedObject,
void decodeData(Buffer::Instance& data, bool end_stream) {
state_.remote_decode_complete_ = end_stream;
decodeData(nullptr, data, end_stream, FilterIterationStartState::CanStartFromCurrent);
if (allow_upstream_half_close_) {
maybeCloseStream();
}
}

/**
Expand All @@ -767,6 +775,9 @@ class FilterManager : public ScopeTrackedObject,
void decodeTrailers(RequestTrailerMap& trailers) {
state_.remote_decode_complete_ = true;
decodeTrailers(nullptr, trailers);
if (allow_upstream_half_close_) {
maybeCloseStream();
}
}

/**
Expand All @@ -783,6 +794,8 @@ class FilterManager : public ScopeTrackedObject,
*/
void maybeEndEncode(bool end_stream);

void maybeCloseStream();

virtual void sendLocalReply(Code code, absl::string_view body,
const std::function<void(ResponseHeaderMap& headers)>& modify_headers,
const absl::optional<Grpc::Status::GrpcStatus> grpc_status,
Expand Down Expand Up @@ -861,19 +874,24 @@ class FilterManager : public ScopeTrackedObject,
bool sawDownstreamReset() { return state_.saw_downstream_reset_; }

virtual bool shouldLoadShed() { return false; };
bool allowUpstreamHalfClose() const { return allow_upstream_half_close_; }

protected:
struct State {
State()
: remote_decode_complete_(false), remote_encode_complete_(false), local_complete_(false),
has_1xx_headers_(false), created_filter_chain_(false), is_head_request_(false),
is_grpc_request_(false), non_100_response_headers_encoded_(false),
under_on_local_reply_(false), decoder_filter_chain_aborted_(false),
encoder_filter_chain_aborted_(false), saw_downstream_reset_(false) {}
: remote_decode_complete_(false), remote_encode_complete_(false),
encoder_end_stream_(false), local_complete_(false), has_1xx_headers_(false),
created_filter_chain_(false), is_head_request_(false), is_grpc_request_(false),
non_100_response_headers_encoded_(false), under_on_local_reply_(false),
decoder_filter_chain_aborted_(false), encoder_filter_chain_aborted_(false),
saw_downstream_reset_(false), stream_closed_(false), force_close_stream_(false) {}
uint32_t filter_call_state_{0};

bool remote_decode_complete_ : 1;
bool remote_encode_complete_ : 1;
bool remote_decode_complete_ : 1; // Set when decoder filter chain iteration has completed.
bool remote_encode_complete_ : 1; // Set when encoder filter chain iteration has completed.
bool encoder_end_stream_ : 1; // This is set the first time the end_stream is observed during
KBaichoo marked this conversation as resolved.
Show resolved Hide resolved
// encoder filter chain iteration and used to set the end_stream
// flag when resuming encoder filter chain iteration.
bool local_complete_ : 1; // This indicates that local is complete prior to filter processing.
// A filter can still stop the stream from being complete as seen
// by the codec.
Expand All @@ -893,6 +911,11 @@ class FilterManager : public ScopeTrackedObject,
bool decoder_filter_chain_aborted_ : 1;
bool encoder_filter_chain_aborted_ : 1;
bool saw_downstream_reset_ : 1;
bool stream_closed_ : 1; // Set when both remote_decode_complete_ and remote_encode_complete_ is
Copy link
Contributor

Choose a reason for hiding this comment

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

why not just use a helper function which checks both booleans?

// true observed for the first time and prevents ending the stream
// multiple times. Only set when allow_upstream_half_close is enabled.
bool force_close_stream_ : 1; // Set to indicate that stream should be closed due to either
Copy link
Contributor

Choose a reason for hiding this comment

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

should we stick with half close naming for consistency? support_half_close?

if not consider renaming to something like should_force_close

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed to should_force_close_stream_

// local reply or error response from the server.

// The following 3 members are booleans rather than part of the space-saving bitfield as they
// are passed as arguments to functions expecting bools. Extend State using the bitfield
Expand Down Expand Up @@ -1086,6 +1109,7 @@ class FilterManager : public ScopeTrackedObject,
State state_;

const bool no_downgrade_to_canonical_name_{};
const bool allow_upstream_half_close_{};
};

// The DownstreamFilterManager has explicit handling to send local replies.
Expand Down
13 changes: 12 additions & 1 deletion source/common/http/http1/codec_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1436,7 +1436,9 @@ ClientConnectionImpl::ClientConnectionImpl(Network::Connection& connection, Code
[&]() -> void { this->onBelowLowWatermark(); },
[&]() -> void { this->onAboveHighWatermark(); },
[]() -> void { /* TODO(adisuissa): handle overflow watermark */ })),
passing_through_proxy_(passing_through_proxy) {
passing_through_proxy_(passing_through_proxy),
force_reset_on_premature_upstream_half_close_(Runtime::runtimeFeatureEnabled(
"envoy.reloadable_features.allow_multiplexed_upstream_half_close")) {
owned_output_buffer_->setWatermarks(connection.bufferLimit());
// Inform parent
output_buffer_ = owned_output_buffer_.get();
Expand Down Expand Up @@ -1587,6 +1589,15 @@ CallbackResult ClientConnectionImpl::onMessageCompleteBase() {
response.decoder_->decodeData(buffer, true);
}

if (force_reset_on_premature_upstream_half_close_) {
KBaichoo marked this conversation as resolved.
Show resolved Hide resolved
// H/1 connections are always reset if upstream is done before downstream.
// When the allow_multiplexed_upstream_half_close is enabled the router filter does not
// reset streams where upstream half closed before downstream. In this case the H/1 codec
// has to reset the stream.
ENVOY_CONN_LOG(trace, "Resetting stream due to premature H/1 upstream close.", connection_);
response.encoder_.runResetCallbacks(StreamResetReason::Http1PrematureUpstreamHalfClose);
}

// Reset to ensure no information from one requests persists to the next.
pending_response_.reset();
headers_or_trailers_.emplace<ResponseHeaderMapPtr>(nullptr);
Expand Down
2 changes: 2 additions & 0 deletions source/common/http/http1/codec_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,8 @@ class ClientConnectionImpl : public ClientConnection, public ConnectionImpl {
// True if the upstream connection is pointed at an HTTP/1.1 proxy, and
// plaintext HTTP should be sent with fully qualified URLs.
bool passing_through_proxy_ = false;

const bool force_reset_on_premature_upstream_half_close_{};
};

} // namespace Http1
Expand Down
2 changes: 2 additions & 0 deletions source/common/http/utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1060,6 +1060,8 @@ const std::string Utility::resetReasonToString(const Http::StreamResetReason res
return "protocol error";
case Http::StreamResetReason::OverloadManager:
return "overload manager reset";
case Http::StreamResetReason::Http1PrematureUpstreamHalfClose:
return "HTTP/1 premature upstream half close";
}

return "";
Expand Down
17 changes: 11 additions & 6 deletions source/common/router/router.cc
Original file line number Diff line number Diff line change
Expand Up @@ -751,9 +751,9 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers,
// will never transition from false to true.
bool can_use_http3 =
!transport_socket_options_ || !transport_socket_options_->http11ProxyInfo().has_value();
UpstreamRequestPtr upstream_request =
std::make_unique<UpstreamRequest>(*this, std::move(generic_conn_pool), can_send_early_data,
can_use_http3, false /*enable_half_close*/);
UpstreamRequestPtr upstream_request = std::make_unique<UpstreamRequest>(
*this, std::move(generic_conn_pool), can_send_early_data, can_use_http3,
allow_multiplexed_upstream_half_close_ /*enable_half_close*/);
LinkedList::moveIntoList(std::move(upstream_request), upstream_requests_);
upstream_requests_.front()->acceptHeadersFromRouter(end_stream);
if (streaming_shadows_) {
Expand Down Expand Up @@ -1441,6 +1441,7 @@ Filter::streamResetReasonToResponseFlag(Http::StreamResetReason reset_reason) {
return StreamInfo::CoreResponseFlag::UpstreamConnectionTermination;
case Http::StreamResetReason::LocalReset:
case Http::StreamResetReason::LocalRefusedStreamReset:
case Http::StreamResetReason::Http1PrematureUpstreamHalfClose:
return StreamInfo::CoreResponseFlag::LocalReset;
case Http::StreamResetReason::Overflow:
return StreamInfo::CoreResponseFlag::UpstreamOverflow;
Expand Down Expand Up @@ -1722,6 +1723,10 @@ void Filter::onUpstreamMetadata(Http::MetadataMapPtr&& metadata_map) {

void Filter::onUpstreamComplete(UpstreamRequest& upstream_request) {
if (!downstream_end_stream_) {
if (allow_multiplexed_upstream_half_close_) {
// Continue request if downstream is not done yet.
KBaichoo marked this conversation as resolved.
Show resolved Hide resolved
return;
Copy link
Contributor

Choose a reason for hiding this comment

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

While we shouldn't be resetting the upstream, we do also skip over doing these various timing, outlier detection, etc. When will we end up updating them in this case when allow_multiplied_upstream_half_close is true?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Never. This code updates the response_time which is response_complete_time - request_complete_time . It is negative for the case where response completes before request and budget percentage computations will be off. Histograms record uint64_t so these will end up being some large values.

Since the response_time becomes nonsensical in this case, I decided to skip recording the stats.

}
upstream_request.resetStream();
}
Event::Dispatcher& dispatcher = callbacks_->dispatcher();
Expand Down Expand Up @@ -1994,9 +1999,9 @@ void Filter::doRetry(bool can_send_early_data, bool can_use_http3, TimeoutRetry
cleanup();
return;
}
UpstreamRequestPtr upstream_request =
std::make_unique<UpstreamRequest>(*this, std::move(generic_conn_pool), can_send_early_data,
can_use_http3, false /*enable_tcp_tunneling*/);
UpstreamRequestPtr upstream_request = std::make_unique<UpstreamRequest>(
*this, std::move(generic_conn_pool), can_send_early_data, can_use_http3,
allow_multiplexed_upstream_half_close_ /*enable_half_close*/);

if (include_attempt_count_in_request_) {
downstream_headers_->setEnvoyAttemptCount(attempt_count_);
Expand Down
5 changes: 4 additions & 1 deletion source/common/router/router.h
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,9 @@ class Filter : Logger::Loggable<Logger::Id::router>,
: config_(config), stats_(stats), grpc_request_(false), exclude_http_code_stats_(false),
downstream_response_started_(false), downstream_end_stream_(false), is_retry_(false),
request_buffer_overflowed_(false), streaming_shadows_(Runtime::runtimeFeatureEnabled(
"envoy.reloadable_features.streaming_shadow")) {}
"envoy.reloadable_features.streaming_shadow")),
allow_multiplexed_upstream_half_close_(Runtime::runtimeFeatureEnabled(
"envoy.reloadable_features.allow_multiplexed_upstream_half_close")) {}

~Filter() override;

Expand Down Expand Up @@ -605,6 +607,7 @@ class Filter : Logger::Loggable<Logger::Id::router>,
bool include_timeout_retry_header_in_request_ : 1;
bool request_buffer_overflowed_ : 1;
const bool streaming_shadows_ : 1;
const bool allow_multiplexed_upstream_half_close_ : 1;
};

class ProdFilter : public Filter {
Expand Down
Loading
Loading