Skip to content

Commit 64e52a3

Browse files
fix: callback inappropriately suppresses asyncio logs (#725)
1 parent dfa6915 commit 64e52a3

File tree

2 files changed

+43
-2
lines changed

2 files changed

+43
-2
lines changed

async_lru/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,10 @@ def _cache_miss(self, key: Hashable) -> None:
178178
self.__misses += 1
179179

180180
def _task_done_callback(self, key: Hashable, task: "asyncio.Task[_R]") -> None:
181-
if task.cancelled() or task.exception() is not None:
181+
# We must use the private attribute instead of `exception()`
182+
# so asyncio does not set `task.__log_traceback = False` on
183+
# the false assumption that the caller read the task Exception
184+
if task.cancelled() or task._exception is not None:
182185
self.__cache.pop(key, None)
183186
return
184187

tests/test_internals.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import asyncio
2+
import gc
3+
import logging
24
from functools import partial
35
from unittest import mock
46

57
import pytest
68

7-
from async_lru import _LRUCacheWrapper
9+
from async_lru import _CacheItem, _LRUCacheWrapper
810

911

1012
async def test_done_callback_cancelled() -> None:
@@ -41,6 +43,42 @@ async def test_done_callback_exception() -> None:
4143
assert task not in wrapped._LRUCacheWrapper__tasks # type: ignore[attr-defined]
4244

4345

46+
async def test_done_callback_exception_logs(caplog: pytest.LogCaptureFixture) -> None:
47+
caplog.set_level(logging.ERROR, logger="asyncio")
48+
49+
wrapped = _LRUCacheWrapper(mock.ANY, None, False, None)
50+
loop = asyncio.get_running_loop()
51+
52+
async def boom() -> None:
53+
await asyncio.sleep(0)
54+
raise RuntimeError("boom")
55+
56+
key = object()
57+
task = loop.create_task(boom())
58+
wrapped._LRUCacheWrapper__cache[key] = _CacheItem(task, None, 1) # type: ignore[attr-defined]
59+
task.add_done_callback(partial(wrapped._task_done_callback, key))
60+
61+
while not task.done():
62+
await asyncio.sleep(0)
63+
await asyncio.sleep(0)
64+
65+
assert key not in wrapped._LRUCacheWrapper__cache # type: ignore[attr-defined]
66+
# asyncio disables logging when exception() is called; keep logging enabled.
67+
assert task._log_traceback
68+
69+
caplog.clear()
70+
71+
del task # Remove reference so task get garbage collected.
72+
for _ in range(5): # pragma: no branch
73+
gc.collect()
74+
await asyncio.sleep(0)
75+
if "Task exception was never retrieved" in caplog.text: # pragma: no branch
76+
break
77+
78+
assert "Task exception was never retrieved" in caplog.text
79+
assert "RuntimeError: boom" in caplog.text
80+
81+
4482
async def test_cache_invalidate_typed() -> None:
4583
wrapped = _LRUCacheWrapper(mock.AsyncMock(return_value=1), None, True, None)
4684

0 commit comments

Comments
 (0)