From f681a978f06baa13da0f0c24f7b1ac3a20d9a02a Mon Sep 17 00:00:00 2001 From: yassin Date: Sat, 12 Sep 2026 01:37:36 +0000 Subject: [PATCH] fix(redis): use a monotonic clock for the timeout log throttle and trim test docstrings Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/caching/redis_cache.py | 2 +- tests/test_litellm/caching/test_dual_cache.py | 7 +------ tests/test_litellm/caching/test_redis_cache.py | 9 +-------- .../proxy/proxy_server/test_spend_counters.py | 6 +----- 4 files changed, 4 insertions(+), 20 deletions(-) diff --git a/litellm/caching/redis_cache.py b/litellm/caching/redis_cache.py index 7a3e689a667..e5e27d1e02b 100644 --- a/litellm/caching/redis_cache.py +++ b/litellm/caching/redis_cache.py @@ -409,7 +409,7 @@ class RedisCircuitBreakerOpenError(Exception): class _RedisTimeoutLogThrottle: """Admits one Redis timeout log line per interval and counts the timeouts it suppressed in between.""" - def __init__(self, interval: float, clock: Callable[[], float] = time.time) -> None: + def __init__(self, interval: float, clock: Callable[[], float] = time.monotonic) -> None: self.interval = interval self._clock = clock self._lock = threading.Lock() diff --git a/tests/test_litellm/caching/test_dual_cache.py b/tests/test_litellm/caching/test_dual_cache.py index 850fa14106b..6f29be00b30 100644 --- a/tests/test_litellm/caching/test_dual_cache.py +++ b/tests/test_litellm/caching/test_dual_cache.py @@ -708,12 +708,7 @@ async def test_open_breaker_keeps_async_batch_read_memory_hits_and_releases_rese @pytest.mark.asyncio async def test_redis_timeouts_falling_back_to_memory_log_once_per_interval(caplog, monkeypatch): - """The in-memory fallback WARNING must not repeat for every timed-out increment during a blip. - - The rate limiter's pipeline increments and the dual cache increments each logged a WARNING per - call while Redis timed out, hundreds of lines per second before the breaker opened. The first - timeout of a streak keeps its WARNING, the rest are DEBUG until the summary interval passes. - """ + """The first fallback WARNING of a timeout streak logs, the rest stay at DEBUG until the summary.""" from redis.exceptions import TimeoutError as RedisTimeoutError from litellm.caching import redis_cache as redis_cache_module diff --git a/tests/test_litellm/caching/test_redis_cache.py b/tests/test_litellm/caching/test_redis_cache.py index 5840f450ac6..4ca33894aed 100644 --- a/tests/test_litellm/caching/test_redis_cache.py +++ b/tests/test_litellm/caching/test_redis_cache.py @@ -1205,14 +1205,7 @@ async def test_a_probe_overtaken_by_a_later_outage_leaves_the_breaker_to_the_new def test_timeouts_during_a_blip_log_once_per_interval_not_once_per_call(sync_batch_redis_cache, caplog, monkeypatch): - """A Redis latency blip must not write one ERROR line per timed-out cache call. - - Before the breaker opens (up to REDIS_CIRCUIT_BREAKER_TIMEOUT_MIN_DURATION of timeouts) every - cache operation logged its own ERROR or WARNING line, so one single-worker proxy wrote - ~1100 lines in 5 s at LITELLM_LOG=WARNING. A timeout streak now logs its first failure, then - one summary line per REDIS_TIMEOUT_LOG_INTERVAL carrying the count of suppressed timeouts, - while every timeout stays visible at DEBUG. Hard connectivity failures keep their per-call line. - """ + """A timeout streak logs its first failure plus one summary per interval; other failures log per call.""" import logging from redis.exceptions import TimeoutError as RedisTimeoutError diff --git a/tests/test_litellm/proxy/proxy_server/test_spend_counters.py b/tests/test_litellm/proxy/proxy_server/test_spend_counters.py index 19b5a11af33..4a1fc389d3e 100644 --- a/tests/test_litellm/proxy/proxy_server/test_spend_counters.py +++ b/tests/test_litellm/proxy/proxy_server/test_spend_counters.py @@ -1182,11 +1182,7 @@ async def test_apply_spend_counter_increments_open_breaker_invalidates_and_retur @pytest.mark.asyncio async def test_apply_spend_counter_increments_redis_timeout_invalidates_and_returns(monkeypatch): - """A Redis timeout is the streak the breaker is already counting and the cache layer already logged. - - Re-raising it sent every request in the pre-open window through the cost callback's error - path, which logged a traceback and fired the failed-tracking alert once per request. - """ + """A Redis timeout invalidates the counters and returns without reaching the cost callback's error path.""" from redis.exceptions import TimeoutError as RedisTimeoutError fake_cache = _make_spend_counter_cache()