Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 24 additions & 29 deletions design/mvp/CanonicalABI.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@ class Thread:
task: Task
index: Optional[int]
storage: tuple[int,int]
cancellable: bool

def running(self):
return self.cont is None
Expand Down Expand Up @@ -422,6 +423,7 @@ state.
self.task = task
self.index = None
self.storage = [0,0]
self.cancellable = False
assert(self.suspended())
```

Expand Down Expand Up @@ -585,8 +587,8 @@ time before or after the callee returns. If the callee returns and the
`OnResolve` callback has *not* yet been called, the caller may invoke the
returned `OnCancel` callback *at most once* to cooperatively request that the
callee "hurry up" and call `OnResolve` (possibly, but not necessarily, passing
`None` and/or skipping the call to `OnStart`). The `OnCancel` may transitively
execute arbitrary guest code but must not block.
`None` and/or skipping the call to `OnStart`). The `OnCancel` callback may
transitively execute arbitrary guest code but must not block.

When `FuncInst` is implemented by wasm guest code (as opposed to the host), each
call creates a `Task` object to track the state of the call and ensure that the
Expand Down Expand Up @@ -731,21 +733,19 @@ returned a value to its caller.
self.inst.threads.remove(thread.index)
```

The `Task.request_cancellation` method implements the `OnCancel` callback
described above and allows a task's caller to indicate that they are no longer
interested in the return value. If a task's implicit thread is waiting to start
(in `Task.enter_implicit_thread`, defined above) due to backpressure, then it is
immediately cancelled without running any guest code. Otherwise, if any of the
threads in the callee's component instance are ready to run, one is resumed
(chosen nondeterministically if there are multiple). Furthermore, the host may
nondeterministically continue resuming ready threads in the callee's component
instance until either the subtask resolves or the host declares that
cancellation has blocked. Note that setting `Task.state` to `PENDING_CANCEL`
makes any implicit `callback` thread contained by the task that is waiting in
its event loop `ready` (as long as the component instance's `exclusive_thread`
lock is not held by some other implicit thread). Thus, in the best case,
`subtask.cancel` directly calls the subtask's `callback` function, passing
`TASK_CANCELLED`.
The `Task.request_cancellation` method implements the `OnCancel` callback that
is called by the `subtask.cancel` built-in. If a task's implicit thread is
waiting to start (in `Task.enter_implicit_thread`, defined above) due to
backpressure, then it is immediately cancelled without running any guest code.
Otherwise, if the task's implicit thread is `cancellable` and `ready`, it is
resumed, allowing it to execute until returning or blocking. Currently, only
`callback` threads that have returned to their event loop are `cancellable`.
When resumed, the `callback` thread will call `Task.deliver_pending_cancel`,
which will return `True`, leading to the `TASK_CANCELLED` event code being
passed to the `callback` function. The `Thread.resume` call will return as soon
as the `callback` function returns or blocks, whether or not the subtask called
`task.{cancel,return}`, and thus the caller of `OnCancel` must handle both the
resolved and not-resolved cases after `OnCancel` returns.
```python
def request_cancellation(self):
if self.state == Task.State.INITIAL:
Expand All @@ -755,18 +755,9 @@ lock is not held by some other implicit thread). Thus, in the best case,
else:
assert(self.state == Task.State.STARTED)
self.state = Task.State.PENDING_CANCEL
while self.state != Task.State.RESOLVED:
candidates = { t for t in self.inst.threads if t.ready() }
if candidates:
random.choice(list(candidates)).resume()
if not candidates or DETERMINISTIC_PROFILE or random.randint(0,1):
break
```
if self.implicit_thread.cancellable and self.implicit_thread.ready():
self.implicit_thread.resume()

