diff options
| author | Damien George | 2020-11-30 17:38:19 +1100 |
|---|---|---|
| committer | Damien George | 2020-12-02 12:07:06 +1100 |
| commit | ca40eb0fdadd5a963b9a065999b092f029a598d5 (patch) | |
| tree | 0e7ba3c032406463a16ab237fb78910c6cdd94c9 /extmod/uasyncio | |
| parent | a14ca31e8579a07f263bca0dd4b0dd03f43befa2 (diff) | |
extmod/uasyncio: Delay calling Loop.call_exception_handler by 1 loop.
When a tasks raises an exception which is uncaught, and no other task
await's on that task, then an error message is printed (or a user function
called) via a call to Loop.call_exception_handler. In CPython this call is
made when the Task object is freed (eg via reference counting) because it's
at that point that it is known that the exception that was raised will
never be handled.
MicroPython does not have reference counting and the current behaviour is
to deal with uncaught exceptions as early as possible, ie as soon as they
terminate the task. But this can be undesirable because in certain cases
a task can start and raise an exception immediately (before any await is
executed in that task's coro) and before any other task gets a chance to
await on it to catch the exception.
This commit changes the behaviour so that tasks which end due to an
uncaught exception are scheduled one more time for execution, and if they
are not await'ed on by the next scheduling loop, then the exception handler
is called (eg the exception is printed out).
Signed-off-by: Damien George <damien@micropython.org>
Diffstat (limited to 'extmod/uasyncio')
| -rw-r--r-- | extmod/uasyncio/core.py | 16 | ||||
| -rw-r--r-- | extmod/uasyncio/funcs.py | 4 | ||||
| -rw-r--r-- | extmod/uasyncio/task.py | 19 |
3 files changed, 26 insertions, 13 deletions
diff --git a/extmod/uasyncio/core.py b/extmod/uasyncio/core.py index 045b4cd13..6a84b0982 100644 --- a/extmod/uasyncio/core.py +++ b/extmod/uasyncio/core.py @@ -185,8 +185,6 @@ def run_until_complete(main_task=None): if isinstance(er, StopIteration): return er.value raise er - # Save return value of coro to pass up to caller - t.data = er # Schedule any other tasks waiting on the completion of this task waiting = False if hasattr(t, "waiting"): @@ -194,13 +192,15 @@ def run_until_complete(main_task=None): _task_queue.push_head(t.waiting.pop_head()) waiting = True t.waiting = None # Free waiting queue head - # Print out exception for detached tasks if not waiting and not isinstance(er, excs_stop): - _exc_context["exception"] = er - _exc_context["future"] = t - Loop.call_exception_handler(_exc_context) - # Indicate task is done - t.coro = None + # 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). + _task_queue.push_head(t) + # Indicate task is done by setting coro to the task object itself + t.coro = t + # Save return value of coro to pass up to caller + t.data = er # Create a new task from a coroutine and run it until it finishes diff --git a/extmod/uasyncio/funcs.py b/extmod/uasyncio/funcs.py index 6e1305c94..d30675231 100644 --- a/extmod/uasyncio/funcs.py +++ b/extmod/uasyncio/funcs.py @@ -21,9 +21,9 @@ async def wait_for(aw, timeout, sleep=core.sleep): pass finally: # Cancel the "cancel" task if it's still active (optimisation instead of cancel_task.cancel()) - if cancel_task.coro is not None: + if cancel_task.coro is not cancel_task: core._task_queue.remove(cancel_task) - if cancel_task.coro is None: + if cancel_task.coro is cancel_task: # Cancel task ran to completion, ie there was a timeout raise core.TimeoutError return ret diff --git a/extmod/uasyncio/task.py b/extmod/uasyncio/task.py index 1788cf0ed..2420ab719 100644 --- a/extmod/uasyncio/task.py +++ b/extmod/uasyncio/task.py @@ -130,13 +130,16 @@ class Task: self.ph_rightmost_parent = None # Paring heap def __iter__(self): - if not hasattr(self, "waiting"): + if self.coro is self: + # Signal that the completed-task has been await'ed on. + self.waiting = None + elif not hasattr(self, "waiting"): # Lazily allocated head of linked list of Tasks waiting on completion of this task. self.waiting = TaskQueue() return self def __next__(self): - if not self.coro: + if self.coro is self: # Task finished, raise return value to caller so it can continue. raise self.data else: @@ -147,7 +150,7 @@ class Task: def cancel(self): # Check if task is already finished. - if self.coro is None: + if self.coro is self: return False # Can't cancel self (not supported yet). if self is core.cur_task: @@ -166,3 +169,13 @@ class Task: core._task_queue.push_head(self) self.data = core.CancelledError return True + + def throw(self, value): + # This task raised an exception which was uncaught; handle that now. + # Set the data because it was cleared by the main scheduling loop. + self.data = value + if not hasattr(self, "waiting"): + # Nothing await'ed on the task so call the exception handler. + core._exc_context["exception"] = value + core._exc_context["future"] = self + core.Loop.call_exception_handler(core._exc_context) |
