Skip to content

fix: apply QuotaPolicy changes to Envoy config without restart - #2766

Open
AyushSawant18588 wants to merge 5 commits into
theagentrouter:mainfrom
AyushSawant18588:fix/quota-policy-live-reconciliation
Open

AyushSawant18588 wants to merge 5 commits into
theagentrouter:mainfrom
AyushSawant18588:fix/quota-policy-live-reconciliation

Conversation

@AyushSawant18588

@AyushSawant18588 AyushSawant18588 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Description

When a QuotaPolicy CR was updated, the new configuration was not applied to the data plane until the AI Gateway controller and the Envoy proxy pod were restarted.

The root cause is that a QuotaPolicy feeds two config planes: the rate limit service config (the numeric limits, pushed over xDS) and the Envoy data-plane config (the rate limit filter, cluster, and per-route descriptors, injected by the extension server's PostTranslateModify). The extension server only runs when Envoy Gateway re-translates, which happens when a resource it watches changes. QuotaPolicy is not such a resource. The controller tried to force re-translation by re-reconciling the HTTPRoute, but the regenerated HTTPRoute was identical (its content does not depend on the QuotaPolicy), so the update was a no-op and Envoy Gateway never re-translated.

This change stamps a hash of the applicable QuotaPolicy specs onto the generated HTTPRoute as the aigateway.envoyproxy.io/quota-policy-hash annotation (mirroring the existing stampGatewayConfigHash approach for GatewayConfig). A QuotaPolicy create/update/delete now changes the annotation, making the HTTPRoute genuinely change, which forces Envoy Gateway to re-translate and re-run PostTranslateModify with the latest policy. The hash covers each policy's full spec, so both value-only changes (e.g. a limit bump) and structural changes (e.g. a new model or bucket rule) are picked up live.

This change also fixes QuotaPolicy deletion not propagating to routes in different namespace. Deleting a QuotaPolicy now notifies the referencing AIGatewayRoutes (via the finalizer callback, while TargetRefs are still available) so their generated HTTPRoutes are re-stamped and Envoy Gateway re-translates without the deleted policy, no controller/Envoy restart required. The quota-policy-hash computation also skips terminating policies (non-zero DeletionTimestamp), closing the issue where a cached, soon-to-be-deleted policy could otherwise leave the hash unchanged.

Unit tests cover the hash computation and annotation behavior, and an e2e test verifies the annotation changes when a QuotaPolicy is updated live (no restart).

@AyushSawant18588
AyushSawant18588 requested a review from a team as a code owner September 30, 2026 16:07
@netlify

netlify Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for theagentrouter canceled.

Name Link
🔨 Latest commit 79f20de
🔍 Latest deploy log https://app.netlify.com/projects/theagentrouter/deploys/6abd740dc72e070008610f73

@codecov

codecov Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.33333% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/controller/ai_gateway_route.go 93.02% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

Signed-off-by: Ayush Sawant <ayush.sawant@nutanix.com>
@missBerg missBerg added bug Something isn't working area/quota Rate limiting, quota, token/cost accounting and attribution area/controller Controller and reconciliation labels Sep 30, 2026
Signed-off-by: Ayush Sawant <ayush.sawant@nutanix.com>
Signed-off-by: Ayush Sawant <ayush.sawant@nutanix.com>
@gavrissh

gavrissh commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

@AyushSawant18588 could you fix the tests?

@vignesh-chaturvedi

Copy link
Copy Markdown
Contributor

One gap in the deletion change, which I ran into while working on the
overlapping #2770. With the cleanup removed from the not-found branch of
Reconcile, a QuotaPolicy that disappears without the finalizer callback keeps
its limits in the rate limit config until the controller restarts, because
configCache is only ever updated one policy at a time.

That can happen two ways:

  • The finalizer is stripped out of band, for example kubectl patch with
    finalizers: null on a stuck object. The controller then only sees not-found.
  • deleteQuotaPolicyConfig fails inside the callback. handleFinalizer logs and
    drops the callback's error and removes the finalizer anyway, so on main the
    not-found branch was the retry.

I checked the first case with a controller test that creates and reconciles a
policy, clears its finalizers, deletes it, and reconciles again. On this branch
the policy's entry is still in configCache afterwards. On main it is removed.
Happy to share the test.

Keeping deleteQuotaPolicyConfig in the not-found branch alongside the new
finalizer-time notification would close it, and since it is idempotent, running
it in both places is safe. If you also want routes refreshed in that case, the
namespace-wide notification in #2770 covers it without needing the targetRefs.

// counted it, the recomputed hash would be unchanged, the HTTPRoute update would be a no-op,
// and Envoy Gateway would not re-translate. Treating a terminating policy as already-absent
// makes the hash change deterministically, regardless of whether the cache has dropped it yet.
if !p.DeletionTimestamp.IsZero() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we add this check to the extension policy block in maybeInjectQuotaRateLimiting? Prevents the extension server from adding a soon-to-be-deleted descriptors

return ctrl.Result{}, err
}
c.updateQuotaPolicyStatus(ctx, &quotaPolicy, aigv1a1.ConditionTypeAccepted, "QuotaPolicy reconciled successfully")
c.notifyAIGatewayRoutes(ctx, &quotaPolicy)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one minor thing to note is that changing a backend in spec.ref may remove an aigatewayroute leaving the annotation defined in ai_gateway_route.go orphaned. I think it should be fine because the extension server will still be triggered if they are the same gateway but something to consider.

// The QuotaPolicy targetRefs index key is "<targetRef.Name>.<quotaPolicy.Namespace>", and a
// QuotaPolicy (LocalPolicyTargetReference) can only target a backend in its own namespace, so
// the backend's namespace is also the QuotaPolicy's namespace.
key := fmt.Sprintf("%s.%s", br.Name, backendNamespace)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you mind making a helper function that does this so we don't duplicate this logic all over ie in syncAIServiceBackend?

ie

  func namespacedNameIndexKey(name, namespace string) string {
      return fmt.Sprintf("%s.%s", name, namespace)
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/controller Controller and reconciliation area/quota Rate limiting, quota, token/cost accounting and attribution bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants