From 62473fc4ffc54e53b3f85b757f1685ac29713d3c Mon Sep 17 00:00:00 2001 From: Stanislav Kostenko Date: Wed, 29 Apr 2026 19:40:30 +0300 Subject: [PATCH] fix(redis_cache): apply namespace in delete_cache and async_delete_cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a Redis namespace is configured, SET and GET correctly prefix the key via check_and_fix_namespace, but DELETE did not — causing the PodLockManager to always target a non-existent key and never explicitly release locks (they would expire by TTL instead, with a warning on every cycle). Adds check_and_fix_namespace to both async_delete_cache and delete_cache, and covers both paths with a parametrized unit test (namespace=None / "myns"). --- litellm/caching/redis_cache.py | 5 ++-- .../test_litellm/caching/test_redis_cache.py | 27 ++++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/litellm/caching/redis_cache.py b/litellm/caching/redis_cache.py index 84a2887f527..b89dc7505ce 100644 --- a/litellm/caching/redis_cache.py +++ b/litellm/caching/redis_cache.py @@ -1259,10 +1259,11 @@ class RedisCache(BaseCache): async def async_delete_cache(self, key: str): # typed as Any, redis python lib has incomplete type stubs for RedisCluster and does not include `delete` _redis_client: Any = self.init_async_client() - # keys is str + key = self.check_and_fix_namespace(key=key) return await _redis_client.delete(key) - def delete_cache(self, key): + def delete_cache(self, key: str): + key = self.check_and_fix_namespace(key=key) self.redis_client.delete(key) async def _pipeline_increment_helper( diff --git a/tests/test_litellm/caching/test_redis_cache.py b/tests/test_litellm/caching/test_redis_cache.py index b39eb42821c..0166c734752 100644 --- a/tests/test_litellm/caching/test_redis_cache.py +++ b/tests/test_litellm/caching/test_redis_cache.py @@ -3,7 +3,6 @@ import sys from unittest.mock import MagicMock, patch import pytest -from fastapi.testclient import TestClient sys.path.insert( 0, os.path.abspath("../../..") @@ -473,3 +472,29 @@ async def test_async_lpop_with_float_redis_version( # Verify the method completed without error assert result is not None + + +@pytest.mark.parametrize("namespace", [None, "myns"]) +@pytest.mark.asyncio +async def test_delete_cache_applies_namespace(namespace, monkeypatch, redis_no_ping): + """async_delete_cache and delete_cache must apply the namespace prefix so that + DEL targets the same key that SET/GET use.""" + monkeypatch.setenv("REDIS_HOST", "https://my-test-host") + redis_cache = RedisCache(namespace=namespace) + + raw_key = "cronjob_lock:db_spend_update_job" + expected_key = f"{namespace}:{raw_key}" if namespace else raw_key + + # --- async path --- + mock_async_client = AsyncMock() + with patch.object( + redis_cache, "init_async_client", return_value=mock_async_client + ): + await redis_cache.async_delete_cache(key=raw_key) + mock_async_client.delete.assert_called_once_with(expected_key) + + # --- sync path --- + mock_sync_client = MagicMock() + redis_cache.redis_client = mock_sync_client + redis_cache.delete_cache(key=raw_key) + mock_sync_client.delete.assert_called_once_with(expected_key)