Closed
Description
Bug report
Bug description:
asyncio.TaskGroup attempts to avoid refcycles in raised exceptions by deleting self._errors
but when I reviewed the code it doesn't actually achieve this:
see
cpython/Lib/asyncio/taskgroups.py
Lines 152 to 156 in 5e9e506
There's a refcycle in me is me.__traceback__.tb_next.tb_frame.f_locals["me"]
I wrote a few tests to route out all the refcycles in tracebacks
import asyncio
import gc
import unittest
class TestTaskGroup(unittest.IsolatedAsyncioTestCase):
async def test_exception_refcycles_direct(self):
"""Test that TaskGroup doesn't keep a reference to the raised ExceptionGroup"""
tg = asyncio.TaskGroup()
exc = None
class _Done(Exception):
pass
try:
async with tg:
raise _Done
except ExceptionGroup as e:
exc = e
self.assertIsNotNone(exc)
self.assertListEqual(gc.get_referrers(exc), [])
async def test_exception_refcycles_errors(self):
"""Test that TaskGroup deletes self._errors, and __aexit__ args"""
tg = asyncio.TaskGroup()
exc = None
class _Done(Exception):
pass
try:
async with tg:
raise _Done
except* _Done as excs:
exc = excs.exceptions[0]
self.assertIsInstance(exc, _Done)
self.assertListEqual(gc.get_referrers(exc), [])
async def test_exception_refcycles_parent_task(self):
"""Test that TaskGroup deletes self._parent_task"""
tg = asyncio.TaskGroup()
exc = None
class _Done(Exception):
pass
async def coro_fn():
async with tg:
raise _Done
try:
async with asyncio.TaskGroup() as tg2:
tg2.create_task(coro_fn())
except* _Done as excs:
exc = excs.exceptions[0].exceptions[0]
self.assertIsInstance(exc, _Done)
self.assertListEqual(gc.get_referrers(exc), [])
async def test_exception_refcycles_propagate_cancellation_error(self):
"""Test that TaskGroup deletes propagate_cancellation_error"""
tg = asyncio.TaskGroup()
exc = None
try:
async with asyncio.timeout(-1):
async with tg:
await asyncio.sleep(0)
except TimeoutError as e:
exc = e.__cause__
self.assertIsInstance(exc, asyncio.CancelledError)
self.assertListEqual(gc.get_referrers(exc), [])
async def test_exception_refcycles_base_error(self):
"""Test that TaskGroup deletes self._base_error"""
class MyKeyboardInterrupt(KeyboardInterrupt):
pass
tg = asyncio.TaskGroup()
exc = None
try:
async with tg:
raise MyKeyboardInterrupt
except MyKeyboardInterrupt as e:
exc = e
self.assertIsNotNone(exc)
self.assertListEqual(gc.get_referrers(exc), [])
in writing all these tests I noticed refcycles in PyFuture:
cpython/Lib/asyncio/futures.py
Lines 197 to 198 in 58f7763
cpython/Lib/asyncio/futures.py
Lines 215 to 216 in 58f7763
class BaseFutureTests:
def test_future_cancelled_result_refcycles(self):
f = self._new_future(loop=self.loop)
f.cancel()
exc = None
try:
f.result()
except asyncio.CancelledError as e:
exc = e
self.assertIsNotNone(exc)
self.assertListEqual(gc.get_referrers(exc), [])
def test_future_cancelled_exception_refcycles(self):
f = self._new_future(loop=self.loop)
f.cancel()
exc = None
try:
f.exception()
except asyncio.CancelledError as e:
exc = e
self.assertIsNotNone(exc)
self.assertListEqual(gc.get_referrers(exc), [])
CPython versions tested on:
3.12, 3.13
Operating systems tested on:
Linux
Linked PRs
- gh-124958: fix asyncio.TaskGroup and _PyFuture refcycles #124959
- [3.13] gh-124958: fix asyncio.TaskGroup and _PyFuture refcycles (GH-124959) #125463
- [3.12] gh-124958: fix asyncio.TaskGroup and _PyFuture refcycles (#124959) #125466
- gh-124958: Revert "gh-125472: Revert "gh-124958: fix asyncio.TaskGroup and _PyFuture refcycles ... #125486
Metadata
Metadata
Assignees
Projects
Status
Done