Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Not released

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

## 0.14.3 (2026-01-07)

Expand Down
14 changes: 7 additions & 7 deletions aiohttp_client_cache/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
import warnings
from asyncio import Lock
from contextlib import asynccontextmanager
from functools import lru_cache
from logging import getLogger
from typing import TYPE_CHECKING, cast
from weakref import WeakValueDictionary

from aiohttp import ClientSession
from aiohttp.typedefs import StrOrURL
Expand Down Expand Up @@ -49,11 +49,6 @@ async def __aexit__(self, *excinfo):
from typing_extensions import Self


@lru_cache(maxsize=16384)
def _get_lock(_: int, __: str) -> Lock:
return Lock()


class CacheMixin(MIXIN_BASE):
"""A mixin class for :py:class:`aiohttp.ClientSession` that adds caching support"""

Expand All @@ -66,6 +61,8 @@ def __init__(
**kwargs,
):
self.cache = cache or CacheBackend()
# Drops a key's Lock as soon as nothing is contending for it
self._locks: WeakValueDictionary[str, Lock] = WeakValueDictionary()
self._null_lock = nullcontext()

# Pass along any valid kwargs for ClientSession (or custom session superclass)
Expand Down Expand Up @@ -93,7 +90,10 @@ async def _request(
if actions.skip_read:
lock: Lock | nullcontext = self._null_lock
else:
lock = _get_lock(id(self), key)
try:
lock = self._locks[key]
except KeyError:
lock = self._locks[key] = Lock()

async with lock:
response = await self.cache.request(actions)
Expand Down
16 changes: 16 additions & 0 deletions test/unit/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,22 @@ class CustomSession(CacheMixin, ClientSession):
assert mock_request.called is False


@patch.object(ClientSession, '_request', return_value=FakeClientResponse)
async def test_session__locks_do_not_leak(mock_request):
"""Locks are only needed while requests are actively contending for a cache key, so distinct
keys should not accumulate indefinitely.
"""
cache = MagicMock(spec=CacheBackend)
cache.request.return_value = None
cache.create_key.side_effect = lambda method, url, **kwargs: str(url)

async with CachedSession(cache=cache) as session:
for i in range(1000):
await session.get(f'http://test.url/{i}')

assert len(session._locks) == 0


@patch.object(ClientSession, '_request', return_value=FakeCachedResponse)
async def test_session__cache_include_headers(mock_request):
async with CachedSession(cache=CacheBackend(include_headers=True)) as session:
Expand Down
Loading