Skip to content

Commit 9028e26

Browse files
committed
Use WeakValueDictionary to drop a key's Lock as soon as nothing is contending for it
1 parent ee0a709 commit 9028e26

3 files changed

Lines changed: 24 additions & 3 deletions

File tree

‎HISTORY.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Not released
44

55
- Added `_ExpandedRequestOptions.refresh` to satisfy Pyright type checking.
6+
- Revert `lru_cache` for request lock to avoid binding to multiple event loops (`RuntimeError: ...Lock is bound to a different event loop`)
67

78
## 0.14.3 (2026-01-07)
89

‎aiohttp_client_cache/session.py‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
import sys
66
import warnings
77
from asyncio import Lock
8-
from collections import defaultdict
98
from contextlib import asynccontextmanager
109
from logging import getLogger
1110
from typing import TYPE_CHECKING, cast
11+
from weakref import WeakValueDictionary
1212

1313
from aiohttp import ClientSession
1414
from aiohttp.typedefs import StrOrURL
@@ -61,7 +61,8 @@ def __init__(
6161
**kwargs,
6262
):
6363
self.cache = cache or CacheBackend()
64-
self._locks: dict[str, Lock] = defaultdict(Lock)
64+
# Drops a key's Lock as soon as nothing is contending for it
65+
self._locks: WeakValueDictionary[str, Lock] = WeakValueDictionary()
6566
self._null_lock = nullcontext()
6667

6768
# Pass along any valid kwargs for ClientSession (or custom session superclass)
@@ -89,7 +90,10 @@ async def _request(
8990
if actions.skip_read:
9091
lock: Lock | nullcontext = self._null_lock
9192
else:
92-
lock = self._locks[key]
93+
try:
94+
lock = self._locks[key]
95+
except KeyError:
96+
lock = self._locks[key] = Lock()
9397

9498
async with lock:
9599
response = await self.cache.request(actions)

‎test/unit/test_session.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,22 @@ class CustomSession(CacheMixin, ClientSession):
194194
assert mock_request.called is False
195195

196196

197+
@patch.object(ClientSession, '_request', return_value=FakeClientResponse)
198+
async def test_session__locks_do_not_leak(mock_request):
199+
"""Locks are only needed while requests are actively contending for a cache key, so distinct
200+
keys should not accumulate indefinitely.
201+
"""
202+
cache = MagicMock(spec=CacheBackend)
203+
cache.request.return_value = None
204+
cache.create_key.side_effect = lambda method, url, **kwargs: str(url)
205+
206+
async with CachedSession(cache=cache) as session:
207+
for i in range(1000):
208+
await session.get(f'http://test.url/{i}')
209+
210+
assert len(session._locks) == 0
211+
212+
197213
@patch.object(ClientSession, '_request', return_value=FakeCachedResponse)
198214
async def test_session__cache_include_headers(mock_request):
199215
async with CachedSession(cache=CacheBackend(include_headers=True)) as session:

0 commit comments

Comments
 (0)