If the pending cancellation request is not delivered to the subtask during
`Task.request_cancellation`, it may still be delivered in the future to a
`callback` via `Task.deliver_pending_cancel`:
```python
def has_pending_cancel(self):
return self.state == Task.State.PENDING_CANCEL

Expand Down Expand Up @@ -3469,6 +3460,7 @@ function (specified as a `funcidx` immediate in `canon lift`) until the
else:
assert(inst.exclusive_thread is task.implicit_thread)
inst.exclusive_thread = None
thread.cancellable = True
match code:
case CallbackCode.YIELD:
thread.wait_until(lambda: inst.exclusive_thread is None)
Expand All @@ -3483,6 +3475,7 @@ function (specified as a `funcidx` immediate in `canon lift`) until the
case _:
trap()
assert(inst.exclusive_thread is None)
thread.cancellable = False
inst.exclusive_thread = task.implicit_thread
event_code, p1, p2 = event
[packed] = call_and_trap_on_throw(opts.callback, [event_code, p1, p2])
Expand Down Expand Up @@ -3510,7 +3503,9 @@ in either case.

Another important property of the event loop as defined above is that it
delivers pending cancellation requests as soon as possible: before waiting,
after waiting, and in `wait_from_callback`, *while* waiting.
after waiting, and while waiting. In particular, setting `Thread.cancellable`
for the duration of the wait allows `subtask.cancel` to synchronously deliver a
`TASK_CANCELLED` event.

The end of `canon_lift` creates a new task/thread pair for the call and then
calls `Thread.resume` on the new thread to synchronously transfer control flow
Expand Down
44 changes: 20 additions & 24 deletions design/mvp/Concurrency.md
Original file line number Diff line number Diff line change
Expand Up @@ -809,26 +809,22 @@ subtask is resolved, the caller knows its lent handles have been returned.
If the subtask was waiting to start due to [backpressure](#backpressure), the
subtask is immediately aborted without running the callee at all, returning
cancelled-before-started. Otherwise, `subtask.cancel` records the "pending
cancellation request" in the subtask and attempts to resume execution in the
subtask's component instance in the hopes that the subtask will quickly resolve
itself. However, if there are no ready threads or, if there are, at least one
thread is resumed and then blocks or exits without having resolved the subtask,
the host is free to declare that cancellation has blocked. In this case,
asynchronous calls to `subtask.cancel` will immediately return a "blocked" code
and the caller must wait for progress using a waitable set. Synchronous calls to
`subtask.cancel` simply block until the subtask is resolved.

The ready threads resumed by cancellation can be ready for all the normal
reasons described above (yielding, I/O progress, etc). However, implicit threads
using the `callback` ABI will *additionally* become ready due to the pending
cancellation request itself (as long as the run-to-completion rules mentioned
above are satisfied). If resumed, the `callback` will be passed a "task
cancelled" event code to indicate that cancellation has been requested and that
`task.cancel` may be called (instead of `task.return`). In the [future](#TODO),
before the stackful ABI is released, other cancellation delivery mechanisms will
be added so that threads waiting via `waitable-set.wait` can also become ready
and receive "task cancelled". Until then, `waitable-set.wait` will never return
"task cancelled".
cancellation request" in the subtask and attempts to resume execution of a
cancellable thread of the subtask in the hopes that the subtask will quickly
resolve itself. However, if no cancellable thread can be resumed (e.g., because
another thread holds the component instance's exclusive lock), or if the resumed
thread blocks or exits without having resolved the subtask, cancellation blocks.
In this case, asynchronous calls to `subtask.cancel` will immediately return a
"blocked" code and the caller must wait for progress using a waitable set.
Synchronous calls to `subtask.cancel` simply block until the subtask is
resolved.

Currently, only threads that use the `callback` ABI and have returned to their
event loop are cancellable, with the cancellation reported as a special "task
cancelled" event. In the [future](#TODO), before the stackful ABI is released,
other cancellation delivery mechanisms will be added so that threads waiting via
`waitable-set.wait` can also become ready and receive "task cancelled". Until
then, `waitable-set.wait` will never return "task cancelled".

The Component Model does not provide a mechanism to force prompt termination of
threads as this can lead to leaks and corrupt state in a still-live component
Expand Down Expand Up @@ -877,15 +873,15 @@ defined by the Component Model:
* If multiple tasks are blocked by backpressure and the backpressure is
disabled, the order in which these pending tasks start, along with how
they interleave with new tasks, is nondeterministic.
* When `subtask.cancel` is called for a task that has started (i.e., passed the
backpressure gate), the choice of which ready thread to resume (if there are
multiple) and how many times to resume (if the first resumption blocks without
resolving the subtask) is nondeterministic.

Despite the above, the following scenarios do behave deterministically:
* If a component `a` asynchronously calls the export of another component `b`,
control flow deterministically transfers to `b` and then back to `a` when
`b` returns or blocks.
* If a component `a` asynchronously cancels a subtask in another component `b`
that has a cancellable thread that can be resumed, control flow
deterministically transfers to `b` and then back to `a` when `b` resolves or
blocks.
* If a component `a` asynchronously cancels a subtask in another component `b`
that was blocked before starting due to backpressure, cancellation completes
deterministically and immediately.
Expand Down
12 changes: 6 additions & 6 deletions design/mvp/canonical-abi/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ class Thread:
task: Task
index: Optional[int]
storage: tuple[int,int]
cancellable: bool

def running(self):
return self.cont is None
Expand All @@ -305,6 +306,7 @@ def cont_func():
self.task = task
self.index = None
self.storage = [0,0]
self.cancellable = False
assert(self.suspended())

def start_waiting(self, ready_func):
Expand Down Expand Up @@ -466,12 +468,8 @@ def request_cancellation(self):
else:
assert(self.state == Task.State.STARTED)
self.state = Task.State.PENDING_CANCEL
while self.state != Task.State.RESOLVED:
candidates = { t for t in self.inst.threads if t.ready() }
if candidates:
random.choice(list(candidates)).resume()
if not candidates or DETERMINISTIC_PROFILE or random.randint(0,1):
break
if self.implicit_thread.cancellable and self.implicit_thread.ready():
self.implicit_thread.resume()

def has_pending_cancel(self):
return self.state == Task.State.PENDING_CANCEL
Expand Down Expand Up @@ -2098,6 +2096,7 @@ def thread_func():
else:
assert(inst.exclusive_thread is task.implicit_thread)
inst.exclusive_thread = None
thread.cancellable = True
match code:
case CallbackCode.YIELD:
thread.wait_until(lambda: inst.exclusive_thread is None)
Expand All @@ -2112,6 +2111,7 @@ def thread_func():
case _:
trap()
assert(inst.exclusive_thread is None)
thread.cancellable = False
inst.exclusive_thread = task.implicit_thread
event_code, p1, p2 = event
[packed] = call_and_trap_on_throw(opts.callback, [event_code, p1, p2])
Expand Down
Loading
Loading