Message403570
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 |
|
Date |
User |
Action |
Args |
2021-10-10 04:13:19 | chris.jerdonek | set | recipients:
+ chris.jerdonek, asvetlov, yselivanov, graingert, bjs, pagliaricci.m |
2021-10-10 04:13:19 | chris.jerdonek | set | messageid: <1633839199.28.0.84122106287.issue45390@roundup.psfhosted.org> |
2021-10-10 04:13:19 | chris.jerdonek | link | issue45390 messages |
2021-10-10 04:13:18 | chris.jerdonek | create | |
|