From 55b7fcd198b1f4a8cbb845a3730f57c0a02cfd6d Mon Sep 17 00:00:00 2001 From: zirain Date: Sun, 4 Jan 2026 13:43:37 +0800 Subject: [PATCH 1/4] local_ratelimit: support per descriptor shadow mode Signed-off-by: zirain --- .../common/ratelimit/v3/ratelimit.proto | 3 ++ .../local_ratelimit/local_ratelimit_impl.cc | 32 ++++++++++--------- .../local_ratelimit/local_ratelimit_impl.h | 9 ++++-- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/api/envoy/extensions/common/ratelimit/v3/ratelimit.proto b/api/envoy/extensions/common/ratelimit/v3/ratelimit.proto index f9cba6de128de..9cc80352667b5 100644 --- a/api/envoy/extensions/common/ratelimit/v3/ratelimit.proto +++ b/api/envoy/extensions/common/ratelimit/v3/ratelimit.proto @@ -144,6 +144,9 @@ message LocalRateLimitDescriptor { // Token Bucket algorithm for local ratelimiting. type.v3.TokenBucket token_bucket = 2 [(validate.rules).message = {required: true}]; + + // Mark the descriptor as shadow. When the values is true, envoy allow requests to the backend. + bool shadow_mode = 3; } // Configuration used to enable local cluster level rate limiting where the token buckets diff --git a/source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.cc b/source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.cc index c41457a38794d..3c404828461e7 100644 --- a/source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.cc +++ b/source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.cc @@ -76,12 +76,11 @@ ShareProviderManagerSharedPtr ShareProviderManager::singleton(Event::Dispatcher& RateLimitTokenBucket::RateLimitTokenBucket(uint64_t max_tokens, uint64_t tokens_per_fill, std::chrono::milliseconds fill_interval, - TimeSource& time_source) + TimeSource& time_source, bool shadow_mode) : token_bucket_(max_tokens, time_source, // Calculate the fill rate in tokens per second. tokens_per_fill / std::chrono::duration(fill_interval).count()), - fill_interval_(fill_interval) {} - + fill_interval_(fill_interval), shadow_mode_(shadow_mode) {} bool RateLimitTokenBucket::consume(double factor, uint64_t to_consume) { ASSERT(!(factor <= 0.0 || factor > 1.0)); auto cb = [tokens = to_consume / factor](double total) { return total < tokens ? 0.0 : tokens; }; @@ -103,8 +102,8 @@ LocalRateLimiterImpl::LocalRateLimiterImpl( 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(max_tokens, tokens_per_fill, - fill_interval, time_source_); + default_token_bucket_ = std::make_shared( + max_tokens, tokens_per_fill, fill_interval, time_source_, false); } for (const auto& descriptor : descriptors) { @@ -123,6 +122,7 @@ LocalRateLimiterImpl::LocalRateLimiterImpl( PROTOBUF_GET_WRAPPED_OR_DEFAULT(descriptor.token_bucket(), tokens_per_fill, 1); const auto per_descriptor_fill_interval = std::chrono::milliseconds( PROTOBUF_GET_MS_OR_DEFAULT(descriptor.token_bucket(), fill_interval, 0)); + 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). @@ -133,14 +133,14 @@ LocalRateLimiterImpl::LocalRateLimiterImpl( if (wildcard_found) { DynamicDescriptorSharedPtr dynamic_descriptor = std::make_shared( per_descriptor_max_tokens, per_descriptor_tokens_per_fill, per_descriptor_fill_interval, - lru_size, dispatcher.timeSource()); + lru_size, dispatcher.timeSource(), shadow_mode); dynamic_descriptors_.addDescriptor(std::move(new_descriptor), std::move(dynamic_descriptor)); continue; } RateLimitTokenBucketSharedPtr per_descriptor_token_bucket = - std::make_shared(per_descriptor_max_tokens, - per_descriptor_tokens_per_fill, - per_descriptor_fill_interval, time_source_); + std::make_shared( + per_descriptor_max_tokens, per_descriptor_tokens_per_fill, per_descriptor_fill_interval, + time_source_, shadow_mode); auto result = descriptors_.emplace(std::move(new_descriptor), std::move(per_descriptor_token_bucket)); if (!result.second) { @@ -194,7 +194,8 @@ LocalRateLimiterImpl::requestAllowed(absl::Span req share_factor, match_result.request_descriptor.get().hits_addend_.value_or(1))) { // If the request is forbidden by a descriptor, return the result and the descriptor // token bucket. - return {false, std::shared_ptr(match_result.token_bucket)}; + return {match_result.token_bucket->shadowMode(), + std::shared_ptr(match_result.token_bucket)}; } ENVOY_LOG(trace, "request allowed by descriptor with fill rate: {}, maxToken: {}, remainingToken {}", @@ -214,7 +215,8 @@ LocalRateLimiterImpl::requestAllowed(absl::Span req if (const bool result = default_token_bucket_->consume(share_factor); !result) { // If the request is forbidden by the default token bucket, return the result and the // default token bucket. - return {false, std::shared_ptr(default_token_bucket_)}; + return {default_token_bucket_->shadowMode(), + std::shared_ptr(default_token_bucket_)}; } // If the request is allowed then return the result the token bucket. The descriptor @@ -282,10 +284,10 @@ DynamicDescriptorMap::getBucket(const RateLimit::Descriptor request_descriptor) DynamicDescriptor::DynamicDescriptor(uint64_t per_descriptor_max_tokens, uint64_t per_descriptor_tokens_per_fill, std::chrono::milliseconds per_descriptor_fill_interval, - uint32_t lru_size, TimeSource& time_source) + uint32_t lru_size, TimeSource& time_source, bool shadow_mode) : max_tokens_(per_descriptor_max_tokens), tokens_per_fill_(per_descriptor_tokens_per_fill), - fill_interval_(per_descriptor_fill_interval), lru_size_(lru_size), time_source_(time_source) { -} + fill_interval_(per_descriptor_fill_interval), lru_size_(lru_size), time_source_(time_source), + shadow_mode_(shadow_mode) {} RateLimitTokenBucketSharedPtr DynamicDescriptor::addOrGetDescriptor(const RateLimit::Descriptor& request_descriptor) { @@ -303,7 +305,7 @@ DynamicDescriptor::addOrGetDescriptor(const RateLimit::Descriptor& request_descr ENVOY_LOG(trace, "max_tokens: {}, tokens_per_fill: {}, fill_interval: {}", max_tokens_, tokens_per_fill_, std::chrono::duration(fill_interval_).count()); per_descriptor_token_bucket = std::make_shared( - max_tokens_, tokens_per_fill_, fill_interval_, time_source_); + max_tokens_, tokens_per_fill_, fill_interval_, time_source_, shadow_mode_); ENVOY_LOG(trace, "DynamicDescriptor::addorGetDescriptor: adding dynamic descriptor: {}", request_descriptor.toString()); diff --git a/source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.h b/source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.h index 40bc8d0c0946a..0db7cea1e1676 100644 --- a/source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.h +++ b/source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.h @@ -29,7 +29,8 @@ using ProtoLocalClusterRateLimit = envoy::extensions::common::ratelimit::v3::Loc class DynamicDescriptor : public Logger::Loggable { public: DynamicDescriptor(uint64_t max_tokens, uint64_t tokens_per_fill, - std::chrono::milliseconds fill_interval, uint32_t lru_size, TimeSource&); + std::chrono::milliseconds fill_interval, uint32_t lru_size, + TimeSource& time_source, bool shadow_mode); // add a new user configured descriptor to the set. RateLimitTokenBucketSharedPtr addOrGetDescriptor(const RateLimit::Descriptor& request_descriptor); @@ -46,6 +47,7 @@ class DynamicDescriptor : public Logger::Loggable LruList lru_list_; uint32_t lru_size_; TimeSource& time_source_; + const bool shadow_mode_{false}; }; using DynamicDescriptorSharedPtr = std::shared_ptr; @@ -104,10 +106,12 @@ class RateLimitTokenBucket : public TokenBucketContext, public Logger::Loggable { public: RateLimitTokenBucket(uint64_t max_tokens, uint64_t tokens_per_fill, - std::chrono::milliseconds fill_interval, TimeSource& time_source); + std::chrono::milliseconds fill_interval, TimeSource& time_source, + bool shadow_mode); // RateLimitTokenBucket bool consume(double factor = 1.0, uint64_t tokens = 1); + bool shadowMode() const { return shadow_mode_; } double fillRate() const { return token_bucket_.fillRate(); } std::chrono::milliseconds fillInterval() const { return fill_interval_; } @@ -122,6 +126,7 @@ class RateLimitTokenBucket : public TokenBucketContext, private: AtomicTokenBucketImpl token_bucket_; const std::chrono::milliseconds fill_interval_; + const bool shadow_mode_{false}; }; using RateLimitTokenBucketSharedPtr = std::shared_ptr; From 7c250aeca26cb7d39ca36a7ca94b60eea2da35f8 Mon Sep 17 00:00:00 2001 From: zirain Date: Sun, 4 Jan 2026 16:46:57 +0800 Subject: [PATCH 2/4] add test Signed-off-by: zirain --- .../local_ratelimit_integration_test.cc | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/test/extensions/filters/http/local_ratelimit/local_ratelimit_integration_test.cc b/test/extensions/filters/http/local_ratelimit/local_ratelimit_integration_test.cc index 39a257ed0fd28..5d4bca5508882 100644 --- a/test/extensions/filters/http/local_ratelimit/local_ratelimit_integration_test.cc +++ b/test/extensions/filters/http/local_ratelimit/local_ratelimit_integration_test.cc @@ -316,6 +316,48 @@ name: envoy.filters.http.local_ratelimit local_rate_limit_per_downstream_connection: {} )EOF"; + static constexpr absl::string_view filter_config_with_shadow_mode_ = + R"EOF( +name: envoy.filters.http.local_ratelimit +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit + stat_prefix: http_local_rate_limiter + max_dynamic_descriptors: {} + token_bucket: + max_tokens: 2 + tokens_per_fill: 1 + fill_interval: 1000s + filter_enabled: + runtime_key: local_rate_limit_enabled + default_value: + numerator: 100 + denominator: HUNDRED + filter_enforced: + runtime_key: local_rate_limit_enforced + default_value: + numerator: 100 + denominator: HUNDRED + response_headers_to_add: + - append_action: OVERWRITE_IF_EXISTS_OR_ADD + header: + key: x-local-rate-limit + value: 'true' + descriptors: + - entries: + - key: client_cluster + token_bucket: + max_tokens: 1 + tokens_per_fill: 1 + fill_interval: 1000s + shadow_mode: true + rate_limits: + - actions: # any actions in here + - request_headers: + header_name: x-envoy-downstream-service-cluster + descriptor_key: client_cluster + local_rate_limit_per_downstream_connection: {} +)EOF"; + const std::string filter_config_with_local_cluster_rate_limit_ = R"EOF( name: envoy.filters.http.local_ratelimit @@ -478,6 +520,33 @@ TEST_P(LocalRateLimitFilterIntegrationTest, DynamicDesciptorsBasicTest) { cleanupUpstreamAndDownstream(); } +TEST_P(LocalRateLimitFilterIntegrationTest, ShadowModeTest) { + initializeFilter(fmt::format(filter_config_with_shadow_mode_, 20, "false")); + // filter is adding dynamic descriptors based on the request header + // 'x-envoy-downstream-service-cluster' and the token bucket is set to 1 token per fill interval + // of 1000s which means only one request is allowed per 1000s for each unique value of + // 'x-envoy-downstream-service-cluster' header. + + codec_client_ = makeHttpConnection(lookupPort("http")); + sendAndVerifyRequest("foo", "200", 0); + cleanupUpstreamAndDownstream(); + + // Since shadow mode is true, should be allowed. + codec_client_ = makeHttpConnection(lookupPort("http")); + sendAndVerifyRequest("foo","200", 0); + cleanupUpstreamAndDownstream(); + + // The next request with a different cluster, 'bar', should be allowed. + codec_client_ = makeHttpConnection(lookupPort("http")); + sendAndVerifyRequest("bar", "200", 0); + cleanupUpstreamAndDownstream(); + + // Since shadow mode is true, should be allowed. + codec_client_ = makeHttpConnection(lookupPort("http")); + sendAndVerifyRequest("bar", "200", 0); + cleanupUpstreamAndDownstream(); +} + TEST_P(LocalRateLimitFilterIntegrationTest, DesciptorsBasicTestWithMinimumMaxDynamicDescriptors) { auto max_dynamic_descriptors = 1; initializeFilter( From 77abfd3d6a81ba96533cb08652394811746f5835 Mon Sep 17 00:00:00 2001 From: zirain Date: Mon, 5 Jan 2026 13:12:27 +0800 Subject: [PATCH 3/4] fix format Signed-off-by: zirain --- .../http/local_ratelimit/local_ratelimit_integration_test.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/extensions/filters/http/local_ratelimit/local_ratelimit_integration_test.cc b/test/extensions/filters/http/local_ratelimit/local_ratelimit_integration_test.cc index 5d4bca5508882..a5b66a2e1d3ca 100644 --- a/test/extensions/filters/http/local_ratelimit/local_ratelimit_integration_test.cc +++ b/test/extensions/filters/http/local_ratelimit/local_ratelimit_integration_test.cc @@ -533,7 +533,7 @@ TEST_P(LocalRateLimitFilterIntegrationTest, ShadowModeTest) { // Since shadow mode is true, should be allowed. codec_client_ = makeHttpConnection(lookupPort("http")); - sendAndVerifyRequest("foo","200", 0); + sendAndVerifyRequest("foo", "200", 0); cleanupUpstreamAndDownstream(); // The next request with a different cluster, 'bar', should be allowed. From 3e3d07d5306fd98c5f97c8c5f9d9314f0a29e98d Mon Sep 17 00:00:00 2001 From: zirain Date: Tue, 6 Jan 2026 15:42:50 +0800 Subject: [PATCH 4/4] changelogs Signed-off-by: zirain --- changelogs/current.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/changelogs/current.yaml b/changelogs/current.yaml index a6a5edaa274f9..b9a7098b4687b 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -678,5 +678,8 @@ new_features: Added comprehensive metrics and tracing tags to the :ref:`Proto API Scrubber ` filter. This includes counters for requests, blocks, and failures, latency histograms, and span tags for scrubbing outcomes. +- area: ratelimit + change: | + Added support for shadow mode in local rate limit filter. deprecated: