feat: request admission control / backpressure middleware (opt-in)#1332
feat: request admission control / backpressure middleware (opt-in)#1332unsupo wants to merge 1 commit into
Conversation
Cluster mode adds capacity, but under saturation the server still queues every request in the anyio threadpool until it times out to a 504 -- the shared-queue collapse where, past a critical backlog, effectively all new requests fail. This adds an OUTERMOST middleware that sheds excess load early with a fast 429 + Retry-After so clients back off instead of piling up: - AdmissionController: a global per-worker in-flight cap (pod capacity = workers x max_inflight_requests). Shed when the worker's in-flight set is full. - PerConsumerRateLimiter: independent per-credential token-bucket fairness (reuses the existing auth TokenBucketManager, keyed by a SHA-256 hash of the caller's Authorization header / session cookie) so one noisy client can't starve the fleet. Cluster-aware when a PG connection pool is attached. Both gates are independent and OFF by default (AdmissionControlConfig, read from the merged server config at app wiring -- not a bootstrap key; a restart applies changes). Health/docs endpoints are always exempt so readiness probes and schema fetches are never rejected. Zero overhead when disabled (middleware not wired). Validated on a 2-replica EKS deployment: at a small in-flight cap, excess concurrent load was shed as 429s (fast) instead of collapsing to 504s, and a single heavy consumer was throttled without affecting others. Adds unit tests for both gates, credential keying, exempt paths, and the 429 + Retry-After path (incl. the inf-refill guard); config classification updated.
|
Reviewed via /implement-backlog and integrated into The
Gates: fast-automation 12394/0 (lone failure = the known #1323 flake, isolation-confirmed); server-fast all 6 chunks PASS/0-fail; Follow-up filed for strict (zero-overshoot) bounding via an atomic |
What
An opt-in, outermost middleware that sheds excess load early with
429 + Retry-After, so requests back off instead of piling into the anyio threadpool until they time out to 504 (the shared-queue collapse).Two independent gates (either/both; both off by default):
AdmissionController— global per-worker in-flight cap. Pod capacity =workers x max_inflight_requests; shed when the worker's in-flight set is full.PerConsumerRateLimiter— per-credential token-bucket fairness, reusing the existingauth.token_bucket.TokenBucketManager, keyed by a SHA-256 hash of the caller'sAuthorizationheader / session cookie so one noisy client can't starve the fleet. Cluster-aware when a PG pool is attached.Why
Cluster mode adds capacity, but under saturation a node still queues every request until 504; past a critical backlog effectively all new requests fail. This is the complementary graceful-degradation layer.
Config / wiring
AdmissionControlConfig(both switches defaultFalse), read from the merged server config at app wiring — not a bootstrap key; a restart applies changes. Classified as runtime in the bootstrap-partition test.app_wiring.create_fastapi_app; not wired when disabled -> zero overhead by default.Validation
Validated on a 2-replica EKS deployment: at a small in-flight cap, excess concurrent load was shed as fast 429s instead of collapsing to 504s, and a single heavy consumer was throttled without affecting others.
Tests
Unit tests for both gates, credential keying (no raw secret in the key), exempt paths, and the 429 + Retry-After path (incl. an
inf-refill guard so a misconfigured 0-rate can't 500). Config-classification test updated.ruff/mypyclean; 443 config tests unregressed.🤖 Generated with Claude Code