Skip to content

Commit d225583

Browse files
authored
cm-async: Fix cancelling a completed host task (#12797) (#12801)
Incorrect behavior on Wasmtime's part led to erroneously raising a trap in the guest about the terminal status of a task being delivered already. This commit refactors some internals to ensure that cancelling a completed host task is not a failure, it instead just returns that the host task is indeed completed.
1 parent b0dc282 commit d225583

File tree

2 files changed

+120
-15
lines changed

2 files changed

+120
-15
lines changed

crates/wasmtime/src/runtime/component/concurrent.rs

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -810,7 +810,7 @@ pub(crate) fn poll_and_block<R: Send + Sync + 'static>(
810810

811811
// Retrieve and return the result.
812812
let host_state = &mut store.concurrent_state_mut().get_mut(task)?.state;
813-
match mem::replace(host_state, HostTaskState::CalleeDone) {
813+
match mem::replace(host_state, HostTaskState::CalleeDone { cancelled: false }) {
814814
HostTaskState::CalleeFinished(result) => Ok(match result.downcast() {
815815
Ok(result) => *result,
816816
Err(_) => bail_bug!("host task finished with wrong type of result"),
@@ -2863,7 +2863,14 @@ impl Instance {
28632863

28642864
lower(store.as_context_mut(), result)?;
28652865
let state = store.0.concurrent_state_mut();
2866-
state.get_mut(task)?.state = HostTaskState::CalleeDone;
2866+
match &mut state.get_mut(task)?.state {
2867+
// The task is already flagged as finished because it was
2868+
// cancelled. No need to transition further.
2869+
HostTaskState::CalleeDone { .. } => {}
2870+
2871+
// Otherwise transition this task to the done state.
2872+
other => *other = HostTaskState::CalleeDone { cancelled: false },
2873+
}
28672874
Waitable::Host(task).set_event(state, Some(Event::Subtask { status }))?;
28682875

28692876
store.0.set_thread(old)?;
@@ -3114,7 +3121,7 @@ impl Instance {
31143121
let task = concurrent_state.get_mut(id)?;
31153122
match &task.state {
31163123
HostTaskState::CalleeRunning(_) => bail!(Trap::SubtaskDropNotResolved),
3117-
HostTaskState::CalleeDone => {}
3124+
HostTaskState::CalleeDone { .. } => {}
31183125
HostTaskState::CalleeStarted | HostTaskState::CalleeFinished(_) => {
31193126
bail_bug!("invalid state for callee in `subtask.drop`")
31203127
}
@@ -3593,15 +3600,28 @@ impl Instance {
35933600
let needs_block;
35943601
if let Waitable::Host(host_task) = waitable {
35953602
let state = &mut concurrent_state.get_mut(host_task)?.state;
3596-
match mem::replace(state, HostTaskState::CalleeDone) {
3603+
match mem::replace(state, HostTaskState::CalleeDone { cancelled: true }) {
35973604
// If the callee is still running, signal an abort is requested.
3598-
// Then fall through to determine what to do next.
3599-
HostTaskState::CalleeRunning(handle) => handle.abort(),
3605+
//
3606+
// After cancelling this falls through to block waiting for the
3607+
// host task to actually finish assuming that `async_` is false.
3608+
// This blocking behavior resolves the race of `handle.abort()`
3609+
// with the task actually getting cancelled or finishing.
3610+
HostTaskState::CalleeRunning(handle) => {
3611+
handle.abort();
3612+
needs_block = true;
3613+
}
36003614

36013615
// Cancellation was already requested, so fail as the task can't
36023616
// be cancelled twice.
3603-
HostTaskState::CalleeDone => {
3604-
bail!(Trap::SubtaskCancelAfterTerminal);
3617+
HostTaskState::CalleeDone { cancelled } => {
3618+
if cancelled {
3619+
bail!(Trap::SubtaskCancelAfterTerminal);
3620+
} else {
3621+
// The callee is already done so there's no need to
3622+
// block further for an event.
3623+
needs_block = false;
3624+
}
36053625
}
36063626

36073627
// These states should not be possible for a subtask that's
@@ -3610,12 +3630,6 @@ impl Instance {
36103630
bail_bug!("invalid states for host callee")
36113631
}
36123632
}
3613-
3614-
// Cancelling host tasks always needs to block on them to await the
3615-
// result of the completion set up in `first_poll`. This'll resolve
3616-
// the race of `handle.abort()` above to see if it actually
3617-
// cancelled something or if the future ended up finishing.
3618-
needs_block = true;
36193633
} else {
36203634
let caller = concurrent_state.current_guest_thread()?;
36213635
let guest_task = TableId::<GuestTask>::new(rep);
@@ -4254,7 +4268,7 @@ enum HostTaskState {
42544268

42554269
/// Terminal state for host tasks meaning that the task was cancelled or the
42564270
/// result was taken.
4257-
CalleeDone,
4271+
CalleeDone { cancelled: bool },
42584272
}
42594273

42604274
impl HostTask {

tests/misc_testsuite/component-model/async/cancel-host.wast

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,3 +383,94 @@
383383
)
384384

385385
(assert_return (invoke "run"))
386+
387+
;; If a host task completes, but the guest doesn't actually receive the
388+
;; notification that it's done, it should be possible to cancel it.
389+
(component
390+
(import "host" (instance $host
391+
(export "return-two-slowly" (func async (result s32)))
392+
))
393+
394+
(core module $Mem (memory (export "mem") 1))
395+
(core instance $mem (instantiate $Mem))
396+
397+
(core module $m
398+
(import "" "slow-sync" (func $slow-sync (result i32)))
399+
(import "" "slow-async" (func $slow-async (param i32) (result i32)))
400+
(import "" "subtask.cancel" (func $subtask.cancel (param i32) (result i32)))
401+
(import "" "subtask.drop" (func $subtask.drop (param i32)))
402+
(func (export "run")
403+
(local $subtask i32)
404+
405+
;; start a subtask for `return-two-slowly`
406+
(local.set $subtask (call $start-slow))
407+
408+
;; Call `return-two-slowly` synchronously a few times. This gives a
409+
;; chance for the host to finish the previous task, which shouldn't
410+
;; interfere with the below...
411+
(call $assert-eq (call $slow-sync) (i32.const 2))
412+
(call $assert-eq (call $slow-sync) (i32.const 2))
413+
(call $assert-eq (call $slow-sync) (i32.const 2))
414+
415+
;; We've never seen the result of `$subtask`, so it should be safe to
416+
;; cancel. This should either yield that the subtask returned or that
417+
;; it's return was cancelled, it's up to hosts.
418+
(call $assert-either (call $subtask.cancel (local.get $subtask))
419+
(i32.const 4) ;; RETURN_CANCELLED
420+
(i32.const 2)) ;; RETURNED
421+
(call $subtask.drop (local.get $subtask))
422+
)
423+
424+
;; asserts param 0 == param 1
425+
(func $assert-eq (param i32 i32)
426+
local.get 0
427+
local.get 1
428+
i32.ne
429+
if unreachable end)
430+
431+
;; Asserts that param 0 is either equal to param 1 or param 2
432+
(func $assert-either (param i32 i32 i32)
433+
local.get 0
434+
local.get 1
435+
i32.eq
436+
if return end
437+
local.get 0
438+
local.get 2
439+
i32.eq
440+
if return end
441+
unreachable)
442+
443+
(func $start-slow (result i32)
444+
(local $tmp i32)
445+
446+
;; Start slow, expect STARTED
447+
(call $slow-async (i32.const 100))
448+
local.tee $tmp
449+
i32.const 0xf
450+
i32.and
451+
i32.const 1 ;; STARTED
452+
i32.ne
453+
if unreachable end
454+
local.get $tmp
455+
i32.const 4
456+
i32.shr_u
457+
)
458+
)
459+
(core func $slow-sync (canon lower (func $host "return-two-slowly") (memory $mem "mem")))
460+
(core func $slow-async (canon lower (func $host "return-two-slowly") async (memory $mem "mem")))
461+
(core func $subtask.cancel (canon subtask.cancel))
462+
(core func $subtask.drop (canon subtask.drop))
463+
(core instance $i (instantiate $m
464+
(with "" (instance
465+
(export "slow-sync" (func $slow-sync))
466+
(export "slow-async" (func $slow-async))
467+
(export "subtask.cancel" (func $subtask.cancel))
468+
(export "subtask.drop" (func $subtask.drop))
469+
))
470+
))
471+
472+
(func (export "run") async
473+
(canon lift (core func $i "run")))
474+
)
475+
476+
(assert_return (invoke "run"))

0 commit comments

Comments
 (0)