summaryrefslogtreecommitdiff
path: root/extmod/moduasyncio.c
diff options
context:
space:
mode:
authorDamien George <damien@micropython.org>2020-11-30 17:38:19 +1100
committerDamien George <damien@micropython.org>2020-12-02 12:07:06 +1100
commitca40eb0fdadd5a963b9a065999b092f029a598d5 (patch)
tree0e7ba3c032406463a16ab237fb78910c6cdd94c9 /extmod/moduasyncio.c
parenta14ca31e8579a07f263bca0dd4b0dd03f43befa2 (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/moduasyncio.c')
-rw-r--r--extmod/moduasyncio.c39
1 files changed, 35 insertions, 4 deletions
diff --git a/extmod/moduasyncio.c b/extmod/moduasyncio.c
index 0b15c9e07..e8822c069 100644
--- a/extmod/moduasyncio.c
+++ b/extmod/moduasyncio.c
@@ -146,6 +146,9 @@ STATIC const mp_obj_type_t task_queue_type = {
/******************************************************************************/
// Task class
+// For efficiency, the task object is stored to the coro entry when the task is done.
+#define TASK_IS_DONE(task) ((task)->coro == MP_OBJ_FROM_PTR(task))
+
// This is the core uasyncio context with cur_task, _task_queue and CancelledError.
STATIC mp_obj_t uasyncio_context = MP_OBJ_NULL;
@@ -167,7 +170,7 @@ STATIC mp_obj_t task_make_new(const mp_obj_type_t *type, size_t n_args, size_t n
STATIC mp_obj_t task_cancel(mp_obj_t self_in) {
mp_obj_task_t *self = MP_OBJ_TO_PTR(self_in);
// Check if task is already finished.
- if (self->coro == mp_const_none) {
+ if (TASK_IS_DONE(self)) {
return mp_const_false;
}
// Can't cancel self (not supported yet).
@@ -209,6 +212,24 @@ STATIC mp_obj_t task_cancel(mp_obj_t self_in) {
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(task_cancel_obj, task_cancel);
+STATIC mp_obj_t task_throw(mp_obj_t self_in, mp_obj_t value_in) {
+ // This task raised an exception which was uncaught; handle that now.
+ mp_obj_task_t *self = MP_OBJ_TO_PTR(self_in);
+ // Set the data because it was cleared by the main scheduling loop.
+ self->data = value_in;
+ if (self->waiting == mp_const_none) {
+ // Nothing await'ed on the task so call the exception handler.
+ mp_obj_t _exc_context = mp_obj_dict_get(uasyncio_context, MP_OBJ_NEW_QSTR(MP_QSTR__exc_context));
+ mp_obj_dict_store(_exc_context, MP_OBJ_NEW_QSTR(MP_QSTR_exception), value_in);
+ mp_obj_dict_store(_exc_context, MP_OBJ_NEW_QSTR(MP_QSTR_future), self_in);
+ mp_obj_t Loop = mp_obj_dict_get(uasyncio_context, MP_OBJ_NEW_QSTR(MP_QSTR_Loop));
+ mp_obj_t call_exception_handler = mp_load_attr(Loop, MP_QSTR_call_exception_handler);
+ mp_call_function_1(call_exception_handler, _exc_context);
+ }
+ return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_2(task_throw_obj, task_throw);
+
STATIC void task_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
mp_obj_task_t *self = MP_OBJ_TO_PTR(self_in);
if (dest[0] == MP_OBJ_NULL) {
@@ -218,12 +239,15 @@ STATIC void task_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
} else if (attr == MP_QSTR_data) {
dest[0] = self->data;
} else if (attr == MP_QSTR_waiting) {
- if (self->waiting != mp_const_none) {
+ if (self->waiting != mp_const_none && self->waiting != mp_const_false) {
dest[0] = self->waiting;
}
} else if (attr == MP_QSTR_cancel) {
dest[0] = MP_OBJ_FROM_PTR(&task_cancel_obj);
dest[1] = self_in;
+ } else if (attr == MP_QSTR_throw) {
+ dest[0] = MP_OBJ_FROM_PTR(&task_throw_obj);
+ dest[1] = self_in;
} else if (attr == MP_QSTR_ph_key) {
dest[0] = self->ph_key;
}
@@ -246,14 +270,21 @@ STATIC mp_obj_t task_getiter(mp_obj_t self_in, mp_obj_iter_buf_t *iter_buf) {
(void)iter_buf;
mp_obj_task_t *self = MP_OBJ_TO_PTR(self_in);
if (self->waiting == mp_const_none) {
- self->waiting = task_queue_make_new(&task_queue_type, 0, 0, NULL);
+ // The is the first access of the "waiting" entry.
+ if (TASK_IS_DONE(self)) {
+ // Signal that the completed-task has been await'ed on.
+ self->waiting = mp_const_false;
+ } else {
+ // Lazily allocate the waiting queue.
+ self->waiting = task_queue_make_new(&task_queue_type, 0, 0, NULL);
+ }
}
return self_in;
}
STATIC mp_obj_t task_iternext(mp_obj_t self_in) {
mp_obj_task_t *self = MP_OBJ_TO_PTR(self_in);
- if (self->coro == mp_const_none) {
+ if (TASK_IS_DONE(self)) {
// Task finished, raise return value to caller so it can continue.
nlr_raise(self->data);
} else {