mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-05 08:07:05 +00:00
fix(pass-through): pop stale route key directly to prevent unbounded registry growth
When pass-through endpoints stored in DB have no `id` field, each 30s reload generates a new UUID. The old cleanup (`remove_endpoint_routes`) scanned by `endpoint_id` (UUID only) and never matched the full route key, so stale entries were never deleted — the dict grew without bound, O(n²). Fix: replace `remove_endpoint_routes(endpoint_key)` with `_registered_pass_through_routes.pop(endpoint_key, None)` to remove the stale route key directly in O(1). Fixes #24833 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ead71c90ae
commit
11cecb3ada
2 changed files with 85 additions and 1 deletions
|
|
@ -2291,7 +2291,11 @@ async def initialize_pass_through_endpoints(
|
|||
# remove the ones that are not visited from the list
|
||||
for endpoint_key in registered_pass_through_endpoints:
|
||||
if endpoint_key not in visited_endpoints:
|
||||
InitPassThroughEndpointHelpers.remove_endpoint_routes(endpoint_key)
|
||||
_registered_pass_through_routes.pop(endpoint_key, None)
|
||||
verbose_proxy_logger.debug(
|
||||
"Removed stale pass-through route from registry: %s",
|
||||
endpoint_key,
|
||||
)
|
||||
|
||||
|
||||
def _get_pass_through_endpoints_from_config() -> List[PassThroughGenericEndpoint]:
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ sys.path.insert(
|
|||
|
||||
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
|
||||
HttpPassThroughEndpointHelpers,
|
||||
_registered_pass_through_routes,
|
||||
pass_through_request,
|
||||
)
|
||||
from litellm.proxy.pass_through_endpoints.success_handler import (
|
||||
|
|
@ -2474,3 +2475,82 @@ async def test_multipart_passthrough_preserves_boundary():
|
|||
# Verify the response
|
||||
assert response.status_code == 200
|
||||
async_client.request.assert_called_once()
|
||||
|
||||
|
||||
class TestRemoveStaleEndpointRoute:
|
||||
"""Regression tests for https://github.com/BerriAI/litellm/issues/24833:
|
||||
When pass-through endpoints stored in the DB have no ``id`` field, each
|
||||
periodic reload generates a new UUID, causing ``_registered_pass_through_routes``
|
||||
to grow without bound because the old cleanup path
|
||||
(``remove_endpoint_routes``) scanned by ``endpoint_id`` and never matched
|
||||
the stale entries. The fix uses dict.pop() to remove by route key in O(1).
|
||||
"""
|
||||
|
||||
def setup_method(self):
|
||||
_registered_pass_through_routes.clear()
|
||||
|
||||
def teardown_method(self):
|
||||
_registered_pass_through_routes.clear()
|
||||
|
||||
def test_pop_removes_stale_route_by_key(self):
|
||||
"""Stale route is removed in O(1) by its route key via dict.pop()."""
|
||||
route_key = "old-uuid:exact:/my-endpoint:GET,POST"
|
||||
_registered_pass_through_routes[route_key] = {
|
||||
"endpoint_id": "old-uuid",
|
||||
"path": "/my-endpoint",
|
||||
"type": "exact",
|
||||
}
|
||||
|
||||
_registered_pass_through_routes.pop(route_key, None)
|
||||
|
||||
assert route_key not in _registered_pass_through_routes
|
||||
|
||||
def test_pop_noop_for_unknown_key(self):
|
||||
"""pop() with default None does not raise for nonexistent keys."""
|
||||
_registered_pass_through_routes["keep-me:exact:/a:GET"] = {
|
||||
"endpoint_id": "keep-me",
|
||||
"path": "/a",
|
||||
"type": "exact",
|
||||
}
|
||||
|
||||
_registered_pass_through_routes.pop("nonexistent:exact:/b:GET", None)
|
||||
|
||||
assert len(_registered_pass_through_routes) == 1
|
||||
|
||||
def test_registry_does_not_grow_across_reload_cycles(self):
|
||||
"""Simulate multiple reload cycles with changing UUIDs.
|
||||
|
||||
Core regression test: without the fix the registry grows by N entries
|
||||
every cycle; with the fix it stays constant.
|
||||
"""
|
||||
path = "/vertex-passthrough"
|
||||
methods_str = "GET,POST"
|
||||
num_cycles = 50
|
||||
|
||||
for cycle in range(num_cycles):
|
||||
old_keys = list(_registered_pass_through_routes.keys())
|
||||
|
||||
new_uuid = f"uuid-{cycle}"
|
||||
new_exact_key = f"{new_uuid}:exact:{path}:{methods_str}"
|
||||
new_subpath_key = f"{new_uuid}:subpath:{path}:{methods_str}"
|
||||
|
||||
_registered_pass_through_routes[new_exact_key] = {
|
||||
"endpoint_id": new_uuid,
|
||||
"path": path,
|
||||
"type": "exact",
|
||||
}
|
||||
_registered_pass_through_routes[new_subpath_key] = {
|
||||
"endpoint_id": new_uuid,
|
||||
"path": path,
|
||||
"type": "subpath",
|
||||
}
|
||||
|
||||
visited = {new_exact_key, new_subpath_key}
|
||||
|
||||
# Clean up stale routes — the fix: pop by key in O(1)
|
||||
for key in old_keys:
|
||||
if key not in visited:
|
||||
_registered_pass_through_routes.pop(key, None)
|
||||
|
||||
# After 50 cycles, only the last cycle's 2 keys should remain
|
||||
assert len(_registered_pass_through_routes) == 2
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue