litellm/tests/unit/router_utils/test_cooldown_cache.py
yujonglee b1b236826a
feat(redis): serve the proxy's common Redis call with Rust stores (#50170)
* feat(redis): route the router, rate limiters and auth through the native store families

* style(redis): satisfy the python lint gates

Format the rate limit store, sort its __all__ and give the routing read batch and two tests concrete list and dict types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(router): give the registry's default usage store a memory-only handle

RouterUsageStore now takes a handle, so the default passed None and any usage read before RustCore swapped in its store raised AttributeError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(redis): build stores from native handles and wait for router tasks

Tests still used the pre-refactor store constructors (Store(None), handle= factories, NativeIncrement, tuple scopes). The router outcome tests counted 20 loop ticks, too few now that the allowed-fails increment resolves on the native runtime, so settle() waits for the scheduled tasks instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 06:01:11 +00:00

44 lines
2 KiB
Python

import time
from typing import Final
import pytest
from litellm.redis.stores.connection import connect
from litellm.redis.stores.router_cooldowns import RouterCooldownStore
from litellm.router_utils.cooldown_cache import CooldownCache
@pytest.mark.parametrize("message", ("Short error", "Rate limit " + "secret" * 100))
@pytest.mark.parametrize("ttl", (None, 5.5, 30.0))
def test_cooldown_masks_exception_and_preserves_status_timestamp_and_ttl(message: str, ttl: float | None) -> None:
now: Final = time.time()
store: Final = RouterCooldownStore(connect(None).router_cooldowns())
cache: Final = CooldownCache(store, default_cooldown_time=60.0, clock=lambda: now)
cache.add_deployment_to_cooldown("dep", Exception(message), 429, ttl)
expected_ttl: Final = 60.0 if ttl is None else ttl
expected_message: Final = message if len(message) <= 50 else message[:50] + "*" * (len(message) - 50)
assert cache.get_active_cooldowns(["dep"], None) == [
(
"dep",
{
"exception_received": expected_message,
"status_code": "429",
"timestamp": now,
"cooldown_time": expected_ttl,
},
)
]
@pytest.mark.asyncio
async def test_sync_and_async_router_cooldown_surfaces_share_active_state() -> None:
now: Final = time.time()
store: Final = RouterCooldownStore(connect(None).router_cooldowns())
cache: Final = CooldownCache(store, default_cooldown_time=60.0, clock=lambda: now)
cache.add_deployment_to_cooldown("dep", Exception("unavailable"), 503, 30.0)
expected: Final = [
("dep", {"exception_received": "unavailable", "status_code": "503", "timestamp": now, "cooldown_time": 30.0})
]
assert cache.get_active_cooldowns(["absent", "dep"], None) == expected
assert await cache.async_get_active_cooldowns(["absent", "dep"], None) == expected
assert cache.get_min_cooldown(["dep"], None) == 30.0
assert cache.get_min_cooldown(["absent"], None) == 60.0