Skip to content
Merged
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
63 changes: 58 additions & 5 deletions design/mvp/Async.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ summary of the motivation and animated sketch of the design in action.
* [Backpressure](#backpressure)
* [Returning](#returning)
* [Borrows](#borrows)
* [Cancellation](#cancellation)
* [Async ABI](#async-abi)
* [Async Import ABI](#async-import-abi)
* [Async Export ABI](#async-export-abi)
Expand Down Expand Up @@ -470,9 +471,9 @@ loop interleaving `stream.read`s (of the readable end passed for `in`) and
`stream.write`s (of the writable end it `stream.new`ed) before exiting the
task.

Once `task.return` is called, the task is in the "returned" state. A task can
only finish once it is in the "returned" state. See the [`canon_task_return`]
function in the Canonical ABI explainer for more details.
Once `task.return` is called, the task is in the "returned" state and can
finish execution any time thereafter. See the [`canon_task_return`] function in
the Canonical ABI explainer for more details.

### Borrows

Expand All @@ -495,6 +496,55 @@ there can be multiple overlapping async tasks executing in a component
instance, a borrowed handle must track *which* task's `num_borrow`s was
incremented so that the correct counter can be decremented.

### Cancellation

Once an async call has started, blocked and been added to the caller's table of
waitables, the caller may decide that it no longer needs the results or effects
of the subtask. In this case, the caller may **cancel** the subtask by calling
the [`subtask.cancel`] built-in.

Once cancellation is requested, since the subtask may have already racily
returned a value, the caller may still receive a return value. However, the
caller may also be notified that the subtask is in one of two additional
terminal states:
* the subtask was **cancelled before it started**, in which case the caller's
arguments were not passed to the callee (in particular, owned handles were
not transferred); or
* the subtask was **cancelled before it returned**, in which case the arguments
were passed, but no values were returned. However, all borrowed handles lent
during the call have been dropped.

Thus there are *three* terminal states for a subtask: returned,
cancelled-before-started and cancelled-before-returned. A subtask in one of
these terminal states is said to be **resolved**. A resolved subtask has always
dropped all the borrowed handles that it was lent during the call.

As with the rest of async, cancellation is *cooperative*, allowing the subtask
a chance to execute and clean up before it transitions to a resolved state (and
relinquishes its borrowed handles). Since there are valid use cases where
successful cancellation requires performing additional I/O using borrowed
handles and potentially blocking in the process, the Component Model does not
impose any limits on what a subtask can do after receiving a cancellation
request nor is there a non-cooperative option to force termination (instead,
this functionality would come as part of a future "[blast zone]" feature).
Thus, the `subtask.cancel` built-in can block and works just like an import
call in that it can be called synchronously or asynchronously.

On the callee side of cancellation: when a caller requests cancellation via
`subtask.cancel`, the callee receives a [`TASK_CANCELLED`] event (as produced
by one of the `waitable-set.{wait,poll}` or `yield` built-ins or as received by
the `callback` function). Upon receiving notice of cancellation, the callee can
call the [`task.cancel`] built-in to resolve the subtask without returning a
value. Alternatively, the callee can still call [`task.return`] as-if there
were no cancellation. `task.cancel` doesn't take a value to return but does
enforce the same [borrow](#borrows) rules as `task.return`. Ideally, a callee
will `task.cancel` itself as soon as possible after receiving a
`TASK_CANCELLED` event so that any caller waiting for the recovery of lent
handles is unblocked ASAP. As with `task.return`, after calling `task.cancel`,
a callee can continue executing before exiting the task.

See the [`canon_subtask_cancel`] and [`canon_task_cancel`] functions in the
Canonical ABI explainer for more details.

## Async ABI

Expand Down Expand Up @@ -924,8 +974,6 @@ will be added in future chunks roughly in the order listed to complete the full
comes after:
* remove the temporary trap mentioned above that occurs when a `read` and
`write` of a stream/future happen from within the same component instance
* `subtask.cancel`: allow a supertask to signal to a subtask that its result is
no longer wanted and to please wrap it up promptly
* zero-copy forwarding/splicing
* some way to say "no more elements are coming for a while"
* `recursive` function type attribute: allow a function to opt in to
Expand Down Expand Up @@ -958,6 +1006,8 @@ comes after:
[`context.set`]: Explainer.md#-contextset
[`backpressure.set`]: Explainer.md#-backpressureset
[`task.return`]: Explainer.md#-taskreturn
[`task.cancel`]: Explainer.md#-taskcancel
[`subtask.cancel`]: Explainer.md#-subtaskcancel
[`yield`]: Explainer.md#-yield
[`waitable-set.wait`]: Explainer.md#-waitable-setwait
[`waitable-set.poll`]: Explainer.md#-waitable-setpoll
Expand All @@ -973,10 +1023,13 @@ comes after:
[`canon_backpressure_set`]: CanonicalABI.md#-canon-backpressureset
[`canon_waitable_set_wait`]: CanonicalABI.md#-canon-waitable-setwait
[`canon_task_return`]: CanonicalABI.md#-canon-taskreturn
[`canon_task_cancel`]: CanonicalABI.md#-canon-taskcancel
[`canon_subtask_cancel`]: CanonicalABI.md#-canon-subtaskcancel
[`Task`]: CanonicalABI.md#task-state
[`Task.enter`]: CanonicalABI.md#task-state
[`Task.wait_on`]: CanonicalABI.md#task-state
[`Waitable`]: CanonicalABI.md#waitable-state
[`TASK_CANCELLED`]: CanonicalABI.md#waitable-state
[`Task`]: CanonicalABI.md#task-state
[`Subtask`]: CanonicalABI.md#subtask-state
[Stream State]: CanonicalABI.md#stream-state
Expand Down
2 changes: 2 additions & 0 deletions design/mvp/Binary.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,9 +291,11 @@ canon ::= 0x00 0x00 f:<core:funcidx> opts:<opts> ft:<typeidx> => (canon lift
| 0x04 rt:<typeidx> => (canon resource.rep rt (core func))
| 0x08 => (canon backpressure.set (core func)) 🔀
| 0x09 rs:<resultlist> opts:<opts> => (canon task.return rs opts (core func)) 🔀
| 0x05 => (canon task.cancel (core func)) 🔀
| 0x0a 0x7f i:<u32> => (canon context.get i32 i (core func)) 🔀
| 0x0b 0x7f i:<u32> => (canon context.set i32 i (core func)) 🔀
| 0x0c async?:<async>? => (canon yield async? (core func)) 🔀
| 0x06 async?:<async?> => (canon subtask.cancel async? (core func)) 🔀
| 0x0d => (canon subtask.drop (core func)) 🔀
| 0x0e t:<typeidx> => (canon stream.new t (core func)) 🔀
| 0x0f t:<typeidx> opts:<opts> => (canon stream.read t opts (core func)) 🔀
Expand Down
Loading