From 14f65e1ac409d5d7047e5ea4f8dd086bf46e7c40 Mon Sep 17 00:00:00 2001 From: Dan Sun Date: Sat, 10 Jan 2026 16:15:44 -0500 Subject: [PATCH 1/5] implement quota mode for rate limit check Signed-off-by: Dan Sun --- src/config/config.go | 1 + src/config/config_impl.go | 20 ++- src/service/ratelimit.go | 60 +++++-- src/service/ratelimit_test.go | 154 +++++++++++++++- src/service_cmd/runner/runner.go | 2 +- src/settings/settings.go | 3 + test/service/ratelimit_test.go | 298 +++++++++++++++++++++++++++++-- 7 files changed, 501 insertions(+), 37 deletions(-) diff --git a/src/config/config.go b/src/config/config.go index b469bcf45..6e31883de 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -22,6 +22,7 @@ type RateLimit struct { Limit *pb.RateLimitResponse_RateLimit Unlimited bool ShadowMode bool + QuotaMode bool Name string Replaces []string DetailedMetric bool diff --git a/src/config/config_impl.go b/src/config/config_impl.go index ca7c6fe50..313579f07 100644 --- a/src/config/config_impl.go +++ b/src/config/config_impl.go @@ -31,6 +31,7 @@ type YamlDescriptor struct { RateLimit *YamlRateLimit `yaml:"rate_limit"` Descriptors []YamlDescriptor ShadowMode bool `yaml:"shadow_mode"` + QuotaMode bool `yaml:"quota_mode"` DetailedMetric bool `yaml:"detailed_metric"` ValueToMetric bool `yaml:"value_to_metric"` ShareThreshold bool `yaml:"share_threshold"` @@ -70,6 +71,7 @@ var validKeys = map[string]bool{ "requests_per_unit": true, "unlimited": true, "shadow_mode": true, + "quota_mode": true, "name": true, "replaces": true, "detailed_metric": true, @@ -84,7 +86,7 @@ var validKeys = map[string]bool{ // @param unlimited supplies whether the rate limit is unlimited // @return the new config entry. func NewRateLimit(requestsPerUnit uint32, unit pb.RateLimitResponse_RateLimit_Unit, rlStats stats.RateLimitStats, - unlimited bool, shadowMode bool, name string, replaces []string, detailedMetric bool, + unlimited bool, shadowMode bool, quotaMode bool, name string, replaces []string, detailedMetric bool, ) *RateLimit { return &RateLimit{ FullKey: rlStats.GetKey(), @@ -96,6 +98,7 @@ func NewRateLimit(requestsPerUnit uint32, unit pb.RateLimitResponse_RateLimit_Un }, Unlimited: unlimited, ShadowMode: shadowMode, + QuotaMode: quotaMode, Name: name, Replaces: replaces, DetailedMetric: detailedMetric, @@ -108,8 +111,8 @@ func (this *rateLimitDescriptor) dump() string { ret := "" if this.limit != nil { ret += fmt.Sprintf( - "%s: unit=%s requests_per_unit=%d, shadow_mode: %t\n", this.limit.FullKey, - this.limit.Limit.Unit.String(), this.limit.Limit.RequestsPerUnit, this.limit.ShadowMode) + "%s: unit=%s requests_per_unit=%d, shadow_mode: %t, quota_mode: %t\n", this.limit.FullKey, + this.limit.Limit.Unit.String(), this.limit.Limit.RequestsPerUnit, this.limit.ShadowMode, this.limit.QuotaMode) } for _, descriptor := range this.descriptors { ret += descriptor.dump() @@ -174,12 +177,12 @@ func (this *rateLimitDescriptor) loadDescriptors(config RateLimitConfigToLoad, p rateLimit = NewRateLimit( descriptorConfig.RateLimit.RequestsPerUnit, pb.RateLimitResponse_RateLimit_Unit(value), - statsManager.NewStats(newParentKey), unlimited, descriptorConfig.ShadowMode, + statsManager.NewStats(newParentKey), unlimited, descriptorConfig.ShadowMode, descriptorConfig.QuotaMode, descriptorConfig.RateLimit.Name, replaces, descriptorConfig.DetailedMetric, ) rateLimitDebugString = fmt.Sprintf( - " ratelimit={requests_per_unit=%d, unit=%s, unlimited=%t, shadow_mode=%t}", rateLimit.Limit.RequestsPerUnit, - rateLimit.Limit.Unit.String(), rateLimit.Unlimited, rateLimit.ShadowMode) + " ratelimit={requests_per_unit=%d, unit=%s, unlimited=%t, shadow_mode=%t, quota_mode=%t}", rateLimit.Limit.RequestsPerUnit, + rateLimit.Limit.Unit.String(), rateLimit.Unlimited, rateLimit.ShadowMode, rateLimit.QuotaMode) for _, replaces := range descriptorConfig.RateLimit.Replaces { if replaces.Name == "" { @@ -336,6 +339,7 @@ func (this *rateLimitConfigImpl) GetLimit( this.statsManager.NewStats(rateLimitKey), false, false, + false, "", []string{}, false, @@ -481,7 +485,7 @@ func (this *rateLimitConfigImpl) GetLimit( if rateLimit != nil && rateLimit.DetailedMetric { // Preserve ShareThresholdKeyPattern when recreating rate limit originalShareThresholdKeyPattern := rateLimit.ShareThresholdKeyPattern - rateLimit = NewRateLimit(rateLimit.Limit.RequestsPerUnit, rateLimit.Limit.Unit, this.statsManager.NewStats(rateLimit.FullKey), rateLimit.Unlimited, rateLimit.ShadowMode, rateLimit.Name, rateLimit.Replaces, rateLimit.DetailedMetric) + rateLimit = NewRateLimit(rateLimit.Limit.RequestsPerUnit, rateLimit.Limit.Unit, this.statsManager.NewStats(rateLimit.FullKey), rateLimit.Unlimited, rateLimit.ShadowMode, rateLimit.QuotaMode, rateLimit.Name, rateLimit.Replaces, rateLimit.DetailedMetric) rateLimit.ShareThresholdKeyPattern = originalShareThresholdKeyPattern } @@ -531,7 +535,7 @@ func (this *rateLimitConfigImpl) GetLimit( if enhancedKey != rateLimit.FullKey { // Recreate to ensure a clean stats struct, then set to enhanced stats originalShareThresholdKeyPattern := rateLimit.ShareThresholdKeyPattern - rateLimit = NewRateLimit(rateLimit.Limit.RequestsPerUnit, rateLimit.Limit.Unit, this.statsManager.NewStats(enhancedKey), rateLimit.Unlimited, rateLimit.ShadowMode, rateLimit.Name, rateLimit.Replaces, rateLimit.DetailedMetric) + rateLimit = NewRateLimit(rateLimit.Limit.RequestsPerUnit, rateLimit.Limit.Unit, this.statsManager.NewStats(enhancedKey), rateLimit.Unlimited, rateLimit.ShadowMode, rateLimit.QuotaMode, rateLimit.Name, rateLimit.Replaces, rateLimit.DetailedMetric) rateLimit.ShareThresholdKeyPattern = originalShareThresholdKeyPattern } } diff --git a/src/service/ratelimit.go b/src/service/ratelimit.go index 6df7fc4c0..559b49533 100644 --- a/src/service/ratelimit.go +++ b/src/service/ratelimit.go @@ -35,7 +35,7 @@ var tracer = otel.Tracer("ratelimit") type RateLimitServiceServer interface { pb.RateLimitServiceServer - GetCurrentConfig() (config.RateLimitConfig, bool) + GetCurrentConfig() (config.RateLimitConfig, bool, bool) SetConfig(updateEvent provider.ConfigUpdateEvent, healthyWithAtLeastOneConfigLoad bool) } @@ -52,6 +52,7 @@ type service struct { customHeaderResetHeader string customHeaderClock utils.TimeSource globalShadowMode bool + globalQuotaMode bool responseDynamicMetadataEnabled bool } @@ -87,6 +88,7 @@ func (this *service) SetConfig(updateEvent provider.ConfigUpdateEvent, healthyWi rlSettings := settings.NewSettings() this.globalShadowMode = rlSettings.GlobalShadowMode + this.globalQuotaMode = rlSettings.GlobalQuotaMode this.responseDynamicMetadataEnabled = rlSettings.ResponseDynamicMetadata if rlSettings.RateLimitResponseHeadersEnabled { @@ -186,7 +188,7 @@ func (this *service) shouldRateLimitWorker( checkServiceErr(request.Domain != "", "rate limit domain must not be empty") checkServiceErr(len(request.Descriptors) != 0, "rate limit descriptor list must not be empty") - snappedConfig, globalShadowMode := this.GetCurrentConfig() + snappedConfig, globalShadowMode, globalQuotaMode := this.GetCurrentConfig() limitsToCheck, isUnlimited := this.constructLimitsToCheck(request, ctx, snappedConfig) assert.Assert(len(limitsToCheck) == len(isUnlimited)) @@ -203,6 +205,9 @@ func (this *service) shouldRateLimitWorker( minLimitRemaining := MaxUint32 var minimumDescriptor *pb.RateLimitResponse_DescriptorStatus = nil + // Track quota mode violations for metadata + var quotaModeViolations []int + for i, descriptorStatus := range responseDescriptorStatuses { // Keep track of the descriptor closest to hit the ratelimit if this.customHeadersEnabled && @@ -220,10 +225,23 @@ func (this *service) shouldRateLimitWorker( } else { response.Statuses[i] = descriptorStatus if descriptorStatus.Code == pb.RateLimitResponse_OVER_LIMIT { - finalCode = descriptorStatus.Code - - minimumDescriptor = descriptorStatus - minLimitRemaining = 0 + // Check if this limit is in quota mode (individual or global) + isQuotaMode := globalQuotaMode || (limitsToCheck[i] != nil && limitsToCheck[i].QuotaMode) + + if isQuotaMode { + // In quota mode: track the violation for metadata but keep response as OK + quotaModeViolations = append(quotaModeViolations, i) + response.Statuses[i] = &pb.RateLimitResponse_DescriptorStatus{ + Code: pb.RateLimitResponse_OK, + CurrentLimit: descriptorStatus.CurrentLimit, + LimitRemaining: descriptorStatus.LimitRemaining, + } + } else { + // Normal rate limit: set final code to OVER_LIMIT + finalCode = descriptorStatus.Code + minimumDescriptor = descriptorStatus + minLimitRemaining = 0 + } } } } @@ -245,14 +263,14 @@ func (this *service) shouldRateLimitWorker( // If response dynamic data enabled, set dynamic data on response. if this.responseDynamicMetadataEnabled { - response.DynamicMetadata = ratelimitToMetadata(request) + response.DynamicMetadata = ratelimitToMetadata(request, quotaModeViolations, limitsToCheck) } response.OverallCode = finalCode return response } -func ratelimitToMetadata(req *pb.RateLimitRequest) *structpb.Struct { +func ratelimitToMetadata(req *pb.RateLimitRequest, quotaModeViolations []int, limitsToCheck []*config.RateLimit) *structpb.Struct { fields := make(map[string]*structpb.Value) // Domain @@ -276,6 +294,27 @@ func ratelimitToMetadata(req *pb.RateLimitRequest) *structpb.Struct { fields["hitsAddend"] = structpb.NewNumberValue(float64(hitsAddend)) } + // Quota mode information + if len(quotaModeViolations) > 0 { + violationValues := make([]*structpb.Value, len(quotaModeViolations)) + for i, violationIndex := range quotaModeViolations { + violationValues[i] = structpb.NewNumberValue(float64(violationIndex)) + } + fields["quotaModeViolations"] = structpb.NewListValue(&structpb.ListValue{ + Values: violationValues, + }) + } + + // Check if any limits have quota mode enabled + quotaModeEnabled := false + for _, limit := range limitsToCheck { + if limit != nil && limit.QuotaMode { + quotaModeEnabled = true + break + } + } + fields["quotaModeEnabled"] = structpb.NewBoolValue(quotaModeEnabled) + return &structpb.Struct{Fields: fields} } @@ -379,10 +418,10 @@ func (this *service) ShouldRateLimit( return response, nil } -func (this *service) GetCurrentConfig() (config.RateLimitConfig, bool) { +func (this *service) GetCurrentConfig() (config.RateLimitConfig, bool, bool) { this.configLock.RLock() defer this.configLock.RUnlock() - return this.config, this.globalShadowMode + return this.config, this.globalShadowMode, this.globalQuotaMode } func NewService(cache limiter.RateLimitCache, configProvider provider.RateLimitConfigProvider, statsManager stats.Manager, @@ -396,6 +435,7 @@ func NewService(cache limiter.RateLimitCache, configProvider provider.RateLimitC stats: statsManager.NewServiceStats(), health: health, globalShadowMode: shadowMode, + globalQuotaMode: false, customHeaderClock: clock, } diff --git a/src/service/ratelimit_test.go b/src/service/ratelimit_test.go index 8cd4f2ee3..17e26937c 100644 --- a/src/service/ratelimit_test.go +++ b/src/service/ratelimit_test.go @@ -3,6 +3,7 @@ package ratelimit import ( "testing" + "github.com/envoyproxy/ratelimit/src/config" ratelimitv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/common/ratelimit/v3" pb "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v3" "github.com/google/go-cmp/cmp" @@ -14,12 +15,14 @@ import ( func TestRatelimitToMetadata(t *testing.T) { cases := []struct { - name string - req *pb.RateLimitRequest - expected string + name string + req *pb.RateLimitRequest + quotaModeViolations []int + limitsToCheck []*config.RateLimit + expected string }{ { - name: "Single descriptor with single entry", + name: "Single descriptor with single entry, no quota violations", req: &pb.RateLimitRequest{ Domain: "fake-domain", Descriptors: []*ratelimitv3.RateLimitDescriptor{ @@ -33,6 +36,8 @@ func TestRatelimitToMetadata(t *testing.T) { }, }, }, + quotaModeViolations: nil, + limitsToCheck: []*config.RateLimit{nil}, expected: `{ "descriptors": [ { @@ -41,14 +46,151 @@ func TestRatelimitToMetadata(t *testing.T) { ] } ], - "domain": "fake-domain" + "domain": "fake-domain", + "quotaModeEnabled": false +}`, + }, + { + name: "Single descriptor with quota mode violation", + req: &pb.RateLimitRequest{ + Domain: "quota-domain", + Descriptors: []*ratelimitv3.RateLimitDescriptor{ + { + Entries: []*ratelimitv3.RateLimitDescriptor_Entry{ + { + Key: "quota_key", + Value: "quota_val", + }, + }, + }, + }, + }, + quotaModeViolations: []int{0}, + limitsToCheck: []*config.RateLimit{ + { + QuotaMode: true, + }, + }, + expected: `{ + "descriptors": [ + { + "entries": [ + "quota_key=quota_val" + ] + } + ], + "domain": "quota-domain", + "quotaModeEnabled": true, + "quotaModeViolations": [0] +}`, + }, + { + name: "Multiple descriptors with mixed quota violations", + req: &pb.RateLimitRequest{ + Domain: "mixed-domain", + Descriptors: []*ratelimitv3.RateLimitDescriptor{ + { + Entries: []*ratelimitv3.RateLimitDescriptor_Entry{ + { + Key: "regular_key", + Value: "regular_val", + }, + }, + }, + { + Entries: []*ratelimitv3.RateLimitDescriptor_Entry{ + { + Key: "quota_key", + Value: "quota_val", + }, + }, + }, + { + Entries: []*ratelimitv3.RateLimitDescriptor_Entry{ + { + Key: "another_quota", + Value: "another_val", + }, + }, + }, + }, + }, + quotaModeViolations: []int{1, 2}, + limitsToCheck: []*config.RateLimit{ + { + QuotaMode: false, + }, + { + QuotaMode: true, + }, + { + QuotaMode: true, + }, + }, + expected: `{ + "descriptors": [ + { + "entries": [ + "regular_key=regular_val" + ] + }, + { + "entries": [ + "quota_key=quota_val" + ] + }, + { + "entries": [ + "another_quota=another_val" + ] + } + ], + "domain": "mixed-domain", + "quotaModeEnabled": true, + "quotaModeViolations": [1, 2] +}`, + }, + { + name: "Request with hits addend", + req: &pb.RateLimitRequest{ + Domain: "addend-domain", + HitsAddend: 5, + Descriptors: []*ratelimitv3.RateLimitDescriptor{ + { + Entries: []*ratelimitv3.RateLimitDescriptor_Entry{ + { + Key: "test_key", + Value: "test_val", + }, + }, + }, + }, + }, + quotaModeViolations: []int{0}, + limitsToCheck: []*config.RateLimit{ + { + QuotaMode: true, + }, + }, + expected: `{ + "descriptors": [ + { + "entries": [ + "test_key=test_val" + ] + } + ], + "domain": "addend-domain", + "hitsAddend": 5, + "quotaModeEnabled": true, + "quotaModeViolations": [0] }`, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - got := ratelimitToMetadata(tc.req) + got := ratelimitToMetadata(tc.req, tc.quotaModeViolations, tc.limitsToCheck) expected := &structpb.Struct{} err := protojson.Unmarshal([]byte(tc.expected), expected) require.NoError(t, err) diff --git a/src/service_cmd/runner/runner.go b/src/service_cmd/runner/runner.go index ada577573..f062d15fc 100644 --- a/src/service_cmd/runner/runner.go +++ b/src/service_cmd/runner/runner.go @@ -173,7 +173,7 @@ func (runner *Runner) Run() { "/rlconfig", "print out the currently loaded configuration for debugging", func(writer http.ResponseWriter, request *http.Request) { - if current, _ := service.GetCurrentConfig(); current != nil { + if current, _, _ := service.GetCurrentConfig(); current != nil { io.WriteString(writer, current.Dump()) } }) diff --git a/src/settings/settings.go b/src/settings/settings.go index 64cc7d928..7ad2d80df 100644 --- a/src/settings/settings.go +++ b/src/settings/settings.go @@ -210,6 +210,9 @@ type Settings struct { // Should the ratelimiting be running in Global shadow-mode, ie. never report a ratelimit status, unless a rate was provided from envoy as an override GlobalShadowMode bool `envconfig:"SHADOW_MODE" default:"false"` + // Should the ratelimiting be running in Global quota-mode, ie. set metadata but never report OVER_LIMIT status when quota limits are exceeded + GlobalQuotaMode bool `envconfig:"QUOTA_MODE" default:"false"` + ResponseDynamicMetadata bool `envconfig:"RESPONSE_DYNAMIC_METADATA" default:"false"` // Allow merging of multiple yaml files referencing the same domain diff --git a/test/service/ratelimit_test.go b/test/service/ratelimit_test.go index 18c3cafcd..f22309348 100644 --- a/test/service/ratelimit_test.go +++ b/test/service/ratelimit_test.go @@ -153,7 +153,7 @@ func TestService(test *testing.T) { request = common.NewRateLimitRequest( "different-domain", [][][2]string{{{"foo", "bar"}}, {{"hello", "world"}}}, 1) limits := []*config.RateLimit{ - config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("key"), false, false, "key_name", nil, false), + config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("key"), false, false, false, "key_name", nil, false), nil, } t.config.EXPECT().GetLimit(context.Background(), "different-domain", request.Descriptors[0]).Return(limits[0]) @@ -187,7 +187,7 @@ func TestService(test *testing.T) { // Config should still be valid. Also make sure order does not affect results. limits = []*config.RateLimit{ nil, - config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("key"), false, false, "", nil, false), + config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("key"), false, false, false, "", nil, false), } t.config.EXPECT().GetLimit(context.Background(), "different-domain", request.Descriptors[0]).Return(limits[0]) t.config.EXPECT().GetLimit(context.Background(), "different-domain", request.Descriptors[1]).Return(limits[1]) @@ -241,7 +241,7 @@ func TestServiceGlobalShadowMode(test *testing.T) { // Global Shadow mode limits := []*config.RateLimit{ - config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("key"), false, false, "", nil, false), + config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("key"), false, false, false, "", nil, false), nil, } t.config.EXPECT().GetLimit(context.Background(), "different-domain", request.Descriptors[0]).Return(limits[0]) @@ -281,8 +281,8 @@ func TestRuleShadowMode(test *testing.T) { request := common.NewRateLimitRequest( "different-domain", [][][2]string{{{"foo", "bar"}}, {{"hello", "world"}}}, 1) limits := []*config.RateLimit{ - config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("key"), false, true, "", nil, false), - config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("key"), false, true, "", nil, false), + config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("key"), false, true, false, "", nil, false), + config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("key"), false, true, false, "", nil, false), } t.config.EXPECT().GetLimit(context.Background(), "different-domain", request.Descriptors[0]).Return(limits[0]) t.config.EXPECT().GetLimit(context.Background(), "different-domain", request.Descriptors[1]).Return(limits[1]) @@ -314,8 +314,8 @@ func TestMixedRuleShadowMode(test *testing.T) { request := common.NewRateLimitRequest( "different-domain", [][][2]string{{{"foo", "bar"}}, {{"hello", "world"}}}, 1) limits := []*config.RateLimit{ - config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("key"), false, true, "", nil, false), - config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("key"), false, false, "", nil, false), + config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("key"), false, true, false, "", nil, false), + config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("key"), false, false, false, "", nil, false), } t.config.EXPECT().GetLimit(context.Background(), "different-domain", request.Descriptors[0]).Return(limits[0]) t.config.EXPECT().GetLimit(context.Background(), "different-domain", request.Descriptors[1]).Return(limits[1]) @@ -374,7 +374,7 @@ func TestServiceWithCustomRatelimitHeaders(test *testing.T) { request := common.NewRateLimitRequest( "different-domain", [][][2]string{{{"foo", "bar"}}, {{"hello", "world"}}}, 1) limits := []*config.RateLimit{ - config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("key"), false, false, "", nil, false), + config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("key"), false, false, false, "", nil, false), nil, } t.config.EXPECT().GetLimit(context.Background(), "different-domain", request.Descriptors[0]).Return(limits[0]) @@ -427,7 +427,7 @@ func TestServiceWithDefaultRatelimitHeaders(test *testing.T) { request := common.NewRateLimitRequest( "different-domain", [][][2]string{{{"foo", "bar"}}, {{"hello", "world"}}}, 1) limits := []*config.RateLimit{ - config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("key"), false, false, "", nil, false), + config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("key"), false, false, false, "", nil, false), nil, } t.config.EXPECT().GetLimit(context.Background(), "different-domain", request.Descriptors[0]).Return(limits[0]) @@ -487,7 +487,7 @@ func TestCacheError(test *testing.T) { service := t.setupBasicService() request := common.NewRateLimitRequest("different-domain", [][][2]string{{{"foo", "bar"}}}, 1) - limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("key"), false, false, "", nil, false)} + limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("key"), false, false, false, "", nil, false)} t.config.EXPECT().GetLimit(context.Background(), "different-domain", request.Descriptors[0]).Return(limits[0]) t.cache.EXPECT().DoLimit(context.Background(), request, limits).Do( func(context.Context, *pb.RateLimitRequest, []*config.RateLimit) { @@ -529,9 +529,9 @@ func TestUnlimited(test *testing.T) { request := common.NewRateLimitRequest( "some-domain", [][][2]string{{{"foo", "bar"}}, {{"hello", "world"}}, {{"baz", "qux"}}}, 1) limits := []*config.RateLimit{ - config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("foo_bar"), false, false, "", nil, false), + config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("foo_bar"), false, false, false, "", nil, false), nil, - config.NewRateLimit(55, pb.RateLimitResponse_RateLimit_SECOND, t.statsManager.NewStats("baz_qux"), true, false, "", nil, false), + config.NewRateLimit(55, pb.RateLimitResponse_RateLimit_SECOND, t.statsManager.NewStats("baz_qux"), true, false, false, "", nil, false), } t.config.EXPECT().GetLimit(context.Background(), "some-domain", request.Descriptors[0]).Return(limits[0]) t.config.EXPECT().GetLimit(context.Background(), "some-domain", request.Descriptors[1]).Return(limits[1]) @@ -665,3 +665,277 @@ func TestServiceHealthStatusAtLeastOneConfigLoaded(test *testing.T) { test.Errorf("expected status NOT_SERVING actual %v", res.Status) } } + +func TestServiceGlobalQuotaMode(test *testing.T) { + os.Setenv("QUOTA_MODE", "true") + defer func() { + os.Unsetenv("QUOTA_MODE") + }() + + t := commonSetup(test) + defer t.controller.Finish() + + // No global quota_mode, this should be picked-up from environment variables during re-load of config + service := t.setupBasicService() + + // Force a config reload. + barrier := newBarrier() + t.configUpdateEvent.EXPECT().GetConfig().DoAndReturn(func() (config.RateLimitConfig, any) { + barrier.signal() + return t.config, nil + }) + t.configUpdateEventChan <- t.configUpdateEvent + barrier.wait() + + // Make a request. + request := common.NewRateLimitRequest( + "quota-domain", [][][2]string{{{"foo", "bar"}}, {{"hello", "world"}}}, 1) + + // Global Quota mode + limits := []*config.RateLimit{ + config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("key"), false, false, false, "", nil, false), + config.NewRateLimit(5, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("key2"), false, false, false, "", nil, false), + } + t.config.EXPECT().GetLimit(context.Background(), "quota-domain", request.Descriptors[0]).Return(limits[0]) + t.config.EXPECT().GetLimit(context.Background(), "quota-domain", request.Descriptors[1]).Return(limits[1]) + t.cache.EXPECT().DoLimit(context.Background(), request, limits).Return( + []*pb.RateLimitResponse_DescriptorStatus{ + {Code: pb.RateLimitResponse_OVER_LIMIT, CurrentLimit: limits[0].Limit, LimitRemaining: 0}, + {Code: pb.RateLimitResponse_OVER_LIMIT, CurrentLimit: limits[1].Limit, LimitRemaining: 0}, + }) + response, err := service.ShouldRateLimit(context.Background(), request) + + // OK overall code even if limit response was OVER_LIMIT, because global quota mode is enabled + common.AssertProtoEqual( + t.assert, + &pb.RateLimitResponse{ + OverallCode: pb.RateLimitResponse_OK, + Statuses: []*pb.RateLimitResponse_DescriptorStatus{ + {Code: pb.RateLimitResponse_OK, CurrentLimit: limits[0].Limit, LimitRemaining: 0}, + {Code: pb.RateLimitResponse_OK, CurrentLimit: limits[1].Limit, LimitRemaining: 0}, + }, + }, + response) + t.assert.Nil(err) +} + +func TestServiceQuotaModeWithMetadata(test *testing.T) { + os.Setenv("QUOTA_MODE", "true") + os.Setenv("RESPONSE_DYNAMIC_METADATA", "true") + defer func() { + os.Unsetenv("QUOTA_MODE") + os.Unsetenv("RESPONSE_DYNAMIC_METADATA") + }() + + t := commonSetup(test) + defer t.controller.Finish() + + service := t.setupBasicService() + + // Force a config reload to pick up environment variables. + barrier := newBarrier() + t.configUpdateEvent.EXPECT().GetConfig().DoAndReturn(func() (config.RateLimitConfig, any) { + barrier.signal() + return t.config, nil + }) + t.configUpdateEventChan <- t.configUpdateEvent + barrier.wait() + + // Make a request. + request := common.NewRateLimitRequest( + "quota-domain", [][][2]string{{{"regular", "limit"}}, {{"quota", "limit"}}}, 1) + + limits := []*config.RateLimit{ + config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("key"), false, false, false, "", nil, false), + config.NewRateLimit(5, pb.RateLimitResponse_RateLimit_MINUTE, t.statsManager.NewStats("key2"), false, false, true, "", nil, false), + } + t.config.EXPECT().GetLimit(context.Background(), "quota-domain", request.Descriptors[0]).Return(limits[0]) + t.config.EXPECT().GetLimit(context.Background(), "quota-domain", request.Descriptors[1]).Return(limits[1]) + t.cache.EXPECT().DoLimit(context.Background(), request, limits).Return( + []*pb.RateLimitResponse_DescriptorStatus{ + {Code: pb.RateLimitResponse_OK, CurrentLimit: limits[0].Limit, LimitRemaining: 5}, + {Code: pb.RateLimitResponse_OVER_LIMIT, CurrentLimit: limits[1].Limit, LimitRemaining: 0}, + }) + response, err := service.ShouldRateLimit(context.Background(), request) + + // Verify response includes metadata about quota violations + t.assert.Nil(err) + t.assert.Equal(pb.RateLimitResponse_OK, response.OverallCode) + t.assert.NotNil(response.DynamicMetadata) + + // Check that quota violation is tracked in metadata for descriptor index 1 + quotaViolations := response.DynamicMetadata.Fields["quotaModeViolations"] + t.assert.NotNil(quotaViolations) + violations := quotaViolations.GetListValue() + t.assert.Len(violations.Values, 1) + t.assert.Equal(float64(1), violations.Values[0].GetNumberValue()) + + // Check that quotaModeEnabled is true + quotaModeEnabled := response.DynamicMetadata.Fields["quotaModeEnabled"] + t.assert.NotNil(quotaModeEnabled) + t.assert.True(quotaModeEnabled.GetBoolValue()) +} + +func TestServicePerDescriptorQuotaMode(test *testing.T) { + t := commonSetup(test) + defer t.controller.Finish() + + // No Global Quota mode + service := t.setupBasicService() + + request := common.NewRateLimitRequest( + "quota-domain", [][][2]string{{{"regular", "limit"}}, {{"quota", "limit"}}}, 1) + + // Create limits with one having quota mode enabled per-descriptor + limits := []*config.RateLimit{ + // Regular limit - should reject when exceeded + { + FullKey: "regular_limit", + Limit: &pb.RateLimitResponse_RateLimit{RequestsPerUnit: 5, Unit: pb.RateLimitResponse_RateLimit_MINUTE}, + QuotaMode: false, + ShadowMode: false, + }, + // Quota mode limit - should not reject when exceeded + { + FullKey: "quota_limit", + Limit: &pb.RateLimitResponse_RateLimit{RequestsPerUnit: 3, Unit: pb.RateLimitResponse_RateLimit_MINUTE}, + QuotaMode: true, + ShadowMode: false, + }, + } + + t.config.EXPECT().GetLimit(context.Background(), "quota-domain", request.Descriptors[0]).Return(limits[0]) + t.config.EXPECT().GetLimit(context.Background(), "quota-domain", request.Descriptors[1]).Return(limits[1]) + t.cache.EXPECT().DoLimit(context.Background(), request, limits).Return( + []*pb.RateLimitResponse_DescriptorStatus{ + {Code: pb.RateLimitResponse_OVER_LIMIT, CurrentLimit: limits[0].Limit, LimitRemaining: 0}, + {Code: pb.RateLimitResponse_OVER_LIMIT, CurrentLimit: limits[1].Limit, LimitRemaining: 0}, + }) + response, err := service.ShouldRateLimit(context.Background(), request) + + // Regular limit should cause OVER_LIMIT overall, but quota mode limit should be converted to OK + common.AssertProtoEqual( + t.assert, + &pb.RateLimitResponse{ + OverallCode: pb.RateLimitResponse_OVER_LIMIT, + Statuses: []*pb.RateLimitResponse_DescriptorStatus{ + {Code: pb.RateLimitResponse_OVER_LIMIT, CurrentLimit: limits[0].Limit, LimitRemaining: 0}, + {Code: pb.RateLimitResponse_OK, CurrentLimit: limits[1].Limit, LimitRemaining: 0}, + }, + }, + response) + t.assert.Nil(err) +} + +func TestServiceQuotaModeOnly(test *testing.T) { + t := commonSetup(test) + defer t.controller.Finish() + + service := t.setupBasicService() + + request := common.NewRateLimitRequest( + "quota-domain", [][][2]string{{{"quota1", "limit"}}, {{"quota2", "limit"}}}, 1) + + // Both limits are in quota mode + limits := []*config.RateLimit{ + { + FullKey: "quota_limit_1", + Limit: &pb.RateLimitResponse_RateLimit{RequestsPerUnit: 5, Unit: pb.RateLimitResponse_RateLimit_MINUTE}, + QuotaMode: true, + ShadowMode: false, + }, + { + FullKey: "quota_limit_2", + Limit: &pb.RateLimitResponse_RateLimit{RequestsPerUnit: 3, Unit: pb.RateLimitResponse_RateLimit_MINUTE}, + QuotaMode: true, + ShadowMode: false, + }, + } + + t.config.EXPECT().GetLimit(context.Background(), "quota-domain", request.Descriptors[0]).Return(limits[0]) + t.config.EXPECT().GetLimit(context.Background(), "quota-domain", request.Descriptors[1]).Return(limits[1]) + t.cache.EXPECT().DoLimit(context.Background(), request, limits).Return( + []*pb.RateLimitResponse_DescriptorStatus{ + {Code: pb.RateLimitResponse_OVER_LIMIT, CurrentLimit: limits[0].Limit, LimitRemaining: 0}, + {Code: pb.RateLimitResponse_OVER_LIMIT, CurrentLimit: limits[1].Limit, LimitRemaining: 0}, + }) + response, err := service.ShouldRateLimit(context.Background(), request) + + // All quota mode limits should result in OK overall code + common.AssertProtoEqual( + t.assert, + &pb.RateLimitResponse{ + OverallCode: pb.RateLimitResponse_OK, + Statuses: []*pb.RateLimitResponse_DescriptorStatus{ + {Code: pb.RateLimitResponse_OK, CurrentLimit: limits[0].Limit, LimitRemaining: 0}, + {Code: pb.RateLimitResponse_OK, CurrentLimit: limits[1].Limit, LimitRemaining: 0}, + }, + }, + response) + t.assert.Nil(err) +} + +func TestServiceQuotaModeWithShadowMode(test *testing.T) { + os.Setenv("SHADOW_MODE", "true") + defer func() { + os.Unsetenv("SHADOW_MODE") + }() + + t := commonSetup(test) + defer t.controller.Finish() + + service := t.setupBasicService() + + // Force a config reload to pick up environment variables. + barrier := newBarrier() + t.configUpdateEvent.EXPECT().GetConfig().DoAndReturn(func() (config.RateLimitConfig, any) { + barrier.signal() + return t.config, nil + }) + t.configUpdateEventChan <- t.configUpdateEvent + barrier.wait() + + request := common.NewRateLimitRequest( + "quota-domain", [][][2]string{{{"regular", "limit"}}, {{"quota", "limit"}}}, 1) + + // Mix of regular and quota mode limits with global shadow mode + limits := []*config.RateLimit{ + { + FullKey: "regular_limit", + Limit: &pb.RateLimitResponse_RateLimit{RequestsPerUnit: 5, Unit: pb.RateLimitResponse_RateLimit_MINUTE}, + QuotaMode: false, + ShadowMode: false, + }, + { + FullKey: "quota_limit", + Limit: &pb.RateLimitResponse_RateLimit{RequestsPerUnit: 3, Unit: pb.RateLimitResponse_RateLimit_MINUTE}, + QuotaMode: true, + ShadowMode: false, + }, + } + + t.config.EXPECT().GetLimit(context.Background(), "quota-domain", request.Descriptors[0]).Return(limits[0]) + t.config.EXPECT().GetLimit(context.Background(), "quota-domain", request.Descriptors[1]).Return(limits[1]) + t.cache.EXPECT().DoLimit(context.Background(), request, limits).Return( + []*pb.RateLimitResponse_DescriptorStatus{ + {Code: pb.RateLimitResponse_OVER_LIMIT, CurrentLimit: limits[0].Limit, LimitRemaining: 0}, + {Code: pb.RateLimitResponse_OVER_LIMIT, CurrentLimit: limits[1].Limit, LimitRemaining: 0}, + }) + response, err := service.ShouldRateLimit(context.Background(), request) + + // Global shadow mode should override everything and result in OK + common.AssertProtoEqual( + t.assert, + &pb.RateLimitResponse{ + OverallCode: pb.RateLimitResponse_OK, + Statuses: []*pb.RateLimitResponse_DescriptorStatus{ + {Code: pb.RateLimitResponse_OVER_LIMIT, CurrentLimit: limits[0].Limit, LimitRemaining: 0}, + {Code: pb.RateLimitResponse_OK, CurrentLimit: limits[1].Limit, LimitRemaining: 0}, + }, + }, + response) + t.assert.Nil(err) + + // Verify global shadow mode counter is incremented + t.assert.EqualValues(1, t.statStore.NewCounter("global_shadow_mode").Value()) +} From 8e2fe71e13eb6f3b3101bca9bde830d1ae3e1943 Mon Sep 17 00:00:00 2001 From: Dan Sun Date: Sat, 10 Jan 2026 16:24:53 -0500 Subject: [PATCH 2/5] fix format Signed-off-by: Dan Sun --- src/service/ratelimit_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/service/ratelimit_test.go b/src/service/ratelimit_test.go index 17e26937c..6760943b3 100644 --- a/src/service/ratelimit_test.go +++ b/src/service/ratelimit_test.go @@ -3,9 +3,9 @@ package ratelimit import ( "testing" - "github.com/envoyproxy/ratelimit/src/config" ratelimitv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/common/ratelimit/v3" pb "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v3" + "github.com/envoyproxy/ratelimit/src/config" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" "google.golang.org/protobuf/encoding/protojson" @@ -15,11 +15,11 @@ import ( func TestRatelimitToMetadata(t *testing.T) { cases := []struct { - name string - req *pb.RateLimitRequest - quotaModeViolations []int - limitsToCheck []*config.RateLimit - expected string + name string + req *pb.RateLimitRequest + quotaModeViolations []int + limitsToCheck []*config.RateLimit + expected string }{ { name: "Single descriptor with single entry, no quota violations", From ea642275f215a548306c968e4e4553c4485007e3 Mon Sep 17 00:00:00 2001 From: Dan Sun Date: Sat, 10 Jan 2026 16:33:16 -0500 Subject: [PATCH 3/5] fix format Signed-off-by: Dan Sun --- src/service/ratelimit_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/service/ratelimit_test.go b/src/service/ratelimit_test.go index 6760943b3..0f43b6fb0 100644 --- a/src/service/ratelimit_test.go +++ b/src/service/ratelimit_test.go @@ -5,12 +5,13 @@ import ( ratelimitv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/common/ratelimit/v3" pb "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v3" - "github.com/envoyproxy/ratelimit/src/config" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/structpb" + + "github.com/envoyproxy/ratelimit/src/config" ) func TestRatelimitToMetadata(t *testing.T) { From 98be9cb18b00697d5ddb6700f57cedd897ac355a Mon Sep 17 00:00:00 2001 From: Dan Sun Date: Sat, 10 Jan 2026 20:35:02 -0500 Subject: [PATCH 4/5] fix tests Signed-off-by: Dan Sun --- test/limiter/base_limiter_test.go | 22 ++++++------ test/memcached/cache_impl_test.go | 36 ++++++++++---------- test/provider/xds_grpc_sotw_provider_test.go | 26 +++++++------- test/redis/bench_test.go | 2 +- test/redis/fixed_cache_impl_test.go | 34 +++++++++--------- 5 files changed, 60 insertions(+), 60 deletions(-) diff --git a/test/limiter/base_limiter_test.go b/test/limiter/base_limiter_test.go index ea1e77cc5..7bc404079 100644 --- a/test/limiter/base_limiter_test.go +++ b/test/limiter/base_limiter_test.go @@ -29,7 +29,7 @@ func TestGenerateCacheKeys(t *testing.T) { timeSource.EXPECT().UnixNow().Return(int64(1234)) baseRateLimit := limiter.NewBaseRateLimit(timeSource, rand.New(jitterSource), 3600, nil, 0.8, "", sm) request := common.NewRateLimitRequest("domain", [][][2]string{{{"key", "value"}}}, 1) - limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, "", nil, false)} + limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, false, "", nil, false)} assert.Equal(uint64(0), limits[0].Stats.TotalHits.Value()) cacheKeys := baseRateLimit.GenerateCacheKeys(request, limits, []uint64{1}) assert.Equal(1, len(cacheKeys)) @@ -48,7 +48,7 @@ func TestGenerateCacheKeysPrefix(t *testing.T) { timeSource.EXPECT().UnixNow().Return(int64(1234)) baseRateLimit := limiter.NewBaseRateLimit(timeSource, rand.New(jitterSource), 3600, nil, 0.8, "prefix:", sm) request := common.NewRateLimitRequest("domain", [][][2]string{{{"key", "value"}}}, 1) - limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, "", nil, false)} + limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, false, "", nil, false)} assert.Equal(uint64(0), limits[0].Stats.TotalHits.Value()) cacheKeys := baseRateLimit.GenerateCacheKeys(request, limits, []uint64{1}) assert.Equal(1, len(cacheKeys)) @@ -68,7 +68,7 @@ func TestGenerateCacheKeysWithShareThreshold(t *testing.T) { baseRateLimit := limiter.NewBaseRateLimit(timeSource, rand.New(jitterSource), 3600, nil, 0.8, "", sm) // Test 1: Simple case - different values with same wildcard prefix generate same cache key - limit := config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("files_files/*"), false, false, "", nil, false) + limit := config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("files_files/*"), false, false, false, "", nil, false) limit.ShareThresholdKeyPattern = []string{"files/*"} // Entry at index 0 request1 := common.NewRateLimitRequest("domain", [][][2]string{{{"files", "files/a.pdf"}}}, 1) @@ -95,7 +95,7 @@ func TestGenerateCacheKeysWithShareThreshold(t *testing.T) { } // Test 3: Nested descriptors with share_threshold at second level - limitNested := config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("parent_files_nested/*"), false, false, "", nil, false) + limitNested := config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("parent_files_nested/*"), false, false, false, "", nil, false) limitNested.ShareThresholdKeyPattern = []string{"", "nested/*"} // First entry no share_threshold, second entry has it request3a := common.NewRateLimitRequest("domain", [][][2]string{ @@ -116,7 +116,7 @@ func TestGenerateCacheKeysWithShareThreshold(t *testing.T) { assert.Equal(cacheKeys3a[0].Key, cacheKeys3b[0].Key) // Test 4: Multiple entries with share_threshold at different positions - limitMulti := config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("files_top/*_files_nested/*"), false, false, "", nil, false) + limitMulti := config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("files_top/*_files_nested/*"), false, false, false, "", nil, false) limitMulti.ShareThresholdKeyPattern = []string{"top/*", "nested/*"} // Both entries have share_threshold request4a := common.NewRateLimitRequest("domain", [][][2]string{ @@ -183,7 +183,7 @@ func TestGetResponseStatusOverLimitWithLocalCache(t *testing.T) { statsStore := stats.NewStore(stats.NewNullSink(), false) sm := mockstats.NewMockStatManager(statsStore) baseRateLimit := limiter.NewBaseRateLimit(timeSource, nil, 3600, nil, 0.8, "", sm) - limits := []*config.RateLimit{config.NewRateLimit(5, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, "", nil, false)} + limits := []*config.RateLimit{config.NewRateLimit(5, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, false, "", nil, false)} limitInfo := limiter.NewRateLimitInfo(limits[0], 2, 6, 4, 5) // As `isOverLimitWithLocalCache` is passed as `true`, immediate response is returned with no checks of the limits. responseStatus := baseRateLimit.GetResponseDescriptorStatus("key", limitInfo, true, 2) @@ -206,7 +206,7 @@ func TestGetResponseStatusOverLimitWithLocalCacheShadowMode(t *testing.T) { sm := mockstats.NewMockStatManager(statsStore) baseRateLimit := limiter.NewBaseRateLimit(timeSource, nil, 3600, nil, 0.8, "", sm) // This limit is in ShadowMode - limits := []*config.RateLimit{config.NewRateLimit(5, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, true, "", nil, false)} + limits := []*config.RateLimit{config.NewRateLimit(5, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, true, false, "", nil, false)} limitInfo := limiter.NewRateLimitInfo(limits[0], 2, 6, 4, 5) // As `isOverLimitWithLocalCache` is passed as `true`, immediate response is returned with no checks of the limits. responseStatus := baseRateLimit.GetResponseDescriptorStatus("key", limitInfo, true, 2) @@ -230,7 +230,7 @@ func TestGetResponseStatusOverLimit(t *testing.T) { localCache := freecache.NewCache(100) sm := mockstats.NewMockStatManager(statsStore) baseRateLimit := limiter.NewBaseRateLimit(timeSource, nil, 3600, localCache, 0.8, "", sm) - limits := []*config.RateLimit{config.NewRateLimit(5, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, "", nil, false)} + limits := []*config.RateLimit{config.NewRateLimit(5, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, false, "", nil, false)} limitInfo := limiter.NewRateLimitInfo(limits[0], 2, 7, 4, 5) responseStatus := baseRateLimit.GetResponseDescriptorStatus("key", limitInfo, false, 1) assert.Equal(pb.RateLimitResponse_OVER_LIMIT, responseStatus.GetCode()) @@ -256,7 +256,7 @@ func TestGetResponseStatusOverLimitShadowMode(t *testing.T) { sm := mockstats.NewMockStatManager(statsStore) baseRateLimit := limiter.NewBaseRateLimit(timeSource, nil, 3600, localCache, 0.8, "", sm) // Key is in shadow_mode: true - limits := []*config.RateLimit{config.NewRateLimit(5, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, true, "", nil, false)} + limits := []*config.RateLimit{config.NewRateLimit(5, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, true, false, "", nil, false)} limitInfo := limiter.NewRateLimitInfo(limits[0], 2, 7, 4, 5) responseStatus := baseRateLimit.GetResponseDescriptorStatus("key", limitInfo, false, 1) assert.Equal(pb.RateLimitResponse_OK, responseStatus.GetCode()) @@ -278,7 +278,7 @@ func TestGetResponseStatusBelowLimit(t *testing.T) { statsStore := stats.NewStore(stats.NewNullSink(), false) sm := mockstats.NewMockStatManager(statsStore) baseRateLimit := limiter.NewBaseRateLimit(timeSource, nil, 3600, nil, 0.8, "", sm) - limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, "", nil, false)} + limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, false, "", nil, false)} limitInfo := limiter.NewRateLimitInfo(limits[0], 2, 6, 9, 10) responseStatus := baseRateLimit.GetResponseDescriptorStatus("key", limitInfo, false, 1) assert.Equal(pb.RateLimitResponse_OK, responseStatus.GetCode()) @@ -299,7 +299,7 @@ func TestGetResponseStatusBelowLimitShadowMode(t *testing.T) { statsStore := stats.NewStore(stats.NewNullSink(), false) sm := mockstats.NewMockStatManager(statsStore) baseRateLimit := limiter.NewBaseRateLimit(timeSource, nil, 3600, nil, 0.8, "", sm) - limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, true, "", nil, false)} + limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, true, false, "", nil, false)} limitInfo := limiter.NewRateLimitInfo(limits[0], 2, 6, 9, 10) responseStatus := baseRateLimit.GetResponseDescriptorStatus("key", limitInfo, false, 1) assert.Equal(pb.RateLimitResponse_OK, responseStatus.GetCode()) diff --git a/test/memcached/cache_impl_test.go b/test/memcached/cache_impl_test.go index f5fd0ceb6..606a9d844 100644 --- a/test/memcached/cache_impl_test.go +++ b/test/memcached/cache_impl_test.go @@ -53,7 +53,7 @@ func TestMemcached(t *testing.T) { client.EXPECT().Increment("domain_key_value_1234", uint64(1)).Return(uint64(5), nil) request := common.NewRateLimitRequest("domain", [][][2]string{{{"key", "value"}}}, 1) - limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, "", nil, false)} + limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, false, "", nil, false)} assert.Equal( []*pb.RateLimitResponse_DescriptorStatus{{Code: pb.RateLimitResponse_OK, CurrentLimit: limits[0].Limit, LimitRemaining: 5, DurationUntilReset: utils.CalculateReset(&limits[0].Limit.Unit, timeSource)}}, @@ -77,7 +77,7 @@ func TestMemcached(t *testing.T) { }, 1) limits = []*config.RateLimit{ nil, - config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, sm.NewStats("key2_value2_subkey2_subvalue2"), false, false, "", nil, false), + config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, sm.NewStats("key2_value2_subkey2_subvalue2"), false, false, false, "", nil, false), } assert.Equal( []*pb.RateLimitResponse_DescriptorStatus{ @@ -111,8 +111,8 @@ func TestMemcached(t *testing.T) { {{"key3", "value3"}, {"subkey3", "subvalue3"}}, }, []uint64{1, 2}) limits = []*config.RateLimit{ - config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_HOUR, sm.NewStats("key3_value3"), false, false, "", nil, false), - config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_DAY, sm.NewStats("key3_value3_subkey3_subvalue3"), false, false, "", nil, false), + config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_HOUR, sm.NewStats("key3_value3"), false, false, false, "", nil, false), + config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_DAY, sm.NewStats("key3_value3_subkey3_subvalue3"), false, false, false, "", nil, false), } assert.Equal( []*pb.RateLimitResponse_DescriptorStatus{ @@ -150,7 +150,7 @@ func TestMemcachedGetError(t *testing.T) { client.EXPECT().Increment("domain_key_value_1234", uint64(1)).Return(uint64(5), nil) request := common.NewRateLimitRequest("domain", [][][2]string{{{"key", "value"}}}, 1) - limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, "", nil, false)} + limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, false, "", nil, false)} assert.Equal( []*pb.RateLimitResponse_DescriptorStatus{{Code: pb.RateLimitResponse_OK, CurrentLimit: limits[0].Limit, LimitRemaining: 9, DurationUntilReset: utils.CalculateReset(&limits[0].Limit.Unit, timeSource)}}, @@ -168,7 +168,7 @@ func TestMemcachedGetError(t *testing.T) { client.EXPECT().Increment("domain_key_value1_1234", uint64(1)).Return(uint64(5), nil) request = common.NewRateLimitRequest("domain", [][][2]string{{{"key", "value1"}}}, 1) - limits = []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value1"), false, false, "", nil, false)} + limits = []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value1"), false, false, false, "", nil, false)} assert.Equal( []*pb.RateLimitResponse_DescriptorStatus{{Code: pb.RateLimitResponse_OK, CurrentLimit: limits[0].Limit, LimitRemaining: 9, DurationUntilReset: utils.CalculateReset(&limits[0].Limit.Unit, timeSource)}}, @@ -242,7 +242,7 @@ func TestOverLimitWithLocalCache(t *testing.T) { request := common.NewRateLimitRequest("domain", [][][2]string{{{"key4", "value4"}}}, 1) limits := []*config.RateLimit{ - config.NewRateLimit(15, pb.RateLimitResponse_RateLimit_HOUR, sm.NewStats("key4_value4"), false, false, "", nil, false), + config.NewRateLimit(15, pb.RateLimitResponse_RateLimit_HOUR, sm.NewStats("key4_value4"), false, false, false, "", nil, false), } assert.Equal( @@ -343,7 +343,7 @@ func TestNearLimit(t *testing.T) { request := common.NewRateLimitRequest("domain", [][][2]string{{{"key4", "value4"}}}, 1) limits := []*config.RateLimit{ - config.NewRateLimit(15, pb.RateLimitResponse_RateLimit_HOUR, sm.NewStats("key4_value4"), false, false, "", nil, false), + config.NewRateLimit(15, pb.RateLimitResponse_RateLimit_HOUR, sm.NewStats("key4_value4"), false, false, false, "", nil, false), } assert.Equal( @@ -400,7 +400,7 @@ func TestNearLimit(t *testing.T) { client.EXPECT().Increment("domain_key5_value5_1234", uint64(3)).Return(uint64(5), nil) request = common.NewRateLimitRequest("domain", [][][2]string{{{"key5", "value5"}}}, 3) - limits = []*config.RateLimit{config.NewRateLimit(20, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key5_value5"), false, false, "", nil, false)} + limits = []*config.RateLimit{config.NewRateLimit(20, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key5_value5"), false, false, false, "", nil, false)} assert.Equal( []*pb.RateLimitResponse_DescriptorStatus{{Code: pb.RateLimitResponse_OK, CurrentLimit: limits[0].Limit, LimitRemaining: 15, DurationUntilReset: utils.CalculateReset(&limits[0].Limit.Unit, timeSource)}}, @@ -418,7 +418,7 @@ func TestNearLimit(t *testing.T) { client.EXPECT().Increment("domain_key6_value6_1234", uint64(2)).Return(uint64(7), nil) request = common.NewRateLimitRequest("domain", [][][2]string{{{"key6", "value6"}}}, 2) - limits = []*config.RateLimit{config.NewRateLimit(8, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key6_value6"), false, false, "", nil, false)} + limits = []*config.RateLimit{config.NewRateLimit(8, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key6_value6"), false, false, false, "", nil, false)} assert.Equal( []*pb.RateLimitResponse_DescriptorStatus{{Code: pb.RateLimitResponse_OK, CurrentLimit: limits[0].Limit, LimitRemaining: 1, DurationUntilReset: utils.CalculateReset(&limits[0].Limit.Unit, timeSource)}}, @@ -436,7 +436,7 @@ func TestNearLimit(t *testing.T) { client.EXPECT().Increment("domain_key7_value7_1234", uint64(3)).Return(uint64(19), nil) request = common.NewRateLimitRequest("domain", [][][2]string{{{"key7", "value7"}}}, 3) - limits = []*config.RateLimit{config.NewRateLimit(20, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key7_value7"), false, false, "", nil, false)} + limits = []*config.RateLimit{config.NewRateLimit(20, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key7_value7"), false, false, false, "", nil, false)} assert.Equal( []*pb.RateLimitResponse_DescriptorStatus{{Code: pb.RateLimitResponse_OK, CurrentLimit: limits[0].Limit, LimitRemaining: 1, DurationUntilReset: utils.CalculateReset(&limits[0].Limit.Unit, timeSource)}}, @@ -454,7 +454,7 @@ func TestNearLimit(t *testing.T) { client.EXPECT().Increment("domain_key8_value8_1234", uint64(3)).Return(uint64(22), nil) request = common.NewRateLimitRequest("domain", [][][2]string{{{"key8", "value8"}}}, 3) - limits = []*config.RateLimit{config.NewRateLimit(20, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key8_value8"), false, false, "", nil, false)} + limits = []*config.RateLimit{config.NewRateLimit(20, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key8_value8"), false, false, false, "", nil, false)} assert.Equal( []*pb.RateLimitResponse_DescriptorStatus{{Code: pb.RateLimitResponse_OVER_LIMIT, CurrentLimit: limits[0].Limit, LimitRemaining: 0, DurationUntilReset: utils.CalculateReset(&limits[0].Limit.Unit, timeSource)}}, @@ -472,7 +472,7 @@ func TestNearLimit(t *testing.T) { client.EXPECT().Increment("domain_key9_value9_1234", uint64(7)).Return(uint64(22), nil) request = common.NewRateLimitRequest("domain", [][][2]string{{{"key9", "value9"}}}, 7) - limits = []*config.RateLimit{config.NewRateLimit(20, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key9_value9"), false, false, "", nil, false)} + limits = []*config.RateLimit{config.NewRateLimit(20, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key9_value9"), false, false, false, "", nil, false)} assert.Equal( []*pb.RateLimitResponse_DescriptorStatus{{Code: pb.RateLimitResponse_OVER_LIMIT, CurrentLimit: limits[0].Limit, LimitRemaining: 0, DurationUntilReset: utils.CalculateReset(&limits[0].Limit.Unit, timeSource)}}, @@ -490,7 +490,7 @@ func TestNearLimit(t *testing.T) { client.EXPECT().Increment("domain_key10_value10_1234", uint64(3)).Return(uint64(30), nil) request = common.NewRateLimitRequest("domain", [][][2]string{{{"key10", "value10"}}}, 3) - limits = []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key10_value10"), false, false, "", nil, false)} + limits = []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key10_value10"), false, false, false, "", nil, false)} assert.Equal( []*pb.RateLimitResponse_DescriptorStatus{{Code: pb.RateLimitResponse_OVER_LIMIT, CurrentLimit: limits[0].Limit, LimitRemaining: 0, DurationUntilReset: utils.CalculateReset(&limits[0].Limit.Unit, timeSource)}}, @@ -534,7 +534,7 @@ func TestMemcacheWithJitter(t *testing.T) { ).Return(nil) request := common.NewRateLimitRequest("domain", [][][2]string{{{"key", "value"}}}, 1) - limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, "", nil, false)} + limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, false, "", nil, false)} assert.Equal( []*pb.RateLimitResponse_DescriptorStatus{{Code: pb.RateLimitResponse_OK, CurrentLimit: limits[0].Limit, LimitRemaining: 9, DurationUntilReset: utils.CalculateReset(&limits[0].Limit.Unit, timeSource)}}, @@ -577,7 +577,7 @@ func TestMemcacheAdd(t *testing.T) { uint64(2), nil) request := common.NewRateLimitRequest("domain", [][][2]string{{{"key", "value"}}}, 1) - limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, "", nil, false)} + limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, false, "", nil, false)} assert.Equal( []*pb.RateLimitResponse_DescriptorStatus{{Code: pb.RateLimitResponse_OK, CurrentLimit: limits[0].Limit, LimitRemaining: 9, DurationUntilReset: utils.CalculateReset(&limits[0].Limit.Unit, timeSource)}}, @@ -601,7 +601,7 @@ func TestMemcacheAdd(t *testing.T) { ).Return(nil) request = common.NewRateLimitRequest("domain", [][][2]string{{{"key2", "value2"}}}, 1) - limits = []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, sm.NewStats("key2_value2"), false, false, "", nil, false)} + limits = []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, sm.NewStats("key2_value2"), false, false, false, "", nil, false)} assert.Equal( []*pb.RateLimitResponse_DescriptorStatus{{Code: pb.RateLimitResponse_OK, CurrentLimit: limits[0].Limit, LimitRemaining: 9, DurationUntilReset: utils.CalculateReset(&limits[0].Limit.Unit, timeSource)}}, @@ -674,7 +674,7 @@ func TestMemcachedTracer(t *testing.T) { client.EXPECT().Increment("domain_key_value_1234", uint64(1)).Return(uint64(5), nil) request := common.NewRateLimitRequest("domain", [][][2]string{{{"key", "value"}}}, 1) - limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, "", nil, false)} + limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, false, "", nil, false)} cache.DoLimit(context.Background(), request, limits) diff --git a/test/provider/xds_grpc_sotw_provider_test.go b/test/provider/xds_grpc_sotw_provider_test.go index 49a124bdf..70fdbeddc 100644 --- a/test/provider/xds_grpc_sotw_provider_test.go +++ b/test/provider/xds_grpc_sotw_provider_test.go @@ -86,7 +86,7 @@ func testInitialXdsConfig(snapVersion *int, setSnapshotFunc common.SetSnapshotFu config, err := configEvent.GetConfig() assert.Nil(err) - assert.Equal("foo.k1_v1: unit=MINUTE requests_per_unit=3, shadow_mode: false\n", config.Dump()) + assert.Equal("foo.k1_v1: unit=MINUTE requests_per_unit=3, shadow_mode: false, quota_mode: false\n", config.Dump()) } } @@ -122,7 +122,7 @@ func testNewXdsConfigUpdate(snapVersion *int, setSnapshotFunc common.SetSnapshot config, err := configEvent.GetConfig() assert.Nil(err) - assert.Equal("foo.k2_v2: unit=MINUTE requests_per_unit=5, shadow_mode: false\n", config.Dump()) + assert.Equal("foo.k2_v2: unit=MINUTE requests_per_unit=5, shadow_mode: false, quota_mode: false\n", config.Dump()) } } @@ -173,8 +173,8 @@ func testMultiDomainXdsConfigUpdate(snapVersion *int, setSnapshotFunc common.Set config, err := configEvent.GetConfig() assert.Nil(err) assert.ElementsMatch([]string{ - "foo.k1_v1: unit=MINUTE requests_per_unit=10, shadow_mode: false", - "bar.k1_v1: unit=MINUTE requests_per_unit=100, shadow_mode: false", + "foo.k1_v1: unit=MINUTE requests_per_unit=10, shadow_mode: false, quota_mode: false", + "bar.k1_v1: unit=MINUTE requests_per_unit=100, shadow_mode: false, quota_mode: false", }, strings.Split(strings.TrimSuffix(config.Dump(), "\n"), "\n")) } } @@ -266,13 +266,13 @@ func testDeeperLimitsXdsConfigUpdate(snapVersion *int, setSnapshotFunc common.Se config, err := configEvent.GetConfig() assert.Nil(err) assert.ElementsMatch([]string{ - "foo.k1_v1: unit=MINUTE requests_per_unit=10, shadow_mode: false", - "foo.k1_v1.k2: unit=UNKNOWN requests_per_unit=0, shadow_mode: false", - "foo.k1_v1.k2_v2: unit=HOUR requests_per_unit=15, shadow_mode: false", - "foo.j1_v2: unit=UNKNOWN requests_per_unit=0, shadow_mode: false", - "foo.j1_v2.j2: unit=UNKNOWN requests_per_unit=0, shadow_mode: false", - "foo.j1_v2.j2_v2: unit=DAY requests_per_unit=15, shadow_mode: true", - "bar.k1_v1: unit=MINUTE requests_per_unit=100, shadow_mode: false", + "foo.k1_v1: unit=MINUTE requests_per_unit=10, shadow_mode: false, quota_mode: false", + "foo.k1_v1.k2: unit=UNKNOWN requests_per_unit=0, shadow_mode: false, quota_mode: false", + "foo.k1_v1.k2_v2: unit=HOUR requests_per_unit=15, shadow_mode: false, quota_mode: false", + "foo.j1_v2: unit=UNKNOWN requests_per_unit=0, shadow_mode: false, quota_mode: false", + "foo.j1_v2.j2: unit=UNKNOWN requests_per_unit=0, shadow_mode: false, quota_mode: false", + "foo.j1_v2.j2_v2: unit=DAY requests_per_unit=15, shadow_mode: true, quota_mode: false", + "bar.k1_v1: unit=MINUTE requests_per_unit=100, shadow_mode: false, quota_mode: false", }, strings.Split(strings.TrimSuffix(config.Dump(), "\n"), "\n")) } } @@ -323,8 +323,8 @@ func testSameDomainMultipleXdsConfigUpdate(setSnapshotFunc common.SetSnapshotFun config, err := configEvent.GetConfig() assert.Nil(err) assert.ElementsMatch([]string{ - "foo.k1_v2: unit=MINUTE requests_per_unit=100, shadow_mode: false", - "foo.k1_v1: unit=MINUTE requests_per_unit=10, shadow_mode: false", + "foo.k1_v2: unit=MINUTE requests_per_unit=100, shadow_mode: false, quota_mode: false", + "foo.k1_v1: unit=MINUTE requests_per_unit=10, shadow_mode: false, quota_mode: false", }, strings.Split(strings.TrimSuffix(config.Dump(), "\n"), "\n")) } } diff --git a/test/redis/bench_test.go b/test/redis/bench_test.go index 3b1d5fa49..2c153ba97 100644 --- a/test/redis/bench_test.go +++ b/test/redis/bench_test.go @@ -49,7 +49,7 @@ func BenchmarkParallelDoLimit(b *testing.B) { cache := redis.NewFixedRateLimitCacheImpl(client, nil, utils.NewTimeSourceImpl(), rand.New(utils.NewLockedSource(time.Now().Unix())), 10, nil, 0.8, "", sm, true) request := common.NewRateLimitRequest("domain", [][][2]string{{{"key", "value"}}}, 1) - limits := []*config.RateLimit{config.NewRateLimit(1000000000, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, "", nil, false)} + limits := []*config.RateLimit{config.NewRateLimit(1000000000, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, false, "", nil, false)} // wait for the pool to fill up for { diff --git a/test/redis/fixed_cache_impl_test.go b/test/redis/fixed_cache_impl_test.go index 73229ab6e..e52abb68d 100644 --- a/test/redis/fixed_cache_impl_test.go +++ b/test/redis/fixed_cache_impl_test.go @@ -72,7 +72,7 @@ func testRedis(usePerSecondRedis bool) func(*testing.T) { clientUsed.EXPECT().PipeDo(gomock.Any()).Return(nil) request := common.NewRateLimitRequest("domain", [][][2]string{{{"key", "value"}}}, 1) - limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, "", nil, false)} + limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, false, "", nil, false)} assert.Equal( []*pb.RateLimitResponse_DescriptorStatus{{Code: pb.RateLimitResponse_OK, CurrentLimit: limits[0].Limit, LimitRemaining: 5, DurationUntilReset: utils.CalculateReset(&limits[0].Limit.Unit, timeSource)}}, @@ -97,7 +97,7 @@ func testRedis(usePerSecondRedis bool) func(*testing.T) { }, []uint64{0, 1}) limits = []*config.RateLimit{ nil, - config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, sm.NewStats("key2_value2_subkey2_subvalue2"), false, false, "", nil, false), + config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_MINUTE, sm.NewStats("key2_value2_subkey2_subvalue2"), false, false, false, "", nil, false), } assert.Equal( []*pb.RateLimitResponse_DescriptorStatus{ @@ -127,8 +127,8 @@ func testRedis(usePerSecondRedis bool) func(*testing.T) { {{"key3", "value3"}, {"subkey3", "subvalue3"}}, }, []uint64{0, 1}) limits = []*config.RateLimit{ - config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_HOUR, sm.NewStats("key3_value3"), false, false, "", nil, false), - config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_DAY, sm.NewStats("key3_value3_subkey3_subvalue3"), false, false, "", nil, false), + config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_HOUR, sm.NewStats("key3_value3"), false, false, false, "", nil, false), + config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_DAY, sm.NewStats("key3_value3_subkey3_subvalue3"), false, false, false, "", nil, false), } assert.Equal( []*pb.RateLimitResponse_DescriptorStatus{ @@ -211,7 +211,7 @@ func TestOverLimitWithLocalCache(t *testing.T) { request := common.NewRateLimitRequest("domain", [][][2]string{{{"key4", "value4"}}}, 1) limits := []*config.RateLimit{ - config.NewRateLimit(15, pb.RateLimitResponse_RateLimit_HOUR, sm.NewStats("key4_value4"), false, false, "", nil, false), + config.NewRateLimit(15, pb.RateLimitResponse_RateLimit_HOUR, sm.NewStats("key4_value4"), false, false, false, "", nil, false), } assert.Equal( @@ -311,7 +311,7 @@ func TestNearLimit(t *testing.T) { request := common.NewRateLimitRequest("domain", [][][2]string{{{"key4", "value4"}}}, 1) limits := []*config.RateLimit{ - config.NewRateLimit(15, pb.RateLimitResponse_RateLimit_HOUR, sm.NewStats("key4_value4"), false, false, "", nil, false), + config.NewRateLimit(15, pb.RateLimitResponse_RateLimit_HOUR, sm.NewStats("key4_value4"), false, false, false, "", nil, false), } assert.Equal( @@ -367,7 +367,7 @@ func TestNearLimit(t *testing.T) { client.EXPECT().PipeDo(gomock.Any()).Return(nil) request = common.NewRateLimitRequest("domain", [][][2]string{{{"key5", "value5"}}}, 3) - limits = []*config.RateLimit{config.NewRateLimit(20, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key5_value5"), false, false, "", nil, false)} + limits = []*config.RateLimit{config.NewRateLimit(20, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key5_value5"), false, false, false, "", nil, false)} assert.Equal( []*pb.RateLimitResponse_DescriptorStatus{{Code: pb.RateLimitResponse_OK, CurrentLimit: limits[0].Limit, LimitRemaining: 15, DurationUntilReset: utils.CalculateReset(&limits[0].Limit.Unit, timeSource)}}, @@ -384,7 +384,7 @@ func TestNearLimit(t *testing.T) { client.EXPECT().PipeDo(gomock.Any()).Return(nil) request = common.NewRateLimitRequest("domain", [][][2]string{{{"key6", "value6"}}}, 2) - limits = []*config.RateLimit{config.NewRateLimit(8, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key6_value6"), false, false, "", nil, false)} + limits = []*config.RateLimit{config.NewRateLimit(8, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key6_value6"), false, false, false, "", nil, false)} assert.Equal( []*pb.RateLimitResponse_DescriptorStatus{{Code: pb.RateLimitResponse_OK, CurrentLimit: limits[0].Limit, LimitRemaining: 1, DurationUntilReset: utils.CalculateReset(&limits[0].Limit.Unit, timeSource)}}, @@ -401,7 +401,7 @@ func TestNearLimit(t *testing.T) { client.EXPECT().PipeDo(gomock.Any()).Return(nil) request = common.NewRateLimitRequest("domain", [][][2]string{{{"key7", "value7"}}}, 3) - limits = []*config.RateLimit{config.NewRateLimit(20, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key7_value7"), false, false, "", nil, false)} + limits = []*config.RateLimit{config.NewRateLimit(20, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key7_value7"), false, false, false, "", nil, false)} assert.Equal( []*pb.RateLimitResponse_DescriptorStatus{{Code: pb.RateLimitResponse_OK, CurrentLimit: limits[0].Limit, LimitRemaining: 1, DurationUntilReset: utils.CalculateReset(&limits[0].Limit.Unit, timeSource)}}, @@ -418,7 +418,7 @@ func TestNearLimit(t *testing.T) { client.EXPECT().PipeDo(gomock.Any()).Return(nil) request = common.NewRateLimitRequest("domain", [][][2]string{{{"key8", "value8"}}}, 3) - limits = []*config.RateLimit{config.NewRateLimit(20, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key8_value8"), false, false, "", nil, false)} + limits = []*config.RateLimit{config.NewRateLimit(20, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key8_value8"), false, false, false, "", nil, false)} assert.Equal( []*pb.RateLimitResponse_DescriptorStatus{{Code: pb.RateLimitResponse_OVER_LIMIT, CurrentLimit: limits[0].Limit, LimitRemaining: 0, DurationUntilReset: utils.CalculateReset(&limits[0].Limit.Unit, timeSource)}}, @@ -435,7 +435,7 @@ func TestNearLimit(t *testing.T) { client.EXPECT().PipeDo(gomock.Any()).Return(nil) request = common.NewRateLimitRequest("domain", [][][2]string{{{"key9", "value9"}}}, 7) - limits = []*config.RateLimit{config.NewRateLimit(20, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key9_value9"), false, false, "", nil, false)} + limits = []*config.RateLimit{config.NewRateLimit(20, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key9_value9"), false, false, false, "", nil, false)} assert.Equal( []*pb.RateLimitResponse_DescriptorStatus{{Code: pb.RateLimitResponse_OVER_LIMIT, CurrentLimit: limits[0].Limit, LimitRemaining: 0, DurationUntilReset: utils.CalculateReset(&limits[0].Limit.Unit, timeSource)}}, @@ -452,7 +452,7 @@ func TestNearLimit(t *testing.T) { client.EXPECT().PipeDo(gomock.Any()).Return(nil) request = common.NewRateLimitRequest("domain", [][][2]string{{{"key10", "value10"}}}, 3) - limits = []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key10_value10"), false, false, "", nil, false)} + limits = []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key10_value10"), false, false, false, "", nil, false)} assert.Equal( []*pb.RateLimitResponse_DescriptorStatus{{Code: pb.RateLimitResponse_OVER_LIMIT, CurrentLimit: limits[0].Limit, LimitRemaining: 0, DurationUntilReset: utils.CalculateReset(&limits[0].Limit.Unit, timeSource)}}, @@ -482,7 +482,7 @@ func TestRedisWithJitter(t *testing.T) { client.EXPECT().PipeDo(gomock.Any()).Return(nil) request := common.NewRateLimitRequest("domain", [][][2]string{{{"key", "value"}}}, 1) - limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, "", nil, false)} + limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, false, "", nil, false)} assert.Equal( []*pb.RateLimitResponse_DescriptorStatus{{Code: pb.RateLimitResponse_OK, CurrentLimit: limits[0].Limit, LimitRemaining: 5, DurationUntilReset: utils.CalculateReset(&limits[0].Limit.Unit, timeSource)}}, @@ -519,7 +519,7 @@ func TestOverLimitWithLocalCacheShadowRule(t *testing.T) { request := common.NewRateLimitRequest("domain", [][][2]string{{{"key4", "value4"}}}, 1) limits := []*config.RateLimit{ - config.NewRateLimit(15, pb.RateLimitResponse_RateLimit_HOUR, sm.NewStats("key4_value4"), false, true, "", nil, false), + config.NewRateLimit(15, pb.RateLimitResponse_RateLimit_HOUR, sm.NewStats("key4_value4"), false, true, false, "", nil, false), } assert.Equal( @@ -627,7 +627,7 @@ func TestRedisTracer(t *testing.T) { client.EXPECT().PipeDo(gomock.Any()).Return(nil) request := common.NewRateLimitRequest("domain", [][][2]string{{{"key", "value"}}}, 1) - limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, "", nil, false)} + limits := []*config.RateLimit{config.NewRateLimit(10, pb.RateLimitResponse_RateLimit_SECOND, sm.NewStats("key_value"), false, false, false, "", nil, false)} cache.DoLimit(context.Background(), request, limits) spanStubs := testSpanExporter.GetSpans() @@ -669,8 +669,8 @@ func TestOverLimitWithStopCacheKeyIncrementWhenOverlimitConfig(t *testing.T) { request := common.NewRateLimitRequestWithPerDescriptorHitsAddend("domain", [][][2]string{{{"key4", "value4"}}, {{"key5", "value5"}}}, []uint64{1, 1}) limits := []*config.RateLimit{ - config.NewRateLimit(15, pb.RateLimitResponse_RateLimit_HOUR, sm.NewStats("key4_value4"), false, false, "", nil, false), - config.NewRateLimit(14, pb.RateLimitResponse_RateLimit_HOUR, sm.NewStats("key5_value5"), false, false, "", nil, false), + config.NewRateLimit(15, pb.RateLimitResponse_RateLimit_HOUR, sm.NewStats("key4_value4"), false, false, false, "", nil, false), + config.NewRateLimit(14, pb.RateLimitResponse_RateLimit_HOUR, sm.NewStats("key5_value5"), false, false, false, "", nil, false), } assert.Equal( From 63a43e0d3ada1e6ec01a5ac700addc2fd0b6b785 Mon Sep 17 00:00:00 2001 From: Dan Sun Date: Sun, 11 Jan 2026 05:59:49 -0500 Subject: [PATCH 5/5] fix quota mode flag Signed-off-by: Dan Sun --- src/config/config_impl.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config/config_impl.go b/src/config/config_impl.go index 313579f07..19abb66fe 100644 --- a/src/config/config_impl.go +++ b/src/config/config_impl.go @@ -456,6 +456,7 @@ func (this *rateLimitConfigImpl) GetLimit( Limit: originalLimit.Limit, Unlimited: originalLimit.Unlimited, ShadowMode: originalLimit.ShadowMode, + QuotaMode: originalLimit.QuotaMode, Name: originalLimit.Name, Replaces: originalLimit.Replaces, DetailedMetric: originalLimit.DetailedMetric,