Skip to content

refcycles in exceptions raised from asyncio.TaskGroup#124958

@graingert

Description

@graingert

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

try:
me=BaseExceptionGroup('unhandled errors in a TaskGroup', self._errors)
raisemefromNone
finally:
self._errors=None

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

importasyncioimportgcimportunittestclassTestTaskGroup(unittest.IsolatedAsyncioTestCase): asyncdeftest_exception_refcycles_direct(self): """Test that TaskGroup doesn't keep a reference to the raised ExceptionGroup"""tg=asyncio.TaskGroup() exc=Noneclass_Done(Exception): passtry: asyncwithtg: raise_DoneexceptExceptionGroupase: exc=eself.assertIsNotNone(exc) self.assertListEqual(gc.get_referrers(exc), []) asyncdeftest_exception_refcycles_errors(self): """Test that TaskGroup deletes self._errors, and __aexit__ args"""tg=asyncio.TaskGroup() exc=Noneclass_Done(Exception): passtry: asyncwithtg: raise_Done except* _Doneasexcs: exc=excs.exceptions[0] self.assertIsInstance(exc, _Done) self.assertListEqual(gc.get_referrers(exc), []) asyncdeftest_exception_refcycles_parent_task(self): """Test that TaskGroup deletes self._parent_task"""tg=asyncio.TaskGroup() exc=Noneclass_Done(Exception): passasyncdefcoro_fn(): asyncwithtg: raise_Donetry: asyncwithasyncio.TaskGroup() astg2: tg2.create_task(coro_fn()) except* _Doneasexcs: exc=excs.exceptions[0].exceptions[0] self.assertIsInstance(exc, _Done) self.assertListEqual(gc.get_referrers(exc), []) asyncdeftest_exception_refcycles_propagate_cancellation_error(self): """Test that TaskGroup deletes propagate_cancellation_error"""tg=asyncio.TaskGroup() exc=Nonetry: asyncwithasyncio.timeout(-1): asyncwithtg: awaitasyncio.sleep(0) exceptTimeoutErrorase: exc=e.__cause__self.assertIsInstance(exc, asyncio.CancelledError) self.assertListEqual(gc.get_referrers(exc), []) asyncdeftest_exception_refcycles_base_error(self): """Test that TaskGroup deletes self._base_error"""classMyKeyboardInterrupt(KeyboardInterrupt): passtg=asyncio.TaskGroup() exc=Nonetry: asyncwithtg: raiseMyKeyboardInterruptexceptMyKeyboardInterruptase: exc=eself.assertIsNotNone(exc) self.assertListEqual(gc.get_referrers(exc), [])

in writing all these tests I noticed refcycles in PyFuture:

exc=self._make_cancelled_error()
raiseexc

exc=self._make_cancelled_error()
raiseexc

classBaseFutureTests: deftest_future_cancelled_result_refcycles(self): f=self._new_future(loop=self.loop) f.cancel() exc=Nonetry: f.result() exceptasyncio.CancelledErrorase: exc=eself.assertIsNotNone(exc) self.assertListEqual(gc.get_referrers(exc), []) deftest_future_cancelled_exception_refcycles(self): f=self._new_future(loop=self.loop) f.cancel() exc=Nonetry: f.exception() exceptasyncio.CancelledErrorase: exc=eself.assertIsNotNone(exc) self.assertListEqual(gc.get_referrers(exc), [])

CPython versions tested on:

3.12, 3.13

Operating systems tested on:

Linux

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directorytopic-asynciotype-bugAn unexpected behavior, bug, or error

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions