-
-
Notifications
You must be signed in to change notification settings - Fork 8.4k
extmod/uasyncio/task.py: Fix crash when non-awaited task is awaited. #8929
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
Conversation
A task that has been sent to the loop's exception handler due to being re-scheduled twice will then subsequently cause a `raise None` if it is subsequently awaited. In the C version of task.py, this causes a segfault. This makes the await succeed (via raising StopIteration instead). Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
What is the CPython behavior for this test case? |
I got curious, so I played around with this a bit. import asyncio
async def task():
raise Exception("task")
async def test():
t = asyncio.create_task(task())
await asyncio.sleep(0)
await asyncio.sleep(0)
await t
async def wait():
await asyncio.sleep(2)
print('done')
async def main():
await asyncio.gather(test(), wait())
asyncio.run(main()) Results in:
If you comment out
Since CPython is reference counted, it knows the task ( If you add
The loop exception handler is not called until the very end of the program. |
@dlech Thanks for reminding me, sorry forgot to include this above. In this test case, CPython raises on the await (and never sends the exception to the loop handler). My understanding is that only when a task becomes unreferenced does it get sent to the loop handler, and until then it's assumed that it will potentially be awaited. If we consider the loop handler to be "informative", then that would be a good argument for alternative 2 above (i.e. retain the exception on the task). |
My first instinct is to change the MicroPython behavior to be like the CPython case with the import asyncio
async def task():
raise Exception("task")
async def test():
async with asyncio.create_task(task()) as t:
await asyncio.sleep(0)
await asyncio.sleep(0)
# await t <- not awaited task
# <-- loop exception handler runs here
async def wait():
await asyncio.sleep(2)
print('done')
async def main():
await asyncio.gather(test(), wait())
asyncio.run(main()) Unfortunately, this wouldn't be compatible with CPython, so probably not a practical solution. |
I think not retaining the exception and raising when awaited is a bug. There could be cases where exceptions are expected. t = asyncio.create_task(task())
...
try:
await t
except SomeError:
... In this case, |
FYI, we incorporated this a while ago into CircuitPython, even though it is currently unmerged into MicroPython, as adafruit#7059. |
Require 64 bytes extra CDC RX buffer
@dhalbert you probably want to revert this fix in CircuitPython. |
A task that has been sent to the loop's exception handler due to being re-scheduled twice will then subsequently cause a
raise None
if it is subsequently awaited. In the C version of task.py, this causes a segfault.This PR makes the await succeed (via raising StopIteration instead).
I'm not sure this is the right fix though... it seems wrong to make the await succeed. The other option that I can think of are:
I think the third option (CancelledError) might be the most pragmatic alternative?