Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions api/envoy/type/v3/token_bucket.proto
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ message TokenBucket {
option (udpa.annotations.versioning).previous_message_type = "envoy.type.TokenBucket";

// The maximum tokens that the bucket can hold. This is also the number of tokens that the bucket
// initially contains.
uint32 max_tokens = 1 [(validate.rules).uint32 = {gt: 0}];
// initially contains. A value of 0 means the bucket will always be empty and all requests will
// be rate limited (i.e., always reject).
uint32 max_tokens = 1 [(validate.rules).uint32 = {gte: 0}];

// The number of tokens added to the bucket during each fill interval. If not specified, defaults
// to a single token.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Added support for always-reject behavior in the :ref:`local rate limit filter
<config_http_filters_local_rate_limit>` by setting ``max_tokens`` to ``0`` in the
:ref:`token bucket <envoy_v3_api_msg_type.v3.TokenBucket>` configuration. This applies to both
the default token bucket and per-descriptor token buckets, including wildcard (dynamic) descriptors.
When ``max_tokens`` is ``0``, the fill interval validation (minimum 50ms) is also skipped since
filling is irrelevant.
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,16 @@ LocalRateLimiterImpl::LocalRateLimiterImpl(
// Ignore the default token bucket if fill_interval is 0 because 0 fill_interval means nothing
// and has undefined behavior.
if (fill_interval.count() > 0) {
if (fill_interval < std::chrono::milliseconds(50)) {
throw EnvoyException("local rate limit token bucket fill timer must be >= 50ms");
if (max_tokens == 0) {
// max_tokens=0 means always reject; no token bucket needed.
always_deny_default_ = true;
} else {
if (fill_interval < std::chrono::milliseconds(50)) {
throw EnvoyException("local rate limit token bucket fill timer must be >= 50ms");
}
default_token_bucket_ = std::make_shared<RateLimitTokenBucket>(
max_tokens, tokens_per_fill, fill_interval, time_source_, false);
}
default_token_bucket_ = std::make_shared<RateLimitTokenBucket>(
max_tokens, tokens_per_fill, fill_interval, time_source_, false);
}

for (const auto& descriptor : descriptors) {
Expand All @@ -141,8 +146,10 @@ LocalRateLimiterImpl::LocalRateLimiterImpl(
const auto shadow_mode = descriptor.shadow_mode();

// Validate that the descriptor's fill interval is logically correct (same
// constraint of >=50msec as for fill_interval).
if (per_descriptor_fill_interval < std::chrono::milliseconds(50)) {
// constraint of >=50msec as for fill_interval). Skip the check when max_tokens=0
// since the fill interval is irrelevant for an always-reject bucket.
if (per_descriptor_max_tokens != 0 &&
per_descriptor_fill_interval < std::chrono::milliseconds(50)) {
throw EnvoyException("local rate limit descriptor token bucket fill timer must be >= 50ms");
}

Expand Down Expand Up @@ -226,6 +233,9 @@ LocalRateLimiterImpl::requestAllowed(absl::Span<const RateLimit::Descriptor> req
// See if the request is forbidden by the default token bucket.
if (matched_results.empty() || always_consume_default_token_bucket_) {
if (default_token_bucket_ == nullptr) {
if (always_deny_default_) {
return {false, nullptr};
}
return {
true,
matched_results.empty()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ class LocalRateLimiterImpl : public Logger::Loggable<Logger::Id::local_rate_limi

mutable Thread::ThreadSynchronizer synchronizer_; // Used for testing only.
const bool always_consume_default_token_bucket_{};
bool always_deny_default_{false};
};

class AlwaysDenyLocalRateLimiter : public LocalRateLimiter {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ TEST_F(LocalRateLimiterImplTest, TooFastFillRate) {
EnvoyException, "local rate limit token bucket fill timer must be >= 50ms");
}

// Verify that max_tokens=0 skips the 50ms fill interval validation since fill is irrelevant.
TEST_F(LocalRateLimiterImplTest, ZeroMaxTokensSkipsFillIntervalCheck) {
VERBOSE_EXPECT_NO_THROW(
LocalRateLimiterImpl(std::chrono::milliseconds(10), 0, 1, dispatcher_, descriptors_));
}

class LocalRateLimiterDescriptorImplTest : public LocalRateLimiterImplTest {
public:
void initializeWithAtomicTokenBucketDescriptor(const std::chrono::milliseconds fill_interval,
Expand Down Expand Up @@ -325,6 +331,51 @@ TEST_F(LocalRateLimiterDescriptorImplTest, DescriptorRateLimitSmallFillInterval)
EnvoyException, "local rate limit descriptor token bucket fill timer must be >= 50ms");
}

// Verify that max_tokens=0 always rejects for both default and per-descriptor token buckets.
TEST_F(LocalRateLimiterDescriptorImplTest, AlwaysRejectWithZeroMaxTokens) {
// Default bucket: max_tokens=0 always rejects regardless of time advancing.
initializeWithAtomicTokenBucket(std::chrono::milliseconds(200), 0, 1);
EXPECT_FALSE(rate_limiter_->requestAllowed(route_descriptors_).allowed);
EXPECT_FALSE(rate_limiter_->requestAllowed(route_descriptors_).allowed);
dispatcher_.globalTimeSystem().advanceTimeWait(std::chrono::milliseconds(1000));
EXPECT_FALSE(rate_limiter_->requestAllowed(route_descriptors_).allowed);

// Per-descriptor bucket: max_tokens=0 always rejects regardless of time advancing.
TestUtility::loadFromYaml(fmt::format(single_descriptor_config_yaml, 0, 1, "0.1s"),
*descriptors_.Add());
initializeWithAtomicTokenBucketDescriptor(std::chrono::milliseconds(50), 1, 1);
EXPECT_FALSE(rate_limiter_->requestAllowed(descriptor_).allowed);
EXPECT_FALSE(rate_limiter_->requestAllowed(descriptor_).allowed);
dispatcher_.globalTimeSystem().advanceTimeWait(std::chrono::milliseconds(1000));
EXPECT_FALSE(rate_limiter_->requestAllowed(descriptor_).allowed);
}

// Verify that a descriptor with max_tokens=0 skips the 50ms fill interval validation.
TEST_F(LocalRateLimiterDescriptorImplTest, DescriptorZeroMaxTokensSkipsFillIntervalCheck) {
TestUtility::loadFromYaml(fmt::format(single_descriptor_config_yaml, 0, 1, "0.010s"),
*descriptors_.Add());
VERBOSE_EXPECT_NO_THROW(
LocalRateLimiterImpl(std::chrono::milliseconds(50), 1, 1, dispatcher_, descriptors_));
}

// Verify that a dynamic (wildcard) descriptor with max_tokens=0 always rejects.
TEST_F(LocalRateLimiterDescriptorImplTest, DynamicDescriptorAlwaysRejectWithZeroMaxTokens) {
TestUtility::loadFromYaml(fmt::format(wildcard_descriptor_config_yaml, 0, 1, "0.1s"),
*descriptors_.Add());
initializeWithAtomicTokenBucketDescriptor(std::chrono::milliseconds(50), 1, 1);

std::vector<RateLimit::Descriptor> descriptors{{{{"user", "A"}}}};
EXPECT_FALSE(rate_limiter_->requestAllowed(descriptors).allowed);
EXPECT_FALSE(rate_limiter_->requestAllowed(descriptors).allowed);

dispatcher_.globalTimeSystem().advanceTimeWait(std::chrono::milliseconds(1000));
EXPECT_FALSE(rate_limiter_->requestAllowed(descriptors).allowed);

// A different wildcard value also always rejects.
std::vector<RateLimit::Descriptor> descriptors2{{{{"user", "B"}}}};
EXPECT_FALSE(rate_limiter_->requestAllowed(descriptors2).allowed);
}

TEST_F(LocalRateLimiterDescriptorImplTest, DuplicateDescriptor) {
TestUtility::loadFromYaml(fmt::format(single_descriptor_config_yaml, 1, 1, "0.1s"),
*descriptors_.Add());
Expand Down
Loading