fix(pass-through): stop pass-through route registry growing every reload (PERF-13)#31314
Conversation
|
|
Greptile SummaryFixes the
Confidence Score: 5/5Safe to merge — the change is a single-line targeted fix to the stale-route cleanup loop, backed by three regression tests that would fail on the unfixed code. The production change is minimal and precise: one line replaces a call that silently did nothing with a direct dict pop. The root cause analysis in the PR description is accurate and traceable in the code. The three new tests exercise the exact failure scenarios (bounded growth, departed-endpoint removal, live-route survivability) using real function calls against properly mocked I/O, and would catch a regression. No existing tests are modified or weakened. The deliberate decision to leave openai_routes un-cleaned is sound — that list is append-deduped so it cannot grow, and removing the path for a same-cycle re-registration would break the live endpoint. No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/proxy/pass_through_endpoints/pass_through_endpoints.py | Replaces the broken remove_endpoint_routes(endpoint_key) call (which matched by endpoint_id, not by key) with a direct O(1) pop on the registry dict, stopping the unbounded growth of _registered_pass_through_routes across 30s reload cycles. |
| tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py | Adds TestStaleRouteCleanupOnReload with three well-structured async tests covering: (1) registry stays bounded across 25 reload cycles for id-less endpoints, (2) departed endpoints are removed on the next reload, and (3) the live route stays resolvable after repeated reloads. All network/IO is mocked via ExitStack patches. |
Reviews (1): Last reviewed commit: "fix(pass-through): remove stale routes b..." | Re-trigger Greptile
Greptile SummaryThis PR fixes a CPU-sink regression where
Confidence Score: 5/5Safe to merge. The change is minimal and surgical, touching only the stale-key eviction path in the 30-second reload loop and adding purely additive tests. The root cause analysis is accurate, the one-line fix is correct, and the three new tests directly exercise the broken path and would fail against the pre-fix code. The intentional omission of No files require special attention. Both changed files are straightforward and well-reasoned.
|
| Filename | Overview |
|---|---|
| litellm/proxy/pass_through_endpoints/pass_through_endpoints.py | Core fix: replaces the broken remove_endpoint_routes(endpoint_key) call with a direct O(1) _registered_pass_through_routes.pop(endpoint_key, None), correctly eliminating the registry-growth leak for id-less endpoints. Cleanup of LiteLLMRoutes.openai_routes for departed auth-required endpoints is intentionally omitted and was already broken before this PR. |
| tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py | Adds three focused regression tests for the reload-leak fix: bounded registry across 25 cycles, departed endpoint removal, and live-route survival — all using mocks with no real network calls, satisfying the no-real-network rule for this test directory. |
Reviews (2): Last reviewed commit: "fix(pass-through): remove stale routes b..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
29daf6d to
9bbddd3
Compare
…rowing every reload
The 30s add_deployment_job re-runs initialize_pass_through_endpoints, which
re-registers every config/DB pass-through endpoint. Endpoints without a
persisted id get a fresh uuid each cycle, so their route key
("{id}:{type}:{path}:{methods}") changes every reload. The stale-route cleanup
called remove_endpoint_routes(route_key), but that helper matches entries by
endpoint_id, so it never matched a route key and never deleted anything. The
registry grew by one entry per route per reload, turning the per-cycle cleanup
and the per-request is_registered_pass_through_route scan into a CPU sink that
eventually pins a core and slows every endpoint.
Pop the stale key from the registry directly in O(1). openai_routes is left
alone: its append is path-deduped and the path is still owned by the live
endpoint re-registered under a new id in the same cycle.
Resolves PERF-13
9bbddd3 to
3f58e73
Compare
Relevant issues
Resolves #19921
Linear ticket
Resolves PERF-13
Pre-Submission checklist
make test-unit@greptileaiand received a Confidence Score of 5/5Type
🐛 Bug Fix
Changes
PERF-13 asked whether the 1.81.x performance regression in #19921 still exists and whether the upstream fix in #26082 is relevant. It still exists on
litellm_internal_staging(1.91.0), and the pass-through registry leak that #26082 targets is the part that is still unfixed here, so this ports a minimal version of it.Root cause
The
add_deployment_jobscheduler re-runsinitialize_pass_through_endpointsevery 30s whenstore_model_in_dbis on (proxy_server.py:8069). Each cycle re-registers every config and DB pass-through endpoint. An endpoint without a persistedidgets a freshuuidevery reload (pass_through_endpoints.py:2673), so its registry key, formatted as"{id}:{type}:{path}:{methods}", changes each cycle and defeats theif route_key in _registered_pass_through_routesdedup.The stale-route cleanup then called
remove_endpoint_routes(endpoint_key)with that route key, butremove_endpoint_routesmatches entries byendpoint_id(pass_through_endpoints.py:2522), so a route key never matched and nothing was ever deleted._registered_pass_through_routesgrew by one entry per route every 30s forever. That unbounded dict is what turns the per-cycle cleanup (O(n) keys, each calling an O(n) scan) and the per-requestis_registered_pass_through_routescan in the auth path (route_checks.py:153) into a CPU sink, which matches the reported symptom of one core pinned at 100% and every management API and login getting slower over time.This is the same root cause as #26082. The other reports in the thread (the
router_settingsper-request Router and Prometheus registry scan, and the slow spend-logs query) were separate issues already addressed by #20087, #20133, and #20720; this one was not.Fix
Pop the stale key from the registry directly in O(1). I deliberately did not also strip the path from
LiteLLMRoutes.openai_routesthe way #26082 does. That list is append-deduped by path (pass_through_endpoints.py:2698, 2737) so it does not grow unboundedly, and on a reload the path is still owned by the live endpoint that was just re-registered under a new id earlier in the same cycle, so removing it would drop a path the live endpoint still needs.Tests
Added
TestStaleRouteCleanupOnReloadto the mapped test file. The core test drives the realinitialize_pass_through_endpointsacross 25 reload cycles with an id-less endpoint and asserts the registry stays at 2 entries; with the oldremove_endpoint_routescall it holds 50, so it fails on unfixed code (confirmed by stashing the production change). A second test asserts a departed endpoint is dropped on the next reload, and a third asserts the live route stays resolvable after repeated reloads.Screenshots / Proof of Fix
The registry size has no HTTP surface, so the leak metric comes from driving the exact reload function the 30s scheduler calls (
initialize_pass_through_endpoints) with one id-less endpoint and printinglen(_registered_pass_through_routes).Before (production fix reverted):
After (with the fix):
Live proxy on the fixed code, with two id-less pass-through endpoints in config (
/github-zenand/anthropic-passthrough,include_subpath: true) andstore_model_in_db: trueso the 30s reload loop runs against real Postgres.The reload loop fires every 30s and re-registers the endpoints (the leaky code path):
The id-less pass-through forwards real upstream traffic at startup and stays working across reload cycles:
The id-less pass-through forwards a real Anthropic request (real provider, real cost):