-
Notifications
You must be signed in to change notification settings - Fork 5.5k
gRPC retries, assuming gRPC status code is in the returned headers #1067
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
66f18e9
9a3f9a6
11281eb
05c6dba
8fb9bbe
e46dbc1
7ed0e35
2775929
9fec1d8
6f613d8
872fad0
a5aac27
0130379
a4e13ff
ab6c25a
bc4f8de
65566c9
e5e7f53
39c9606
f0cfd1c
b0643f2
d8a7a28
3d33288
bbba9a8
3dec86d
1dcc41e
36aae46
5df0a7e
6692aef
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 |
|---|---|---|
|
|
@@ -25,6 +25,34 @@ class Exception : public EnvoyException { | |
|
|
||
| class Common { | ||
| public: | ||
| enum GrpcStatus { | ||
|
Member
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. It would be helpful if this could be moved to
Member
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. gRPC status code are identical with protobuf status code, so just re-use it might be enough.
Member
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. +1 to what @htuch said. I would like to re-use those for fault injection as well. Do we also have HTTP2 specific error codes in a similar place (in addition to HTTP1)? All I see are HTTP1 codes in
Member
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'm generally a fan of decoupling protobuf from gRPC. As we know from the other stuff going on with grpc-web, there's not necessarily a direct correspondence between gRPC and protobuf. |
||
| Ok = 0, | ||
| Canceled = 1, | ||
| Unknown = 2, | ||
| InvalidArgument = 3, | ||
| DeadlineExceeded = 4, | ||
| NotFound = 5, | ||
| AlreadyExists = 6, | ||
| PermissionDenied = 7, | ||
| Unauthenticated = 16, | ||
| ResourceExhausted = 8, | ||
| FailedPrecondition = 9, | ||
| Aborted = 10, | ||
| OutOfRange = 11, | ||
| Unimplemented = 12, | ||
| Internal = 13, | ||
| Unavailable = 14, | ||
| DataLoss = 15, | ||
| InvalidCode = 16, | ||
| }; | ||
|
|
||
| /** | ||
| * Returns the GrpcStatus code from a given set of headers, if present. | ||
| * @headers the headers to parse. | ||
| * @returns the parsed status code or InvalidCode if no valid status is found. | ||
| */ | ||
| static GrpcStatus getGrpcStatus(const Http::HeaderMap& headers); | ||
|
|
||
| /** | ||
| * Charge a success/failure stat to a cluster/service/method. | ||
| * @param cluster supplies the target cluster. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
|
|
||
| #include "common/common/assert.h" | ||
| #include "common/common/utility.h" | ||
| #include "common/grpc/common.h" | ||
| #include "common/http/codes.h" | ||
| #include "common/http/headers.h" | ||
| #include "common/http/utility.h" | ||
|
|
@@ -19,6 +20,11 @@ namespace Router { | |
| const uint32_t RetryPolicy::RETRY_ON_5XX; | ||
| const uint32_t RetryPolicy::RETRY_ON_CONNECT_FAILURE; | ||
| const uint32_t RetryPolicy::RETRY_ON_RETRIABLE_4XX; | ||
| const uint32_t RetryPolicy::RETRY_ON_GRPC_CANCELLED; | ||
| const uint32_t RetryPolicy::RETRY_ON_GRPC_DEADLINE_EXCEEDED; | ||
| const uint32_t RetryPolicy::RETRY_ON_GRPC_RESOURCE_EXHAUSTED; | ||
|
|
||
|
|
||
|
|
||
| RetryStatePtr RetryStateImpl::create(const RetryPolicy& route_policy, | ||
| Http::HeaderMap& request_headers, | ||
|
|
@@ -29,7 +35,7 @@ RetryStatePtr RetryStateImpl::create(const RetryPolicy& route_policy, | |
| RetryStatePtr ret; | ||
|
|
||
| // We short circuit here and do not both with an allocation if there is no chance we will retry. | ||
| if (request_headers.EnvoyRetryOn() || route_policy.retryOn()) { | ||
| if (request_headers.EnvoyRetryOn() || request_headers.EnvoyRetryGrpcOn() || route_policy.retryOn()) { | ||
|
Member
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. quick comment (not full review), can we also add inline route support as is done in route_policy.retryOn() ?
Member
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. Rather than extending this if block with more conditions (gRPC, HTTP2 error codes, etc.), would it make sense to abstract them under EnvoyRetryOn() ?
Contributor
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 think request_headers.EnvoyRetryOn() returning true if there were a literal header x-envoy-grpc-retry-on would be confusing as all generally header functions map cleanly to the header specified. Most other code points could be covered under the general "retry" header if folks think that's cleaner. Working on route policy now. I'm inclined to add the gRPC strings to the existing retry_on field rather than have retry_on and grpc_retry_on (and num_retries and num_grpc_retries) in router config. If anyone has concerns about HTTP vs gRPC collisions and prefers splitting them out, let me know!
Contributor
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. And done. |
||
| ret.reset(new RetryStateImpl(route_policy, request_headers, cluster, runtime, random, | ||
| dispatcher, priority)); | ||
| } | ||
|
|
@@ -48,12 +54,15 @@ RetryStateImpl::RetryStateImpl(const RetryPolicy& route_policy, Http::HeaderMap& | |
|
|
||
| if (request_headers.EnvoyRetryOn()) { | ||
| retry_on_ = parseRetryOn(request_headers.EnvoyRetryOn()->value().c_str()); | ||
| if (retry_on_ != 0 && request_headers.EnvoyMaxRetries()) { | ||
| const char* max_retries = request_headers.EnvoyMaxRetries()->value().c_str(); | ||
| uint64_t temp; | ||
| if (StringUtil::atoul(max_retries, temp)) { | ||
| retries_remaining_ = temp; | ||
| } | ||
| } | ||
| if (request_headers.EnvoyRetryGrpcOn()) { | ||
| retry_on_ |= parseRetryGrpcOn(request_headers.EnvoyRetryGrpcOn()->value().c_str()); | ||
| } | ||
| if (retry_on_ != 0 && request_headers.EnvoyMaxRetries()) { | ||
| const char* max_retries = request_headers.EnvoyMaxRetries()->value().c_str(); | ||
| uint64_t temp; | ||
| if (StringUtil::atoul(max_retries, temp)) { | ||
| retries_remaining_ = temp; | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -96,6 +105,22 @@ uint32_t RetryStateImpl::parseRetryOn(const std::string& config) { | |
| return ret; | ||
| } | ||
|
|
||
| uint32_t RetryStateImpl::parseRetryGrpcOn(const std::string& retry_grpc_on_header) { | ||
| uint32_t ret = 0; | ||
| std::vector<std::string> retry_on_list = StringUtil::split(retry_grpc_on_header, ','); | ||
| for (const std::string& retry_on : retry_on_list) { | ||
| if (retry_on == Http::Headers::get().EnvoyRetryOnGrpcValues.Cancelled) { | ||
| ret |= RetryPolicy::RETRY_ON_GRPC_CANCELLED; | ||
| } else if (retry_on == Http::Headers::get().EnvoyRetryOnGrpcValues.DeadlineExceeded) { | ||
| ret |= RetryPolicy::RETRY_ON_GRPC_DEADLINE_EXCEEDED; | ||
| } else if (retry_on == Http::Headers::get().EnvoyRetryOnGrpcValues.ResourceExhausted) { | ||
| ret |= RetryPolicy::RETRY_ON_GRPC_RESOURCE_EXHAUSTED; | ||
| } | ||
| } | ||
|
|
||
| return ret; | ||
| } | ||
|
|
||
| void RetryStateImpl::resetRetry() { | ||
| if (callback_) { | ||
| cluster_.resourceManager(priority_).retries().dec(); | ||
|
|
@@ -169,6 +194,17 @@ bool RetryStateImpl::wouldRetry(const Http::HeaderMap* response_headers, | |
| } | ||
| } | ||
|
|
||
| if (retry_on_ & (RetryPolicy::RETRY_ON_GRPC_CANCELLED | | ||
| RetryPolicy::RETRY_ON_GRPC_DEADLINE_EXCEEDED | | ||
| RetryPolicy::RETRY_ON_GRPC_RESOURCE_EXHAUSTED) && response_headers) { | ||
| Grpc::Common::GrpcStatus status = Grpc::Common::getGrpcStatus(*response_headers); | ||
| if ((status == Grpc::Common::Canceled && (retry_on_ & RetryPolicy::RETRY_ON_GRPC_CANCELLED)) || | ||
| (status == Grpc::Common::DeadlineExceeded && (retry_on_ & RetryPolicy::RETRY_ON_GRPC_DEADLINE_EXCEEDED)) || | ||
| (status == Grpc::Common::ResourceExhausted && (retry_on_ & RetryPolicy::RETRY_ON_GRPC_RESOURCE_EXHAUSTED))) { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -722,6 +722,48 @@ TEST_F(RouterTest, RetryUpstream5xxNotComplete) { | |
| .value()); | ||
| } | ||
|
|
||
|
|
||
| TEST_F(RouterTest, RetryUpstreamGrpcCancelled) { | ||
| NiceMock<Http::MockStreamEncoder> encoder1; | ||
| Http::StreamDecoder* response_decoder = nullptr; | ||
| EXPECT_CALL(cm_.conn_pool_, newStream(_, _)) | ||
| .WillOnce(Invoke([&](Http::StreamDecoder& decoder, Http::ConnectionPool::Callbacks& callbacks) | ||
| -> Http::ConnectionPool::Cancellable* { | ||
| response_decoder = &decoder; | ||
| callbacks.onPoolReady(encoder1, cm_.conn_pool_.host_); | ||
| return nullptr; | ||
| })); | ||
| expectResponseTimerCreate(); | ||
|
|
||
| Http::TestHeaderMapImpl headers{{"x-envoy-grpc-retry-on", "cancelled"}, {"x-envoy-internal", "true"}}; | ||
| HttpTestUtility::addDefaultHeaders(headers); | ||
| router_.decodeHeaders(headers, true); | ||
|
|
||
| // 5xx response. | ||
|
Member
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. 5xx? |
||
| router_.retry_state_->expectRetry(); | ||
| Http::HeaderMapPtr response_headers1(new Http::TestHeaderMapImpl{{":status", "200"}, {":grpc-status", "1"}}); | ||
| EXPECT_CALL(cm_.conn_pool_.host_->outlier_detector_, putHttpResponseCode(200)); | ||
| response_decoder->decodeHeaders(std::move(response_headers1), true); | ||
|
|
||
| // We expect the grpc-status to result in a retried request. | ||
| EXPECT_CALL(encoder1.stream_, resetStream(_)).Times(0); | ||
| NiceMock<Http::MockStreamEncoder> encoder2; | ||
| EXPECT_CALL(cm_.conn_pool_, newStream(_, _)) | ||
| .WillOnce(Invoke([&](Http::StreamDecoder& decoder, Http::ConnectionPool::Callbacks& callbacks) | ||
| -> Http::ConnectionPool::Cancellable* { | ||
| response_decoder = &decoder; | ||
| callbacks.onPoolReady(encoder2, cm_.conn_pool_.host_); | ||
| return nullptr; | ||
| })); | ||
| router_.retry_state_->callback_(); | ||
|
|
||
| // Normal response. | ||
| EXPECT_CALL(*router_.retry_state_, shouldRetry(_, _, _)).WillOnce(Return(false)); | ||
| Http::HeaderMapPtr response_headers(new Http::TestHeaderMapImpl{{":status", "200"}}); | ||
| EXPECT_CALL(cm_.conn_pool_.host_->outlier_detector_, putHttpResponseCode(200)); | ||
| response_decoder->decodeHeaders(std::move(response_headers), true); | ||
| } | ||
|
|
||
| TEST_F(RouterTest, Shadow) { | ||
| callbacks_.route_->route_entry_.shadow_policy_.cluster_ = "foo"; | ||
| callbacks_.route_->route_entry_.shadow_policy_.runtime_key_ = "bar"; | ||
|
|
||
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.
0x10, 0x20, 0x40
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.
facepalm Done :-P
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.
Don't see this in the updated diff yet.