-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Per connection local rate limiting #15843
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 16 commits
dc475ff
24ba562
476e5d7
e8f8e83
745bf66
840742a
edb7ab8
9300cea
b256ffd
6351e40
5bdf0f9
efc3a07
eca4c02
a79905a
8656cd7
6c77508
bbc11c2
3d850ef
443def6
35f35aa
b3e1702
564571a
8719660
9e43177
4e239f9
9bfcf1a
489d66c
fcf83d8
44e4e45
f38f0a7
0e95698
eb71d7b
972758d
8faa072
1c3ec7a
d1397ac
91c8ca2
533254a
9ff513b
61b464a
2054d3e
e2c2280
3ad96b8
0aec425
79afd6b
676588a
fb2178e
e29dc13
8cb5806
99e941a
9a00865
e4eba7c
5876f6c
ef28b68
0e4b716
725816f
83500a7
2960c69
67eb5e2
875e059
8c7e0c0
98fb985
37d353f
32f82c5
44bbf4d
443fdc9
174431b
166241d
707d3b9
aa4832e
93ca37a
0cb5c40
263a0a8
3df15d2
c2dd672
8119596
7366497
fcf28ca
bcf0348
ac1d176
cfa681a
b5be190
a921fee
33ee0c4
d2f6f8f
7bbec8c
fe8d26e
fdfd990
5ca8184
dfcc7bb
89ed219
8f0f92f
7efd4fa
0082d5a
d202a27
dd5eaf5
8c85cec
199cb5a
0dd325d
f00c9fd
c93f6fe
9b4fef0
eccaba5
ad0242d
8711bd4
9e9dfab
a857dd3
6b0235c
0417587
e6408bb
9f17367
2f603d8
562be5e
add870b
6bf6698
f7aa00d
f712bc8
0dd2f98
454fa95
65fb735
ed858b4
badf219
26e73c4
8e338b2
fbcf203
33bedd1
0566b86
762fb6a
5a91170
633098c
1ab6a0c
44ae575
1741a79
50307b6
41fd116
1ff2c4b
2916b64
27460f6
bc12ea0
64bb450
cdf57ee
bb2e153
0e17903
f921dc5
4e54def
99a6c6c
74fcf4e
8e9880a
eacc127
13577b9
c014687
f75eb68
f5bae8e
a57014f
0e2843c
eb93f69
ded5417
06aa1d8
3146b6a
e5065e7
7eae147
d522352
59bd835
5d3f343
3ce48c8
24be394
b6e3303
188b61f
70ee15b
47380a8
14b1705
f68f942
cda66d3
c452d32
6ace0e0
7bf71a4
47d4acc
4c8af40
b7db037
6966d3e
eda8994
a0afd00
99849a0
1ab83de
420fa93
22e72c9
c67e4ba
aeeae43
b2322d5
1bb38e4
09ff689
1ac2759
4c85898
9a4dd7b
c0944a6
45a3a17
a5d5136
33cf77c
89ba8f7
adddd73
1a995bb
cbc0a36
7b8c058
8522075
6de3b44
293060d
5febd9c
c8e32e2
d64c987
671559b
df340bf
ffe87db
1e44f09
bebdce7
0b047ae
4603b74
9d95544
5861909
7ca878b
39c389f
d44697c
aab4d01
e405cf9
e061e08
d7471ea
14d6198
de06957
a8b8d4c
1d36b8c
ea3fe62
57bd864
fc0415e
52ffa87
7525e6d
d4e077a
2e77edd
323eb26
95d8f9a
318c4dc
b076788
8a53a6d
a1d6427
43648c2
9c76913
c1d49bf
2d12dae
c288dc6
a6b15b0
d994fee
b2b2d67
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 |
|---|---|---|
|
|
@@ -19,7 +19,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; | |
| // Local Rate limit :ref:`configuration overview <config_http_filters_local_rate_limit>`. | ||
| // [#extension: envoy.filters.http.local_ratelimit] | ||
|
|
||
| // [#next-free-field: 11] | ||
| // [#next-free-field: 12] | ||
| message LocalRateLimit { | ||
| // The human readable prefix to use when emitting stats. | ||
| string stat_prefix = 1 [(validate.rules).string = {min_len: 1}]; | ||
|
|
@@ -97,4 +97,12 @@ message LocalRateLimit { | |
| // | ||
| // The filter supports a range of 0 - 10 inclusively for stage numbers. | ||
| uint32 stage = 9 [(validate.rules).uint32 = {lte: 10}]; | ||
|
|
||
| // Specifies the scope of the rate limiter's token bucket. | ||
| // If set to false, the token bucket is shared across all worker threads, | ||
|
alyssawilk marked this conversation as resolved.
|
||
| // thus the rate limits are applied per Envoy process. | ||
| // If set to true, a token bucket is allocated for each connection. | ||
| // Thus the rate limits are applied per connection thereby allowing | ||
| // one to rate limit requests on a per connection basis. | ||
| bool local_rate_limit_per_downstream_connection = 11; | ||
|
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. Is it possible for a proxy to have both per-process and per downstream connection rate limits, or just one or the other? Another potential question is if we should consider some additional rate limit criteria in the future like downstream IP or HTTP Cookie. If we expect additional criteria in the future, we may want to make this an enum field.
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. It's definitely one or the other as it stands currently as I'm not sure the added complexity of dealing with potentially conflicting token bucket quotas between the per process and per connection configurations and the precedence rules we'd have to handle buys us much in the way of functionality. We did indeed consider an enum initially but it didn't seem like there were very many realistic use cases mainly because a lot of the toggles that were based on certain request characteristics such as IP, Cookie etc can be handled today by rate limiting on request descriptors ... |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,8 +22,9 @@ configured to be returned. | |
| <envoy_v3_api_field_extensions.filters.http.local_ratelimit.v3.LocalRateLimit.request_headers_to_add_when_not_enforced>` can be | ||
| configured to be added to forwarded requests to the upstream when the local rate limit filter is enabled but not enforced. | ||
|
|
||
| .. note:: | ||
| The token bucket is shared across all workers, thus the rate limits are applied per Envoy process. | ||
| Depending on the value of the config :ref:`local_rate_limit_per_downstream_connection <envoy_v3_api_field_extensions.filters.http.local_ratelimit.v3.LocalRateLimit.local_rate_limit_per_downstream_connection>`, | ||
| the token bucket is either shared across all workers or on a per connection basis. This results in the local rate limits being applied either per Envoy process or per downstream connection. | ||
| If unspecified, this config is set to false by default, which means the rate limits are applied per Envoy process. | ||
|
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. Probably simplify this last sentence into: By default rate limits are applied per Envoy process. |
||
|
|
||
| Example configuration | ||
| --------------------- | ||
|
|
@@ -55,6 +56,7 @@ Example filter configuration for a globally set rate limiter (e.g.: all vhosts/r | |
| header: | ||
| key: x-local-rate-limit | ||
| value: 'true' | ||
| local_rate_limit_per_downstream_connection: false | ||
|
|
||
|
|
||
| Example filter configuration for a globally disabled rate limiter but enabled for a specific route: | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,18 +13,24 @@ namespace Extensions { | |
| namespace HttpFilters { | ||
| namespace LocalRateLimitFilter { | ||
|
|
||
| const std::string& PerConnectionRateLimiter::key() { | ||
| CONSTRUCT_ON_FIRST_USE(std::string, "per_connection_local_rate_limiter"); | ||
| } | ||
|
|
||
| FilterConfig::FilterConfig( | ||
| const envoy::extensions::filters::http::local_ratelimit::v3::LocalRateLimit& config, | ||
| const LocalInfo::LocalInfo& local_info, Event::Dispatcher& dispatcher, Stats::Scope& scope, | ||
| Runtime::Loader& runtime, const bool per_route) | ||
| : status_(toErrorCode(config.status().code())), | ||
| stats_(generateStats(config.stat_prefix(), scope)), | ||
| fill_interval_(std::chrono::milliseconds( | ||
| PROTOBUF_GET_MS_OR_DEFAULT(config.token_bucket(), fill_interval, 0))), | ||
| max_tokens_(config.token_bucket().max_tokens()), | ||
| tokens_per_fill_(PROTOBUF_GET_WRAPPED_OR_DEFAULT(config.token_bucket(), tokens_per_fill, 1)), | ||
| descriptors_(config.descriptors()), | ||
| rate_limit_per_connection_(config.local_rate_limit_per_downstream_connection()), | ||
| rate_limiter_(Filters::Common::LocalRateLimit::LocalRateLimiterImpl( | ||
| std::chrono::milliseconds( | ||
| PROTOBUF_GET_MS_OR_DEFAULT(config.token_bucket(), fill_interval, 0)), | ||
| config.token_bucket().max_tokens(), | ||
| PROTOBUF_GET_WRAPPED_OR_DEFAULT(config.token_bucket(), tokens_per_fill, 1), dispatcher, | ||
| config.descriptors())), | ||
| fill_interval_, max_tokens_, tokens_per_fill_, dispatcher, descriptors_)), | ||
| local_info_(local_info), runtime_(runtime), | ||
| filter_enabled_( | ||
| config.has_filter_enabled() | ||
|
|
@@ -84,7 +90,10 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, | |
| populateDescriptors(descriptors, headers); | ||
| } | ||
|
|
||
| if (config->requestAllowed(descriptors)) { | ||
| bool isRequestAllowed = config->rateLimitPerConnection() ? requestAllowed(descriptors) | ||
|
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. style: |
||
| : config->requestAllowed(descriptors); | ||
|
|
||
| if (isRequestAllowed) { | ||
| config->stats().ok_.inc(); | ||
| return Http::FilterHeadersStatus::Continue; | ||
| } | ||
|
|
@@ -109,6 +118,31 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, | |
| return Http::FilterHeadersStatus::StopIteration; | ||
| } | ||
|
|
||
| bool Filter::requestAllowed(absl::Span<const RateLimit::LocalDescriptor> request_descriptors) { | ||
| return getRateLimiter()->requestAllowed(request_descriptors); | ||
| } | ||
|
|
||
| LocalRateLimiterImplSharedPtr Filter::getRateLimiter() { | ||
| const auto* config = getConfig(); | ||
|
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. move this closer to where it's used, e.g.: just before
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. Moved it back up a couple of lines to add an assertion. |
||
|
|
||
| if (!decoder_callbacks_->streamInfo().filterState()->hasData<PerConnectionRateLimiter>( | ||
| PerConnectionRateLimiter::key())) { | ||
| auto rate_limiter = std::make_shared<Filters::Common::LocalRateLimit::LocalRateLimiterImpl>( | ||
| config->fillInterval(), config->maxTokens(), config->tokensPerFill(), | ||
| decoder_callbacks_->dispatcher(), config->descriptors()); | ||
|
|
||
|
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. nit: extra whitespace? |
||
| decoder_callbacks_->streamInfo().filterState()->setData( | ||
| PerConnectionRateLimiter::key(), std::make_unique<PerConnectionRateLimiter>(rate_limiter), | ||
| StreamInfo::FilterState::StateType::ReadOnly, | ||
| StreamInfo::FilterState::LifeSpan::Connection); | ||
| } | ||
|
|
||
| return decoder_callbacks_->streamInfo() | ||
| .filterState() | ||
| ->getDataReadOnly<PerConnectionRateLimiter>(PerConnectionRateLimiter::key()) | ||
| .value(); | ||
| } | ||
|
|
||
| void Filter::populateDescriptors(std::vector<RateLimit::LocalDescriptor>& descriptors, | ||
| Http::RequestHeaderMap& headers) { | ||
| Router::RouteConstSharedPtr route = decoder_callbacks_->route(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,6 +42,19 @@ struct LocalRateLimitStats { | |
| ALL_LOCAL_RATE_LIMIT_STATS(GENERATE_COUNTER_STRUCT) | ||
| }; | ||
|
|
||
| using LocalRateLimiterImplSharedPtr = | ||
| std::shared_ptr<Filters::Common::LocalRateLimit::LocalRateLimiterImpl>; | ||
|
|
||
| class PerConnectionRateLimiter : public StreamInfo::FilterState::Object { | ||
| public: | ||
| PerConnectionRateLimiter(LocalRateLimiterImplSharedPtr rl) : rate_limiter_(rl) {} | ||
| static const std::string& key(); | ||
| LocalRateLimiterImplSharedPtr value() const { return rate_limiter_; } | ||
|
|
||
| private: | ||
| LocalRateLimiterImplSharedPtr rate_limiter_; | ||
|
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. does this really need to be shared? Sounds like it could be unique or -- possibly better -- just an instance constructed when PerConnectionRateLimiter is created?
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. Seems like this can only be a |
||
| }; | ||
|
|
||
| /** | ||
| * Global configuration for the HTTP local rate limit filter. | ||
| */ | ||
|
|
@@ -62,6 +75,15 @@ class FilterConfig : public Router::RouteSpecificFilterConfig { | |
| Http::Code status() const { return status_; } | ||
| uint64_t stage() const { return stage_; } | ||
| bool hasDescriptors() const { return has_descriptors_; } | ||
| std::chrono::milliseconds fillInterval() const { return fill_interval_; } | ||
|
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 return |
||
| uint32_t maxTokens() const { return max_tokens_; } | ||
| uint32_t tokensPerFill() const { return tokens_per_fill_; } | ||
| const Protobuf::RepeatedPtrField< | ||
| envoy::extensions::common::ratelimit::v3::LocalRateLimitDescriptor> | ||
|
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: (a reference) |
||
| descriptors() const { | ||
| return descriptors_; | ||
| } | ||
| bool rateLimitPerConnection() const { return rate_limit_per_connection_; } | ||
|
|
||
| private: | ||
| friend class FilterTest; | ||
|
|
@@ -78,6 +100,13 @@ class FilterConfig : public Router::RouteSpecificFilterConfig { | |
|
|
||
| const Http::Code status_; | ||
| mutable LocalRateLimitStats stats_; | ||
| const std::chrono::milliseconds fill_interval_; | ||
| const uint32_t max_tokens_; | ||
| const uint32_t tokens_per_fill_; | ||
| const Protobuf::RepeatedPtrField< | ||
| envoy::extensions::common::ratelimit::v3::LocalRateLimitDescriptor> | ||
| descriptors_; | ||
| const bool rate_limit_per_connection_; | ||
| Filters::Common::LocalRateLimit::LocalRateLimiterImpl rate_limiter_; | ||
| const LocalInfo::LocalInfo& local_info_; | ||
| Runtime::Loader& runtime_; | ||
|
|
@@ -108,6 +137,8 @@ class Filter : public Http::PassThroughFilter { | |
|
|
||
| void populateDescriptors(std::vector<RateLimit::LocalDescriptor>& descriptors, | ||
| Http::RequestHeaderMap& headers); | ||
| LocalRateLimiterImplSharedPtr getRateLimiter(); | ||
|
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. return |
||
| bool requestAllowed(absl::Span<const RateLimit::LocalDescriptor> request_descriptors); | ||
|
|
||
| const FilterConfig* getConfig() const; | ||
| FilterConfigSharedPtr config_; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,6 +39,7 @@ stat_prefix: test | |
| header: | ||
| key: x-local-ratelimited | ||
| value: 'true' | ||
| local_rate_limit_per_downstream_connection: {} | ||
|
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. Seems like the {} is used in substitutions below. It would be good to add an end-of-line comment explaining the {}. I think that the yaml comment character is #
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. Added an explanation below. |
||
| )"; | ||
|
|
||
| class FilterTest : public testing::Test { | ||
|
|
@@ -58,12 +59,18 @@ class FilterTest : public testing::Test { | |
| testing::Matcher<const envoy::type::v3::FractionalPercent&>(Percent(100)))) | ||
| .WillRepeatedly(testing::Return(enforced)); | ||
|
|
||
| ON_CALL(decoder_callbacks_, dispatcher()).WillByDefault(ReturnRef(dispatcher_)); | ||
| ON_CALL(decoder_callbacks_2_, dispatcher()).WillByDefault(ReturnRef(dispatcher_)); | ||
|
|
||
| envoy::extensions::filters::http::local_ratelimit::v3::LocalRateLimit config; | ||
| TestUtility::loadFromYaml(yaml, config); | ||
| config_ = std::make_shared<FilterConfig>(config, local_info_, dispatcher_, stats_, runtime_, | ||
| per_route); | ||
| filter_ = std::make_shared<Filter>(config_); | ||
| filter_->setDecoderFilterCallbacks(decoder_callbacks_); | ||
|
|
||
| filter_2_ = std::make_shared<Filter>(config_); | ||
| filter_2_->setDecoderFilterCallbacks(decoder_callbacks_2_); | ||
| } | ||
| void setup(const std::string& yaml, const bool enabled = true, const bool enforced = true) { | ||
| setupPerRoute(yaml, enabled, enforced); | ||
|
|
@@ -78,44 +85,59 @@ class FilterTest : public testing::Test { | |
|
|
||
| Stats::IsolatedStoreImpl stats_; | ||
| testing::NiceMock<Http::MockStreamDecoderFilterCallbacks> decoder_callbacks_; | ||
| testing::NiceMock<Http::MockStreamDecoderFilterCallbacks> decoder_callbacks_2_; | ||
| NiceMock<Event::MockDispatcher> dispatcher_; | ||
| NiceMock<Runtime::MockLoader> runtime_; | ||
| NiceMock<LocalInfo::MockLocalInfo> local_info_; | ||
| std::shared_ptr<FilterConfig> config_; | ||
| std::shared_ptr<Filter> filter_; | ||
| std::shared_ptr<Filter> filter_2_; | ||
| }; | ||
|
|
||
| TEST_F(FilterTest, Runtime) { | ||
| setup(fmt::format(config_yaml, "1"), false, false); | ||
| setup(fmt::format(config_yaml, "1", "false"), false, false); | ||
| EXPECT_EQ(&runtime_, &(config_->runtime())); | ||
| } | ||
|
|
||
| TEST_F(FilterTest, ToErrorCode) { | ||
| setup(fmt::format(config_yaml, "1"), false, false); | ||
| setup(fmt::format(config_yaml, "1", "false"), false, false); | ||
| EXPECT_EQ(Http::Code::BadRequest, toErrorCode(400)); | ||
| } | ||
|
|
||
| TEST_F(FilterTest, Disabled) { | ||
| setup(fmt::format(config_yaml, "1"), false, false); | ||
| setup(fmt::format(config_yaml, "1", "false"), false, false); | ||
| auto headers = Http::TestRequestHeaderMapImpl(); | ||
| EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); | ||
| EXPECT_EQ(0U, findCounter("test.http_local_rate_limit.enabled")); | ||
| EXPECT_EQ(0U, findCounter("test.http_local_rate_limit.enforced")); | ||
| } | ||
|
|
||
| TEST_F(FilterTest, RequestOk) { | ||
| setup(fmt::format(config_yaml, "1")); | ||
| setup(fmt::format(config_yaml, "1", "false")); | ||
| auto headers = Http::TestRequestHeaderMapImpl(); | ||
| EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); | ||
| EXPECT_EQ(1U, findCounter("test.http_local_rate_limit.enabled")); | ||
| EXPECT_EQ(0U, findCounter("test.http_local_rate_limit.enforced")); | ||
| EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, filter_2_->decodeHeaders(headers, false)); | ||
| EXPECT_EQ(2U, findCounter("test.http_local_rate_limit.enabled")); | ||
| EXPECT_EQ(1U, findCounter("test.http_local_rate_limit.enforced")); | ||
| EXPECT_EQ(1U, findCounter("test.http_local_rate_limit.ok")); | ||
| EXPECT_EQ(1U, findCounter("test.http_local_rate_limit.rate_limited")); | ||
| } | ||
|
|
||
| TEST_F(FilterTest, RequestOkPerConnection) { | ||
| setup(fmt::format(config_yaml, "1", "true")); | ||
| auto headers = Http::TestRequestHeaderMapImpl(); | ||
| EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); | ||
| EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_2_->decodeHeaders(headers, false)); | ||
| EXPECT_EQ(2U, findCounter("test.http_local_rate_limit.enabled")); | ||
| EXPECT_EQ(0U, findCounter("test.http_local_rate_limit.enforced")); | ||
| EXPECT_EQ(2U, findCounter("test.http_local_rate_limit.ok")); | ||
| EXPECT_EQ(0U, findCounter("test.http_local_rate_limit.rate_limited")); | ||
| } | ||
|
|
||
| TEST_F(FilterTest, RequestRateLimited) { | ||
| setup(fmt::format(config_yaml, "0")); | ||
| setup(fmt::format(config_yaml, "1", "false")); | ||
|
|
||
| EXPECT_CALL(decoder_callbacks_, sendLocalReply(Http::Code::TooManyRequests, _, _, _, _)) | ||
| EXPECT_CALL(decoder_callbacks_2_, sendLocalReply(Http::Code::TooManyRequests, _, _, _, _)) | ||
| .WillOnce(Invoke([](Http::Code code, absl::string_view body, | ||
| std::function<void(Http::ResponseHeaderMap & headers)> modify_headers, | ||
| const absl::optional<Grpc::Status::GrpcStatus> grpc_status, | ||
|
|
@@ -136,16 +158,55 @@ TEST_F(FilterTest, RequestRateLimited) { | |
| auto request_headers = Http::TestRequestHeaderMapImpl(); | ||
| auto expected_headers = Http::TestRequestHeaderMapImpl(); | ||
|
|
||
| EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, | ||
| filter_->decodeHeaders(request_headers, false)); | ||
| EXPECT_EQ(request_headers, expected_headers); | ||
| EXPECT_EQ(1U, findCounter("test.http_local_rate_limit.enabled")); | ||
| EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, false)); | ||
| EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, | ||
| filter_2_->decodeHeaders(request_headers, false)); | ||
| EXPECT_EQ(2U, findCounter("test.http_local_rate_limit.enabled")); | ||
| EXPECT_EQ(1U, findCounter("test.http_local_rate_limit.enforced")); | ||
| EXPECT_EQ(1U, findCounter("test.http_local_rate_limit.ok")); | ||
| EXPECT_EQ(1U, findCounter("test.http_local_rate_limit.rate_limited")); | ||
| } | ||
|
|
||
| TEST_F(FilterTest, RequestRateLimitedPerConnection) { | ||
|
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. WDYT of adding an integration test? I think you can crib off of existing tests in ratelimit_integration_test.cc plus some of the h2 tests to make sure if you stick a limit of one stream, that a second stream on that connection won't (immediately) go upstream but a stream on a separate connection will pass through.
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. Sure, I can take a look at adding an integration test for the above scenario. The remaining comments should be addressed and good to go.
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. Ah! After taking a closer look, seems like the tests in ratelimit_integration_test.cc are all targeting the 'rate limiter as an external service' use case and it makes complete sense to have integration tests for that.
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 think integration tests generally have value - I wasn't convinced setting connection data on the per-request StreamInfo would work as expected (there are no currently filters which do this, but I went ahead and tweaked some existing code to verify it worked for my own satiscation =P) but given the review delay this PR already suffered and lack of local rate limit tests to crib off of, I'm inclined to let it go this once though if you want to do a follow-up you'd totally earn brownie points :-)
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. Brownie points it is then :-) I do have a draft up but I'm fighting the integration test api a bit at the moment, specifically with trying to get it to send multiple requests over the same connection while still ensuring that the first one generates an upstream request over its Thanks for taking a look!! |
||
| setup(fmt::format(config_yaml, "1", "true")); | ||
|
|
||
| EXPECT_CALL(decoder_callbacks_, sendLocalReply(Http::Code::TooManyRequests, _, _, _, _)) | ||
| .WillOnce(Invoke([](Http::Code code, absl::string_view body, | ||
| std::function<void(Http::ResponseHeaderMap & headers)> modify_headers, | ||
| const absl::optional<Grpc::Status::GrpcStatus> grpc_status, | ||
| absl::string_view details) { | ||
| EXPECT_EQ(Http::Code::TooManyRequests, code); | ||
| EXPECT_EQ("local_rate_limited", body); | ||
|
|
||
| Http::TestResponseHeaderMapImpl response_headers{{":status", "200"}}; | ||
| modify_headers(response_headers); | ||
| EXPECT_EQ("true", response_headers.get(Http::LowerCaseString("x-test-rate-limit"))[0] | ||
| ->value() | ||
| .getStringView()); | ||
|
|
||
| EXPECT_EQ(grpc_status, absl::nullopt); | ||
| EXPECT_EQ(details, "local_rate_limited"); | ||
| })); | ||
|
|
||
| auto request_headers = Http::TestRequestHeaderMapImpl(); | ||
| auto expected_headers = Http::TestRequestHeaderMapImpl(); | ||
|
|
||
| EXPECT_EQ(request_headers, expected_headers); | ||
| EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, false)); | ||
| EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, | ||
| filter_->decodeHeaders(request_headers, false)); | ||
| EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_2_->decodeHeaders(request_headers, false)); | ||
| EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, | ||
| filter_2_->decodeHeaders(request_headers, false)); | ||
|
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. A comment explaining the differences between this test and FilterTest.RequestRateLimited may be helpful for future readers. I think this shows that the limit is applied by connection by verifying that filter_2_ allows requests after filter_ hits the rate limit.
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. Added a brief explanation to the test. |
||
| EXPECT_EQ(4U, findCounter("test.http_local_rate_limit.enabled")); | ||
| EXPECT_EQ(2U, findCounter("test.http_local_rate_limit.enforced")); | ||
| EXPECT_EQ(2U, findCounter("test.http_local_rate_limit.ok")); | ||
| EXPECT_EQ(2U, findCounter("test.http_local_rate_limit.rate_limited")); | ||
| } | ||
|
|
||
| TEST_F(FilterTest, RequestRateLimitedButNotEnforced) { | ||
| setup(fmt::format(config_yaml, "0"), true, false); | ||
| setup(fmt::format(config_yaml, "0", "false"), true, false); | ||
|
|
||
| EXPECT_CALL(decoder_callbacks_, sendLocalReply(Http::Code::TooManyRequests, _, _, _, _)).Times(0); | ||
|
|
||
|
|
@@ -181,6 +242,7 @@ stat_prefix: test | |
| header: | ||
| key: x-test-rate-limit | ||
| value: 'true' | ||
| local_rate_limit_per_downstream_connection: true | ||
| descriptors: | ||
| - entries: | ||
| - key: hello | ||
|
|
||
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.
these shouldnt be here - i think they have just been removed