Skip to content

Commit a28b84d

Browse files
authored
implement quota mode for soft rate limit check (#1045)
* implement quota mode for rate limit check Signed-off-by: Dan Sun <dsun20@bloomberg.net> * fix format Signed-off-by: Dan Sun <dsun20@bloomberg.net> * fix format Signed-off-by: Dan Sun <dsun20@bloomberg.net> * fix tests Signed-off-by: Dan Sun <dsun20@bloomberg.net> * fix quota mode flag Signed-off-by: Dan Sun <dsun20@bloomberg.net> --------- Signed-off-by: Dan Sun <dsun20@bloomberg.net>
1 parent e9ce92c commit a28b84d

File tree

12 files changed

+563
-97
lines changed

12 files changed

+563
-97
lines changed

src/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type RateLimit struct {
2222
Limit *pb.RateLimitResponse_RateLimit
2323
Unlimited bool
2424
ShadowMode bool
25+
QuotaMode bool
2526
Name string
2627
Replaces []string
2728
DetailedMetric bool

src/config/config_impl.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type YamlDescriptor struct {
3131
RateLimit *YamlRateLimit `yaml:"rate_limit"`
3232
Descriptors []YamlDescriptor
3333
ShadowMode bool `yaml:"shadow_mode"`
34+
QuotaMode bool `yaml:"quota_mode"`
3435
DetailedMetric bool `yaml:"detailed_metric"`
3536
ValueToMetric bool `yaml:"value_to_metric"`
3637
ShareThreshold bool `yaml:"share_threshold"`
@@ -70,6 +71,7 @@ var validKeys = map[string]bool{
7071
"requests_per_unit": true,
7172
"unlimited": true,
7273
"shadow_mode": true,
74+
"quota_mode": true,
7375
"name": true,
7476
"replaces": true,
7577
"detailed_metric": true,
@@ -84,7 +86,7 @@ var validKeys = map[string]bool{
8486
// @param unlimited supplies whether the rate limit is unlimited
8587
// @return the new config entry.
8688
func NewRateLimit(requestsPerUnit uint32, unit pb.RateLimitResponse_RateLimit_Unit, rlStats stats.RateLimitStats,
87-
unlimited bool, shadowMode bool, name string, replaces []string, detailedMetric bool,
89+
unlimited bool, shadowMode bool, quotaMode bool, name string, replaces []string, detailedMetric bool,
8890
) *RateLimit {
8991
return &RateLimit{
9092
FullKey: rlStats.GetKey(),
@@ -96,6 +98,7 @@ func NewRateLimit(requestsPerUnit uint32, unit pb.RateLimitResponse_RateLimit_Un
9698
},
9799
Unlimited: unlimited,
98100
ShadowMode: shadowMode,
101+
QuotaMode: quotaMode,
99102
Name: name,
100103
Replaces: replaces,
101104
DetailedMetric: detailedMetric,
@@ -108,8 +111,8 @@ func (this *rateLimitDescriptor) dump() string {
108111
ret := ""
109112
if this.limit != nil {
110113
ret += fmt.Sprintf(
111-
"%s: unit=%s requests_per_unit=%d, shadow_mode: %t\n", this.limit.FullKey,
112-
this.limit.Limit.Unit.String(), this.limit.Limit.RequestsPerUnit, this.limit.ShadowMode)
114+
"%s: unit=%s requests_per_unit=%d, shadow_mode: %t, quota_mode: %t\n", this.limit.FullKey,
115+
this.limit.Limit.Unit.String(), this.limit.Limit.RequestsPerUnit, this.limit.ShadowMode, this.limit.QuotaMode)
113116
}
114117
for _, descriptor := range this.descriptors {
115118
ret += descriptor.dump()
@@ -174,12 +177,12 @@ func (this *rateLimitDescriptor) loadDescriptors(config RateLimitConfigToLoad, p
174177

175178
rateLimit = NewRateLimit(
176179
descriptorConfig.RateLimit.RequestsPerUnit, pb.RateLimitResponse_RateLimit_Unit(value),
177-
statsManager.NewStats(newParentKey), unlimited, descriptorConfig.ShadowMode,
180+
statsManager.NewStats(newParentKey), unlimited, descriptorConfig.ShadowMode, descriptorConfig.QuotaMode,
178181
descriptorConfig.RateLimit.Name, replaces, descriptorConfig.DetailedMetric,
179182
)
180183
rateLimitDebugString = fmt.Sprintf(
181-
" ratelimit={requests_per_unit=%d, unit=%s, unlimited=%t, shadow_mode=%t}", rateLimit.Limit.RequestsPerUnit,
182-
rateLimit.Limit.Unit.String(), rateLimit.Unlimited, rateLimit.ShadowMode)
184+
" ratelimit={requests_per_unit=%d, unit=%s, unlimited=%t, shadow_mode=%t, quota_mode=%t}", rateLimit.Limit.RequestsPerUnit,
185+
rateLimit.Limit.Unit.String(), rateLimit.Unlimited, rateLimit.ShadowMode, rateLimit.QuotaMode)
183186

184187
for _, replaces := range descriptorConfig.RateLimit.Replaces {
185188
if replaces.Name == "" {
@@ -336,6 +339,7 @@ func (this *rateLimitConfigImpl) GetLimit(
336339
this.statsManager.NewStats(rateLimitKey),
337340
false,
338341
false,
342+
false,
339343
"",
340344
[]string{},
341345
false,
@@ -452,6 +456,7 @@ func (this *rateLimitConfigImpl) GetLimit(
452456
Limit: originalLimit.Limit,
453457
Unlimited: originalLimit.Unlimited,
454458
ShadowMode: originalLimit.ShadowMode,
459+
QuotaMode: originalLimit.QuotaMode,
455460
Name: originalLimit.Name,
456461
Replaces: originalLimit.Replaces,
457462
DetailedMetric: originalLimit.DetailedMetric,
@@ -481,7 +486,7 @@ func (this *rateLimitConfigImpl) GetLimit(
481486
if rateLimit != nil && rateLimit.DetailedMetric {
482487
// Preserve ShareThresholdKeyPattern when recreating rate limit
483488
originalShareThresholdKeyPattern := rateLimit.ShareThresholdKeyPattern
484-
rateLimit = NewRateLimit(rateLimit.Limit.RequestsPerUnit, rateLimit.Limit.Unit, this.statsManager.NewStats(rateLimit.FullKey), rateLimit.Unlimited, rateLimit.ShadowMode, rateLimit.Name, rateLimit.Replaces, rateLimit.DetailedMetric)
489+
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)
485490
rateLimit.ShareThresholdKeyPattern = originalShareThresholdKeyPattern
486491
}
487492

@@ -531,7 +536,7 @@ func (this *rateLimitConfigImpl) GetLimit(
531536
if enhancedKey != rateLimit.FullKey {
532537
// Recreate to ensure a clean stats struct, then set to enhanced stats
533538
originalShareThresholdKeyPattern := rateLimit.ShareThresholdKeyPattern
534-
rateLimit = NewRateLimit(rateLimit.Limit.RequestsPerUnit, rateLimit.Limit.Unit, this.statsManager.NewStats(enhancedKey), rateLimit.Unlimited, rateLimit.ShadowMode, rateLimit.Name, rateLimit.Replaces, rateLimit.DetailedMetric)
539+
rateLimit = NewRateLimit(rateLimit.Limit.RequestsPerUnit, rateLimit.Limit.Unit, this.statsManager.NewStats(enhancedKey), rateLimit.Unlimited, rateLimit.ShadowMode, rateLimit.QuotaMode, rateLimit.Name, rateLimit.Replaces, rateLimit.DetailedMetric)
535540
rateLimit.ShareThresholdKeyPattern = originalShareThresholdKeyPattern
536541
}
537542
}

src/service/ratelimit.go

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ var tracer = otel.Tracer("ratelimit")
3535

3636
type RateLimitServiceServer interface {
3737
pb.RateLimitServiceServer
38-
GetCurrentConfig() (config.RateLimitConfig, bool)
38+
GetCurrentConfig() (config.RateLimitConfig, bool, bool)
3939
SetConfig(updateEvent provider.ConfigUpdateEvent, healthyWithAtLeastOneConfigLoad bool)
4040
}
4141

@@ -52,6 +52,7 @@ type service struct {
5252
customHeaderResetHeader string
5353
customHeaderClock utils.TimeSource
5454
globalShadowMode bool
55+
globalQuotaMode bool
5556
responseDynamicMetadataEnabled bool
5657
}
5758

@@ -87,6 +88,7 @@ func (this *service) SetConfig(updateEvent provider.ConfigUpdateEvent, healthyWi
8788

8889
rlSettings := settings.NewSettings()
8990
this.globalShadowMode = rlSettings.GlobalShadowMode
91+
this.globalQuotaMode = rlSettings.GlobalQuotaMode
9092
this.responseDynamicMetadataEnabled = rlSettings.ResponseDynamicMetadata
9193

9294
if rlSettings.RateLimitResponseHeadersEnabled {
@@ -186,7 +188,7 @@ func (this *service) shouldRateLimitWorker(
186188
checkServiceErr(request.Domain != "", "rate limit domain must not be empty")
187189
checkServiceErr(len(request.Descriptors) != 0, "rate limit descriptor list must not be empty")
188190

189-
snappedConfig, globalShadowMode := this.GetCurrentConfig()
191+
snappedConfig, globalShadowMode, globalQuotaMode := this.GetCurrentConfig()
190192
limitsToCheck, isUnlimited := this.constructLimitsToCheck(request, ctx, snappedConfig)
191193

192194
assert.Assert(len(limitsToCheck) == len(isUnlimited))
@@ -203,6 +205,9 @@ func (this *service) shouldRateLimitWorker(
203205
minLimitRemaining := MaxUint32
204206
var minimumDescriptor *pb.RateLimitResponse_DescriptorStatus = nil
205207

208+
// Track quota mode violations for metadata
209+
var quotaModeViolations []int
210+
206211
for i, descriptorStatus := range responseDescriptorStatuses {
207212
// Keep track of the descriptor closest to hit the ratelimit
208213
if this.customHeadersEnabled &&
@@ -220,10 +225,23 @@ func (this *service) shouldRateLimitWorker(
220225
} else {
221226
response.Statuses[i] = descriptorStatus
222227
if descriptorStatus.Code == pb.RateLimitResponse_OVER_LIMIT {
223-
finalCode = descriptorStatus.Code
224-
225-
minimumDescriptor = descriptorStatus
226-
minLimitRemaining = 0
228+
// Check if this limit is in quota mode (individual or global)
229+
isQuotaMode := globalQuotaMode || (limitsToCheck[i] != nil && limitsToCheck[i].QuotaMode)
230+
231+
if isQuotaMode {
232+
// In quota mode: track the violation for metadata but keep response as OK
233+
quotaModeViolations = append(quotaModeViolations, i)
234+
response.Statuses[i] = &pb.RateLimitResponse_DescriptorStatus{
235+
Code: pb.RateLimitResponse_OK,
236+
CurrentLimit: descriptorStatus.CurrentLimit,
237+
LimitRemaining: descriptorStatus.LimitRemaining,
238+
}
239+
} else {
240+
// Normal rate limit: set final code to OVER_LIMIT
241+
finalCode = descriptorStatus.Code
242+
minimumDescriptor = descriptorStatus
243+
minLimitRemaining = 0
244+
}
227245
}
228246
}
229247
}
@@ -245,14 +263,14 @@ func (this *service) shouldRateLimitWorker(
245263

246264
// If response dynamic data enabled, set dynamic data on response.
247265
if this.responseDynamicMetadataEnabled {
248-
response.DynamicMetadata = ratelimitToMetadata(request)
266+
response.DynamicMetadata = ratelimitToMetadata(request, quotaModeViolations, limitsToCheck)
249267
}
250268

251269
response.OverallCode = finalCode
252270
return response
253271
}
254272

255-
func ratelimitToMetadata(req *pb.RateLimitRequest) *structpb.Struct {
273+
func ratelimitToMetadata(req *pb.RateLimitRequest, quotaModeViolations []int, limitsToCheck []*config.RateLimit) *structpb.Struct {
256274
fields := make(map[string]*structpb.Value)
257275

258276
// Domain
@@ -276,6 +294,27 @@ func ratelimitToMetadata(req *pb.RateLimitRequest) *structpb.Struct {
276294
fields["hitsAddend"] = structpb.NewNumberValue(float64(hitsAddend))
277295
}
278296

297+
// Quota mode information
298+
if len(quotaModeViolations) > 0 {
299+
violationValues := make([]*structpb.Value, len(quotaModeViolations))
300+
for i, violationIndex := range quotaModeViolations {
301+
violationValues[i] = structpb.NewNumberValue(float64(violationIndex))
302+
}
303+
fields["quotaModeViolations"] = structpb.NewListValue(&structpb.ListValue{
304+
Values: violationValues,
305+
})
306+
}
307+
308+
// Check if any limits have quota mode enabled
309+
quotaModeEnabled := false
310+
for _, limit := range limitsToCheck {
311+
if limit != nil && limit.QuotaMode {
312+
quotaModeEnabled = true
313+
break
314+
}
315+
}
316+
fields["quotaModeEnabled"] = structpb.NewBoolValue(quotaModeEnabled)
317+
279318
return &structpb.Struct{Fields: fields}
280319
}
281320

@@ -379,10 +418,10 @@ func (this *service) ShouldRateLimit(
379418
return response, nil
380419
}
381420

382-
func (this *service) GetCurrentConfig() (config.RateLimitConfig, bool) {
421+
func (this *service) GetCurrentConfig() (config.RateLimitConfig, bool, bool) {
383422
this.configLock.RLock()
384423
defer this.configLock.RUnlock()
385-
return this.config, this.globalShadowMode
424+
return this.config, this.globalShadowMode, this.globalQuotaMode
386425
}
387426

388427
func NewService(cache limiter.RateLimitCache, configProvider provider.RateLimitConfigProvider, statsManager stats.Manager,
@@ -396,6 +435,7 @@ func NewService(cache limiter.RateLimitCache, configProvider provider.RateLimitC
396435
stats: statsManager.NewServiceStats(),
397436
health: health,
398437
globalShadowMode: shadowMode,
438+
globalQuotaMode: false,
399439
customHeaderClock: clock,
400440
}
401441

0 commit comments

Comments
 (0)