summaryrefslogtreecommitdiffstatshomepage
path: root/extmod/asyncio/core.py
diff options
context:
space:
mode:
authorYoctopuce dev <dev@yoctopuce.com>2025-02-15 15:24:50 +0100
committerDamien George <damien@micropython.org>2025-05-07 14:56:47 +1000
commitbdb7e036d2400c25beeb2b4252ff51c5255c4020 (patch)
treebc09484a6bf1cdff6efccea1999b68ef6706c068 /extmod/asyncio/core.py
parent79abdad9e97f18f45650e1abce64ee51c3372953 (diff)
downloadmicropython-bdb7e036d2400c25beeb2b4252ff51c5255c4020.tar.gz
micropython-bdb7e036d2400c25beeb2b4252ff51c5255c4020.zip
extmod/asyncio: Fix early exit of asyncio scheduler.
This commit fixes three open issues related to the asyncio scheduler exiting prematurely when the main task queue is empty, in cases where CPython would not exit (for example, because the main task is not done because it's on a different queue). In the first case, the scheduler exits because running a task via `run_until_complete` did not schedule any dependent tasks. In the other two cases, the scheduler exits because the tasks are queued in an event queue. Tests have been added which reproduce the original issues. These test cases document the unauthorized use of `Event.set()` from a soft IRQ, and are skipped in unsupported environments (webassembly and native emitter). Fixes issues #16759, #16569 and #16318. Signed-off-by: Yoctopuce dev <dev@yoctopuce.com>
Diffstat (limited to 'extmod/asyncio/core.py')
-rw-r--r--extmod/asyncio/core.py36
1 files changed, 24 insertions, 12 deletions
diff --git a/extmod/asyncio/core.py b/extmod/asyncio/core.py
index 8aad234514..5d46b4b80e 100644
--- a/extmod/asyncio/core.py
+++ b/extmod/asyncio/core.py
@@ -163,9 +163,16 @@ def run_until_complete(main_task=None):
# A task waiting on _task_queue; "ph_key" is time to schedule task at
dt = max(0, ticks_diff(t.ph_key, ticks()))
elif not _io_queue.map:
- # No tasks can be woken so finished running
+ # No tasks can be woken
cur_task = None
- return
+ if not main_task or not main_task.state:
+ # no main_task, or main_task is done so finished running
+ return
+ # At this point, there is theoretically nothing that could wake the
+ # scheduler, but it is not allowed to exit either. We keep the code
+ # running so that a hypothetical debugger (or other such meta-process)
+ # can get a view of what is happening and possibly abort.
+ dt = 3
# print('(poll {})'.format(dt), len(_io_queue.map))
_io_queue.wait_io_event(dt)
@@ -187,31 +194,33 @@ def run_until_complete(main_task=None):
except excs_all as er:
# Check the task is not on any event queue
assert t.data is None
- # This task is done, check if it's the main task and then loop should stop
- if t is main_task:
+ # If it's the main task, it is considered as awaited by the caller
+ awaited = t is main_task
+ if awaited:
cur_task = None
- if isinstance(er, StopIteration):
- return er.value
- raise er
+ if not isinstance(er, StopIteration):
+ t.state = False
+ raise er
+ if t.state is None:
+ t.state = False
if t.state:
# Task was running but is now finished.
- waiting = False
if t.state is True:
# "None" indicates that the task is complete and not await'ed on (yet).
- t.state = None
+ t.state = False if awaited else None
elif callable(t.state):
# The task has a callback registered to be called on completion.
t.state(t, er)
t.state = False
- waiting = True
+ awaited = True
else:
# Schedule any other tasks waiting on the completion of this task.
while t.state.peek():
_task_queue.push(t.state.pop())
- waiting = True
+ awaited = True
# "False" indicates that the task is complete and has been await'ed on.
t.state = False
- if not waiting and not isinstance(er, excs_stop):
+ if not awaited and not isinstance(er, excs_stop):
# An exception ended this detached task, so queue it for later
# execution to handle the uncaught exception if no other task retrieves
# the exception in the meantime (this is handled by Task.throw).
@@ -229,6 +238,9 @@ def run_until_complete(main_task=None):
_exc_context["exception"] = exc
_exc_context["future"] = t
Loop.call_exception_handler(_exc_context)
+ # If it's the main task then the loop should stop
+ if t is main_task:
+ return er.value
# Create a new task from a coroutine and run it until it finishes