Implement grouped quota enforcement and failover metadata in RLS - #1243
Conversation
Signed-off-by: Ayush Sawant <ayush.sawant@nutanix.com>
2bb7f86 to
a8251da
Compare
|
@yanavlasov can we get someone from Envoy proxy to review this? |
|
@AyushSawant18588 there are format errors. /wait |
soumya-acharya
left a comment
There was a problem hiding this comment.
Thanks for the changes! A few requirements and suggestions from Envoy's perspective.
| availableDescriptors = append(availableDescriptors, idx) | ||
| } | ||
| } | ||
| response.DynamicMetadata = ratelimitToMetadata(request, availableDescriptors, limitsToCheck) |
There was a problem hiding this comment.
Filtering exhausted groups out of availableDescriptors is the right hook. ratelimitToMetadata (pre-existing) still dumps all req.Descriptors into descriptors and merges limit.Metadata into one map. Envoy cannot join that to hosts: agent-router does not set descriptor metadata, and two live backends collapse on the same key.
We should emit a list from availableDescriptors, deduped by quotaGroupKey:
"passed_backends": [
{"backend_name": "ns/be", "model_name_override": "model-b"}
]
Leave domain / descriptors / metadata if other callers need them. Envoy will read only passed_backends.
There was a problem hiding this comment.
Will be making change on agent-router to set descriptor metadata and yes in metadata two live backends collapse on the same key but instead of having keys like backend_name and model_name_override which will collapse, it can directly be the values as the keys itself like:
"metadata": [
{"model-a": "ns/be"},
{"model-b": "ns/be"},
]
So that none of them will be collapsed. With this we don't need to add new passed_backends field. Let me know if this structure is fine for envoy.
There was a problem hiding this comment.
We should not invert keys into metadata and please don’t block this on agent-router descriptor.metadata.
ratelimitToMetadata still emits metadata as one merged Struct from limit.Metadata. A list of {model: backend} is a different type and a different source. If that field stays a map, two backends with the same model still collapse on one key. That is the case we have to route.
Descriptor entries already have backend_name and model_name_override. availableDescriptors is the right filter. From those indices, the best way is to emit a list as suggested above, we don’t need to change the existing metadata map at all. Envoy will read only passed_backends.
There was a problem hiding this comment.
Oh yes model name can be same for different backends in a route so model-name or backed-name can't be a reliable key. I agree that it will be better to return a list of model and backend pairs of which quota is not exhausted and as this information is already available with RLS the agent-router does not need to send limit.Metadata. I have added passed backends field in dynamic metadata as you proposed. Can you review the new changes?
Thanks!
| totalQuotaDescriptors += 1 | ||
| // OR the statuses within a quota group. | ||
| if over { | ||
| group.over = true |
There was a problem hiding this comment.
I think group.over doesn't not check limitsToCheck[i].ShadowMode. A shadow over-limit bucket would mark the whole group over, it'd be better to ignore ShadowMode buckets. Can you verify this?
There was a problem hiding this comment.
over is derived purely from descriptorStatus.Code == OVER_LIMIT. But a per-rule shadow-mode bucket never reaches this loop with an OVER_LIMIT code. ShadowMode is resolved much earlier, inside the cache/limiter layer. DoLimit (redis fixed_cache_impl.go) generates every descriptor status through this GetResponseDescriptorStatus, which downgrades a shadow over-limit bucket's Code to OK before it's returned. By the time shouldRateLimitWorker sees it, descriptorStatus.Code == OK, so over is false and it does not set group.over = true.
| t.assert.True(ok) | ||
| // Only model-b (the group that still has quota) is advertised; model-a's | ||
| // passed default bucket is excluded because model-a's group is exhausted. | ||
| t.assert.Equal("model_b", nameVal.GetStringValue()) |
There was a problem hiding this comment.
This makes merged metadata.name the failover signal. That will not exist in our stack. Please assert passed_backends has {backend_name=ns/be, model_name_override=model-b} and not model-a. Add two available groups so the list cannot collapse.
There was a problem hiding this comment.
Whether the metadata lists collapses or not and what data ratelimit puts in metadata is depends on what we set as limit.metadata in agent-router, If we sent it as mentioned here: #1243 (comment) then it should be fine right?
There was a problem hiding this comment.
Based on the above explanation we would need the test changes
There was a problem hiding this comment.
I have updated the test to check for passed backends too
| // TestServiceQuotaModeMultiModelFailover verifies that with two model groups on a | ||
| // route, one model being fully exhausted does not reject the request while | ||
| // another model still has quota (AND across groups → failover preserved). | ||
| func TestServiceQuotaModeMultiModelFailover(test *testing.T) { |
There was a problem hiding this comment.
Can we add two backend_names, same model_name_override, one group over -> overall_code=OK and only the live backend in passed_backends?
There was a problem hiding this comment.
I have added this test case
Signed-off-by: Ayush Sawant <ayush.sawant@nutanix.com>
Signed-off-by: Ayush Sawant <ayush.sawant@nutanix.com>
soumya-acharya
left a comment
There was a problem hiding this comment.
Thanks for the changes Ayush.
Problem
When a QuotaPolicy defines multiple quota buckets that apply to the same request, for example a per-tenant bucketRule plus a model defaultBucket, or several models on one route, the rate limit service treated all quota-mode descriptors as one flat set. Its rule was:
return OVER_LIMIT only when every quota descriptor is over its limit.This produced two problems:
The root cause was that the aggregation logic did not understand that some descriptors belong to the same enforcement scope (a model) while others are independent scopes that should support failover.
Overview of changes
Testing