[issue45390] asyncio.Task doesn't propagate CancelledError() exception correctly.

Chris Jerdonek report at bugs.python.org
Sun Oct 10 00:13:19 EDT 2021


Chris Jerdonek <chris.jerdonek at gmail.com> added the comment:

Here's a simplification of Marco's snippet to focus the discussion.

import asyncio

async def job():
    # raise RuntimeError('error!')
    await asyncio.sleep(5)

async def main():
    task = asyncio.create_task(job())
    await asyncio.sleep(1)
    task.cancel('cancel job')
    await task

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

----
Running this pre-Python 3.9 gives something like this--

Traceback (most recent call last):
  File "test.py", line 15, in <module>
    asyncio.run(main())
  File "/.../python3.7/asyncio/runners.py", line 43, in run
    return loop.run_until_complete(main)
  File "/.../python3.7/asyncio/base_events.py", line 579, in run_until_complete
    return future.result()
concurrent.futures._base.CancelledError

----
Running this with Python 3.9+ gives something like the following. The difference is that the traceback now starts at the sleep() call:

Traceback (most recent call last):
  File "/.../test.py", line 6, in job
    await asyncio.sleep(5)
  File "/.../python3.9/asyncio/tasks.py", line 654, in sleep
    return await future
asyncio.exceptions.CancelledError: cancel job

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/.../test.py", line 12, in main
    await task
asyncio.exceptions.CancelledError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/.../test.py", line 15, in <module>
    asyncio.run(main())
  File "/.../python3.9/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/.../python3.9/asyncio/base_events.py", line 642, in run_until_complete
    return future.result()
asyncio.exceptions.CancelledError

----
Uncommenting the RuntimeError turns it into this--

Traceback (most recent call last):
  File "/.../test.py", line 15, in <module>
    asyncio.run(main())
  File "/.../python3.9/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/.../python3.9/asyncio/base_events.py", line 642, in run_until_complete
    return future.result()
  File "/.../test.py", line 12, in main
    await task
  File "/.../test.py", line 5, in job
    raise RuntimeError('error!')
RuntimeError: error!

----
I agree it would be a lot nicer if the original CancelledError('cancel job') could bubble up just like the RuntimeError does, instead of creating a new CancelledError at each await and chaining it to the previous CancelledError. asyncio's creation of a new CancelledError at each stage predates the PR that added the chaining, so this could be viewed as an evolution of the change that added the chaining.

I haven't checked to be sure, but the difference in behavior between CancelledError and other exceptions might be explained by the following lines:
https://github.com/python/cpython/blob/3d1ca867ed0e3ae343166806f8ddd9739e568ab4/Lib/asyncio/tasks.py#L242-L250
You can see that for exceptions other than CancelledError, the exception is propagated by calling super().set_exception(exc), whereas with CancelledError, it is propagated by calling super().cancel() again.

Maybe this would even be an easy change to make. Instead of asyncio creating a new CancelledError and chaining it to the previous, asyncio can just raise the existing one. For the pure Python implementation at least, it may be as simple as making a change here, inside _make_cancelled_error():
https://github.com/python/cpython/blob/3d1ca867ed0e3ae343166806f8ddd9739e568ab4/Lib/asyncio/futures.py#L135-L142

----------

_______________________________________
Python tracker <report at bugs.python.org>
<https://bugs.python.org/issue45390>
_______________________________________


More information about the Python-bugs-list mailing list