This commit is contained in:
Donghyeon Jeon 2026-08-27 19:23:14 -05:00 committed by GitHub
commit dc245e66f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 120 additions and 16 deletions

View file

@ -111,21 +111,35 @@ class DualCache(BaseCache):
without it, backfilled entries fall to ``InMemoryCache``'s own default
TTL and can outlive the TTL this cache was configured with.
"""
if "ttl" not in kwargs and self.default_in_memory_ttl is not None:
return {**kwargs, "ttl": self.default_in_memory_ttl}
return kwargs
return self._write_kwargs(kwargs, self.default_in_memory_ttl)
@property
def _redis_write_ttl(self) -> float | None:
"""
Default TTL for a Redis write.
``default_redis_ttl`` when set, else ``default_in_memory_ttl`` so callers that
configure only the in-memory tier keep the Redis TTL they have always had.
"""
if self.default_redis_ttl is not None:
return self.default_redis_ttl
return self.default_in_memory_ttl
@staticmethod
def _write_kwargs(kwargs: "dict[str, object]", default_ttl: float | None) -> "dict[str, object]":
"""Kwargs for one tier's write: apply ``default_ttl`` unless the caller passed an explicit ``ttl``."""
if "ttl" in kwargs or default_ttl is None:
return kwargs
return {**kwargs, "ttl": default_ttl}
def set_cache(self, key, value, local_only: bool = False, **kwargs):
# Update both Redis and in-memory cache
try:
if self.in_memory_cache is not None:
if "ttl" not in kwargs and self.default_in_memory_ttl is not None:
kwargs["ttl"] = self.default_in_memory_ttl
self.in_memory_cache.set_cache(key, value, **kwargs)
self.in_memory_cache.set_cache(key, value, **self._write_kwargs(kwargs, self.default_in_memory_ttl))
if self.redis_cache is not None and local_only is False:
self.redis_cache.set_cache(key, value, **kwargs)
self.redis_cache.set_cache(key, value, **self._write_kwargs(kwargs, self._redis_write_ttl))
except Exception as e:
print_verbose(e)
@ -340,12 +354,12 @@ class DualCache(BaseCache):
print_verbose(f"async set cache: cache key: {key}; local_only: {local_only}; value: {value}")
try:
if self.in_memory_cache is not None:
if "ttl" not in kwargs and self.default_in_memory_ttl is not None:
kwargs["ttl"] = self.default_in_memory_ttl
await self.in_memory_cache.async_set_cache(key, value, **kwargs)
await self.in_memory_cache.async_set_cache(
key, value, **self._write_kwargs(kwargs, self.default_in_memory_ttl)
)
if self.redis_cache is not None and local_only is False:
await self.redis_cache.async_set_cache(key, value, **kwargs)
await self.redis_cache.async_set_cache(key, value, **self._write_kwargs(kwargs, self._redis_write_ttl))
except Exception as e:
verbose_logger.exception("LiteLLM Cache: Excepton async add_cache: %s", e)
@ -357,13 +371,13 @@ class DualCache(BaseCache):
print_verbose(f"async batch set cache: cache keys: {cache_list}; local_only: {local_only}")
try:
if self.in_memory_cache is not None:
if "ttl" not in kwargs and self.default_in_memory_ttl is not None:
kwargs["ttl"] = self.default_in_memory_ttl
await self.in_memory_cache.async_set_cache_pipeline(cache_list=cache_list, **kwargs)
await self.in_memory_cache.async_set_cache_pipeline(
cache_list=cache_list, **self._write_kwargs(kwargs, self.default_in_memory_ttl)
)
if self.redis_cache is not None and local_only is False:
await self.redis_cache.async_set_cache_pipeline(
cache_list=cache_list, ttl=kwargs.pop("ttl", None), **kwargs
cache_list=cache_list, **self._write_kwargs(kwargs, self._redis_write_ttl)
)
except Exception as e:
verbose_logger.exception("LiteLLM Cache: Excepton async add_cache: %s", e)

View file

@ -445,3 +445,93 @@ async def test_dual_cache_late_attach_redis_wires_writes_and_ttl_async():
assert mock_redis.async_set_cache.call_args[0][:2] == (key_after, val_after)
assert in_memory.get_cache(key_after) == val_after
def _redis_ttl(mock_call):
return mock_call.kwargs.get("ttl")
def test_default_redis_ttl_applies_to_sync_redis_write():
mock_redis = MagicMock(spec=RedisCache)
dual_cache = DualCache(
in_memory_cache=InMemoryCache(), redis_cache=mock_redis, default_redis_ttl=600.0
)
dual_cache.set_cache("key", "value")
assert _redis_ttl(mock_redis.set_cache.call_args) == 600.0
@pytest.mark.asyncio
async def test_default_redis_ttl_applies_to_async_redis_writes():
mock_redis = MagicMock(spec=RedisCache)
mock_redis.async_set_cache = AsyncMock()
mock_redis.async_set_cache_pipeline = AsyncMock()
dual_cache = DualCache(
in_memory_cache=InMemoryCache(), redis_cache=mock_redis, default_redis_ttl=600.0
)
await dual_cache.async_set_cache("key", "value")
await dual_cache.async_set_cache_pipeline([("key", "value")])
assert _redis_ttl(mock_redis.async_set_cache.call_args) == 600.0
assert _redis_ttl(mock_redis.async_set_cache_pipeline.call_args) == 600.0
def test_update_cache_ttl_redis_only_applies_to_redis_write():
mock_redis = MagicMock(spec=RedisCache)
dual_cache = DualCache(in_memory_cache=InMemoryCache(), redis_cache=mock_redis)
dual_cache.update_cache_ttl(default_in_memory_ttl=None, default_redis_ttl=600.0)
dual_cache.set_cache("key", "value")
assert _redis_ttl(mock_redis.set_cache.call_args) == 600.0
def test_attach_redis_cache_default_redis_ttl_applies_to_redis_write():
mock_redis = MagicMock(spec=RedisCache)
dual_cache = DualCache(in_memory_cache=InMemoryCache())
dual_cache.attach_redis_cache(mock_redis, default_redis_ttl=600.0)
dual_cache.set_cache("key", "value")
assert _redis_ttl(mock_redis.set_cache.call_args) == 600.0
def test_explicit_ttl_overrides_default_redis_ttl():
mock_redis = MagicMock(spec=RedisCache)
dual_cache = DualCache(
in_memory_cache=InMemoryCache(),
redis_cache=mock_redis,
default_in_memory_ttl=300.0,
default_redis_ttl=600.0,
)
dual_cache.set_cache("key", "value", ttl=42)
assert _redis_ttl(mock_redis.set_cache.call_args) == 42
def test_default_in_memory_ttl_still_propagates_to_redis_when_no_redis_ttl():
mock_redis = MagicMock(spec=RedisCache)
dual_cache = DualCache(
in_memory_cache=InMemoryCache(), redis_cache=mock_redis, default_in_memory_ttl=5.0
)
dual_cache.set_cache("key", "value")
assert _redis_ttl(mock_redis.set_cache.call_args) == 5.0
def test_default_redis_ttl_does_not_leak_into_in_memory_write():
mock_in_memory = MagicMock(spec=InMemoryCache)
dual_cache = DualCache(
in_memory_cache=mock_in_memory,
redis_cache=MagicMock(spec=RedisCache),
default_in_memory_ttl=300.0,
default_redis_ttl=600.0,
)
dual_cache.set_cache("key", "value")
assert _redis_ttl(mock_in_memory.set_cache.call_args) == 300.0