diff --git a/design/mvp/CanonicalABI.md b/design/mvp/CanonicalABI.md index a40d6f72..af69f9d2 100644 --- a/design/mvp/CanonicalABI.md +++ b/design/mvp/CanonicalABI.md @@ -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 @@ -422,6 +423,7 @@ state. self.task = task self.index = None self.storage = [0,0] + self.cancellable = False assert(self.suspended()) ``` @@ -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 @@ -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: @@ -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 @@ -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) @@ -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]) @@ -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 diff --git a/design/mvp/Concurrency.md b/design/mvp/Concurrency.md index ebf925a6..c851ef70 100644 --- a/design/mvp/Concurrency.md +++ b/design/mvp/Concurrency.md @@ -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 @@ -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. diff --git a/design/mvp/canonical-abi/definitions.py b/design/mvp/canonical-abi/definitions.py index 450efd71..17e62a52 100644 --- a/design/mvp/canonical-abi/definitions.py +++ b/design/mvp/canonical-abi/definitions.py @@ -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 @@ -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): @@ -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 @@ -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) @@ -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]) diff --git a/test/async/cancel-delivery.wast b/test/async/cancel-delivery.wast index dcd97aea..4b3ab56c 100644 --- a/test/async/cancel-delivery.wast +++ b/test/async/cancel-delivery.wast @@ -1,26 +1,6 @@ ;; This test exercises how a request for cancellation is delivered to the ;; callee: the request is recorded as pending on the callee's task and is only -;; delivered, as a TASK_CANCELLED event, via the `async callback` event loop. -;; -;; Component $C exports three async callback-lifted functions: -;; yield-until-cancel: loops returning YIELD to its event loop until the -;; TASK_CANCELLED event arrives; one more YIELD must then report NONE -;; (cancellation is delivered at most once) before it calls task.cancel -;; deferred: blocks in a synchronous future.read in its initial core -;; function, during which a cancellation request stays pending; the pending -;; request is delivered at the first return to the event loop -;; pending-survives: like deferred, but additionally calls thread.yield and -;; waitable-set.poll after the request is pending: neither may report the -;; pending cancellation (they never return TASK_CANCELLED), which must -;; survive until the task returns to its event loop -;; -;; Component $D calls each function and cancels it. `subtask.cancel async` -;; resumes the cancelled task directly, but the host is also allowed (not -;; required) to keep resuming ready threads, so yield-until-cancel may or may -;; not get far enough to resolve before the built-in returns and $D accepts -;; both outcomes. While a task is blocked in its initial core function it -;; cannot be cancelled at all, so those cancellations deterministically report -;; BLOCKED. +;; delivered via the `async callback` event loop. (component (component $C (core module $Memory (memory (export "mem") 1)) @@ -32,6 +12,8 @@ (import "" "waitable-set.new" (func $waitable-set.new (result i32))) (import "" "waitable-set.poll" (func $waitable-set.poll (param i32 i32) (result i32))) (import "" "thread.yield" (func $thread.yield (result i32))) + (import "" "future.read-async" (func $future.read-async (param i32 i32) (result i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) ;; Test 1: cancellation is delivered to a task parked in its event loop ;; after returning YIELD, and is delivered at most once @@ -92,6 +74,41 @@ ;; only the event loop delivers the still-pending cancellation (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (local.get $ws) (i32.const 4))) ) + + ;; Test 5: a callee that still needs an external event after receiving + ;; TASK_CANCELLED cannot resolve during the cancellation, so the request + ;; is reported BLOCKED even though it has already been delivered + (global $gated-delivered (mut i32) (i32.const 0)) + (global $gated-ws (mut i32) (i32.const 0)) + (func $gated-delivered (export "gated-delivered") (result i32) + (global.get $gated-delivered)) + (func $gated (export "gated") (param $futr i32) (result i32) + ;; arm a read that only the caller can complete, then park in the + ;; event loop + (if (i32.ne (call $future.read-async (local.get $futr) (i32.const 0)) + (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + (global.set $gated-ws (call $waitable-set.new)) + (call $waitable.join (local.get $futr) (global.get $gated-ws)) + (i32.const 1 (; YIELD ;)) + ) + (func $gated_cb (export "gated_cb") (param $event_code i32) (param $index i32) (param $payload i32) (result i32) + (if (i32.eq (local.get $event_code) (i32.const 6 (; TASK_CANCELLED ;))) + (then + (if (global.get $gated-delivered) (then unreachable)) + (global.set $gated-delivered (i32.const 1)) + ;; the read is still outstanding, so this task cannot resolve yet + (return (i32.const 1 (; YIELD ;))))) + (if (i32.ne (local.get $event_code) (i32.const 0 (; NONE ;))) + (then unreachable)) + (if (i32.eq (i32.const 0 (; NONE ;)) + (call $waitable-set.poll (global.get $gated-ws) (i32.const 8))) + (then (return (i32.const 1 (; YIELD ;))))) + ;; the read completed; the cancellation must already have arrived + (if (i32.eqz (global.get $gated-delivered)) (then unreachable)) + (call $task.cancel) + (i32.const 0 (; EXIT ;)) + ) ) (type $FT (future)) (canon task.cancel (core func $task.cancel)) @@ -99,6 +116,8 @@ (canon waitable-set.new (core func $waitable-set.new)) (canon waitable-set.poll (memory (core memory $memory "mem")) (core func $waitable-set.poll)) (canon thread.yield (core func $thread.yield)) + (canon future.read $FT async (memory (core memory $memory "mem")) (core func $future.read-async)) + (canon waitable.join (core func $waitable.join)) (core instance $cm (instantiate $CM (with "" (instance (export "mem" (memory $memory "mem")) (export "task.cancel" (func $task.cancel)) @@ -106,6 +125,8 @@ (export "waitable-set.new" (func $waitable-set.new)) (export "waitable-set.poll" (func $waitable-set.poll)) (export "thread.yield" (func $thread.yield)) + (export "future.read-async" (func $future.read-async)) + (export "waitable.join" (func $waitable.join)) )))) (func (export "yield-until-cancel") async (canon lift (core func $cm "yield-until-cancel") @@ -119,6 +140,11 @@ (core func $cm "pending-survives") async (callback (core func $cm "expect-cancelled_cb")) )) + (func (export "gated") async (param "fut" $FT) (canon lift + (core func $cm "gated") + async (callback (core func $cm "gated_cb")) + )) + (func (export "gated-delivered") (result u32) (canon lift (core func $cm "gated-delivered"))) ) (component $D @@ -126,6 +152,8 @@ (import "yield-until-cancel" (func $yield-until-cancel async)) (import "deferred" (func $deferred async (param "fut" $FT))) (import "pending-survives" (func $pending-survives async (param "fut" $FT))) + (import "gated" (func $gated async (param "fut" $FT))) + (import "gated-delivered" (func $gated-delivered (result u32))) (core module $Memory (memory (export "mem") 1)) (core instance $memory (instantiate $Memory)) @@ -141,6 +169,8 @@ (import "" "yield-until-cancel" (func $yield-until-cancel (result i32))) (import "" "deferred" (func $deferred (param i32) (result i32))) (import "" "pending-survives" (func $pending-survives (param i32) (result i32))) + (import "" "gated" (func $gated (param i32) (result i32))) + (import "" "gated-delivered" (func $gated-delivered (result i32))) ;; wait until $sub resolves to CANCELLED_BEFORE_RETURNED, then drop it (func $await-cancelled (param $sub i32) @@ -239,6 +269,65 @@ ;; cancellation, which is delivered at the callee's event loop (call $await-cancelled (local.get $subtask)) + ;; ========================================== + ;; Test 4: a thread that has not returned to its event loop stays + ;; uncancellable even once it is ready to run again + ;; ========================================== + + ;; create future for deferred to read + (local.set $ret64 (call $future.new)) + (local.set $futr (i32.wrap_i64 (local.get $ret64))) + (local.set $futw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + + ;; call deferred; it blocks in a synchronous future.read + (local.set $ret (call $deferred (local.get $futr))) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + + ;; write first, so that the callee's thread is ready before the cancel + (if (i32.ne (call $future.write (local.get $futw) (i32.const 0)) (i32.const 0 (; COMPLETED ;))) + (then unreachable)) + + ;; still BLOCKED: a ready thread is only resumed while parked in its + ;; event loop, and deferred would resolve immediately if it were + ;; resumed here + (if (i32.ne (call $subtask.cancel (local.get $subtask)) (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + + ;; blocking lets the callee reach its event loop and resolve + (call $await-cancelled (local.get $subtask)) + + ;; ========================================== + ;; Test 5: a delivered cancellation still reports BLOCKED when the + ;; callee needs an external event before it can resolve + ;; ========================================== + + ;; create future for gated to read + (local.set $ret64 (call $future.new)) + (local.set $futr (i32.wrap_i64 (local.get $ret64))) + (local.set $futw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + + ;; call gated; it parks in its event loop with the read outstanding + (local.set $ret (call $gated (local.get $futr))) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + + ;; unlike test 1, this callee cannot resolve on its own, so the outcome + ;; is deterministic + (if (i32.ne (call $subtask.cancel (local.get $subtask)) (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + + ;; ...yet the request was already delivered before BLOCKED was reported + (if (i32.ne (call $gated-delivered) (i32.const 1)) + (then unreachable)) + + ;; writing the future now lets it resolve + (if (i32.ne (call $future.write (local.get $futw) (i32.const 0)) (i32.const 0 (; COMPLETED ;))) + (then unreachable)) + (call $await-cancelled (local.get $subtask)) + ;; all tests passed (i32.const 42) ) @@ -253,6 +342,8 @@ (canon lower (func $yield-until-cancel) async (memory (core memory $memory "mem")) (core func $yield-until-cancel')) (canon lower (func $deferred) async (memory (core memory $memory "mem")) (core func $deferred')) (canon lower (func $pending-survives) async (memory (core memory $memory "mem")) (core func $pending-survives')) + (canon lower (func $gated) async (memory (core memory $memory "mem")) (core func $gated')) + (canon lower (func $gated-delivered) (core func $gated-delivered')) (core instance $dm (instantiate $DM (with "" (instance (export "mem" (memory $memory "mem")) (export "subtask.cancel" (func $subtask.cancel)) @@ -265,6 +356,8 @@ (export "yield-until-cancel" (func $yield-until-cancel')) (export "deferred" (func $deferred')) (export "pending-survives" (func $pending-survives')) + (export "gated" (func $gated')) + (export "gated-delivered" (func $gated-delivered')) )))) (func (export "run") async (result u32) (canon lift (core func $dm "run"))) ) @@ -274,14 +367,18 @@ (with "yield-until-cancel" (func $c "yield-until-cancel")) (with "deferred" (func $c "deferred")) (with "pending-survives" (func $c "pending-survives")) + (with "gated" (func $c "gated")) + (with "gated-delivered" (func $c "gated-delivered")) )) (func (export "run") (alias export $d "run")) ) (assert_return (invoke "run") (u32.const 42)) -;; subtask.cancel only resumes ready threads. A stackful (non-callback) task -;; parked in 'waitable-set.wait' that contains no ready waitable is not ready -;; and cannot return TASK_CANCELLED. +;; subtask.cancel only resumes a thread that is both ready and cancellable, and +;; only a `callback` thread parked in its event loop is ever cancellable. A +;; stackful (non-callback) task therefore can never be cancelled: neither while +;; parked in 'waitable-set.wait' with no ready waitable (not ready), nor once +;; the caller has completed its read (ready, but still not cancellable). (component (component $C (core module $Memory (memory (export "mem") 1)) @@ -351,9 +448,10 @@ (import "" "waitable.join" (func $waitable.join (param i32 i32))) (import "" "waitable-set.new" (func $waitable-set.new (result i32))) (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) - (func (export "run") (result i32) + (func $round (param $write-first i32) (local $ret64 i64) (local $futr i32) (local $futw i32) (local $packed i32) (local $sub i32) (local $ws i32) + (i32.store (i32.const 24) (i32.const 0xbad0bad0)) (local.set $ret64 (call $future.new)) (local.set $futr (i32.wrap_i64 (local.get $ret64))) (local.set $futw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) @@ -362,14 +460,26 @@ (if (i32.ne (i32.and (local.get $packed) (i32.const 0xf)) (i32.const 1 (; STARTED ;))) (then unreachable)) (local.set $sub (i32.shr_u (local.get $packed) (i32.const 4))) - ;; no ready thread to resume, so the request is only recorded + ;; optionally complete the read first, which leaves the stackful task's + ;; thread ready to run + (if (local.get $write-first) + (then + (if (i32.ne (call $future.write (local.get $futw) (i32.const 16)) + (i32.const 0 (; COMPLETED ;))) + (then unreachable)))) + + ;; a stackful thread is never cancellable, ready or not, so the request + ;; is only recorded (if (i32.ne (call $subtask.cancel-async (local.get $sub)) (i32.const -1 (; BLOCKED ;))) (then unreachable)) + ;; unblock the stackful task; it never sees the cancellation - (if (i32.ne (call $future.write (local.get $futw) (i32.const 16)) - (i32.const 0 (; COMPLETED ;))) - (then unreachable)) + (if (i32.eqz (local.get $write-first)) + (then + (if (i32.ne (call $future.write (local.get $futw) (i32.const 16)) + (i32.const 0 (; COMPLETED ;))) + (then unreachable)))) (local.set $ws (call $waitable-set.new)) (call $waitable.join (local.get $sub) (local.get $ws)) (if (i32.ne (call $waitable-set.wait (local.get $ws) (i32.const 0)) @@ -382,7 +492,11 @@ (if (i32.ne (i32.load (i32.const 24)) (i32.const 42)) (then unreachable)) (call $waitable.join (local.get $sub) (i32.const 0)) - (call $subtask.drop (local.get $sub)) + (call $subtask.drop (local.get $sub))) + + (func (export "run") (result i32) + (call $round (i32.const 0 (; not ready when cancelled ;))) + (call $round (i32.const 1 (; ready when cancelled ;))) (i32.const 42)) ) (core instance $main (instantiate $Main (with "" (instance diff --git a/test/async/cancel-instance-wide-resume.wast b/test/async/cancel-instance-wide-resume.wast deleted file mode 100644 index 7fe242a1..00000000 --- a/test/async/cancel-instance-wide-resume.wast +++ /dev/null @@ -1,185 +0,0 @@ -;; The resume performed by 'subtask.cancel' may pick any ready thread of the -;; callee's instance, not just a thread of the task being cancelled. This test -;; forces that distinction by arranging for the only ready thread in $C to -;; belong to a different task than the one being cancelled. -;; -;; At least one resume always happens, so hold-lock always runs to completion -;; here, which the caller observes by polling. Whether the host then keeps -;; resuming threads is nondeterministic: it may stop there, in which case the -;; cancel reports BLOCKED and park is woken later once the caller blocks, or it -;; may go on to resume park (now that hold-lock has released the lock) and -;; report CANCELLED_BEFORE_RETURNED eagerly. -(component - (component $C - (core module $Memory (memory (export "mem") 1)) - (core instance $memory (instantiate $Memory)) - (core module $CM - (import "" "mem" (memory 1)) - (import "" "task.return" (func $task.return (param i32))) - (import "" "task.cancel" (func $task.cancel)) - (import "" "future.read" (func $future.read (param i32 i32) (result i32))) - (import "" "waitable.join" (func $waitable.join (param i32 i32))) - (import "" "waitable-set.new" (func $waitable-set.new (result i32))) - (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) - - ;; Parks in the event loop on a waitable set that never gets an event, so - ;; only the delivery of a pending cancellation can wake this task which - ;; cannot happen while the exclusive lock is held. - (global $never (mut i32) (i32.const 0)) - (func $start (global.set $never (call $waitable-set.new))) - (start $start) - (func (export "park") (result i32) - (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (global.get $never) (i32.const 4)))) - (func (export "park-cb") (param $event i32) (param i32 i32) (result i32) - (if (i32.ne (local.get $event) (i32.const 6 (; TASK_CANCELLED ;))) - (then unreachable)) - (call $task.cancel) - (i32.const 0 (; EXIT ;))) - - ;; Blocks holding the instance's exclusive lock for as long as it is - ;; blocked. Once the caller writes the future, this task's thread is ready - ;; but the lock is still held. - (func (export "hold-lock") (param $futr i32) (result i32) - (local $ws i32) - (if (i32.ne (i32.const -1 (; BLOCKED ;)) - (call $future.read (local.get $futr) (i32.const 0))) - (then unreachable)) - (local.set $ws (call $waitable-set.new)) - (call $waitable.join (local.get $futr) (local.get $ws)) - (if (i32.ne (i32.const 4 (; FUTURE_READ ;)) - (call $waitable-set.wait (local.get $ws) (i32.const 8))) - (then unreachable)) - (call $task.return (i32.const 43)) - (i32.const 0 (; EXIT ;))) - (func (export "unreachable-cb") (param i32 i32 i32) (result i32) - unreachable) - ) - (type $FT (future)) - (canon task.return (result u32) (core func $task.return)) - (canon task.cancel (core func $task.cancel)) - (canon future.read $FT async (memory (core memory $memory "mem")) (core func $future.read)) - (canon waitable.join (core func $waitable.join)) - (canon waitable-set.new (core func $waitable-set.new)) - (canon waitable-set.wait (memory (core memory $memory "mem")) (core func $waitable-set.wait)) - (core instance $cm (instantiate $CM (with "" (instance - (export "mem" (memory $memory "mem")) - (export "task.return" (func $task.return)) - (export "task.cancel" (func $task.cancel)) - (export "future.read" (func $future.read)) - (export "waitable.join" (func $waitable.join)) - (export "waitable-set.new" (func $waitable-set.new)) - (export "waitable-set.wait" (func $waitable-set.wait)))))) - (func (export "park") async - (canon lift (core func $cm "park") async (callback (core func $cm "park-cb")))) - (func (export "hold-lock") async (param "fut" $FT) (result u32) - (canon lift (core func $cm "hold-lock") async (callback (core func $cm "unreachable-cb")))) - ) - (instance $c (instantiate $C)) - (core module $Memory (memory (export "mem") 1)) - (core instance $memory (instantiate $Memory)) - (type $FT (future)) - (canon future.new $FT (core func $future.new)) - (canon future.write $FT (memory (core memory $memory "mem")) (core func $future.write)) - (canon lower (func $c "park") async (core func $park')) - (canon lower (func $c "hold-lock") async (memory (core memory $memory "mem")) (core func $hold-lock')) - (canon subtask.cancel async (core func $subtask.cancel-async)) - (canon subtask.drop (core func $subtask.drop)) - (canon waitable.join (core func $waitable.join)) - (canon waitable-set.new (core func $waitable-set.new)) - (canon waitable-set.poll (memory (core memory $memory "mem")) (core func $waitable-set.poll)) - (canon waitable-set.wait (memory (core memory $memory "mem")) (core func $waitable-set.wait)) - - (core module $Main - (import "" "mem" (memory 1)) - (import "" "future.new" (func $future.new (result i64))) - (import "" "future.write" (func $future.write (param i32 i32) (result i32))) - (import "" "park" (func $park (result i32))) - (import "" "hold-lock" (func $hold-lock (param i32 i32) (result i32))) - (import "" "subtask.cancel-async" (func $subtask.cancel-async (param i32) (result i32))) - (import "" "subtask.drop" (func $subtask.drop (param i32))) - (import "" "waitable.join" (func $waitable.join (param i32 i32))) - (import "" "waitable-set.new" (func $waitable-set.new (result i32))) - (import "" "waitable-set.poll" (func $waitable-set.poll (param i32 i32) (result i32))) - (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) - (func (export "run") (result i32) - (local $ret64 i64) (local $futr i32) (local $futw i32) - (local $packed i32) (local $ret i32) - (local $park-sub i32) (local $hold-sub i32) - (local $park-ws i32) (local $hold-ws i32) - - ;; start "park"; returning WAIT releases the lock, so it is free here - (local.set $packed (call $park)) - (if (i32.ne (i32.and (local.get $packed) (i32.const 0xf)) (i32.const 1 (; STARTED ;))) - (then unreachable)) - (local.set $park-sub (i32.shr_u (local.get $packed) (i32.const 4))) - - ;; start hold-lock; it takes the lock and blocks while holding it - (local.set $ret64 (call $future.new)) - (local.set $futr (i32.wrap_i64 (local.get $ret64))) - (local.set $futw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) - (local.set $packed (call $hold-lock (local.get $futr) (i32.const 24))) - (if (i32.ne (i32.and (local.get $packed) (i32.const 0xf)) (i32.const 1 (; STARTED ;))) - (then unreachable)) - (local.set $hold-sub (i32.shr_u (local.get $packed) (i32.const 4))) - (local.set $hold-ws (call $waitable-set.new)) - (call $waitable.join (local.get $hold-sub) (local.get $hold-ws)) - - ;; complete the read: hold-lock's thread is now ready, but it still - ;; holds the lock and nothing has resumed it - (if (i32.ne (i32.const 0 (; COMPLETED ;)) - (call $future.write (local.get $futw) (i32.const 16))) - (then unreachable)) - (if (i32.ne (call $waitable-set.poll (local.get $hold-ws) (i32.const 0)) - (i32.const 0 (; NONE ;))) - (then unreachable)) - - ;; cancel park: its own thread is not ready while the lock is held, so - ;; the only candidate belongs to hold-lock - (local.set $ret (call $subtask.cancel-async (local.get $park-sub))) - - ;; hold-lock must have been resumed by the cancel and run to completion - (if (i32.ne (call $waitable-set.poll (local.get $hold-ws) (i32.const 0)) - (i32.const 1 (; SUBTASK ;))) - (then unreachable)) - (if (i32.ne (i32.load (i32.const 0)) (local.get $hold-sub)) - (then unreachable)) - (if (i32.ne (i32.load (i32.const 4)) (i32.const 2 (; RETURNED ;))) - (then unreachable)) - (if (i32.ne (i32.load (i32.const 24)) (i32.const 43)) - (then unreachable)) - (call $waitable.join (local.get $hold-sub) (i32.const 0)) - (call $subtask.drop (local.get $hold-sub)) - - ;; if the host stopped after resuming hold-lock, the request is still - ;; only pending; with the lock now free, blocking lets park receive it - (if (i32.eq (local.get $ret) (i32.const -1 (; BLOCKED ;))) - (then - (local.set $park-ws (call $waitable-set.new)) - (call $waitable.join (local.get $park-sub) (local.get $park-ws)) - (if (i32.ne (call $waitable-set.wait (local.get $park-ws) (i32.const 0)) - (i32.const 1 (; SUBTASK ;))) - (then unreachable)) - (if (i32.ne (i32.load (i32.const 0)) (local.get $park-sub)) - (then unreachable)) - (local.set $ret (i32.load (i32.const 4))) - (call $waitable.join (local.get $park-sub) (i32.const 0)))) - (if (i32.ne (local.get $ret) (i32.const 4 (; CANCELLED_BEFORE_RETURNED ;))) - (then unreachable)) - (call $subtask.drop (local.get $park-sub)) - (i32.const 42)) - ) - (core instance $main (instantiate $Main (with "" (instance - (export "mem" (memory $memory "mem")) - (export "future.new" (func $future.new)) - (export "future.write" (func $future.write)) - (export "park" (func $park')) - (export "hold-lock" (func $hold-lock')) - (export "subtask.cancel-async" (func $subtask.cancel-async)) - (export "subtask.drop" (func $subtask.drop)) - (export "waitable.join" (func $waitable.join)) - (export "waitable-set.new" (func $waitable-set.new)) - (export "waitable-set.poll" (func $waitable-set.poll)) - (export "waitable-set.wait" (func $waitable-set.wait)))))) - (func (export "run") async (result u32) (canon lift (core func $main "run"))) -) -(assert_return (invoke "run") (u32.const 42)) diff --git a/test/async/cancel-resumed-callback-switch.wast b/test/async/cancel-resumed-callback-switch.wast new file mode 100644 index 00000000..9741375a --- /dev/null +++ b/test/async/cancel-resumed-callback-switch.wast @@ -0,0 +1,331 @@ +;; A `callback` function resumed by 'subtask.cancel' to receive TASK_CANCELLED +;; runs like any other callback: it may switch to an explicit thread with +;; 'thread.suspend-then-resume', including one belonging to a different task of +;; the same component instance. If the thread it switches to then blocks, +;; control flows back out through 'subtask.cancel', which reports BLOCKED +;; because the cancelled task has not resolved; its callback is still +;; suspended part-way through. + +;; The explicit thread belongs to the cancelled task itself. +(component + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $Table (table (export "tbl") 1 funcref)) + (core instance $table (instantiate $Table)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "tbl" (table $tbl 1 funcref)) + (import "" "task.cancel" (func $task.cancel)) + (import "" "future.read" (func $future.read (param i32 i32) (result i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "thread.index" (func $thread.index (result i32))) + (import "" "thread.new-indirect" (func $thread.new-indirect (param i32 i32) (result i32))) + (import "" "thread.resume-later" (func $thread.resume-later (param i32))) + (import "" "thread.suspend-then-resume" (func $thread.suspend-then-resume (param i32) (result i32))) + + (global $futr (mut i32) (i32.const 0)) + (global $park-thread (mut i32) (i32.const 0xdead)) + (global $worker-ran (mut i32) (i32.const 0)) + (func (export "ran") (result i32) (global.get $worker-ran)) + + (func $worker (param i32) + (global.set $worker-ran (i32.const 1)) + ;; block; control returns out through 'subtask.cancel' + (if (i32.ne (i32.const 0 (; COMPLETED ;)) + (call $future.read (global.get $futr) (i32.const 16))) + (then unreachable)) + ;; hand control back to the parked callback + (call $thread.resume-later (global.get $park-thread))) + (elem (table $tbl) (i32.const 0) func $worker) + + (func (export "park") (param $f i32) (result i32) + (global.set $futr (local.get $f)) + (global.set $park-thread (call $thread.index)) + (i32.or (i32.const 2 (; WAIT ;)) + (i32.shl (call $waitable-set.new) (i32.const 4)))) + (func (export "park-cb") (param $event i32) (param i32 i32) (result i32) + (if (i32.ne (local.get $event) (i32.const 6 (; TASK_CANCELLED ;))) + (then unreachable)) + (drop (call $thread.suspend-then-resume + (call $thread.new-indirect (i32.const 0) (i32.const 0)))) + (call $task.cancel) + (i32.const 0 (; EXIT ;))) + ) + (type $FT (future)) + (core type $ThreadFT (func (param i32))) + (alias core export $table "tbl" (core table $tbl)) + (canon task.cancel (core func $task.cancel)) + (canon future.read $FT (memory (core memory $memory "mem")) (core func $future.read)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon thread.index (core func $thread.index)) + (canon thread.new-indirect $ThreadFT (core table $tbl) (core func $thread.new-indirect)) + (canon thread.resume-later (core func $thread.resume-later)) + (canon thread.suspend-then-resume (core func $thread.suspend-then-resume)) + (core instance $cm (instantiate $CM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "tbl" (table $tbl)) + (export "task.cancel" (func $task.cancel)) + (export "future.read" (func $future.read)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "thread.index" (func $thread.index)) + (export "thread.new-indirect" (func $thread.new-indirect)) + (export "thread.resume-later" (func $thread.resume-later)) + (export "thread.suspend-then-resume" (func $thread.suspend-then-resume)))))) + (func (export "park") async (param "fut" $FT) + (canon lift (core func $cm "park") async (callback (core func $cm "park-cb")))) + (func (export "ran") (result u32) (canon lift (core func $cm "ran"))) + ) + (instance $c (instantiate $C)) + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (type $FT (future)) + (canon future.new $FT (core func $future.new)) + (canon future.write $FT async (memory (core memory $memory "mem")) (core func $future.write)) + (canon lower (func $c "park") async (core func $park')) + (canon lower (func $c "ran") (core func $ran')) + (canon subtask.cancel async (core func $subtask.cancel)) + (canon subtask.drop (core func $subtask.drop)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.wait (memory (core memory $memory "mem")) (core func $waitable-set.wait)) + + (core module $Main + (import "" "mem" (memory 1)) + (import "" "future.new" (func $future.new (result i64))) + (import "" "future.write" (func $future.write (param i32 i32) (result i32))) + (import "" "park" (func $park (param i32) (result i32))) + (import "" "ran" (func $ran (result i32))) + (import "" "subtask.cancel" (func $subtask.cancel (param i32) (result i32))) + (import "" "subtask.drop" (func $subtask.drop (param i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (func (export "run") (result i32) + (local $ret64 i64) (local $futr i32) (local $futw i32) + (local $packed i32) (local $sub i32) (local $ws i32) + (local.set $ret64 (call $future.new)) + (local.set $futr (i32.wrap_i64 (local.get $ret64))) + (local.set $futw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + + (local.set $packed (call $park (local.get $futr))) + (if (i32.ne (i32.and (local.get $packed) (i32.const 0xf)) (i32.const 1 (; STARTED ;))) + (then unreachable)) + (local.set $sub (i32.shr_u (local.get $packed) (i32.const 4))) + + ;; the callback is resumed, switches to the explicit thread, and that + ;; thread blocks, so control lands back here + (if (i32.ne (call $subtask.cancel (local.get $sub)) (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + (if (i32.eqz (call $ran)) (then unreachable)) + + ;; unblock the explicit thread, which hands control back to the callback + (if (i32.ne (i32.const 0 (; COMPLETED ;)) + (call $future.write (local.get $futw) (i32.const 16))) + (then unreachable)) + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $sub) (local.get $ws)) + (if (i32.ne (call $waitable-set.wait (local.get $ws) (i32.const 0)) + (i32.const 1 (; SUBTASK ;))) + (then unreachable)) + (if (i32.ne (i32.load (i32.const 0)) (local.get $sub)) (then unreachable)) + (if (i32.ne (i32.load (i32.const 4)) (i32.const 4 (; CANCELLED_BEFORE_RETURNED ;))) + (then unreachable)) + (call $waitable.join (local.get $sub) (i32.const 0)) + (call $subtask.drop (local.get $sub)) + (i32.const 42)) + ) + (core instance $main (instantiate $Main (with "" (instance + (export "mem" (memory $memory "mem")) + (export "future.new" (func $future.new)) + (export "future.write" (func $future.write)) + (export "park" (func $park')) + (export "ran" (func $ran')) + (export "subtask.cancel" (func $subtask.cancel)) + (export "subtask.drop" (func $subtask.drop)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)))))) + (func (export "run") async (result u32) (canon lift (core func $main "run"))) +) +(assert_return (invoke "run") (u32.const 42)) + +;; The same, but the explicit thread belongs to a different task of the same +;; component instance: 'holder', which spawned it and then blocked. +(component + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $Table (table (export "tbl") 1 funcref)) + (core instance $table (instantiate $Table)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "tbl" (table $tbl 1 funcref)) + (import "" "task.return" (func $task.return (param i32))) + (import "" "task.cancel" (func $task.cancel)) + (import "" "future.read" (func $future.read (param i32 i32) (result i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "thread.index" (func $thread.index (result i32))) + (import "" "thread.new-indirect" (func $thread.new-indirect (param i32 i32) (result i32))) + (import "" "thread.resume-later" (func $thread.resume-later (param i32))) + (import "" "thread.suspend-then-resume" (func $thread.suspend-then-resume (param i32) (result i32))) + + (global $worker-futr (mut i32) (i32.const 0)) + (global $worker-thread (mut i32) (i32.const 0xdead)) + (global $park-thread (mut i32) (i32.const 0xdead)) + (global $worker-ran (mut i32) (i32.const 0)) + (func (export "ran") (result i32) (global.get $worker-ran)) + + ;; an explicit thread of 'holder', left suspended until 'park's callback + ;; switches to it + (func $worker (param i32) + (global.set $worker-ran (i32.const 1)) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) + (call $future.read (global.get $worker-futr) (i32.const 16))) + (then unreachable)) + (call $thread.resume-later (global.get $park-thread))) + (elem (table $tbl) (i32.const 0) func $worker) + + (func (export "holder") (param $f i32) + (global.set $worker-thread (call $thread.new-indirect (i32.const 0) (i32.const 0))) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) + (call $future.read (local.get $f) (i32.const 16))) + (then unreachable)) + (call $task.return (i32.const 43))) + + (func (export "park") (param $f i32) (result i32) + (global.set $worker-futr (local.get $f)) + (global.set $park-thread (call $thread.index)) + (i32.or (i32.const 2 (; WAIT ;)) + (i32.shl (call $waitable-set.new) (i32.const 4)))) + (func (export "park-cb") (param $event i32) (param i32 i32) (result i32) + (if (i32.ne (local.get $event) (i32.const 6 (; TASK_CANCELLED ;))) + (then unreachable)) + (drop (call $thread.suspend-then-resume (global.get $worker-thread))) + (call $task.cancel) + (i32.const 0 (; EXIT ;))) + ) + (type $FT (future)) + (core type $ThreadFT (func (param i32))) + (alias core export $table "tbl" (core table $tbl)) + (canon task.return (result u32) (core func $task.return)) + (canon task.cancel (core func $task.cancel)) + (canon future.read $FT (memory (core memory $memory "mem")) (core func $future.read)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon thread.index (core func $thread.index)) + (canon thread.new-indirect $ThreadFT (core table $tbl) (core func $thread.new-indirect)) + (canon thread.resume-later (core func $thread.resume-later)) + (canon thread.suspend-then-resume (core func $thread.suspend-then-resume)) + (core instance $cm (instantiate $CM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "tbl" (table $tbl)) + (export "task.return" (func $task.return)) + (export "task.cancel" (func $task.cancel)) + (export "future.read" (func $future.read)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "thread.index" (func $thread.index)) + (export "thread.new-indirect" (func $thread.new-indirect)) + (export "thread.resume-later" (func $thread.resume-later)) + (export "thread.suspend-then-resume" (func $thread.suspend-then-resume)))))) + (func (export "holder") async (param "fut" $FT) (result u32) + (canon lift (core func $cm "holder") async)) + (func (export "park") async (param "fut" $FT) + (canon lift (core func $cm "park") async (callback (core func $cm "park-cb")))) + (func (export "ran") (result u32) (canon lift (core func $cm "ran"))) + ) + (instance $c (instantiate $C)) + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (type $FT (future)) + (canon future.new $FT (core func $future.new)) + (canon future.write $FT async (memory (core memory $memory "mem")) (core func $future.write)) + (canon lower (func $c "holder") async (memory (core memory $memory "mem")) (core func $holder')) + (canon lower (func $c "park") async (core func $park')) + (canon lower (func $c "ran") (core func $ran')) + (canon subtask.cancel async (core func $subtask.cancel)) + (canon subtask.drop (core func $subtask.drop)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.wait (memory (core memory $memory "mem")) (core func $waitable-set.wait)) + + (core module $Main + (import "" "mem" (memory 1)) + (import "" "future.new" (func $future.new (result i64))) + (import "" "future.write" (func $future.write (param i32 i32) (result i32))) + (import "" "holder" (func $holder (param i32 i32) (result i32))) + (import "" "park" (func $park (param i32) (result i32))) + (import "" "ran" (func $ran (result i32))) + (import "" "subtask.cancel" (func $subtask.cancel (param i32) (result i32))) + (import "" "subtask.drop" (func $subtask.drop (param i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (func (export "run") (result i32) + (local $ret64 i64) + (local $hr i32) (local $hw i32) (local $wr i32) (local $ww i32) + (local $packed i32) (local $hsub i32) (local $psub i32) (local $ws i32) + (local.set $ret64 (call $future.new)) + (local.set $hr (i32.wrap_i64 (local.get $ret64))) + (local.set $hw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (local.set $ret64 (call $future.new)) + (local.set $wr (i32.wrap_i64 (local.get $ret64))) + (local.set $ww (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + + ;; holder spawns the explicit thread and then blocks + (local.set $packed (call $holder (local.get $hr) (i32.const 24))) + (if (i32.ne (i32.and (local.get $packed) (i32.const 0xf)) (i32.const 1 (; STARTED ;))) + (then unreachable)) + (local.set $hsub (i32.shr_u (local.get $packed) (i32.const 4))) + (local.set $packed (call $park (local.get $wr))) + (if (i32.ne (i32.and (local.get $packed) (i32.const 0xf)) (i32.const 1 (; STARTED ;))) + (then unreachable)) + (local.set $psub (i32.shr_u (local.get $packed) (i32.const 4))) + + ;; park's callback switches to holder's explicit thread, which blocks + (if (i32.ne (call $subtask.cancel (local.get $psub)) (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + (if (i32.eqz (call $ran)) (then unreachable)) + + (local.set $ws (call $waitable-set.new)) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) + (call $future.write (local.get $ww) (i32.const 16))) + (then unreachable)) + (call $waitable.join (local.get $psub) (local.get $ws)) + (if (i32.ne (call $waitable-set.wait (local.get $ws) (i32.const 0)) + (i32.const 1 (; SUBTASK ;))) + (then unreachable)) + (if (i32.ne (i32.load (i32.const 0)) (local.get $psub)) (then unreachable)) + (if (i32.ne (i32.load (i32.const 4)) (i32.const 4 (; CANCELLED_BEFORE_RETURNED ;))) + (then unreachable)) + (call $waitable.join (local.get $psub) (i32.const 0)) + (call $subtask.drop (local.get $psub)) + + (if (i32.ne (i32.const 0 (; COMPLETED ;)) + (call $future.write (local.get $hw) (i32.const 16))) + (then unreachable)) + (call $waitable.join (local.get $hsub) (local.get $ws)) + (if (i32.ne (call $waitable-set.wait (local.get $ws) (i32.const 0)) + (i32.const 1 (; SUBTASK ;))) + (then unreachable)) + (if (i32.ne (i32.load (i32.const 0)) (local.get $hsub)) (then unreachable)) + (if (i32.ne (i32.load (i32.const 4)) (i32.const 2 (; RETURNED ;))) (then unreachable)) + (if (i32.ne (i32.load (i32.const 24)) (i32.const 43)) (then unreachable)) + (call $waitable.join (local.get $hsub) (i32.const 0)) + (call $subtask.drop (local.get $hsub)) + (i32.const 42)) + ) + (core instance $main (instantiate $Main (with "" (instance + (export "mem" (memory $memory "mem")) + (export "future.new" (func $future.new)) + (export "future.write" (func $future.write)) + (export "holder" (func $holder')) + (export "park" (func $park')) + (export "ran" (func $ran')) + (export "subtask.cancel" (func $subtask.cancel)) + (export "subtask.drop" (func $subtask.drop)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)))))) + (func (export "run") async (result u32) (canon lift (core func $main "run"))) +) +(assert_return (invoke "run") (u32.const 42)) diff --git a/test/async/cancel-targeted-resume.wast b/test/async/cancel-targeted-resume.wast new file mode 100644 index 00000000..997fa22c --- /dev/null +++ b/test/async/cancel-targeted-resume.wast @@ -0,0 +1,699 @@ +;; 'subtask.cancel' resumes at most one thread: the implicit thread of the task +;; being cancelled, and only while that thread is parked in its `callback` event +;; loop; nothing else. Each case checks that the thread in question did not run, +;; whether the cancellation ends up BLOCKED or the targeted thread resolves it. + +;; The implicit thread of a `callback` task that has already returned EXIT is +;; gone, so there is nothing for the cancellation to resume, even though the +;; task is still unresolved because an explicit thread is keeping it alive. +;; 'exiter' never blocks, so it has left its event loop by the time the caller +;; regains control. +(component + (component $C + (core module $Table (table (export "tbl") 1 funcref)) + (core instance $table (instantiate $Table)) + (core module $CM + (import "" "tbl" (table $tbl 1 funcref)) + (import "" "task.return" (func $task.return (param i32))) + (import "" "thread.new-indirect" (func $thread.new-indirect (param i32 i32) (result i32))) + (import "" "thread.resume-later" (func $thread.resume-later (param i32))) + + ;; keeps the task alive once the implicit thread is gone, then resolves it + (func $keeper (param i32) (call $task.return (i32.const 43))) + (elem (table $tbl) (i32.const 0) func $keeper) + + (func (export "exiter") (result i32) + (call $thread.resume-later (call $thread.new-indirect (i32.const 0) (i32.const 0))) + (i32.const 0 (; EXIT ;))) + (func (export "unreachable-cb") (param i32 i32 i32) (result i32) + unreachable) + ) + (core type $ThreadFT (func (param i32))) + (alias core export $table "tbl" (core table $tbl)) + (canon task.return (result u32) (core func $task.return)) + (canon thread.new-indirect $ThreadFT (core table $tbl) (core func $thread.new-indirect)) + (canon thread.resume-later (core func $thread.resume-later)) + (core instance $cm (instantiate $CM (with "" (instance + (export "tbl" (table $tbl)) + (export "task.return" (func $task.return)) + (export "thread.new-indirect" (func $thread.new-indirect)) + (export "thread.resume-later" (func $thread.resume-later)))))) + (func (export "exiter") async (result u32) + (canon lift (core func $cm "exiter") async (callback (core func $cm "unreachable-cb")))) + ) + (instance $c (instantiate $C)) + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (canon lower (func $c "exiter") async (memory (core memory $memory "mem")) (core func $exiter')) + (canon subtask.cancel async (core func $subtask.cancel)) + (canon subtask.drop (core func $subtask.drop)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.wait (memory (core memory $memory "mem")) (core func $waitable-set.wait)) + + (core module $Main + (import "" "mem" (memory 1)) + (import "" "exiter" (func $exiter (param i32) (result i32))) + (import "" "subtask.cancel" (func $subtask.cancel (param i32) (result i32))) + (import "" "subtask.drop" (func $subtask.drop (param i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (func (export "run") (result i32) + (local $packed i32) (local $sub i32) (local $ws i32) + (local.set $packed (call $exiter (i32.const 24))) + (if (i32.ne (i32.and (local.get $packed) (i32.const 0xf)) (i32.const 1 (; STARTED ;))) + (then unreachable)) + (local.set $sub (i32.shr_u (local.get $packed) (i32.const 4))) + + ;; nothing to resume: the implicit thread is gone and the explicit thread + ;; is not it + (if (i32.ne (call $subtask.cancel (local.get $sub)) (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + + ;; the explicit thread resolves the task, never seeing the cancellation + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $sub) (local.get $ws)) + (if (i32.ne (call $waitable-set.wait (local.get $ws) (i32.const 0)) + (i32.const 1 (; SUBTASK ;))) + (then unreachable)) + (if (i32.ne (i32.load (i32.const 0)) (local.get $sub)) (then unreachable)) + (if (i32.ne (i32.load (i32.const 4)) (i32.const 2 (; RETURNED ;))) (then unreachable)) + (if (i32.ne (i32.load (i32.const 24)) (i32.const 43)) (then unreachable)) + (call $waitable.join (local.get $sub) (i32.const 0)) + (call $subtask.drop (local.get $sub)) + (i32.const 42)) + ) + (core instance $main (instantiate $Main (with "" (instance + (export "mem" (memory $memory "mem")) + (export "exiter" (func $exiter')) + (export "subtask.cancel" (func $subtask.cancel)) + (export "subtask.drop" (func $subtask.drop)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)))))) + (func (export "run") async (result u32) (canon lift (core func $main "run"))) +) +(assert_return (invoke "run") (u32.const 42)) + +;; Cancelling a task must not resume a ready thread that belongs to a different +;; task, whether that task lives in the same component instance as the cancelled +;; one or in another instance. $C is instantiated twice so that both hold a +;; 'worker' parked in its event loop with a pending FUTURE_READ event, ready to +;; run as soon as the caller writes its future. Cancelling 'stuck', which is +;; stackful and blocked and so neither cancellable nor ready, must leave both +;; workers exactly where they are. +(component + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "task.return" (func $task.return (param i32))) + (import "" "future.read" (func $future.read (param i32 i32) (result i32))) + (import "" "future.read-sync" (func $future.read-sync (param i32 i32) (result i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + + ;; Parks in its event loop with a *pending* FUTURE_READ event, so its + ;; thread is ready as soon as the caller writes the future. + (func (export "worker") (param $futr i32) (result i32) + (local $ws i32) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) + (call $future.read (local.get $futr) (i32.const 16))) + (then unreachable)) + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $futr) (local.get $ws)) + (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (local.get $ws) (i32.const 4)))) + (func (export "worker-cb") (param $event i32) (param i32 i32) (result i32) + (if (i32.ne (local.get $event) (i32.const 4 (; FUTURE_READ ;))) + (then unreachable)) + (call $task.return (i32.const 43)) + (i32.const 0 (; EXIT ;))) + + ;; The cancellation target: stackful, so never cancellable, and blocked + ;; on a future nobody has written, so not ready either. + (func (export "stuck") (param $futr i32) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) + (call $future.read-sync (local.get $futr) (i32.const 16))) + (then unreachable)) + (call $task.return (i32.const 44))) + ) + (type $FT (future)) + (canon task.return (result u32) (core func $task.return)) + (canon future.read $FT async (memory (core memory $memory "mem")) (core func $future.read)) + (canon future.read $FT (memory (core memory $memory "mem")) (core func $future.read-sync)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (core instance $cm (instantiate $CM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "task.return" (func $task.return)) + (export "future.read" (func $future.read)) + (export "future.read-sync" (func $future.read-sync)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)))))) + (func (export "worker") async (param "fut" $FT) (result u32) + (canon lift (core func $cm "worker") async (callback (core func $cm "worker-cb")))) + (func (export "stuck") async (param "fut" $FT) (result u32) + (canon lift (core func $cm "stuck") async)) + ) + (instance $c1 (instantiate $C)) + (instance $c2 (instantiate $C)) + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (type $FT (future)) + (canon future.new $FT (core func $future.new)) + (canon future.write $FT async (memory (core memory $memory "mem")) (core func $future.write)) + (canon lower (func $c1 "worker") async (memory (core memory $memory "mem")) (core func $worker-same')) + (canon lower (func $c2 "worker") async (memory (core memory $memory "mem")) (core func $worker-other')) + (canon lower (func $c1 "stuck") async (memory (core memory $memory "mem")) (core func $stuck')) + (canon subtask.cancel async (core func $subtask.cancel)) + (canon subtask.drop (core func $subtask.drop)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.poll (memory (core memory $memory "mem")) (core func $waitable-set.poll)) + (canon waitable-set.wait (memory (core memory $memory "mem")) (core func $waitable-set.wait)) + + (core module $Main + (import "" "mem" (memory 1)) + (import "" "future.new" (func $future.new (result i64))) + (import "" "future.write" (func $future.write (param i32 i32) (result i32))) + (import "" "worker-same" (func $worker-same (param i32 i32) (result i32))) + (import "" "worker-other" (func $worker-other (param i32 i32) (result i32))) + (import "" "stuck" (func $stuck (param i32 i32) (result i32))) + (import "" "subtask.cancel" (func $subtask.cancel (param i32) (result i32))) + (import "" "subtask.drop" (func $subtask.drop (param i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.poll" (func $waitable-set.poll (param i32 i32) (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + + ;; checks that a callee started and returns its subtask index + (func $start (param $packed i32) (result i32) + (if (i32.ne (i32.and (local.get $packed) (i32.const 0xf)) (i32.const 1 (; STARTED ;))) + (then unreachable)) + (i32.shr_u (local.get $packed) (i32.const 4))) + (func $ready (param $futw i32) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) + (call $future.write (local.get $futw) (i32.const 16))) + (then unreachable))) + (func $watch (param $sub i32) (result i32) + (local $set i32) + (local.set $set (call $waitable-set.new)) + (call $waitable.join (local.get $sub) (local.get $set)) + (local.get $set)) + (func $assert-quiet (param $set i32) + (if (i32.ne (call $waitable-set.poll (local.get $set) (i32.const 0)) + (i32.const 0 (; NONE ;))) + (then unreachable))) + (func $await-returned (param $set i32) (param $sub i32) (param $retp i32) (param $expected i32) + (if (i32.ne (call $waitable-set.wait (local.get $set) (i32.const 0)) + (i32.const 1 (; SUBTASK ;))) + (then unreachable)) + (if (i32.ne (i32.load (i32.const 0)) (local.get $sub)) (then unreachable)) + (if (i32.ne (i32.load (i32.const 4)) (i32.const 2 (; RETURNED ;))) (then unreachable)) + (if (i32.ne (i32.load (local.get $retp)) (local.get $expected)) (then unreachable)) + (call $waitable.join (local.get $sub) (i32.const 0)) + (call $subtask.drop (local.get $sub))) + + (func (export "run") (result i32) + (local $ret64 i64) + (local $same-r i32) (local $same-w i32) + (local $other-r i32) (local $other-w i32) + (local $stuck-r i32) (local $stuck-w i32) + (local $same i32) (local $other i32) (local $stuck-sub i32) + (local $same-set i32) (local $other-set i32) (local $stuck-set i32) + + (local.set $ret64 (call $future.new)) + (local.set $same-r (i32.wrap_i64 (local.get $ret64))) + (local.set $same-w (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (local.set $ret64 (call $future.new)) + (local.set $other-r (i32.wrap_i64 (local.get $ret64))) + (local.set $other-w (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (local.set $ret64 (call $future.new)) + (local.set $stuck-r (i32.wrap_i64 (local.get $ret64))) + (local.set $stuck-w (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + + ;; both workers park in their event loops, releasing their instances' + ;; exclusive locks + (local.set $same (call $start (call $worker-same (local.get $same-r) (i32.const 24)))) + (local.set $other (call $start (call $worker-other (local.get $other-r) (i32.const 28)))) + (local.set $stuck-sub (call $start (call $stuck (local.get $stuck-r) (i32.const 32)))) + + ;; make both workers' threads ready without resuming them + (call $ready (local.get $same-w)) + (call $ready (local.get $other-w)) + (local.set $same-set (call $watch (local.get $same))) + (local.set $other-set (call $watch (local.get $other))) + (call $assert-quiet (local.get $same-set)) + (call $assert-quiet (local.get $other-set)) + + ;; stuck is neither cancellable nor ready, and both workers belong to + ;; other tasks, so nothing is resumed + (if (i32.ne (call $subtask.cancel (local.get $stuck-sub)) (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + (call $assert-quiet (local.get $same-set)) + (call $assert-quiet (local.get $other-set)) + + ;; all three still finish normally once this task blocks + (call $await-returned (local.get $same-set) (local.get $same) (i32.const 24) (i32.const 43)) + (call $await-returned (local.get $other-set) (local.get $other) (i32.const 28) (i32.const 43)) + (call $ready (local.get $stuck-w)) + (local.set $stuck-set (call $watch (local.get $stuck-sub))) + (call $await-returned (local.get $stuck-set) (local.get $stuck-sub) (i32.const 32) (i32.const 44)) + (i32.const 42)) + ) + (core instance $main (instantiate $Main (with "" (instance + (export "mem" (memory $memory "mem")) + (export "future.new" (func $future.new)) + (export "future.write" (func $future.write)) + (export "worker-same" (func $worker-same')) + (export "worker-other" (func $worker-other')) + (export "stuck" (func $stuck')) + (export "subtask.cancel" (func $subtask.cancel)) + (export "subtask.drop" (func $subtask.drop)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.poll" (func $waitable-set.poll)) + (export "waitable-set.wait" (func $waitable-set.wait)))))) + (func (export "run") async (result u32) (canon lift (core func $main "run"))) +) +(assert_return (invoke "run") (u32.const 42)) + +;; Test that a callback is not called while another thread is holding +;; the exclusive lock. +(component + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "task.return" (func $task.return (param i32))) + (import "" "task.cancel" (func $task.cancel)) + (import "" "future.read" (func $future.read (param i32 i32) (result i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + + ;; Parks in the event loop on a waitable set that never gets an event, so + ;; only the delivery of a pending cancellation can wake this task which + ;; cannot happen while the exclusive lock is held. + (global $never (mut i32) (i32.const 0)) + (func $start (global.set $never (call $waitable-set.new))) + (start $start) + (func (export "park") (result i32) + (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (global.get $never) (i32.const 4)))) + (func (export "park-cb") (param $event i32) (param i32 i32) (result i32) + (if (i32.ne (local.get $event) (i32.const 6 (; TASK_CANCELLED ;))) + (then unreachable)) + (call $task.cancel) + (i32.const 0 (; EXIT ;))) + + ;; Blocks holding the instance's exclusive lock for as long as it is + ;; blocked. Once the caller writes the future, this task's thread is ready + ;; but the lock is still held. + (func (export "hold-lock") (param $futr i32) (result i32) + (local $ws i32) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) + (call $future.read (local.get $futr) (i32.const 0))) + (then unreachable)) + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $futr) (local.get $ws)) + (if (i32.ne (i32.const 4 (; FUTURE_READ ;)) + (call $waitable-set.wait (local.get $ws) (i32.const 8))) + (then unreachable)) + (call $task.return (i32.const 43)) + (i32.const 0 (; EXIT ;))) + (func (export "unreachable-cb") (param i32 i32 i32) (result i32) + unreachable) + ) + (type $FT (future)) + (canon task.return (result u32) (core func $task.return)) + (canon task.cancel (core func $task.cancel)) + (canon future.read $FT async (memory (core memory $memory "mem")) (core func $future.read)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.wait (memory (core memory $memory "mem")) (core func $waitable-set.wait)) + (core instance $cm (instantiate $CM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "task.return" (func $task.return)) + (export "task.cancel" (func $task.cancel)) + (export "future.read" (func $future.read)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)))))) + (func (export "park") async + (canon lift (core func $cm "park") async (callback (core func $cm "park-cb")))) + (func (export "hold-lock") async (param "fut" $FT) (result u32) + (canon lift (core func $cm "hold-lock") async (callback (core func $cm "unreachable-cb")))) + ) + (instance $c (instantiate $C)) + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (type $FT (future)) + (canon future.new $FT (core func $future.new)) + (canon future.write $FT (memory (core memory $memory "mem")) (core func $future.write)) + (canon lower (func $c "park") async (core func $park')) + (canon lower (func $c "hold-lock") async (memory (core memory $memory "mem")) (core func $hold-lock')) + (canon subtask.cancel async (core func $subtask.cancel-async)) + (canon subtask.drop (core func $subtask.drop)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.poll (memory (core memory $memory "mem")) (core func $waitable-set.poll)) + (canon waitable-set.wait (memory (core memory $memory "mem")) (core func $waitable-set.wait)) + + (core module $Main + (import "" "mem" (memory 1)) + (import "" "future.new" (func $future.new (result i64))) + (import "" "future.write" (func $future.write (param i32 i32) (result i32))) + (import "" "park" (func $park (result i32))) + (import "" "hold-lock" (func $hold-lock (param i32 i32) (result i32))) + (import "" "subtask.cancel-async" (func $subtask.cancel-async (param i32) (result i32))) + (import "" "subtask.drop" (func $subtask.drop (param i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.poll" (func $waitable-set.poll (param i32 i32) (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (func (export "run") (result i32) + (local $ret64 i64) (local $futr i32) (local $futw i32) + (local $packed i32) + (local $park-sub i32) (local $hold-sub i32) + (local $park-ws i32) (local $hold-ws i32) + + ;; start "park"; returning WAIT releases the lock, so it is free here + (local.set $packed (call $park)) + (if (i32.ne (i32.and (local.get $packed) (i32.const 0xf)) (i32.const 1 (; STARTED ;))) + (then unreachable)) + (local.set $park-sub (i32.shr_u (local.get $packed) (i32.const 4))) + + ;; start hold-lock; it takes the lock and blocks while holding it + (local.set $ret64 (call $future.new)) + (local.set $futr (i32.wrap_i64 (local.get $ret64))) + (local.set $futw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (local.set $packed (call $hold-lock (local.get $futr) (i32.const 24))) + (if (i32.ne (i32.and (local.get $packed) (i32.const 0xf)) (i32.const 1 (; STARTED ;))) + (then unreachable)) + (local.set $hold-sub (i32.shr_u (local.get $packed) (i32.const 4))) + (local.set $hold-ws (call $waitable-set.new)) + (call $waitable.join (local.get $hold-sub) (local.get $hold-ws)) + + ;; complete the read: hold-lock's thread is now ready, but it still + ;; holds the lock and nothing has resumed it + (if (i32.ne (i32.const 0 (; COMPLETED ;)) + (call $future.write (local.get $futw) (i32.const 16))) + (then unreachable)) + (if (i32.ne (call $waitable-set.poll (local.get $hold-ws) (i32.const 0)) + (i32.const 0 (; NONE ;))) + (then unreachable)) + + ;; cancel park: its own thread is not ready while the lock is held, and + ;; hold-lock's ready thread belongs to a different task, so nothing at + ;; all is resumed + (if (i32.ne (call $subtask.cancel-async (local.get $park-sub)) + (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + (if (i32.ne (call $waitable-set.poll (local.get $hold-ws) (i32.const 0)) + (i32.const 0 (; NONE ;))) + (then unreachable)) + + ;; blocking here lets hold-lock finish and release the lock + (if (i32.ne (call $waitable-set.wait (local.get $hold-ws) (i32.const 0)) + (i32.const 1 (; SUBTASK ;))) + (then unreachable)) + (if (i32.ne (i32.load (i32.const 0)) (local.get $hold-sub)) + (then unreachable)) + (if (i32.ne (i32.load (i32.const 4)) (i32.const 2 (; RETURNED ;))) + (then unreachable)) + (if (i32.ne (i32.load (i32.const 24)) (i32.const 43)) + (then unreachable)) + (call $waitable.join (local.get $hold-sub) (i32.const 0)) + (call $subtask.drop (local.get $hold-sub)) + + ;; with the lock now free, blocking lets park receive the request + (local.set $park-ws (call $waitable-set.new)) + (call $waitable.join (local.get $park-sub) (local.get $park-ws)) + (if (i32.ne (call $waitable-set.wait (local.get $park-ws) (i32.const 0)) + (i32.const 1 (; SUBTASK ;))) + (then unreachable)) + (if (i32.ne (i32.load (i32.const 0)) (local.get $park-sub)) + (then unreachable)) + (if (i32.ne (i32.load (i32.const 4)) (i32.const 4 (; CANCELLED_BEFORE_RETURNED ;))) + (then unreachable)) + (call $waitable.join (local.get $park-sub) (i32.const 0)) + (call $subtask.drop (local.get $park-sub)) + (i32.const 42)) + ) + (core instance $main (instantiate $Main (with "" (instance + (export "mem" (memory $memory "mem")) + (export "future.new" (func $future.new)) + (export "future.write" (func $future.write)) + (export "park" (func $park')) + (export "hold-lock" (func $hold-lock')) + (export "subtask.cancel-async" (func $subtask.cancel-async)) + (export "subtask.drop" (func $subtask.drop)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.poll" (func $waitable-set.poll)) + (export "waitable-set.wait" (func $waitable-set.wait)))))) + (func (export "run") async (result u32) (canon lift (core func $main "run"))) +) +(assert_return (invoke "run") (u32.const 42)) + +;; Once the cancelled task's own callback thread has been resumed and the task +;; has resolved, 'subtask.cancel' returns without resuming anything else, even +;; though 'worker' has been sitting ready in its event loop the whole time. +(component + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "task.return" (func $task.return (param i32))) + (import "" "task.cancel" (func $task.cancel)) + (import "" "future.read" (func $future.read (param i32 i32) (result i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + + ;; ready before the cancel: parked in its event loop with a pending event + (func (export "worker") (param $futr i32) (result i32) + (local $ws i32) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) + (call $future.read (local.get $futr) (i32.const 16))) + (then unreachable)) + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $futr) (local.get $ws)) + (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (local.get $ws) (i32.const 4)))) + (func (export "worker-cb") (param $event i32) (param i32 i32) (result i32) + (if (i32.ne (local.get $event) (i32.const 4 (; FUTURE_READ ;))) + (then unreachable)) + (call $task.return (i32.const 43)) + (i32.const 0 (; EXIT ;))) + + ;; the cancellation target + (func (export "park") (result i32) + (i32.or (i32.const 2 (; WAIT ;)) + (i32.shl (call $waitable-set.new) (i32.const 4)))) + (func (export "park-cb") (param $event i32) (param i32 i32) (result i32) + (if (i32.ne (local.get $event) (i32.const 6 (; TASK_CANCELLED ;))) + (then unreachable)) + (call $task.cancel) + (i32.const 0 (; EXIT ;))) + ) + (type $FT (future)) + (canon task.return (result u32) (core func $task.return)) + (canon task.cancel (core func $task.cancel)) + (canon future.read $FT async (memory (core memory $memory "mem")) (core func $future.read)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (core instance $cm (instantiate $CM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "task.return" (func $task.return)) + (export "task.cancel" (func $task.cancel)) + (export "future.read" (func $future.read)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)))))) + (func (export "worker") async (param "fut" $FT) (result u32) + (canon lift (core func $cm "worker") async (callback (core func $cm "worker-cb")))) + (func (export "park") async + (canon lift (core func $cm "park") async (callback (core func $cm "park-cb")))) + ) + (instance $c (instantiate $C)) + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (type $FT (future)) + (canon future.new $FT (core func $future.new)) + (canon future.write $FT async (memory (core memory $memory "mem")) (core func $future.write)) + (canon lower (func $c "worker") async (memory (core memory $memory "mem")) (core func $worker')) + (canon lower (func $c "park") async (core func $park')) + (canon subtask.cancel async (core func $subtask.cancel)) + (canon subtask.drop (core func $subtask.drop)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.poll (memory (core memory $memory "mem")) (core func $waitable-set.poll)) + (canon waitable-set.wait (memory (core memory $memory "mem")) (core func $waitable-set.wait)) + + (core module $Main + (import "" "mem" (memory 1)) + (import "" "future.new" (func $future.new (result i64))) + (import "" "future.write" (func $future.write (param i32 i32) (result i32))) + (import "" "worker" (func $worker (param i32 i32) (result i32))) + (import "" "park" (func $park (result i32))) + (import "" "subtask.cancel" (func $subtask.cancel (param i32) (result i32))) + (import "" "subtask.drop" (func $subtask.drop (param i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.poll" (func $waitable-set.poll (param i32 i32) (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (func (export "run") (result i32) + (local $ret64 i64) (local $futr i32) (local $futw i32) + (local $packed i32) (local $wsub i32) (local $psub i32) (local $ws i32) + (local.set $ret64 (call $future.new)) + (local.set $futr (i32.wrap_i64 (local.get $ret64))) + (local.set $futw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + + (local.set $packed (call $worker (local.get $futr) (i32.const 24))) + (if (i32.ne (i32.and (local.get $packed) (i32.const 0xf)) (i32.const 1 (; STARTED ;))) + (then unreachable)) + (local.set $wsub (i32.shr_u (local.get $packed) (i32.const 4))) + (local.set $packed (call $park)) + (if (i32.ne (i32.and (local.get $packed) (i32.const 0xf)) (i32.const 1 (; STARTED ;))) + (then unreachable)) + (local.set $psub (i32.shr_u (local.get $packed) (i32.const 4))) + + ;; worker's thread is now ready, but nothing has resumed it + (if (i32.ne (i32.const 0 (; COMPLETED ;)) + (call $future.write (local.get $futw) (i32.const 16))) + (then unreachable)) + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $wsub) (local.get $ws)) + (if (i32.ne (call $waitable-set.poll (local.get $ws) (i32.const 0)) + (i32.const 0 (; NONE ;))) + (then unreachable)) + + ;; park is resumed and resolves eagerly, but worker stays put + (if (i32.ne (call $subtask.cancel (local.get $psub)) + (i32.const 4 (; CANCELLED_BEFORE_RETURNED ;))) + (then unreachable)) + (if (i32.ne (call $waitable-set.poll (local.get $ws) (i32.const 0)) + (i32.const 0 (; NONE ;))) + (then unreachable)) + (call $subtask.drop (local.get $psub)) + + ;; worker still runs once this task blocks + (if (i32.ne (call $waitable-set.wait (local.get $ws) (i32.const 0)) + (i32.const 1 (; SUBTASK ;))) + (then unreachable)) + (if (i32.ne (i32.load (i32.const 0)) (local.get $wsub)) (then unreachable)) + (if (i32.ne (i32.load (i32.const 4)) (i32.const 2 (; RETURNED ;))) (then unreachable)) + (if (i32.ne (i32.load (i32.const 24)) (i32.const 43)) (then unreachable)) + (call $waitable.join (local.get $wsub) (i32.const 0)) + (call $subtask.drop (local.get $wsub)) + (i32.const 42)) + ) + (core instance $main (instantiate $Main (with "" (instance + (export "mem" (memory $memory "mem")) + (export "future.new" (func $future.new)) + (export "future.write" (func $future.write)) + (export "worker" (func $worker')) + (export "park" (func $park')) + (export "subtask.cancel" (func $subtask.cancel)) + (export "subtask.drop" (func $subtask.drop)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.poll" (func $waitable-set.poll)) + (export "waitable-set.wait" (func $waitable-set.wait)))))) + (func (export "run") async (result u32) (canon lift (core func $main "run"))) +) +(assert_return (invoke "run") (u32.const 42)) + +;; A thread made ready by the resumed callback itself, from inside the +;; cancellation, is not resumed either: 'subtask.cancel' returns as soon as the +;; callback is done. Yielding afterwards lets the host schedule it as it would +;; any other ready thread. +(component + (component $C + (core module $Table (table (export "tbl") 1 funcref)) + (core instance $table (instantiate $Table)) + (core module $CM + (import "" "tbl" (table $tbl 1 funcref)) + (import "" "task.cancel" (func $task.cancel)) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "thread.new-indirect" (func $thread.new-indirect (param i32 i32) (result i32))) + (import "" "thread.resume-later" (func $thread.resume-later (param i32))) + + (global $spawned-ran (mut i32) (i32.const 0)) + (func $spawned (param i32) (global.set $spawned-ran (i32.const 1))) + (elem (table $tbl) (i32.const 0) func $spawned) + (func (export "ran") (result i32) (global.get $spawned-ran)) + + (func (export "park") (result i32) + (i32.or (i32.const 2 (; WAIT ;)) + (i32.shl (call $waitable-set.new) (i32.const 4)))) + (func (export "park-cb") (param $event i32) (param i32 i32) (result i32) + (if (i32.ne (local.get $event) (i32.const 6 (; TASK_CANCELLED ;))) + (then unreachable)) + ;; make a fresh thread ready from inside the cancellation itself + (call $thread.resume-later (call $thread.new-indirect (i32.const 0) (i32.const 0))) + (call $task.cancel) + (i32.const 0 (; EXIT ;))) + ) + (core type $ThreadFT (func (param i32))) + (alias core export $table "tbl" (core table $tbl)) + (canon task.cancel (core func $task.cancel)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon thread.new-indirect $ThreadFT (core table $tbl) (core func $thread.new-indirect)) + (canon thread.resume-later (core func $thread.resume-later)) + (core instance $cm (instantiate $CM (with "" (instance + (export "tbl" (table $tbl)) + (export "task.cancel" (func $task.cancel)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "thread.new-indirect" (func $thread.new-indirect)) + (export "thread.resume-later" (func $thread.resume-later)))))) + (func (export "park") async + (canon lift (core func $cm "park") async (callback (core func $cm "park-cb")))) + (func (export "ran") (result u32) (canon lift (core func $cm "ran"))) + ) + (instance $c (instantiate $C)) + (canon lower (func $c "park") async (core func $park')) + (canon lower (func $c "ran") (core func $ran')) + (canon subtask.cancel async (core func $subtask.cancel)) + (canon subtask.drop (core func $subtask.drop)) + (canon thread.yield (core func $thread.yield)) + + (core module $Main + (import "" "park" (func $park (result i32))) + (import "" "ran" (func $ran (result i32))) + (import "" "subtask.cancel" (func $subtask.cancel (param i32) (result i32))) + (import "" "subtask.drop" (func $subtask.drop (param i32))) + (import "" "thread.yield" (func $thread.yield (result i32))) + (func (export "run") (result i32) + (local $packed i32) (local $sub i32) (local $i i32) + (local.set $packed (call $park)) + (if (i32.ne (i32.and (local.get $packed) (i32.const 0xf)) (i32.const 1 (; STARTED ;))) + (then unreachable)) + (local.set $sub (i32.shr_u (local.get $packed) (i32.const 4))) + (if (i32.ne (call $subtask.cancel (local.get $sub)) + (i32.const 4 (; CANCELLED_BEFORE_RETURNED ;))) + (then unreachable)) + (if (call $ran) (then unreachable)) + (call $subtask.drop (local.get $sub)) + + ;; yielding hands the deferred thread to the host, which runs it + (block $done + (loop $again + (br_if $done (call $ran)) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (if (i32.gt_u (local.get $i) (i32.const 32)) (then unreachable)) + (drop (call $thread.yield)) + (br $again))) + (i32.const 42)) + ) + (core instance $main (instantiate $Main (with "" (instance + (export "park" (func $park')) + (export "ran" (func $ran')) + (export "subtask.cancel" (func $subtask.cancel)) + (export "subtask.drop" (func $subtask.drop)) + (export "thread.yield" (func $thread.yield)))))) + (func (export "run") async (result u32) (canon lift (core func $main "run"))) +) +(assert_return (invoke "run") (u32.const 42)) diff --git a/test/async/cancel-yield-loop.wast b/test/async/cancel-yield-loop.wast deleted file mode 100644 index b8d4dd4d..00000000 --- a/test/async/cancel-yield-loop.wast +++ /dev/null @@ -1,135 +0,0 @@ -;; 'subtask.cancel' may keep resuming ready threads of the callee's instance -;; until the cancelled task resolves, but it is only *allowed* to, never -;; required to: the host may stop at any point. This test checks that, given an -;; infinite yield loop, the host eventually declares the cancellation blocked. -(component - (component $C - (core module $Memory (memory (export "mem") 1)) - (core instance $memory (instantiate $Memory)) - (core module $CM - (import "" "mem" (memory 1)) - (import "" "task.cancel" (func $task.cancel)) - (import "" "future.read" (func $future.read (param i32 i32) (result i32))) - (import "" "waitable.join" (func $waitable.join (param i32 i32))) - (import "" "waitable-set.new" (func $waitable-set.new (result i32))) - (import "" "waitable-set.poll" (func $waitable-set.poll (param i32 i32) (result i32))) - (global $ws (mut i32) (i32.const 0)) - (global $cancelled (mut i32) (i32.const 0)) - - (func (export "yielder") (param $futr i32) (result i32) - (if (i32.ne (i32.const -1 (; BLOCKED ;)) - (call $future.read (local.get $futr) (i32.const 0))) - (then unreachable)) - (global.set $ws (call $waitable-set.new)) - (call $waitable.join (local.get $futr) (global.get $ws)) - (i32.const 1 (; YIELD ;))) - - (func (export "yielder-cb") (param $event i32) (param i32 i32) (result i32) - ;; A delivered cancellation is remembered but cannot be acted on yet: - ;; this task must first see its future read complete. - (if (i32.eq (local.get $event) (i32.const 6 (; TASK_CANCELLED ;))) - (then - (if (global.get $cancelled) (then unreachable)) - (global.set $cancelled (i32.const 1)) - (return (i32.const 1 (; YIELD ;))))) - (if (i32.ne (local.get $event) (i32.const 0 (; NONE ;))) - (then unreachable)) - ;; Keep spinning until the caller writes the future. Every turn of this - ;; loop leaves the thread ready again, so a host that resumed until the - ;; task resolved would never get here. - (if (i32.eq (i32.const 0 (; NONE ;)) - (call $waitable-set.poll (global.get $ws) (i32.const 8))) - (then (return (i32.const 1 (; YIELD ;))))) - ;; the read completed; the cancellation must already have arrived - (if (i32.eqz (global.get $cancelled)) (then unreachable)) - (call $task.cancel) - (i32.const 0 (; EXIT ;))) - ) - (type $FT (future)) - (canon task.cancel (core func $task.cancel)) - (canon future.read $FT async (memory (core memory $memory "mem")) (core func $future.read)) - (canon waitable.join (core func $waitable.join)) - (canon waitable-set.new (core func $waitable-set.new)) - (canon waitable-set.poll (memory (core memory $memory "mem")) (core func $waitable-set.poll)) - (core instance $cm (instantiate $CM (with "" (instance - (export "mem" (memory $memory "mem")) - (export "task.cancel" (func $task.cancel)) - (export "future.read" (func $future.read)) - (export "waitable.join" (func $waitable.join)) - (export "waitable-set.new" (func $waitable-set.new)) - (export "waitable-set.poll" (func $waitable-set.poll)))))) - (func (export "yielder") async (param "fut" $FT) - (canon lift (core func $cm "yielder") async (callback (core func $cm "yielder-cb")))) - ) - (instance $c (instantiate $C)) - (core module $Memory (memory (export "mem") 1)) - (core instance $memory (instantiate $Memory)) - (type $FT (future)) - (canon future.new $FT (core func $future.new)) - (canon future.write $FT (memory (core memory $memory "mem")) (core func $future.write)) - (canon lower (func $c "yielder") async (core func $yielder')) - (canon subtask.cancel async (core func $subtask.cancel-async)) - (canon subtask.drop (core func $subtask.drop)) - (canon waitable.join (core func $waitable.join)) - (canon waitable-set.new (core func $waitable-set.new)) - (canon waitable-set.wait (memory (core memory $memory "mem")) (core func $waitable-set.wait)) - - (core module $Main - (import "" "mem" (memory 1)) - (import "" "future.new" (func $future.new (result i64))) - (import "" "future.write" (func $future.write (param i32 i32) (result i32))) - (import "" "yielder" (func $yielder (param i32) (result i32))) - (import "" "subtask.cancel-async" (func $subtask.cancel-async (param i32) (result i32))) - (import "" "subtask.drop" (func $subtask.drop (param i32))) - (import "" "waitable.join" (func $waitable.join (param i32 i32))) - (import "" "waitable-set.new" (func $waitable-set.new (result i32))) - (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) - (func (export "run") (result i32) - (local $ret64 i64) (local $futr i32) (local $futw i32) - (local $packed i32) (local $sub i32) (local $ws i32) - (local.set $ret64 (call $future.new)) - (local.set $futr (i32.wrap_i64 (local.get $ret64))) - (local.set $futw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) - - (local.set $packed (call $yielder (local.get $futr))) - (if (i32.ne (i32.and (local.get $packed) (i32.const 0xf)) (i32.const 1 (; STARTED ;))) - (then unreachable)) - (local.set $sub (i32.shr_u (local.get $packed) (i32.const 4))) - - ;; The callee cannot resolve until this task writes the future, which it - ;; cannot do from inside the call, so the cancel must give up and report - ;; BLOCKED rather than resuming the yield loop forever. - (if (i32.ne (call $subtask.cancel-async (local.get $sub)) - (i32.const -1 (; BLOCKED ;))) - (then unreachable)) - - ;; now let the yield loop see its read complete and resolve - (if (i32.ne (i32.const 0 (; COMPLETED ;)) - (call $future.write (local.get $futw) (i32.const 16))) - (then unreachable)) - (local.set $ws (call $waitable-set.new)) - (call $waitable.join (local.get $sub) (local.get $ws)) - (if (i32.ne (call $waitable-set.wait (local.get $ws) (i32.const 0)) - (i32.const 1 (; SUBTASK ;))) - (then unreachable)) - (if (i32.ne (i32.load (i32.const 0)) (local.get $sub)) - (then unreachable)) - (if (i32.ne (i32.load (i32.const 4)) (i32.const 4 (; CANCELLED_BEFORE_RETURNED ;))) - (then unreachable)) - (call $waitable.join (local.get $sub) (i32.const 0)) - (call $subtask.drop (local.get $sub)) - (i32.const 42)) - ) - (core instance $main (instantiate $Main (with "" (instance - (export "mem" (memory $memory "mem")) - (export "future.new" (func $future.new)) - (export "future.write" (func $future.write)) - (export "yielder" (func $yielder')) - (export "subtask.cancel-async" (func $subtask.cancel-async)) - (export "subtask.drop" (func $subtask.drop)) - (export "waitable.join" (func $waitable.join)) - (export "waitable-set.new" (func $waitable-set.new)) - (export "waitable-set.wait" (func $waitable-set.wait)))))) - (func (export "run") async (result u32) (canon lift (core func $main "run"))) -) -(assert_return (invoke "run") (u32.const 42)) diff --git a/test/nyi.txt b/test/nyi.txt index bdf85930..a7bf0ad7 100644 --- a/test/nyi.txt +++ b/test/nyi.txt @@ -1,4 +1,6 @@ # See README.md +./async/cancel-delivery.wast +./async/cancel-targeted-resume.wast ./validation/kebab.wast ./binary/binary.wast ./values/post-return.wast