extmod/uasyncio: Fix edge case for cancellation of wait_for.

This fixes the cases where the task being waited on finishes just before or
just after the wait_for itself is cancelled.

Fixes issue #8717.

Signed-off-by: Damien George <damien@micropython.org>
This commit is contained in:
Damien George 2022-06-01 14:52:38 +10:00
parent efe23aca71
commit a1afb337d2
3 changed files with 66 additions and 22 deletions

View File

@ -1,49 +1,51 @@
# MicroPython uasyncio module
# MIT license; Copyright (c) 2019-2020 Damien P. George
# MIT license; Copyright (c) 2019-2022 Damien P. George
from . import core
def _run(waiter, aw):
try:
result = await aw
status = True
except BaseException as er:
result = None
status = er
if waiter.data is None:
# The waiter is still waiting, cancel it.
if waiter.cancel():
# Waiter was cancelled by us, change its CancelledError to an instance of
# CancelledError that contains the status and result of waiting on aw.
# If the wait_for task subsequently gets cancelled externally then this
# instance will be reset to a CancelledError instance without arguments.
waiter.data = core.CancelledError(status, result)
async def wait_for(aw, timeout, sleep=core.sleep):
aw = core._promote_to_task(aw)
if timeout is None:
return await aw
def runner(waiter, aw):
nonlocal status, result
try:
result = await aw
s = True
except BaseException as er:
s = er
if status is None:
# The waiter is still waiting, set status for it and cancel it.
status = s
waiter.cancel()
# Run aw in a separate runner task that manages its exceptions.
status = None
result = None
runner_task = core.create_task(runner(core.cur_task, aw))
runner_task = core.create_task(_run(core.cur_task, aw))
try:
# Wait for the timeout to elapse.
await sleep(timeout)
except core.CancelledError as er:
if status is True:
# aw completed successfully and cancelled the sleep, so return aw's result.
return result
elif status is None:
status = er.value
if status is None:
# This wait_for was cancelled externally, so cancel aw and re-raise.
status = True
runner_task.cancel()
raise er
elif status is True:
# aw completed successfully and cancelled the sleep, so return aw's result.
return er.args[1]
else:
# aw raised an exception, propagate it out to the caller.
raise status
# The sleep finished before aw, so cancel aw and raise TimeoutError.
status = True
runner_task.cancel()
await runner_task
raise core.TimeoutError

View File

@ -111,6 +111,21 @@ async def main():
await asyncio.sleep(0.01)
print(sep)
# When wait_for gets cancelled and the task it's waiting on finishes around the
# same time as the cancellation of the wait_for
for num_sleep in range(1, 5):
t = asyncio.create_task(task_wait_for_cancel(4 + num_sleep, 0, 2))
for _ in range(num_sleep):
await asyncio.sleep(0)
assert not t.done()
print("cancel wait_for")
t.cancel()
try:
await t
except asyncio.CancelledError as er:
print(repr(er))
print(sep)
print("finish")

View File

@ -32,4 +32,31 @@ task_wait_for_cancel_ignore cancelled
ignore cancel
task_catch done
----------
task_wait_for_cancel start
cancel wait_for
task start 5
task_wait_for_cancel cancelled
CancelledError()
----------
task_wait_for_cancel start
task start 6
cancel wait_for
task end 6
task_wait_for_cancel cancelled
CancelledError()
----------
task_wait_for_cancel start
task start 7
task end 7
cancel wait_for
task_wait_for_cancel cancelled
CancelledError()
----------
task_wait_for_cancel start
task start 8
task end 8
cancel wait_for
task_wait_for_cancel cancelled
CancelledError()
----------
finish