GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — asyncio.TaskGroup corrupts uncancel() stack when the parent task replaces the cancelled error · Issue #95289 · python/cpython

asyncio.TaskGroup corrupts uncancel() stack when the parent task replaces the cancelled error #95289

New issue
Copy link
New issue
Copy link
Closed
Closed
asyncio.TaskGroup corrupts uncancel() stack when the parent task replaces the cancelled error#95289
Copy link
Assignees
kumaraditya303
Labels
3.11only security fixesonly security fixes3.12only security fixesonly security fixesrelease-blockertopic-asynciotype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

Description

@graingert
graingert
opened on Jul 26, 2022
Issue body actions

Bug report

import asyncio

class MyException(Exception):
    pass

async def async_fn():
    await asyncio.sleep(0)
    raise MyException

async def main():
    task = asyncio.current_task()

    try:
        async with asyncio.TaskGroup() as tg:
            tg.create_task(async_fn())
            try:
                await asyncio.sleep(1)
            except asyncio.CancelledError:
                pass
    except* MyException:
        print("done!")

    print(f"{task.cancelling()=} should be 0")

asyncio.run(main())

Activity

Next
  1. graingert
    added
    type-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
    on Jul 26, 2022
  2. graingert commented on Jul 26, 2022

    @graingert
    graingert
    on Jul 26, 2022
    ContributorAuthor
    More actions

    this is also quite easy to trigger in typical task group code:

    import asyncio
    import contextlib
    
    class ConnectionClosedError(Exception):
        pass
    
    
    async def poll_database(database):
        raise ConnectionClosedError
    
    
    async def close_connection():
        raise ConnectionClosedError
    
    
    @contextlib.asynccontextmanager
    async def database():
        try:
            yield
        finally:
            await close_connection()
    
    
    async def main():
        task = asyncio.current_task()
    
        try:
            async with asyncio.TaskGroup() as tg:
                async with database() as db:
                    tg.create_task(poll_database(db))
                    await asyncio.sleep(1)
        except* ConnectionClosedError:
            print("done!")
    
        print(f"{task.cancelling()=} should be 0")
    
    
    asyncio.run(main())
  3. graingert commented on Jul 26, 2022

    @graingert
    graingert
    on Jul 26, 2022
    ContributorAuthor
    More actions

    cc @ambv who helped me find this

  4. graingert
    added
    3.11only security fixesonly security fixes
    3.12only security fixesonly security fixes
    on Jul 26, 2022
  5. AlexWaygood
    added
    topic-asyncio
    on Jul 26, 2022
  6. graingert commented on Jul 27, 2022

    @graingert
    graingert
    on Jul 27, 2022
    ContributorAuthor
    More actions

    Tagging @pablogsal as potential release-blocker

  7. graingert
    added
    release-blocker
    on Jul 27, 2022
  8. ezio-melotti
    added this to Release and Deferred blockers 🚫on Jul 27, 2022
  9. ezio-melotti
    moved this to Todo in Release and Deferred blockers 🚫on Jul 27, 2022
  10. YvesDup commented on Jul 27, 2022

    @YvesDup
    YvesDup
    on Jul 27, 2022
    Contributor
    More actions

    Hello, sorry for my question but I have never seen a * after an except statement. I couldn't find an explanation on the web. What does it mean, please?

    In your first example, if you replace pass with raise, you have the expected result.
    May be this will help you ?

  11. graingert commented on Jul 27, 2022

    @graingert
    graingert
    on Jul 27, 2022
    ContributorAuthor
    More actions

    Hello, sorry for my question but I have never seen a * after an except statement. I couldn't find an explanation on the web. What does it mean, please?

    Hi there - it's the new 3.11 ExceptionGroup feature https://peps.python.org/pep-0654/ it's required to handle an issue where multiple errors can happen concurrently. Eg a connection error canceles a group of concurrent operations, and the cancellation error leads to some teardown that also fails. In 3.10 library authors had to either choose an error to raise or wrap them in a list and raise a generic "MultiError"

    Reacted by Duprat
  12. YvesDup commented on Jul 29, 2022

    @YvesDup
    YvesDup
    on Jul 29, 2022
    Contributor
    More actions

    Hi @graingert, thank you for your response about except * feature.

    About the issue, I read the documentation about 'Task groups' and IMHO there is a part about the two cases, in the second paragraph below the exemple:

    ... At this point, if the body of the async with statement is still active (__aexit__() hasn’t been called yet), the task directly containing the async with statement is also cancelled.

    Do you think that your both examples match the situation described ?

  13. ambv commented on Jul 29, 2022

    @ambv
    ambv
    on Jul 29, 2022
    Contributor
    More actions

    @YvesDup, you stopped your quote just before the sentence that describes why what Thomas reported is a bug:

    The resulting asyncio.CancelledError will interrupt an await, but it will not bubble out of the containing async with statement.

    In other words, after leaving async with we want print(f"{task.cancelling()=} should be 0").

  14. kumaraditya303 commented on Jul 29, 2022

    @kumaraditya303
    kumaraditya303
    on Jul 29, 2022
    Contributor
    More actions

    Have you tried this on the edgedb's task group 1 implementation from which the current one is derived from?

    Footnotes

    1. https://github.com/edgedb/edgedb/blob/master/edb/common/taskgroup.py ↩

  15. 7 remaining items

  16. gvanrossum commented on Jul 30, 2022

    @gvanrossum
    gvanrossum
    on Jul 30, 2022
    Member
    More actions

    Alas, that doesn't fix the original bug report. But here's a variant that does:

    diff --git a/Lib/asyncio/taskgroups.py b/Lib/asyncio/taskgroups.py
    index 3ca65062ef..0dde87f1ca 100644
    --- a/Lib/asyncio/taskgroups.py
    +++ b/Lib/asyncio/taskgroups.py
    @@ -61,14 +61,12 @@ async def __aexit__(self, et, exc, tb):
                     self._base_error is None):
                 self._base_error = exc
     
    -        if et is not None:
    -            if et is exceptions.CancelledError:
    -                if self._parent_cancel_requested and not self._parent_task.uncancel():
    -                    # Do nothing, i.e. swallow the error.
    -                    pass
    -                else:
    -                    propagate_cancellation_error = exc
    +        if (self._parent_cancel_requested and
    +                self._parent_task.uncancel() != 0 and
    +                et is exceptions.CancelledError):
    +            propagate_cancellation_error = exc
     
    +        if et is not None:
                 if not self._aborting:
                     # Our parent task is being cancelled:
                     #

    I have other things to do, maybe @graingert and/or @kumaraditya303 can check my fix and turn it into a PR with the two examples as new tests?

  17. gvanrossum commented on Jul 31, 2022

    @gvanrossum
    gvanrossum
    on Jul 31, 2022
    Member
    More actions

    Hm, there’s one case where the effect is different from the original: if a CancellationError occurs while _parent_cancel_request is not set. Then the original sets propagate_cancellation_error = exc but the new code does nothing. Does that matter? No tests fail.

  18. gvanrossum commented on Jul 31, 2022

    @gvanrossum
    gvanrossum
    on Jul 31, 2022
    Member
    More actions

    That would affect the case where the parent task is cancelled externally — it should propagate the cancellation but with my code it will exit cleanly. This can be fixed and requires a separate test.

  19. gvanrossum commented on Aug 1, 2022

    @gvanrossum
    gvanrossum
    on Aug 1, 2022
    Member
    More actions

    Oh, I think I understand why that doesn't matter. In this case we end up returning None from __aexit__() and that causes the interpreter to re-raise the original exception that was being handled. (In fact we may use this fact to simplify the code even further?)

  20. kumaraditya303
    added a commit that references this issue on Aug 3, 2022

    pythonGH-95289: Always call uncancel() when required

    14a5674
  21. kumaraditya303
    self-assigned this
    on Aug 4, 2022
  22. gvanrossum
    added a commit that references this issue on Aug 4, 2022

    GH-95289: Always call uncancel() when parent cancellation is requested (

    2fef275
  23. miss-islington
    added 2 commits that reference this issue on Aug 4, 2022

    pythonGH-95289: Always call uncancel() when parent cancellation is re…

    fb5fe45

    GH-95289: Always call uncancel() when parent cancellation is requested (

    2d84fe5
  24. kumaraditya303 commented on Aug 4, 2022

    @kumaraditya303
    kumaraditya303
    on Aug 4, 2022
    Contributor
    More actions

    Fixed by #95602

  25. kumaraditya303
    closed this as completedon Aug 4, 2022
  26. Repository owner moved this from Todo to Done in Release and Deferred blockers 🚫on Aug 4, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

  • @kumaraditya303
    kumaraditya303

Labels

3.11only security fixesonly security fixes3.12only security fixesonly security fixesrelease-blockertopic-asynciotype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

Projects

  • Release and Deferred blockers 🚫
    Status
    Done
    Show more project fields

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.