Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refcycles in exceptions raised from asyncio.TaskGroup #124958

Closed
graingert opened this issue Oct 4, 2024 · 1 comment
Closed

refcycles in exceptions raised from asyncio.TaskGroup #124958

graingert opened this issue Oct 4, 2024 · 1 comment
Labels
stdlib Python modules in the Lib dir topic-asyncio type-bug An unexpected behavior, bug, or error

Comments

@graingert
Copy link
Contributor

graingert commented Oct 4, 2024

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)
raise me from None
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

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:

exc = self._make_cancelled_error()
raise exc

exc = self._make_cancelled_error()
raise exc

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

@graingert graingert added the type-bug An unexpected behavior, bug, or error label Oct 4, 2024
graingert added a commit to graingert/cpython that referenced this issue Oct 4, 2024
miss-islington pushed a commit to miss-islington/cpython that referenced this issue Oct 14, 2024
…nGH-124959)

(cherry picked from commit d5dbbf4)

Co-authored-by: Thomas Grainger <tagrain@gmail.com>
1st1 pushed a commit that referenced this issue Oct 14, 2024
…24959) (#125463)

gh-124958: fix asyncio.TaskGroup and _PyFuture refcycles (GH-124959)
(cherry picked from commit d5dbbf4)

Co-authored-by: Thomas Grainger <tagrain@gmail.com>
@picnixz picnixz added topic-asyncio stdlib Python modules in the Lib dir labels Oct 14, 2024
Eclips4 added a commit to Eclips4/cpython that referenced this issue Oct 14, 2024
kumaraditya303 pushed a commit that referenced this issue Oct 14, 2024
…cycles (#12… (#125476)

Revert "gh-124958: fix asyncio.TaskGroup and _PyFuture refcycles (#124959)"

This reverts commit d5dbbf4.
miss-islington pushed a commit to miss-islington/cpython that referenced this issue Oct 14, 2024
…PyFuture refcycles (pythonGH-12… (pythonGH-125476)

Revert "pythongh-124958: fix asyncio.TaskGroup and _PyFuture refcycles (pythonGH-124959)"

This reverts commit d5dbbf4.
(cherry picked from commit e99650b)

Co-authored-by: Kirill Podoprigora <kirill.bast9@mail.ru>
graingert added a commit to graingert/cpython that referenced this issue Oct 14, 2024
1st1 pushed a commit that referenced this issue Oct 14, 2024
…p and _PyFuture refcycles ... (#125486)

* Revert "gh-125472: Revert "gh-124958: fix asyncio.TaskGroup and _PyFuture refcycles (#12… (#125476)"

This reverts commit e99650b.

* fix incompatability with gh-124392
1st1 pushed a commit that referenced this issue Oct 17, 2024
@graingert
Copy link
Contributor Author

This is merged and backported now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stdlib Python modules in the Lib dir topic-asyncio type-bug An unexpected behavior, bug, or error
Projects
Status: Done
Development

No branches or pull requests

2 participants