Skip to content

Commit b944df4

Browse files
committed
Tighten async cache pool synchronization
1 parent 6d81ecf commit b944df4

4 files changed

Lines changed: 68 additions & 4 deletions

File tree

redis/asyncio/connection.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2979,7 +2979,10 @@ def __init__(
29792979
raise ValueError("Cache must implement CacheInterface")
29802980
self.cache = cache
29812981
elif self._cache_factory is not None:
2982-
self.cache = CacheProxy(self._cache_factory.get_cache())
2982+
cache = self._cache_factory.get_cache()
2983+
self.cache = (
2984+
cache if isinstance(cache, CacheProxy) else CacheProxy(cache)
2985+
)
29832986
else:
29842987
self.cache = CacheFactory(
29852988
connection_kwargs.get("cache_config")
@@ -3442,7 +3445,10 @@ def set_in_maintenance(self, in_maintenance: bool) -> None:
34423445
@contextlib.asynccontextmanager
34433446
async def _maybe_pool_lock(self) -> AsyncIterator[None]:
34443447
"""Serialize pool mutations with cache-owner availability checks."""
3445-
async with self._lock:
3448+
if self.cache is not None or self._in_maintenance:
3449+
async with self._lock:
3450+
yield
3451+
else:
34463452
yield
34473453

34483454
@deprecated_args(

redis/connection.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3021,7 +3021,10 @@ def __init__(
30213021
self.cache = cache
30223022
else:
30233023
if self._cache_factory is not None:
3024-
self.cache = CacheProxy(self._cache_factory.get_cache())
3024+
cache = self._cache_factory.get_cache()
3025+
self.cache = (
3026+
cache if isinstance(cache, CacheProxy) else CacheProxy(cache)
3027+
)
30253028
else:
30263029
self.cache = CacheFactory(
30273030
self._connection_kwargs.get("cache_config")

tests/test_asyncio/test_connection.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
CacheEntryStatus,
3232
CacheFactory,
3333
CacheKey,
34+
CacheProxy,
3435
)
3536
from redis.observability.attributes import CSCReason
3637
from redis.exceptions import ConnectionError, InvalidResponse, RedisError, TimeoutError
@@ -256,7 +257,9 @@ async def test_async_cache_proxy_signals_evicted_in_progress_fill():
256257

257258

258259
async def test_blocking_pool_serializes_cache_owner_checks():
259-
pool = BlockingConnectionPool(max_connections=1)
260+
pool = BlockingConnectionPool(
261+
max_connections=1, protocol=3, cache_config=CacheConfig()
262+
)
260263
entered = asyncio.Event()
261264

262265
async def acquire_pool_lock():
@@ -272,6 +275,41 @@ async def acquire_pool_lock():
272275
await pool.aclose()
273276

274277

278+
async def test_blocking_pool_skips_lock_without_cache():
279+
pool = BlockingConnectionPool(max_connections=1)
280+
entered = asyncio.Event()
281+
282+
async def acquire_pool_lock():
283+
async with pool._maybe_pool_lock():
284+
entered.set()
285+
286+
async with pool._lock:
287+
task = asyncio.create_task(acquire_pool_lock())
288+
await asyncio.sleep(0)
289+
assert entered.is_set() is True
290+
291+
await task
292+
await pool.aclose()
293+
294+
295+
async def test_async_connection_pool_does_not_double_wrap_custom_cache_factory():
296+
cache = CacheFactory(CacheConfig()).get_cache()
297+
cache_factory = mock.Mock()
298+
cache_factory.get_cache.return_value = cache
299+
pool = ConnectionPool(
300+
protocol=3,
301+
cache_config=CacheConfig(),
302+
cache_factory=cache_factory,
303+
)
304+
305+
try:
306+
assert pool.cache is cache
307+
assert isinstance(pool.cache, CacheProxy)
308+
assert not isinstance(pool.cache._cache, CacheProxy)
309+
finally:
310+
await pool.aclose()
311+
312+
275313
async def test_async_cache_proxy_waits_for_replacement_in_progress_fill():
276314
connection = mock.Mock()
277315
connection.can_read = mock.AsyncMock(return_value=False)

tests/test_connection.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
CacheConfig,
2525
CacheEntry,
2626
CacheEntryStatus,
27+
CacheFactory,
2728
CacheInterface,
2829
CacheKey,
2930
CacheProxy,
@@ -945,6 +946,22 @@ def test_creates_cache_with_custom_cache_factory(
945946
assert connection_pool.cache._cache == mock_cache
946947
connection_pool.disconnect()
947948

949+
def test_does_not_double_wrap_custom_cache_factory(self):
950+
cache = CacheFactory(CacheConfig()).get_cache()
951+
cache_factory = mock.Mock()
952+
cache_factory.get_cache.return_value = cache
953+
954+
connection_pool = ConnectionPool(
955+
protocol=3,
956+
cache_config=CacheConfig(),
957+
cache_factory=cache_factory,
958+
)
959+
960+
assert connection_pool.cache is cache
961+
assert isinstance(connection_pool.cache, CacheProxy)
962+
assert not isinstance(connection_pool.cache._cache, CacheProxy)
963+
connection_pool.disconnect()
964+
948965
def test_creates_cache_with_given_configuration(self, mock_cache):
949966
connection_pool = ConnectionPool(
950967
protocol=3, cache_config=CacheConfig(max_size=100)

0 commit comments

Comments
 (0)