Skip to content

Commit f2023ef

Browse files
authored
ratelimit: support new filter enabled and filter enforced (envoyproxy#38653)
1 parent 41caf64 commit f2023ef

File tree

7 files changed

+148
-12
lines changed

7 files changed

+148
-12
lines changed

api/envoy/extensions/filters/http/ratelimit/v3/rate_limit.proto

+18-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE;
2323
// Rate limit :ref:`configuration overview <config_http_filters_rate_limit>`.
2424
// [#extension: envoy.filters.http.ratelimit]
2525

26-
// [#next-free-field: 14]
26+
// [#next-free-field: 16]
2727
message RateLimit {
2828
option (udpa.annotations.versioning).previous_message_type =
2929
"envoy.config.filter.http.rate_limit.v2.RateLimit";
@@ -132,6 +132,23 @@ message RateLimit {
132132
// Optional additional prefix to use when emitting statistics. This allows to distinguish
133133
// emitted statistics between configured ``ratelimit`` filters in an HTTP filter chain.
134134
string stat_prefix = 13;
135+
136+
// If set, this will enable -- but not necessarily enforce -- the rate limit for the given
137+
// fraction of requests.
138+
//
139+
// If not set then ``ratelimit.http_filter_enabled`` runtime key will be used to determine
140+
// the fraction of requests to enforce rate limits on. And the default percentage of the
141+
// runtime key is 100% for backwards compatibility.
142+
config.core.v3.RuntimeFractionalPercent filter_enabled = 14;
143+
144+
// If set, this will enforce the rate limit decisions for the given fraction of requests.
145+
//
146+
// Note: this only applies to the fraction of enabled requests.
147+
//
148+
// If not set then ``ratelimit.http_filter_enforcing`` runtime key will be used to determine
149+
// the fraction of requests to enforce rate limits on. And the default percentage of the
150+
// runtime key is 100% for backwards compatibility.
151+
config.core.v3.RuntimeFractionalPercent filter_enforced = 15;
135152
}
136153

137154
message RateLimitPerRoute {

changelogs/current.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,12 @@ new_features:
250250
added :ref:`an option <config_network_filters_tcp_proxy_receive_before_connect>` to allow filters to read from the
251251
downstream connection before TCP proxy has opened the upstream connection, by setting a filter state object for the key
252252
``envoy.tcp_proxy.receive_before_connect``.
253+
- area: rate_limit
254+
change: |
255+
Added the explict runtime switch
256+
:ref:`filter_enabled <envoy_v3_api_field_extensions.filters.http.ratelimit.v3.RateLimit.filter_enabled>` and
257+
:ref:`filter_enforced <envoy_v3_api_field_extensions.filters.http.ratelimit.v3.RateLimit.filter_enforced>` to control the
258+
filter behavior.
253259
- area: tcp_proxy
254260
change: |
255261
Added :ref:`proxy_protocol_tlvs

docs/root/configuration/http/http_filters/rate_limit_filter.rst

-7
Original file line numberDiff line numberDiff line change
@@ -178,13 +178,6 @@ Runtime
178178

179179
The HTTP rate limit filter supports the following runtime settings:
180180

181-
ratelimit.http_filter_enabled
182-
% of requests that will call the rate limit service. Defaults to 100.
183-
184-
ratelimit.http_filter_enforcing
185-
% of requests that that will have the rate limit service decision enforced. Defaults to 100.
186-
This can be used to test what would happen before fully enforcing the outcome.
187-
188181
ratelimit.<route_key>.http_filter_enabled
189182
% of requests that will call the rate limit service for a given *route_key* specified in the
190183
:ref:`rate limit configuration <envoy_v3_api_msg_config.route.v3.RateLimit>`. Defaults to 100.

source/extensions/filters/http/ratelimit/BUILD

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ envoy_cc_library(
2626
"//source/common/common:enum_to_int",
2727
"//source/common/http:codes_lib",
2828
"//source/common/router:config_lib",
29+
"//source/common/runtime:runtime_protos_lib",
2930
"//source/common/stream_info:uint32_accessor_lib",
3031
"//source/extensions/filters/common/ratelimit:ratelimit_client_interface",
3132
"//source/extensions/filters/common/ratelimit:stat_names_lib",

source/extensions/filters/http/ratelimit/ratelimit.cc

+16-3
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ double Filter::getHitAddend() {
127127
}
128128

129129
Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, bool) {
130-
if (!config_->runtime().snapshot().featureEnabled("ratelimit.http_filter_enabled", 100)) {
130+
if (!config_->enabled()) {
131131
return Http::FilterHeadersStatus::Continue;
132132
}
133133

@@ -257,8 +257,7 @@ void Filter::complete(Filters::Common::RateLimit::LimitStatus status,
257257
descriptor_statuses = nullptr;
258258
}
259259

260-
if (status == Filters::Common::RateLimit::LimitStatus::OverLimit &&
261-
config_->runtime().snapshot().featureEnabled("ratelimit.http_filter_enforcing", 100)) {
260+
if (status == Filters::Common::RateLimit::LimitStatus::OverLimit && config_->enforced()) {
262261
state_ = State::Responded;
263262
callbacks_->streamInfo().setResponseFlag(StreamInfo::CoreResponseFlag::RateLimited);
264263
callbacks_->sendLocalReply(
@@ -370,6 +369,20 @@ void OnStreamDoneCallBack::complete(Filters::Common::RateLimit::LimitStatus,
370369
delete this;
371370
}
372371

372+
bool FilterConfig::enabled() const {
373+
if (filter_enabled_.has_value()) {
374+
return filter_enabled_->enabled();
375+
}
376+
return runtime_.snapshot().featureEnabled("ratelimit.http_filter_enabled", 100);
377+
}
378+
379+
bool FilterConfig::enforced() const {
380+
if (filter_enforced_.has_value()) {
381+
return filter_enforced_->enabled();
382+
}
383+
return runtime_.snapshot().featureEnabled("ratelimit.http_filter_enforcing", 100);
384+
}
385+
373386
} // namespace RateLimitFilter
374387
} // namespace HttpFilters
375388
} // namespace Extensions

source/extensions/filters/http/ratelimit/ratelimit.h

+16-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include "source/common/common/assert.h"
1818
#include "source/common/http/header_map_impl.h"
1919
#include "source/common/router/header_parser.h"
20+
#include "source/common/runtime/runtime_protos.h"
2021
#include "source/extensions/filters/common/ratelimit/ratelimit.h"
2122
#include "source/extensions/filters/common/ratelimit/stat_names.h"
2223
#include "source/extensions/filters/common/ratelimit_config/ratelimit_config.h"
@@ -59,7 +60,17 @@ class FilterConfig {
5960
: absl::nullopt),
6061
http_context_(http_context), stat_names_(scope.symbolTable(), config.stat_prefix()),
6162
rate_limited_status_(toErrorCode(config.rate_limited_status().code())),
62-
status_on_error_(toRatelimitServerErrorCode(config.status_on_error().code())) {
63+
status_on_error_(toRatelimitServerErrorCode(config.status_on_error().code())),
64+
filter_enabled_(
65+
config.has_filter_enabled()
66+
? absl::optional<Envoy::Runtime::FractionalPercent>(
67+
Envoy::Runtime::FractionalPercent(config.filter_enabled(), runtime_))
68+
: absl::nullopt),
69+
filter_enforced_(
70+
config.has_filter_enforced()
71+
? absl::optional<Envoy::Runtime::FractionalPercent>(
72+
Envoy::Runtime::FractionalPercent(config.filter_enforced(), runtime_))
73+
: absl::nullopt) {
6374
absl::StatusOr<Router::HeaderParserPtr> response_headers_parser_or_ =
6475
Envoy::Router::HeaderParser::configure(config.response_headers_to_add());
6576
SET_AND_RETURN_IF_NOT_OK(response_headers_parser_or_.status(), creation_status);
@@ -83,6 +94,8 @@ class FilterConfig {
8394
Http::Code rateLimitedStatus() { return rate_limited_status_; }
8495
const Router::HeaderParser& responseHeadersParser() const { return *response_headers_parser_; }
8596
Http::Code statusOnError() const { return status_on_error_; }
97+
bool enabled() const;
98+
bool enforced() const;
8699

87100
private:
88101
static FilterRequestType stringToType(const std::string& request_type) {
@@ -127,6 +140,8 @@ class FilterConfig {
127140
const Http::Code rate_limited_status_;
128141
Router::HeaderParserPtr response_headers_parser_;
129142
const Http::Code status_on_error_;
143+
const absl::optional<Envoy::Runtime::FractionalPercent> filter_enabled_;
144+
const absl::optional<Envoy::Runtime::FractionalPercent> filter_enforced_;
130145
};
131146

132147
using FilterConfigSharedPtr = std::shared_ptr<FilterConfig>;

test/extensions/filters/http/ratelimit/ratelimit_test.cc

+91
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,24 @@ class HttpRateLimitFilterTest : public testing::Test {
131131
stat_prefix: with_stat_prefix
132132
)EOF";
133133

134+
const std::string filter_config_with_filter_enabled_ = R"EOF(
135+
domain: foo
136+
filter_enabled:
137+
runtime_key: test_enabled
138+
default_value:
139+
numerator: 30
140+
denominator: HUNDRED
141+
)EOF";
142+
143+
const std::string filter_config_with_filter_enforced_ = R"EOF(
144+
domain: foo
145+
filter_enforced:
146+
runtime_key: test_enforced
147+
default_value:
148+
numerator: 50
149+
denominator: HUNDRED
150+
)EOF";
151+
134152
Filters::Common::RateLimit::MockClient* client_;
135153
NiceMock<Http::MockStreamDecoderFilterCallbacks> filter_callbacks_;
136154
Stats::StatNamePool pool_{filter_callbacks_.clusterInfo()->statsScope().symbolTable()};
@@ -233,6 +251,29 @@ TEST_F(HttpRateLimitFilterTest, RuntimeDisabled) {
233251
EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->encodeTrailers(response_trailers_));
234252
}
235253

254+
TEST_F(HttpRateLimitFilterTest, RuntimeDisabledFromFilterConfig) {
255+
setUpTest(filter_config_with_filter_enabled_);
256+
257+
EXPECT_CALL(
258+
factory_context_.runtime_loader_.snapshot_,
259+
featureEnabled(absl::string_view("test_enabled"),
260+
testing::Matcher<const envoy::type::v3::FractionalPercent&>(Percent(30))))
261+
.WillOnce(testing::Return(false));
262+
263+
// Explicit configuration in the filter config should override the default runtime key.
264+
EXPECT_CALL(factory_context_.runtime_loader_.snapshot_,
265+
featureEnabled("ratelimit.http_filter_enabled", 100))
266+
.Times(0);
267+
268+
EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers_, false));
269+
EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, false));
270+
EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_));
271+
EXPECT_EQ(Http::Filter1xxHeadersStatus::Continue, filter_->encode1xxHeaders(response_headers_));
272+
EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->encodeHeaders(response_headers_, false));
273+
EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->encodeData(response_data_, false));
274+
EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->encodeTrailers(response_trailers_));
275+
}
276+
236277
TEST_F(HttpRateLimitFilterTest, OkResponse) {
237278
setUpTest(filter_config_);
238279
InSequence s;
@@ -1043,6 +1084,56 @@ TEST_F(HttpRateLimitFilterTest, LimitResponseRuntimeDisabled) {
10431084
filter_callbacks_.clusterInfo()->statsScope().counterFromStatName(upstream_rq_429_).value());
10441085
}
10451086

1087+
TEST_F(HttpRateLimitFilterTest, LimitResponseRuntimeDisabledFromFilterConfig) {
1088+
setUpTest(filter_config_with_filter_enforced_);
1089+
InSequence s;
1090+
1091+
EXPECT_CALL(route_rate_limit_, populateDescriptors(_, _, _, _))
1092+
.WillOnce(SetArgReferee<0>(descriptor_));
1093+
EXPECT_CALL(*client_, limit(_, _, _, _, _, 0))
1094+
.WillOnce(
1095+
WithArgs<0>(Invoke([&](Filters::Common::RateLimit::RequestCallbacks& callbacks) -> void {
1096+
request_callbacks_ = &callbacks;
1097+
})));
1098+
1099+
EXPECT_EQ(Http::FilterHeadersStatus::StopIteration,
1100+
filter_->decodeHeaders(request_headers_, false));
1101+
1102+
EXPECT_CALL(
1103+
factory_context_.runtime_loader_.snapshot_,
1104+
featureEnabled(absl::string_view("test_enforced"),
1105+
testing::Matcher<const envoy::type::v3::FractionalPercent&>(Percent(50))))
1106+
.WillOnce(testing::Return(false));
1107+
1108+
// Explicit configuration in the filter config should override the default runtime key.
1109+
EXPECT_CALL(factory_context_.runtime_loader_.snapshot_,
1110+
featureEnabled("ratelimit.http_filter_enforcing", 100))
1111+
.Times(0);
1112+
1113+
EXPECT_CALL(filter_callbacks_, continueDecoding());
1114+
Http::ResponseHeaderMapPtr h{new Http::TestResponseHeaderMapImpl()};
1115+
request_callbacks_->complete(Filters::Common::RateLimit::LimitStatus::OverLimit, nullptr,
1116+
std::move(h), nullptr, "", nullptr);
1117+
1118+
EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, false));
1119+
EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_));
1120+
EXPECT_EQ(Http::Filter1xxHeadersStatus::Continue, filter_->encode1xxHeaders(response_headers_));
1121+
EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->encodeHeaders(response_headers_, false));
1122+
EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->encodeData(response_data_, false));
1123+
EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->encodeTrailers(response_trailers_));
1124+
1125+
EXPECT_EQ(1U, filter_callbacks_.clusterInfo()
1126+
->statsScope()
1127+
.counterFromStatName(ratelimit_over_limit_)
1128+
.value());
1129+
EXPECT_EQ(
1130+
1U,
1131+
filter_callbacks_.clusterInfo()->statsScope().counterFromStatName(upstream_rq_4xx_).value());
1132+
EXPECT_EQ(
1133+
1U,
1134+
filter_callbacks_.clusterInfo()->statsScope().counterFromStatName(upstream_rq_429_).value());
1135+
}
1136+
10461137
TEST_F(HttpRateLimitFilterTest, LimitResponseWithRateLimitedStatus) {
10471138
setUpTest(rate_limited_status_config_);
10481139
InSequence s;

0 commit comments

Comments
 (0)