-
Notifications
You must be signed in to change notification settings - Fork 5.5k
router: allow retry of streaming/incomplete requests #10725
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
Changes from 1 commit
88f0248
3ccf534
fd0ccc9
e6e1e4b
a5be725
52ff4ad
b922b96
aba43ca
3f6aa36
055fe62
19a74ec
3954bc3
c9f4e38
c4382e2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -667,11 +667,12 @@ void Filter::sendNoHealthyUpstreamResponse() { | |
| } | ||
|
|
||
| Http::FilterDataStatus Filter::decodeData(Buffer::Instance& data, bool end_stream) { | ||
| // upstream_requests_.size() cannot be 0 because we add to it unconditionally | ||
| // in decodeHeaders(). It cannot be > 1 because that only happens when a per | ||
| // upstream_requests_.size() cannot be > 1 because that only happens when a per | ||
| // try timeout occurs with hedge_on_per_try_timeout enabled but the per | ||
| // try timeout timer is not started until onUpstreamComplete(). | ||
| ASSERT(upstream_requests_.size() == 1); | ||
| // try timeout timer is not started until onRequestComplete(). It could be zero | ||
| // if the first request attempt has already failed and a retry is waiting for | ||
| // a backoff timer. | ||
| ASSERT(upstream_requests_.size() <= 1); | ||
|
|
||
| bool buffering = (retry_state_ && retry_state_->enabled()) || !active_shadow_policies_.empty(); | ||
| if (buffering && | ||
|
|
@@ -681,13 +682,31 @@ Http::FilterDataStatus Filter::decodeData(Buffer::Instance& data, bool end_strea | |
| retry_state_.reset(); | ||
| buffering = false; | ||
| active_shadow_policies_.clear(); | ||
|
|
||
| // If we had to abandon buffering and there's no request in progress, abort the request and | ||
| // cleanup. | ||
| if (upstream_requests_.empty()) { | ||
| cleanup(); | ||
| callbacks_->sendLocalReply( | ||
| Http::Code::ServiceUnavailable, "exceeded request buffer limit while retrying upstream", | ||
| modify_headers_, absl::nullopt, | ||
| StreamInfo::ResponseCodeDetails::get() | ||
| .RequestPayloadTooLarge /* TODO: better code than RequestPayloadTooLarge */); | ||
| return Http::FilterDataStatus::StopIterationNoBuffer; | ||
| } | ||
| } | ||
|
|
||
| // If we aren't buffering and there is no active request, an abort should have occurred | ||
| // already. | ||
| ASSERT(buffering || !upstream_requests_.empty()); | ||
|
|
||
| if (buffering) { | ||
| // If we are going to buffer for retries or shadowing, we need to make a copy before encoding | ||
| // since it's all moves from here on. | ||
| Buffer::OwnedImpl copy(data); | ||
| upstream_requests_.front()->encodeData(copy, end_stream); | ||
| if (!upstream_requests_.empty()) { | ||
| Buffer::OwnedImpl copy(data); | ||
| upstream_requests_.front()->encodeData(copy, end_stream); | ||
| } | ||
|
|
||
| // If we are potentially going to retry or shadow this request we need to buffer. | ||
| // This will not cause the connection manager to 413 because before we hit the | ||
|
|
@@ -709,11 +728,12 @@ Http::FilterDataStatus Filter::decodeData(Buffer::Instance& data, bool end_strea | |
| Http::FilterTrailersStatus Filter::decodeTrailers(Http::RequestTrailerMap& trailers) { | ||
| ENVOY_STREAM_LOG(debug, "router decoding trailers:\n{}", *callbacks_, trailers); | ||
|
|
||
| // upstream_requests_.size() cannot be 0 because we add to it unconditionally | ||
| // in decodeHeaders(). It cannot be > 1 because that only happens when a per | ||
| // upstream_requests_.size() cannot be > 1 because that only happens when a per | ||
| // try timeout occurs with hedge_on_per_try_timeout enabled but the per | ||
| // try timeout timer is not started until onUpstreamComplete(). | ||
| ASSERT(upstream_requests_.size() == 1); | ||
| // try timeout timer is not started until onRequestComplete(). It could be zero | ||
| // if the first request attempt has already failed and a retry is waiting for | ||
| // a backoff timer. | ||
| ASSERT(upstream_requests_.size() <= 1); | ||
| downstream_trailers_ = &trailers; | ||
| for (auto& upstream_request : upstream_requests_) { | ||
| upstream_request->encodeTrailers(trailers); | ||
|
|
@@ -724,8 +744,11 @@ Http::FilterTrailersStatus Filter::decodeTrailers(Http::RequestTrailerMap& trail | |
|
|
||
| Http::FilterMetadataStatus Filter::decodeMetadata(Http::MetadataMap& metadata_map) { | ||
| Http::MetadataMapPtr metadata_map_ptr = std::make_unique<Http::MetadataMap>(metadata_map); | ||
| ASSERT(upstream_requests_.size() == 1); | ||
| upstream_requests_.front()->encodeMetadata(std::move(metadata_map_ptr)); | ||
| if (!upstream_requests_.empty()) { | ||
| // TODO: buffer this if there's no request? It doesn't look like the upstream request has | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. router.h: TODO(soya3129): Save metadata for retry, redirect and shadowing case. |
||
| // any handling for metadata in retries already, so I don't think this is a regression. | ||
| upstream_requests_.front()->encodeMetadata(std::move(metadata_map_ptr)); | ||
| } | ||
| return Http::FilterMetadataStatus::Continue; | ||
| } | ||
|
|
||
|
|
@@ -869,7 +892,7 @@ void Filter::onSoftPerTryTimeout(UpstreamRequest& upstream_request) { | |
| RetryStatus retry_status = | ||
| retry_state_->shouldHedgeRetryPerTryTimeout([this]() -> void { doRetry(); }); | ||
|
|
||
| if (retry_status == RetryStatus::Yes && setupRetry()) { | ||
| if (retry_status == RetryStatus::Yes) { | ||
| setupRetry(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. curious, did we double count stats here?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I think so; I found this while investigating this a few months ago. See the Fixed issue in description. |
||
| // Don't increment upstream_host->stats().rq_error_ here, we'll do that | ||
| // later if 1) we hit global timeout or 2) we get bad response headers | ||
|
|
@@ -996,7 +1019,8 @@ bool Filter::maybeRetryReset(Http::StreamResetReason reset_reason, | |
|
|
||
| const RetryStatus retry_status = | ||
| retry_state_->shouldRetryReset(reset_reason, [this]() -> void { doRetry(); }); | ||
| if (retry_status == RetryStatus::Yes && setupRetry()) { | ||
| if (retry_status == RetryStatus::Yes) { | ||
| setupRetry(); | ||
| if (upstream_request.upstreamHost()) { | ||
| upstream_request.upstreamHost()->stats().rq_error_.inc(); | ||
| } | ||
|
|
@@ -1187,7 +1211,8 @@ void Filter::onUpstreamHeaders(uint64_t response_code, Http::ResponseHeaderMapPt | |
| // Capture upstream_host since setupRetry() in the following line will clear | ||
| // upstream_request. | ||
| const auto upstream_host = upstream_request.upstreamHost(); | ||
| if (retry_status == RetryStatus::Yes && setupRetry()) { | ||
| if (retry_status == RetryStatus::Yes) { | ||
| setupRetry(); | ||
| if (!end_stream) { | ||
| upstream_request.resetStream(); | ||
| } | ||
|
|
@@ -1375,21 +1400,10 @@ void Filter::onUpstreamComplete(UpstreamRequest& upstream_request) { | |
| cleanup(); | ||
| } | ||
|
|
||
| bool Filter::setupRetry() { | ||
| // If we responded before the request was complete we don't bother doing a retry. This may not | ||
| // catch certain cases where we are in full streaming mode and we have a connect timeout or an | ||
| // overflow of some kind. However, in many cases deployments will use the buffer filter before | ||
| // this filter which will make this a non-issue. The implementation of supporting retry in cases | ||
| // where the request is not complete is more complicated so we will start with this for now. | ||
| if (!downstream_end_stream_) { | ||
| config_.stats_.rq_retry_skipped_request_not_complete_.inc(); | ||
| return false; | ||
| } | ||
| void Filter::setupRetry() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we should rename, or just do this inline - it's not really setting up anything or performing the retry. |
||
| pending_retries_++; | ||
|
|
||
| ENVOY_STREAM_LOG(debug, "performing retry", *callbacks_); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| bool Filter::setupRedirect(const Http::ResponseHeaderMap& headers, | ||
|
|
@@ -1412,7 +1426,7 @@ bool Filter::setupRedirect(const Http::ResponseHeaderMap& headers, | |
|
|
||
| const StreamInfo::FilterStateSharedPtr& filter_state = callbacks_->streamInfo().filterState(); | ||
|
|
||
| // As with setupRetry, redirects are not supported for streaming requests yet. | ||
| // Redirects are not supported for streaming requests yet. | ||
| if (downstream_end_stream_ && | ||
| !callbacks_->decodingBuffer() && // Redirects with body not yet supported. | ||
| location != nullptr && | ||
|
|
@@ -1454,18 +1468,18 @@ void Filter::doRetry() { | |
| downstream_headers_->setEnvoyAttemptCount(attempt_count_); | ||
| } | ||
|
|
||
| ASSERT(response_timeout_ || timeout_.global_timeout_.count() == 0); | ||
| UpstreamRequest* upstream_request_tmp = upstream_request.get(); | ||
| upstream_request->moveIntoList(std::move(upstream_request), upstream_requests_); | ||
| upstream_requests_.front()->encodeHeaders(!callbacks_->decodingBuffer() && !downstream_trailers_); | ||
| upstream_requests_.front()->encodeHeaders(!callbacks_->decodingBuffer() && | ||
| !downstream_trailers_ && downstream_end_stream_); | ||
| // It's possible we got immediately reset which means the upstream request we just | ||
| // added to the front of the list might have been removed, so we need to check to make | ||
| // sure we don't encodeData on the wrong request. | ||
| if (!upstream_requests_.empty() && (upstream_requests_.front().get() == upstream_request_tmp)) { | ||
| if (callbacks_->decodingBuffer()) { | ||
| // If we are doing a retry we need to make a copy. | ||
| Buffer::OwnedImpl copy(*callbacks_->decodingBuffer()); | ||
| upstream_requests_.front()->encodeData(copy, !downstream_trailers_); | ||
| upstream_requests_.front()->encodeData(copy, !downstream_trailers_ && downstream_end_stream_); | ||
| } | ||
|
|
||
| if (downstream_trailers_) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -299,6 +299,117 @@ TEST_P(ProtocolIntegrationTest, Retry) { | |
| EXPECT_EQ(512U, response->body().size()); | ||
| } | ||
|
|
||
| TEST_P(ProtocolIntegrationTest, RetryStreaming) { | ||
| initialize(); | ||
| codec_client_ = makeHttpConnection(lookupPort("http")); | ||
| auto encoder_decoder = | ||
| codec_client_->startRequest(Http::TestRequestHeaderMapImpl{{":method", "POST"}, | ||
| {":path", "/test/long/url"}, | ||
| {":scheme", "http"}, | ||
| {":authority", "host"}, | ||
| {"x-forwarded-for", "10.0.0.1"}, | ||
| {"x-envoy-retry-on", "5xx"}}); | ||
| auto& encoder = encoder_decoder.first; | ||
| auto& response = encoder_decoder.second; | ||
|
|
||
| // Send some data, but not the entire body. | ||
| std::string data(1024, 'a'); | ||
| Buffer::OwnedImpl send1(data); | ||
| encoder.encodeData(send1, false); | ||
|
|
||
| ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); | ||
| ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); | ||
|
|
||
| // Send back an upstream failure. | ||
| upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "503"}}, false); | ||
|
|
||
| if (fake_upstreams_[0]->httpType() == FakeHttpConnection::Type::HTTP1) { | ||
| ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); | ||
| ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); | ||
| } else { | ||
| ASSERT_TRUE(upstream_request_->waitForReset()); | ||
| } | ||
|
|
||
| // Wait for a retry. Ensure all data, both before and after the retry, is received. | ||
| ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); | ||
|
|
||
| // Finish the request. | ||
| std::string data2(512, 'b'); | ||
| Buffer::OwnedImpl send2(data2); | ||
| encoder.encodeData(send2, true); | ||
| std::string combined_request_data = data + data2; | ||
| ASSERT_TRUE(upstream_request_->waitForData(*dispatcher_, combined_request_data)); | ||
|
|
||
| upstream_request_->encodeHeaders(default_response_headers_, false); | ||
| upstream_request_->encodeData(512, true); | ||
|
|
||
| response->waitForEndStream(); | ||
| EXPECT_TRUE(upstream_request_->complete()); | ||
| EXPECT_EQ(combined_request_data.size(), upstream_request_->bodyLength()); | ||
|
|
||
| EXPECT_TRUE(response->complete()); | ||
| EXPECT_EQ("200", response->headers().Status()->value().getStringView()); | ||
| EXPECT_EQ(512U, response->body().size()); | ||
| } | ||
|
|
||
| TEST_P(ProtocolIntegrationTest, RetryStreamingCancelDueToBufferOverflow) { | ||
| config_helper_.addConfigModifier( | ||
| [](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& | ||
| hcm) { | ||
| auto* route = hcm.mutable_route_config()->mutable_virtual_hosts(0)->mutable_routes(0); | ||
|
|
||
| route->mutable_per_request_buffer_limit_bytes()->set_value(1024); | ||
| route->mutable_route() | ||
| ->mutable_retry_policy() | ||
| ->mutable_retry_back_off() | ||
| ->mutable_base_interval() | ||
| ->MergeFrom( | ||
| ProtobufUtil::TimeUtil::MillisecondsToDuration(100000000)); // Effectively infinity. | ||
| }); | ||
| initialize(); | ||
|
|
||
| codec_client_ = makeHttpConnection(lookupPort("http")); | ||
| auto encoder_decoder = | ||
| codec_client_->startRequest(Http::TestRequestHeaderMapImpl{{":method", "POST"}, | ||
| {":path", "/test/long/url"}, | ||
| {":scheme", "http"}, | ||
| {":authority", "host"}, | ||
| {"x-forwarded-for", "10.0.0.1"}, | ||
| {"x-envoy-retry-on", "5xx"}}); | ||
| auto& encoder = encoder_decoder.first; | ||
| auto& response = encoder_decoder.second; | ||
|
|
||
| // Send some data, but less than the buffer limit, and not end-stream | ||
| std::string data(64, 'a'); | ||
| Buffer::OwnedImpl send1(data); | ||
| encoder.encodeData(send1, false); | ||
|
|
||
| ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); | ||
| ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); | ||
|
|
||
| // Send back an upstream failure. | ||
| upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "503"}}, false); | ||
|
|
||
| if (fake_upstreams_[0]->httpType() == FakeHttpConnection::Type::HTTP1) { | ||
| ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); | ||
| } else { | ||
| ASSERT_TRUE(upstream_request_->waitForReset()); | ||
| } | ||
|
|
||
| // Overflow the request buffer limit. Because the retry base interval is infinity, no | ||
| // request will be in progress. This will cause the request to be aborted and an error | ||
| // to be returned to the client. | ||
| std::string data2(2048, 'b'); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I could see this for HTTP/2, where we do flow control via stream window, but only stop acking when we're at the limit, but for HTTP/1, we generally readDisable when we don't want more data. I'd think that if we're in a case where we were waiting on upstream, we'd want to readDisable (which would immediately cause data to cease) and we'd get the retry.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought about this, and briefly looked into it, but I think it will be much more complicated. The problematic case is when the router has an upstream request, but we don't yet know whether it will need to be retried. The upstream may require more data than we can buffer to determine if it will return a 5xx or not. Given that, we could stop flow control in the router when we do not have an upstream request. But then it's racy whether this behavior is in effect, because there are (at least) 3 states: no upstream request at all, an upstream request on a connection that isn't established yet, and an upstream request where we're sending the request currently. So I think all that means it is out-of-scope for this PR. But worth pursuing at some point. |
||
| Buffer::OwnedImpl send2(data2); | ||
| encoder.encodeData(send2, false); | ||
|
|
||
| response->waitForEndStream(); | ||
|
|
||
| EXPECT_TRUE(response->complete()); | ||
| EXPECT_EQ("503", response->headers().Status()->value().getStringView()); | ||
| test_server_->waitForCounterEq("cluster.cluster_0.retry_or_shadow_abandoned", 1); | ||
| } | ||
|
|
||
| // Tests that the x-envoy-attempt-count header is properly set on the upstream request and the | ||
| // downstream response, and updated after the request is retried. | ||
| TEST_P(DownstreamProtocolIntegrationTest, RetryAttemptCountHeader) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a bit of commenting of how we might get here? is this just during the retry-timer interval?
Also cleanup -> clean up?