Skip to content
Open
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
3 changes: 3 additions & 0 deletions api/envoy/extensions/common/ratelimit/v3/ratelimit.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,9 @@ new_features:
Added comprehensive metrics and tracing tags to the
:ref:`Proto API Scrubber <config_http_filters_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.
- area: network_filter
change: |
Added support for
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<double>(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; };
Expand All @@ -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<RateLimitTokenBucket>(max_tokens, tokens_per_fill,
fill_interval, time_source_);
default_token_bucket_ = std::make_shared<RateLimitTokenBucket>(
max_tokens, tokens_per_fill, fill_interval, time_source_, false);
}

for (const auto& descriptor : descriptors) {
Expand All @@ -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).
Expand All @@ -133,14 +133,14 @@ LocalRateLimiterImpl::LocalRateLimiterImpl(
if (wildcard_found) {
DynamicDescriptorSharedPtr dynamic_descriptor = std::make_shared<DynamicDescriptor>(
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<RateLimitTokenBucket>(per_descriptor_max_tokens,
per_descriptor_tokens_per_fill,
per_descriptor_fill_interval, time_source_);
std::make_shared<RateLimitTokenBucket>(
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) {
Expand Down Expand Up @@ -194,7 +194,8 @@ LocalRateLimiterImpl::requestAllowed(absl::Span<const RateLimit::Descriptor> 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<TokenBucketContext>(match_result.token_bucket)};
return {match_result.token_bucket->shadowMode(),
std::shared_ptr<TokenBucketContext>(match_result.token_bucket)};
}
ENVOY_LOG(trace,
"request allowed by descriptor with fill rate: {}, maxToken: {}, remainingToken {}",
Expand All @@ -214,7 +215,8 @@ LocalRateLimiterImpl::requestAllowed(absl::Span<const RateLimit::Descriptor> 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<TokenBucketContext>(default_token_bucket_)};
return {default_token_bucket_->shadowMode(),
std::shared_ptr<TokenBucketContext>(default_token_bucket_)};
}

// If the request is allowed then return the result the token bucket. The descriptor
Expand Down Expand Up @@ -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) {
Expand All @@ -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<double>(fill_interval_).count());
per_descriptor_token_bucket = std::make_shared<RateLimitTokenBucket>(
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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ using ProtoLocalClusterRateLimit = envoy::extensions::common::ratelimit::v3::Loc
class DynamicDescriptor : public Logger::Loggable<Logger::Id::rate_limit_quota> {
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);

Expand All @@ -46,6 +47,7 @@ class DynamicDescriptor : public Logger::Loggable<Logger::Id::rate_limit_quota>
LruList lru_list_;
uint32_t lru_size_;
TimeSource& time_source_;
const bool shadow_mode_{false};
};

using DynamicDescriptorSharedPtr = std::shared_ptr<DynamicDescriptor>;
Expand Down Expand Up @@ -104,10 +106,12 @@ class RateLimitTokenBucket : public TokenBucketContext,
public Logger::Loggable<Logger::Id::local_rate_limit> {
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_; }

Expand All @@ -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<RateLimitTokenBucket>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Loading