Skip to content

Commit 6aafceb

Browse files
authored
Remove keys from cache on exception (#595)
1 parent 4f7e63a commit 6aafceb

File tree

3 files changed

+44
-21
lines changed

3 files changed

+44
-21
lines changed

async_lru/__init__.py

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -168,22 +168,24 @@ def _task_done_callback(
168168
) -> None:
169169
self.__tasks.discard(task)
170170

171-
cache_item = self.__cache.get(key)
172-
if self.__ttl is not None and cache_item is not None:
173-
loop = asyncio.get_running_loop()
174-
cache_item.later_call = loop.call_later(
175-
self.__ttl, self.__cache.pop, key, None
176-
)
177-
178171
if task.cancelled():
179172
fut.cancel()
173+
self.__cache.pop(key, None)
180174
return
181175

182176
exc = task.exception()
183177
if exc is not None:
184178
fut.set_exception(exc)
179+
self.__cache.pop(key, None)
185180
return
186181

182+
cache_item = self.__cache.get(key)
183+
if self.__ttl is not None and cache_item is not None:
184+
loop = asyncio.get_running_loop()
185+
cache_item.later_call = loop.call_later(
186+
self.__ttl, self.__cache.pop, key, None
187+
)
188+
187189
fut.set_result(task.result())
188190

189191
async def __call__(self, /, *fn_args: Any, **fn_kwargs: Any) -> _R:
@@ -197,19 +199,11 @@ async def __call__(self, /, *fn_args: Any, **fn_kwargs: Any) -> _R:
197199
cache_item = self.__cache.get(key)
198200

199201
if cache_item is not None:
202+
self._cache_hit(key)
200203
if not cache_item.fut.done():
201-
self._cache_hit(key)
202204
return await asyncio.shield(cache_item.fut)
203205

204-
exc = cache_item.fut._exception
205-
206-
if exc is None:
207-
self._cache_hit(key)
208-
return cache_item.fut.result()
209-
else:
210-
# exception here
211-
cache_item = self.__cache.pop(key)
212-
cache_item.cancel()
206+
return cache_item.fut.result()
213207

214208
fut = loop.create_future()
215209
coro = self.__wrapped__(*fn_args, **fn_kwargs)

tests/test_close.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@ async def coro(val: int) -> int:
3131

3232
await close
3333

34-
check_lru(coro, hits=0, misses=5, cache=5, tasks=0)
34+
check_lru(coro, hits=0, misses=5, cache=0, tasks=0)
3535
assert coro.cache_parameters()["closed"]
3636

3737
with pytest.raises(asyncio.CancelledError):
3838
await gather
3939

40-
check_lru(coro, hits=0, misses=5, cache=5, tasks=0)
40+
check_lru(coro, hits=0, misses=5, cache=0, tasks=0)
4141
assert coro.cache_parameters()["closed"]
4242

4343
# double call is no-op

tests/test_exception.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import asyncio
2+
import gc
3+
import sys
24
from typing import Callable
35

46
import pytest
@@ -16,12 +18,39 @@ async def coro(val: int) -> None:
1618

1719
ret = await asyncio.gather(*coros, return_exceptions=True)
1820

19-
check_lru(coro, hits=2, misses=1, cache=1, tasks=0)
21+
check_lru(coro, hits=2, misses=1, cache=0, tasks=0)
2022

2123
for item in ret:
2224
assert isinstance(item, ZeroDivisionError)
2325

2426
with pytest.raises(ZeroDivisionError):
2527
await coro(1)
2628

27-
check_lru(coro, hits=2, misses=2, cache=1, tasks=0)
29+
check_lru(coro, hits=2, misses=2, cache=0, tasks=0)
30+
31+
32+
@pytest.mark.xfail(
33+
reason="Memory leak is not fixed for PyPy3.9",
34+
condition=sys.implementation.name == "pypy",
35+
)
36+
async def test_alru_exception_reference_cleanup(check_lru: Callable[..., None]) -> None:
37+
class CustomClass:
38+
...
39+
40+
@alru_cache()
41+
async def coro(val: int) -> None:
42+
_ = CustomClass() # object we are verifying not to leak
43+
1 / 0
44+
45+
coros = [coro(v) for v in range(1000)]
46+
47+
await asyncio.gather(*coros, return_exceptions=True)
48+
49+
check_lru(coro, hits=0, misses=1000, cache=0, tasks=0)
50+
51+
await asyncio.sleep(0.00001)
52+
gc.collect()
53+
54+
assert (
55+
len([obj for obj in gc.get_objects() if isinstance(obj, CustomClass)]) == 0
56+
), "Only objects in the cache should be left in memory."

0 commit comments

Comments
 (0)