Skip to content

Commit ffcbfde

Browse files
authored
Add subtask.cancel/task.cancel built-ins (#506)
* Add subtask.cancel/task.cancel built-ins Closes #495 * Don't forget to mention the callback case * Don't forget the 'async' immediate of subtask.cancel * Fix grammar, as suggested by @dicej
1 parent 900b821 commit ffcbfde

File tree

6 files changed

+934
-203
lines changed

6 files changed

+934
-203
lines changed

design/mvp/Async.md

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ summary of the motivation and animated sketch of the design in action.
2222
* [Backpressure](#backpressure)
2323
* [Returning](#returning)
2424
* [Borrows](#borrows)
25+
* [Cancellation](#cancellation)
2526
* [Async ABI](#async-abi)
2627
* [Async Import ABI](#async-import-abi)
2728
* [Async Export ABI](#async-export-abi)
@@ -470,9 +471,9 @@ loop interleaving `stream.read`s (of the readable end passed for `in`) and
470471
`stream.write`s (of the writable end it `stream.new`ed) before exiting the
471472
task.
472473

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

477478
### Borrows
478479

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

499+
### Cancellation
500+
501+
Once an async call has started, blocked and been added to the caller's table of
502+
waitables, the caller may decide that it no longer needs the results or effects
503+
of the subtask. In this case, the caller may **cancel** the subtask by calling
504+
the [`subtask.cancel`] built-in.
505+
506+
Once cancellation is requested, since the subtask may have already racily
507+
returned a value, the caller may still receive a return value. However, the
508+
caller may also be notified that the subtask is in one of two additional
509+
terminal states:
510+
* the subtask was **cancelled before it started**, in which case the caller's
511+
arguments were not passed to the callee (in particular, owned handles were
512+
not transferred); or
513+
* the subtask was **cancelled before it returned**, in which case the arguments
514+
were passed, but no values were returned. However, all borrowed handles lent
515+
during the call have been dropped.
516+
517+
Thus there are *three* terminal states for a subtask: returned,
518+
cancelled-before-started and cancelled-before-returned. A subtask in one of
519+
these terminal states is said to be **resolved**. A resolved subtask has always
520+
dropped all the borrowed handles that it was lent during the call.
521+
522+
As with the rest of async, cancellation is *cooperative*, allowing the subtask
523+
a chance to execute and clean up before it transitions to a resolved state (and
524+
relinquishes its borrowed handles). Since there are valid use cases where
525+
successful cancellation requires performing additional I/O using borrowed
526+
handles and potentially blocking in the process, the Component Model does not
527+
impose any limits on what a subtask can do after receiving a cancellation
528+
request nor is there a non-cooperative option to force termination (instead,
529+
this functionality would come as part of a future "[blast zone]" feature).
530+
Thus, the `subtask.cancel` built-in can block and works just like an import
531+
call in that it can be called synchronously or asynchronously.
532+
533+
On the callee side of cancellation: when a caller requests cancellation via
534+
`subtask.cancel`, the callee receives a [`TASK_CANCELLED`] event (as produced
535+
by one of the `waitable-set.{wait,poll}` or `yield` built-ins or as received by
536+
the `callback` function). Upon receiving notice of cancellation, the callee can
537+
call the [`task.cancel`] built-in to resolve the subtask without returning a
538+
value. Alternatively, the callee can still call [`task.return`] as-if there
539+
were no cancellation. `task.cancel` doesn't take a value to return but does
540+
enforce the same [borrow](#borrows) rules as `task.return`. Ideally, a callee
541+
will `task.cancel` itself as soon as possible after receiving a
542+
`TASK_CANCELLED` event so that any caller waiting for the recovery of lent
543+
handles is unblocked ASAP. As with `task.return`, after calling `task.cancel`,
544+
a callee can continue executing before exiting the task.
545+
546+
See the [`canon_subtask_cancel`] and [`canon_task_cancel`] functions in the
547+
Canonical ABI explainer for more details.
498548

499549
## Async ABI
500550

@@ -924,8 +974,6 @@ will be added in future chunks roughly in the order listed to complete the full
924974
comes after:
925975
* remove the temporary trap mentioned above that occurs when a `read` and
926976
`write` of a stream/future happen from within the same component instance
927-
* `subtask.cancel`: allow a supertask to signal to a subtask that its result is
928-
no longer wanted and to please wrap it up promptly
929977
* zero-copy forwarding/splicing
930978
* some way to say "no more elements are coming for a while"
931979
* `recursive` function type attribute: allow a function to opt in to
@@ -958,6 +1006,8 @@ comes after:
9581006
[`context.set`]: Explainer.md#-contextset
9591007
[`backpressure.set`]: Explainer.md#-backpressureset
9601008
[`task.return`]: Explainer.md#-taskreturn
1009+
[`task.cancel`]: Explainer.md#-taskcancel
1010+
[`subtask.cancel`]: Explainer.md#-subtaskcancel
9611011
[`yield`]: Explainer.md#-yield
9621012
[`waitable-set.wait`]: Explainer.md#-waitable-setwait
9631013
[`waitable-set.poll`]: Explainer.md#-waitable-setpoll
@@ -973,10 +1023,13 @@ comes after:
9731023
[`canon_backpressure_set`]: CanonicalABI.md#-canon-backpressureset
9741024
[`canon_waitable_set_wait`]: CanonicalABI.md#-canon-waitable-setwait
9751025
[`canon_task_return`]: CanonicalABI.md#-canon-taskreturn
1026+
[`canon_task_cancel`]: CanonicalABI.md#-canon-taskcancel
1027+
[`canon_subtask_cancel`]: CanonicalABI.md#-canon-subtaskcancel
9761028
[`Task`]: CanonicalABI.md#task-state
9771029
[`Task.enter`]: CanonicalABI.md#task-state
9781030
[`Task.wait_on`]: CanonicalABI.md#task-state
9791031
[`Waitable`]: CanonicalABI.md#waitable-state
1032+
[`TASK_CANCELLED`]: CanonicalABI.md#waitable-state
9801033
[`Task`]: CanonicalABI.md#task-state
9811034
[`Subtask`]: CanonicalABI.md#subtask-state
9821035
[Stream State]: CanonicalABI.md#stream-state

design/mvp/Binary.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,9 +291,11 @@ canon ::= 0x00 0x00 f:<core:funcidx> opts:<opts> ft:<typeidx> => (canon lift
291291
| 0x04 rt:<typeidx> => (canon resource.rep rt (core func))
292292
| 0x08 => (canon backpressure.set (core func)) 🔀
293293
| 0x09 rs:<resultlist> opts:<opts> => (canon task.return rs opts (core func)) 🔀
294+
| 0x05 => (canon task.cancel (core func)) 🔀
294295
| 0x0a 0x7f i:<u32> => (canon context.get i32 i (core func)) 🔀
295296
| 0x0b 0x7f i:<u32> => (canon context.set i32 i (core func)) 🔀
296297
| 0x0c async?:<async>? => (canon yield async? (core func)) 🔀
298+
| 0x06 async?:<async?> => (canon subtask.cancel async? (core func)) 🔀
297299
| 0x0d => (canon subtask.drop (core func)) 🔀
298300
| 0x0e t:<typeidx> => (canon stream.new t (core func)) 🔀
299301
| 0x0f t:<typeidx> opts:<opts> => (canon stream.read t opts (core func)) 🔀

0 commit comments

Comments
 (0)