pythonpython-asynciopython-3.11

TaskGroup in Python 3.11 freezes if one task raises an exception - is it a known bug?


There are two tasks in a task group. One of them raises, the other one should be cancelled. This works fine in Python 3.12+, but freezes in Python 3.11. Older versions did not support task groups.

Is this a known problem? They will probably not fix a bug in 3.11 at this stage of its life-cycle. I'm looking for information how to avoid, solve or mitigate the issue. So far I found out that small changes in the code make a difference. And it looks like the wait_for plays some role there.

import asyncio

ev = asyncio.Event()

async def t1():
    try:
        while True:
            try:
                print("Waiting")
                await asyncio.wait_for(ev.wait(), 99)
                print("Done waiting")
                ev.clear()
            except TimeoutError:
                print("Timeout")
                raise
    except asyncio.CancelledError:
        print("Cancelled -  as expected")
        raise

async def t2():
    ev.set()
    raise RuntimeError()

async def main():
    try:
        async with asyncio.TaskGroup() as tg:
            tg.create_task(t1())
            tg.create_task(t2())
    except* RuntimeError:
        print("RuntimeError -  as expected")

if __name__ == "__main__":
    asyncio.run(main())

Normal output:

Waiting
Done waiting
Waiting
Cancelled -  as expected
RuntimeError -  as expected

Wrong output in Python 3.11:

Waiting
Done waiting
Waiting

And then it hangs until Ctrl-C is pressed twice.


Solution

  • Thanks to @mkrieger1's comment, it is likely related to bpo-42130 - indeed, "wait_for suppresses a cancellation occurring in the wait_for inner subtask" seems to be the issue.

    The official Python resolution was to rewrite asyncio.wait_for using asyncio.timeout. I suggest that, instead of using asyncio.wait_for, you use asyncio.timeout directly.

    That is, instead of:

                    await asyncio.wait_for(ev.wait(), 99)
    

    To use:

                    async with asyncio.timeout(99):
                        await ev.wait()