-
Notifications
You must be signed in to change notification settings - Fork 5.5k
ratelimit: add support for failure_mode_allow configuration #4073
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 5 commits
f0a6a88
84b081a
88e49e6
83e75ec
ef281ad
91439ea
2934cc4
37b6913
a6ca282
6296e00
3289db0
cf357e2
d561b19
4404df2
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 |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ package envoy.config.filter.http.rate_limit.v2; | |
| option go_package = "v2"; | ||
|
|
||
| import "google/protobuf/duration.proto"; | ||
| import "google/protobuf/wrappers.proto"; | ||
|
|
||
| import "validate/validate.proto"; | ||
| import "gogoproto/gogo.proto"; | ||
|
|
@@ -34,4 +35,10 @@ message RateLimit { | |
| // The timeout in milliseconds for the rate limit service RPC. If not | ||
| // set, this defaults to 20ms. | ||
| google.protobuf.Duration timeout = 4 [(gogoproto.stdduration) = true]; | ||
|
|
||
| // The filter's behaviour in case the rate limiting service does | ||
| // not respond back. When it is set to true, Envoy will also allow traffic in case of | ||
| // communication failure between rate limiting service and the proxy. | ||
| // Defaults to true. | ||
| google.protobuf.BoolValue failure_mode_allow = 5; | ||
|
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's preferable to use bool instead of g.p.BoolValue. Can you change this to type bool and name failure_mode_deny (so that the default value is false)? Or is there a good reason for making this a tri-state?
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. There is no other reason except to be consistent with external_authz proto naming. I will change. |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ option go_package = "v2"; | |
|
|
||
| import "envoy/api/v2/ratelimit/ratelimit.proto"; | ||
| import "google/protobuf/duration.proto"; | ||
| import "google/protobuf/wrappers.proto"; | ||
|
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. ditto |
||
|
|
||
| import "validate/validate.proto"; | ||
| import "gogoproto/gogo.proto"; | ||
|
|
@@ -26,4 +27,10 @@ message RateLimit { | |
| // The timeout in milliseconds for the rate limit service RPC. If not | ||
| // set, this defaults to 20ms. | ||
| google.protobuf.Duration timeout = 4 [(gogoproto.stdduration) = true]; | ||
|
|
||
| // The filter's behaviour in case the rate limiting service does | ||
| // not respond back. When it is set to true, Envoy will also allow traffic in case of | ||
| // communication failure between rate limiting service and the proxy. | ||
| // Defaults to true. | ||
| google.protobuf.BoolValue failure_mode_allow = 5; | ||
|
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. ditto |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,9 @@ apply to a request. Each configuration results in a descriptor being sent to the | |
| If the rate limit service is called, and the response for any of the descriptors is over limit, a | ||
| 429 response is returned. | ||
|
|
||
| If there is an error in calling rate limit service or rate limit service returns an error and :ref:`failure_mode_allow <envoy_api_msg_config.filter.http.rate_limit.v2.RateLimit>` is | ||
|
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. failure_mode_deny |
||
| set to false, a 500 response is returned. | ||
|
|
||
| .. _config_http_filters_rate_limit_composing_actions: | ||
|
|
||
| Composing Actions | ||
|
|
@@ -108,6 +111,8 @@ The buffer filter outputs statistics in the *cluster.<route target cluster>.rate | |
| ok, Counter, Total under limit responses from the rate limit service | ||
| error, Counter, Total errors contacting the rate limit service | ||
| over_limit, Counter, total over limit responses from the rate limit service | ||
| failure_mode_allowed, Counter, "Total requests that were error(s) but were allowed through because | ||
| of :ref:`failure_mode_allow <envoy_api_msg_config.filter.http.rate_limit.v2.RateLimit>` set to true." | ||
|
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. failure_mode_deny |
||
|
|
||
| Runtime | ||
| ------- | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,8 @@ following statistics: | |
| ok, Counter, Total under limit responses from the rate limit service | ||
| cx_closed, Counter, Total connections closed due to an over limit response from the rate limit service | ||
| active, Gauge, Total active requests to the rate limit service | ||
| failure_mode_allowed, Counter, "Total requests that were error(s) but were allowed through because | ||
|
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. failure_mode_deny: I'm fine with the stat being named either way, but the doc link needs to be updated. |
||
| of :ref:`failure_mode_allow <envoy_api_msg_config.filter.http.rate_limit.v2.RateLimit>` set to true." | ||
|
|
||
| Runtime | ||
| ------- | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,8 +48,10 @@ enum ResponseFlag { | |
| RateLimited = 0x800, | ||
| // Request was unauthorized by external authorization service. | ||
| UnauthorizedExternalService = 0x1000, | ||
| // Unable to call Ratelimiting service. | ||
| RateLimitingServiceError = 0x1200, | ||
|
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. nit: RateLimitServiceError |
||
| // ATTENTION: MAKE SURE THIS REMAINS EQUAL TO THE LAST FLAG. | ||
| LastFlag = UnauthorizedExternalService | ||
| LastFlag = RateLimitingServiceError | ||
| }; | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,7 +31,7 @@ void ResponseFlagUtils::appendString(std::string& result, const std::string& app | |
| const std::string ResponseFlagUtils::toShortString(const RequestInfo& request_info) { | ||
| std::string result; | ||
|
|
||
| static_assert(ResponseFlag::LastFlag == 0x1000, "A flag has been added. Fix this code."); | ||
| static_assert(ResponseFlag::LastFlag == 0x1200, "A flag has been added. Fix this code."); | ||
|
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. For this, and all the other asserts related to the LastFlag, we not only need to fix the value. There is accompanying code that needs to be fixed. For example here we need to add the new flag to the mapping between the enum and the string. Any suggestions appreciated on how to make this maintenance more obvious.
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. Oh. Sorry..did not realize that. Will change |
||
|
|
||
| if (request_info.hasResponseFlag(ResponseFlag::FailedLocalHealthCheck)) { | ||
| appendString(result, FAILED_LOCAL_HEALTH_CHECK); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -125,6 +125,16 @@ void Filter::complete(RateLimit::LimitStatus status) { | |
| state_ = State::Responded; | ||
| callbacks_->sendLocalReply(Http::Code::TooManyRequests, "", nullptr); | ||
| callbacks_->requestInfo().setResponseFlag(RequestInfo::ResponseFlag::RateLimited); | ||
| } else if (status == RateLimit::LimitStatus::Error) { | ||
| if (config_->failureModeAllow()) { | ||
|
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 did not change the method name in config because it seems more readable like this. LMK if you think otherwise
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. This is fine as is |
||
| cluster_->statsScope().counter("ratelimit.failure_mode_allowed").inc(); | ||
|
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. Do we need this new counter at all? We already have a counter for ratelimit service failures, and presumably the user knows which way they have this configured (failure_mode_deny value). In one case this counter will always be zero, in the other it will be equal to
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 this would help in general, while looking at stats user do not have to check back on what his configuration is.
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. Ok, I'm fine with that. |
||
| callbacks_->continueDecoding(); | ||
| } else { | ||
| state_ = State::Responded; | ||
| callbacks_->sendLocalReply(Http::Code::InternalServerError, "", nullptr); | ||
| callbacks_->requestInfo().setResponseFlag( | ||
| RequestInfo::ResponseFlag::RateLimitingServiceError); | ||
| } | ||
| } else if (!initiating_call_) { | ||
| callbacks_->continueDecoding(); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,8 +37,8 @@ class FilterConfig { | |
| : domain_(config.domain()), stage_(static_cast<uint64_t>(config.stage())), | ||
| request_type_(config.request_type().empty() ? stringToType("both") | ||
| : stringToType(config.request_type())), | ||
| local_info_(local_info), scope_(scope), runtime_(runtime), cm_(cm) {} | ||
|
|
||
| local_info_(local_info), scope_(scope), runtime_(runtime), cm_(cm), | ||
| failure_mode_allow_(PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, failure_mode_allow, true)) {} | ||
| const std::string& domain() const { return domain_; } | ||
| const LocalInfo::LocalInfo& localInfo() const { return local_info_; } | ||
| uint64_t stage() const { return stage_; } | ||
|
|
@@ -47,6 +47,8 @@ class FilterConfig { | |
| Upstream::ClusterManager& cm() { return cm_; } | ||
| FilterRequestType requestType() const { return request_type_; } | ||
|
|
||
| bool failureModeAllow() const { return failure_mode_allow_; } | ||
|
|
||
| private: | ||
| static FilterRequestType stringToType(const std::string& request_type) { | ||
| if (request_type == "internal") { | ||
|
|
@@ -66,6 +68,7 @@ class FilterConfig { | |
| Stats::Scope& scope_; | ||
| Runtime::Loader& runtime_; | ||
| Upstream::ClusterManager& cm_; | ||
| bool failure_mode_allow_; | ||
|
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. const |
||
| }; | ||
|
|
||
| typedef std::shared_ptr<FilterConfig> FilterConfigSharedPtr; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,7 @@ namespace RateLimitFilter { | |
| COUNTER(error) \ | ||
| COUNTER(over_limit) \ | ||
| COUNTER(ok) \ | ||
| COUNTER(failure_mode_allowed) \ | ||
| COUNTER(cx_closed) \ | ||
| GAUGE (active) | ||
| // clang-format on | ||
|
|
@@ -49,6 +50,7 @@ class Config { | |
| const std::vector<RateLimit::Descriptor>& descriptors() { return descriptors_; } | ||
| Runtime::Loader& runtime() { return runtime_; } | ||
| const InstanceStats& stats() { return stats_; } | ||
| bool failureModeAllow() const { return failure_mode_allow_; }; | ||
|
|
||
| private: | ||
| static InstanceStats generateStats(const std::string& name, Stats::Scope& scope); | ||
|
|
@@ -57,6 +59,7 @@ class Config { | |
| std::vector<RateLimit::Descriptor> descriptors_; | ||
| const InstanceStats stats_; | ||
| Runtime::Loader& runtime_; | ||
| bool failure_mode_allow_; | ||
|
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. const |
||
| }; | ||
|
|
||
| typedef std::shared_ptr<Config> ConfigSharedPtr; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,10 +46,13 @@ class HttpRateLimitFilterTest : public testing::Test { | |
| .WillByDefault(Return(true)); | ||
| } | ||
|
|
||
| void SetUpTest(const std::string json) { | ||
| void SetUpTest(const std::string json, const bool failure_mode = true) { | ||
| Json::ObjectSharedPtr json_config = Json::Factory::loadFromString(json); | ||
| envoy::config::filter::http::rate_limit::v2::RateLimit proto_config{}; | ||
| Config::FilterJson::translateHttpRateLimitFilter(*json_config, proto_config); | ||
| // TODO(ramaraochavali): move filter_config_ to yaml and use a different config for | ||
|
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 would go ahead and do this real quick. |
||
| // failure_mode. | ||
| proto_config.mutable_failure_mode_allow()->set_value(failure_mode); | ||
| config_.reset(new FilterConfig(proto_config, local_info_, stats_store_, runtime_, cm_)); | ||
|
|
||
| client_ = new RateLimit::MockClient(); | ||
|
|
@@ -239,6 +242,37 @@ TEST_F(HttpRateLimitFilterTest, ErrorResponse) { | |
| EXPECT_EQ( | ||
| 1U, | ||
| cm_.thread_local_cluster_.cluster_.info_->stats_store_.counter("ratelimit.error").value()); | ||
| EXPECT_EQ(1U, cm_.thread_local_cluster_.cluster_.info_->stats_store_ | ||
| .counter("ratelimit.failure_mode_allowed") | ||
| .value()); | ||
| } | ||
|
|
||
| TEST_F(HttpRateLimitFilterTest, ErrorResponseWithFailureAllowModeOff) { | ||
|
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. nit: FailureModeAllow for naming consistency |
||
| SetUpTest(filter_config_, false); | ||
| InSequence s; | ||
|
|
||
| EXPECT_CALL(route_rate_limit_, populateDescriptors(_, _, _, _, _)) | ||
| .WillOnce(SetArgReferee<1>(descriptor_)); | ||
| EXPECT_CALL(*client_, limit(_, _, _, _)) | ||
| .WillOnce(WithArgs<0>(Invoke([&](RateLimit::RequestCallbacks& callbacks) -> void { | ||
| request_callbacks_ = &callbacks; | ||
| }))); | ||
|
|
||
| EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, | ||
| filter_->decodeHeaders(request_headers_, false)); | ||
|
|
||
| request_callbacks_->complete(RateLimit::LimitStatus::Error); | ||
|
|
||
| EXPECT_CALL(filter_callbacks_.request_info_, | ||
| setResponseFlag(RequestInfo::ResponseFlag::RateLimitingServiceError)) | ||
| .Times(0); | ||
|
|
||
| EXPECT_EQ( | ||
| 1U, | ||
| cm_.thread_local_cluster_.cluster_.info_->stats_store_.counter("ratelimit.error").value()); | ||
| EXPECT_EQ(0U, cm_.thread_local_cluster_.cluster_.info_->stats_store_ | ||
| .counter("ratelimit.failure_mode_allowed") | ||
| .value()); | ||
| } | ||
|
|
||
| TEST_F(HttpRateLimitFilterTest, LimitResponse) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,7 +33,9 @@ namespace RateLimitFilter { | |
|
|
||
| class RateLimitFilterTest : public testing::Test { | ||
| public: | ||
| RateLimitFilterTest() { | ||
| RateLimitFilterTest() {} | ||
|
|
||
| void SetUp() override { | ||
| std::string json = R"EOF( | ||
| { | ||
| "domain": "foo", | ||
|
|
@@ -53,6 +55,8 @@ class RateLimitFilterTest : public testing::Test { | |
| Json::ObjectSharedPtr json_config = Json::Factory::loadFromString(json); | ||
| envoy::config::filter::network::rate_limit::v2::RateLimit proto_config{}; | ||
| Envoy::Config::FilterJson::translateTcpRateLimitFilter(*json_config, proto_config); | ||
| // TODO(ramaraochavali): move the config to yaml and use a different config for failure_mode. | ||
|
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 would go ahead and do this, and change these tests to use a SetUpTest(const std::string& yaml) setup function. Prevents the subclass addition here, as you point out in the TODO. |
||
| proto_config.mutable_failure_mode_allow()->set_value(failure_mode_); | ||
| config_.reset(new Config(proto_config, stats_store_, runtime_)); | ||
| client_ = new RateLimit::MockClient(); | ||
| filter_.reset(new Filter(config_, RateLimit::ClientPtr{client_})); | ||
|
|
@@ -76,6 +80,16 @@ class RateLimitFilterTest : public testing::Test { | |
| std::unique_ptr<Filter> filter_; | ||
| NiceMock<Network::MockReadFilterCallbacks> filter_callbacks_; | ||
| RateLimit::RequestCallbacks* request_callbacks_{}; | ||
| bool failure_mode_{true}; | ||
| }; | ||
|
|
||
| class RateLimitFilterFailureModeTest : public RateLimitFilterTest { | ||
| public: | ||
| RateLimitFilterFailureModeTest() { | ||
| failure_mode_ = false; | ||
| std::cout << "setting failure mode to false..." | ||
| << "\n"; | ||
| } | ||
| }; | ||
|
|
||
| TEST_F(RateLimitFilterTest, BadRatelimitConfig) { | ||
|
|
@@ -194,6 +208,7 @@ TEST_F(RateLimitFilterTest, Error) { | |
|
|
||
| EXPECT_EQ(1U, stats_store_.counter("ratelimit.name.total").value()); | ||
| EXPECT_EQ(1U, stats_store_.counter("ratelimit.name.error").value()); | ||
| EXPECT_EQ(1U, stats_store_.counter("ratelimit.name.failure_mode_allowed").value()); | ||
| } | ||
|
|
||
| TEST_F(RateLimitFilterTest, Disconnect) { | ||
|
|
@@ -247,6 +262,29 @@ TEST_F(RateLimitFilterTest, RuntimeDisable) { | |
| EXPECT_EQ(Network::FilterStatus::Continue, filter_->onData(data, false)); | ||
| } | ||
|
|
||
| TEST_F(RateLimitFilterFailureModeTest, ErrorResponseWithFailureAllowModeOff) { | ||
| InSequence s; | ||
|
|
||
| EXPECT_CALL(*client_, limit(_, "foo", _, _)) | ||
| .WillOnce(WithArgs<0>(Invoke([&](RateLimit::RequestCallbacks& callbacks) -> void { | ||
| request_callbacks_ = &callbacks; | ||
| }))); | ||
|
|
||
| EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onNewConnection()); | ||
| Buffer::OwnedImpl data("hello"); | ||
| EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(data, false)); | ||
| request_callbacks_->complete(RateLimit::LimitStatus::Error); | ||
|
|
||
| EXPECT_EQ(Network::FilterStatus::Continue, filter_->onData(data, false)); | ||
|
|
||
| EXPECT_CALL(*client_, cancel()).Times(0); | ||
| filter_callbacks_.connection_.raiseEvent(Network::ConnectionEvent::RemoteClose); | ||
|
|
||
| EXPECT_EQ(1U, stats_store_.counter("ratelimit.name.total").value()); | ||
| EXPECT_EQ(1U, stats_store_.counter("ratelimit.name.error").value()); | ||
| EXPECT_EQ(0U, stats_store_.counter("ratelimit.name.failure_mode_allowed").value()); | ||
| } | ||
|
|
||
| } // namespace RateLimitFilter | ||
| } // namespace NetworkFilters | ||
| } // namespace Extensions | ||
|
|
||
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.
nit: this isn't needed anymore