2019-11-13 10:07:58 +00:00
|
|
|
# MicroPython uasyncio module
|
|
|
|
# MIT license; Copyright (c) 2019-2020 Damien P. George
|
|
|
|
|
|
|
|
# This file contains the core TaskQueue based on a pairing heap, and the core Task class.
|
|
|
|
# They can optionally be replaced by C implementations.
|
|
|
|
|
|
|
|
from . import core
|
|
|
|
|
|
|
|
|
|
|
|
# pairing-heap meld of 2 heaps; O(1)
|
|
|
|
def ph_meld(h1, h2):
|
|
|
|
if h1 is None:
|
|
|
|
return h2
|
|
|
|
if h2 is None:
|
|
|
|
return h1
|
|
|
|
lt = core.ticks_diff(h1.ph_key, h2.ph_key) < 0
|
|
|
|
if lt:
|
|
|
|
if h1.ph_child is None:
|
|
|
|
h1.ph_child = h2
|
|
|
|
else:
|
|
|
|
h1.ph_child_last.ph_next = h2
|
|
|
|
h1.ph_child_last = h2
|
|
|
|
h2.ph_next = None
|
|
|
|
h2.ph_rightmost_parent = h1
|
|
|
|
return h1
|
|
|
|
else:
|
|
|
|
h1.ph_next = h2.ph_child
|
|
|
|
h2.ph_child = h1
|
|
|
|
if h1.ph_next is None:
|
|
|
|
h2.ph_child_last = h1
|
|
|
|
h1.ph_rightmost_parent = h2
|
|
|
|
return h2
|
|
|
|
|
|
|
|
|
|
|
|
# pairing-heap pairing operation; amortised O(log N)
|
|
|
|
def ph_pairing(child):
|
|
|
|
heap = None
|
|
|
|
while child is not None:
|
|
|
|
n1 = child
|
|
|
|
child = child.ph_next
|
|
|
|
n1.ph_next = None
|
|
|
|
if child is not None:
|
|
|
|
n2 = child
|
|
|
|
child = child.ph_next
|
|
|
|
n2.ph_next = None
|
|
|
|
n1 = ph_meld(n1, n2)
|
|
|
|
heap = ph_meld(heap, n1)
|
|
|
|
return heap
|
|
|
|
|
|
|
|
|
|
|
|
# pairing-heap delete of a node; stable, amortised O(log N)
|
|
|
|
def ph_delete(heap, node):
|
|
|
|
if node is heap:
|
|
|
|
child = heap.ph_child
|
|
|
|
node.ph_child = None
|
|
|
|
return ph_pairing(child)
|
|
|
|
# Find parent of node
|
|
|
|
parent = node
|
|
|
|
while parent.ph_next is not None:
|
|
|
|
parent = parent.ph_next
|
|
|
|
parent = parent.ph_rightmost_parent
|
|
|
|
# Replace node with pairing of its children
|
|
|
|
if node is parent.ph_child and node.ph_child is None:
|
|
|
|
parent.ph_child = node.ph_next
|
|
|
|
node.ph_next = None
|
|
|
|
return heap
|
|
|
|
elif node is parent.ph_child:
|
|
|
|
child = node.ph_child
|
|
|
|
next = node.ph_next
|
|
|
|
node.ph_child = None
|
|
|
|
node.ph_next = None
|
|
|
|
node = ph_pairing(child)
|
|
|
|
parent.ph_child = node
|
|
|
|
else:
|
|
|
|
n = parent.ph_child
|
|
|
|
while node is not n.ph_next:
|
|
|
|
n = n.ph_next
|
|
|
|
child = node.ph_child
|
|
|
|
next = node.ph_next
|
|
|
|
node.ph_child = None
|
|
|
|
node.ph_next = None
|
|
|
|
node = ph_pairing(child)
|
|
|
|
if node is None:
|
|
|
|
node = n
|
|
|
|
else:
|
|
|
|
n.ph_next = node
|
|
|
|
node.ph_next = next
|
|
|
|
if next is None:
|
|
|
|
node.ph_rightmost_parent = parent
|
|
|
|
parent.ph_child_last = node
|
|
|
|
return heap
|
|
|
|
|
|
|
|
|
|
|
|
# TaskQueue class based on the above pairing-heap functions.
|
|
|
|
class TaskQueue:
|
|
|
|
def __init__(self):
|
|
|
|
self.heap = None
|
|
|
|
|
|
|
|
def peek(self):
|
|
|
|
return self.heap
|
|
|
|
|
|
|
|
def push_sorted(self, v, key):
|
2022-04-21 04:17:33 +01:00
|
|
|
assert v.ph_child is None
|
|
|
|
assert v.ph_next is None
|
2019-11-13 10:07:58 +00:00
|
|
|
v.data = None
|
|
|
|
v.ph_key = key
|
|
|
|
self.heap = ph_meld(v, self.heap)
|
|
|
|
|
|
|
|
def push_head(self, v):
|
|
|
|
self.push_sorted(v, core.ticks())
|
|
|
|
|
|
|
|
def pop_head(self):
|
|
|
|
v = self.heap
|
2022-04-21 04:17:33 +01:00
|
|
|
assert v.ph_next is None
|
|
|
|
self.heap = ph_pairing(v.ph_child)
|
|
|
|
v.ph_child = None
|
2019-11-13 10:07:58 +00:00
|
|
|
return v
|
|
|
|
|
|
|
|
def remove(self, v):
|
|
|
|
self.heap = ph_delete(self.heap, v)
|
|
|
|
|
|
|
|
|
|
|
|
# Task class representing a coroutine, can be waited on and cancelled.
|
|
|
|
class Task:
|
|
|
|
def __init__(self, coro, globals=None):
|
|
|
|
self.coro = coro # Coroutine of this Task
|
|
|
|
self.data = None # General data for queue it is waiting on
|
2022-03-29 02:27:56 +01:00
|
|
|
self.state = True # None, False, True, a callable, or a TaskQueue instance
|
2019-11-13 10:07:58 +00:00
|
|
|
self.ph_key = 0 # Pairing heap
|
|
|
|
self.ph_child = None # Paring heap
|
|
|
|
self.ph_child_last = None # Paring heap
|
|
|
|
self.ph_next = None # Paring heap
|
|
|
|
self.ph_rightmost_parent = None # Paring heap
|
|
|
|
|
|
|
|
def __iter__(self):
|
extmod/uasyncio: Fix race with cancelled task waiting on finished task.
This commit fixes a problem with a race between cancellation of task A and
completion of task B, when A waits on B. If task B completes just before
task A is cancelled then the cancellation of A does not work. Instead,
the CancelledError meant to cancel A gets passed through to B (that's
expected behaviour) but B handles it as a "Task exception wasn't retrieved"
scenario, printing out such a message (this is because finished tasks point
their "coro" attribute to themselves to indicate they are done, and
implement the throw() method, but that method inadvertently catches the
CancelledError). The correct behaviour is for B to bounce that
CancelledError back out.
This bug is mainly seen when wait_for() is used, and in that context the
symptoms are:
- occurs when using wait_for(T, S), if the task T being waited on finishes
at exactly the same time as the wait-for timeout S expires
- task T will have run to completion
- the "Task exception wasn't retrieved message" is printed with
"<class 'CancelledError'>" as the error (ie no traceback)
- the wait_for(T, S) call never returns (it's never put back on the
uasyncio run queue) and all tasks waiting on this are blocked forever
from running
- uasyncio otherwise continues to function and other tasks continue to be
scheduled as normal
The fix here reworks the "waiting" attribute of Task to be called "state"
and uses it to indicate whether a task is: running and not awaited on,
running and awaited on, finished and not awaited on, or finished and
awaited on. This means the task does not need to point "coro" to itself to
indicate finished, and also allows removal of the throw() method.
A benefit of this is that "Task exception wasn't retrieved" messages can go
back to being able to print the name of the coroutine function.
Fixes issue #7386.
Signed-off-by: Damien George <damien@micropython.org>
2021-06-14 13:32:51 +01:00
|
|
|
if not self.state:
|
|
|
|
# Task finished, signal that is has been await'ed on.
|
|
|
|
self.state = False
|
|
|
|
elif self.state is True:
|
|
|
|
# Allocated head of linked list of Tasks waiting on completion of this task.
|
|
|
|
self.state = TaskQueue()
|
2022-03-29 02:27:56 +01:00
|
|
|
elif type(self.state) is not TaskQueue:
|
|
|
|
# Task has state used for another purpose, so can't also wait on it.
|
|
|
|
raise RuntimeError("can't wait")
|
2019-11-13 10:07:58 +00:00
|
|
|
return self
|
|
|
|
|
|
|
|
def __next__(self):
|
extmod/uasyncio: Fix race with cancelled task waiting on finished task.
This commit fixes a problem with a race between cancellation of task A and
completion of task B, when A waits on B. If task B completes just before
task A is cancelled then the cancellation of A does not work. Instead,
the CancelledError meant to cancel A gets passed through to B (that's
expected behaviour) but B handles it as a "Task exception wasn't retrieved"
scenario, printing out such a message (this is because finished tasks point
their "coro" attribute to themselves to indicate they are done, and
implement the throw() method, but that method inadvertently catches the
CancelledError). The correct behaviour is for B to bounce that
CancelledError back out.
This bug is mainly seen when wait_for() is used, and in that context the
symptoms are:
- occurs when using wait_for(T, S), if the task T being waited on finishes
at exactly the same time as the wait-for timeout S expires
- task T will have run to completion
- the "Task exception wasn't retrieved message" is printed with
"<class 'CancelledError'>" as the error (ie no traceback)
- the wait_for(T, S) call never returns (it's never put back on the
uasyncio run queue) and all tasks waiting on this are blocked forever
from running
- uasyncio otherwise continues to function and other tasks continue to be
scheduled as normal
The fix here reworks the "waiting" attribute of Task to be called "state"
and uses it to indicate whether a task is: running and not awaited on,
running and awaited on, finished and not awaited on, or finished and
awaited on. This means the task does not need to point "coro" to itself to
indicate finished, and also allows removal of the throw() method.
A benefit of this is that "Task exception wasn't retrieved" messages can go
back to being able to print the name of the coroutine function.
Fixes issue #7386.
Signed-off-by: Damien George <damien@micropython.org>
2021-06-14 13:32:51 +01:00
|
|
|
if not self.state:
|
2019-11-13 10:07:58 +00:00
|
|
|
# Task finished, raise return value to caller so it can continue.
|
|
|
|
raise self.data
|
|
|
|
else:
|
|
|
|
# Put calling task on waiting queue.
|
extmod/uasyncio: Fix race with cancelled task waiting on finished task.
This commit fixes a problem with a race between cancellation of task A and
completion of task B, when A waits on B. If task B completes just before
task A is cancelled then the cancellation of A does not work. Instead,
the CancelledError meant to cancel A gets passed through to B (that's
expected behaviour) but B handles it as a "Task exception wasn't retrieved"
scenario, printing out such a message (this is because finished tasks point
their "coro" attribute to themselves to indicate they are done, and
implement the throw() method, but that method inadvertently catches the
CancelledError). The correct behaviour is for B to bounce that
CancelledError back out.
This bug is mainly seen when wait_for() is used, and in that context the
symptoms are:
- occurs when using wait_for(T, S), if the task T being waited on finishes
at exactly the same time as the wait-for timeout S expires
- task T will have run to completion
- the "Task exception wasn't retrieved message" is printed with
"<class 'CancelledError'>" as the error (ie no traceback)
- the wait_for(T, S) call never returns (it's never put back on the
uasyncio run queue) and all tasks waiting on this are blocked forever
from running
- uasyncio otherwise continues to function and other tasks continue to be
scheduled as normal
The fix here reworks the "waiting" attribute of Task to be called "state"
and uses it to indicate whether a task is: running and not awaited on,
running and awaited on, finished and not awaited on, or finished and
awaited on. This means the task does not need to point "coro" to itself to
indicate finished, and also allows removal of the throw() method.
A benefit of this is that "Task exception wasn't retrieved" messages can go
back to being able to print the name of the coroutine function.
Fixes issue #7386.
Signed-off-by: Damien George <damien@micropython.org>
2021-06-14 13:32:51 +01:00
|
|
|
self.state.push_head(core.cur_task)
|
2019-11-13 10:07:58 +00:00
|
|
|
# Set calling task's data to this task that it waits on, to double-link it.
|
|
|
|
core.cur_task.data = self
|
|
|
|
|
2020-11-30 06:38:30 +00:00
|
|
|
def done(self):
|
extmod/uasyncio: Fix race with cancelled task waiting on finished task.
This commit fixes a problem with a race between cancellation of task A and
completion of task B, when A waits on B. If task B completes just before
task A is cancelled then the cancellation of A does not work. Instead,
the CancelledError meant to cancel A gets passed through to B (that's
expected behaviour) but B handles it as a "Task exception wasn't retrieved"
scenario, printing out such a message (this is because finished tasks point
their "coro" attribute to themselves to indicate they are done, and
implement the throw() method, but that method inadvertently catches the
CancelledError). The correct behaviour is for B to bounce that
CancelledError back out.
This bug is mainly seen when wait_for() is used, and in that context the
symptoms are:
- occurs when using wait_for(T, S), if the task T being waited on finishes
at exactly the same time as the wait-for timeout S expires
- task T will have run to completion
- the "Task exception wasn't retrieved message" is printed with
"<class 'CancelledError'>" as the error (ie no traceback)
- the wait_for(T, S) call never returns (it's never put back on the
uasyncio run queue) and all tasks waiting on this are blocked forever
from running
- uasyncio otherwise continues to function and other tasks continue to be
scheduled as normal
The fix here reworks the "waiting" attribute of Task to be called "state"
and uses it to indicate whether a task is: running and not awaited on,
running and awaited on, finished and not awaited on, or finished and
awaited on. This means the task does not need to point "coro" to itself to
indicate finished, and also allows removal of the throw() method.
A benefit of this is that "Task exception wasn't retrieved" messages can go
back to being able to print the name of the coroutine function.
Fixes issue #7386.
Signed-off-by: Damien George <damien@micropython.org>
2021-06-14 13:32:51 +01:00
|
|
|
return not self.state
|
2020-11-30 06:38:30 +00:00
|
|
|
|
2019-11-13 10:07:58 +00:00
|
|
|
def cancel(self):
|
|
|
|
# Check if task is already finished.
|
extmod/uasyncio: Fix race with cancelled task waiting on finished task.
This commit fixes a problem with a race between cancellation of task A and
completion of task B, when A waits on B. If task B completes just before
task A is cancelled then the cancellation of A does not work. Instead,
the CancelledError meant to cancel A gets passed through to B (that's
expected behaviour) but B handles it as a "Task exception wasn't retrieved"
scenario, printing out such a message (this is because finished tasks point
their "coro" attribute to themselves to indicate they are done, and
implement the throw() method, but that method inadvertently catches the
CancelledError). The correct behaviour is for B to bounce that
CancelledError back out.
This bug is mainly seen when wait_for() is used, and in that context the
symptoms are:
- occurs when using wait_for(T, S), if the task T being waited on finishes
at exactly the same time as the wait-for timeout S expires
- task T will have run to completion
- the "Task exception wasn't retrieved message" is printed with
"<class 'CancelledError'>" as the error (ie no traceback)
- the wait_for(T, S) call never returns (it's never put back on the
uasyncio run queue) and all tasks waiting on this are blocked forever
from running
- uasyncio otherwise continues to function and other tasks continue to be
scheduled as normal
The fix here reworks the "waiting" attribute of Task to be called "state"
and uses it to indicate whether a task is: running and not awaited on,
running and awaited on, finished and not awaited on, or finished and
awaited on. This means the task does not need to point "coro" to itself to
indicate finished, and also allows removal of the throw() method.
A benefit of this is that "Task exception wasn't retrieved" messages can go
back to being able to print the name of the coroutine function.
Fixes issue #7386.
Signed-off-by: Damien George <damien@micropython.org>
2021-06-14 13:32:51 +01:00
|
|
|
if not self.state:
|
2019-11-13 10:07:58 +00:00
|
|
|
return False
|
|
|
|
# Can't cancel self (not supported yet).
|
|
|
|
if self is core.cur_task:
|
2020-04-14 12:51:25 +01:00
|
|
|
raise RuntimeError("can't cancel self")
|
2019-11-13 10:07:58 +00:00
|
|
|
# If Task waits on another task then forward the cancel to the one it's waiting on.
|
|
|
|
while isinstance(self.data, Task):
|
|
|
|
self = self.data
|
|
|
|
# Reschedule Task as a cancelled task.
|
|
|
|
if hasattr(self.data, "remove"):
|
|
|
|
# Not on the main running queue, remove the task from the queue it's on.
|
|
|
|
self.data.remove(self)
|
|
|
|
core._task_queue.push_head(self)
|
|
|
|
elif core.ticks_diff(self.ph_key, core.ticks()) > 0:
|
|
|
|
# On the main running queue but scheduled in the future, so bring it forward to now.
|
|
|
|
core._task_queue.remove(self)
|
|
|
|
core._task_queue.push_head(self)
|
|
|
|
self.data = core.CancelledError
|
|
|
|
return True
|