Skip to content
This repository was archived by the owner on Sep 8, 2025. It is now read-only.

Commit abe8340

Browse files
authored
Merge pull request #155 from bytecodealliance/dicej/fix-152
change the error returned by `Instance::run` etc. on deadlock
2 parents fd9fd49 + 915b3ee commit abe8340

3 files changed

Lines changed: 140 additions & 4 deletions

File tree

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

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1744,9 +1744,34 @@ impl ComponentInstance {
17441744
if ready.is_empty() {
17451745
return match next {
17461746
Poll::Ready(true) => Poll::Ready(Ok(Either::Right(Vec::new()))),
1747-
Poll::Ready(false) => {
1748-
Poll::Ready(Err(anyhow!(crate::Trap::NoAsyncResult)))
1749-
}
1747+
// Here we return an error indicating we can't
1748+
// make further progress. The underlying
1749+
// assumption is that `future` depends on this
1750+
// component instance making such progress, and
1751+
// thus there's no point in continuing to poll
1752+
// it given we've run out of work to do.
1753+
//
1754+
// Note that we'd also reach this point if the
1755+
// host embedder passed e.g. a
1756+
// `std::future::Pending` to `Instance::run`, in
1757+
// which case we'd return a "deadlock" error
1758+
// even when any and all tasks have completed
1759+
// normally. However, that's not how
1760+
// `Instance::run` is intended (and documented)
1761+
// to be used, so it seems reasonable to lump
1762+
// that case in with "real" deadlocks.
1763+
//
1764+
// TODO: Once we've added host APIs for
1765+
// cancelling in-progress tasks, we can return
1766+
// some other, non-error value here, treating it
1767+
// as "normal" and giving the host embedder a
1768+
// chance to intervene by cancelling one or more
1769+
// tasks and/or starting new tasks capable of
1770+
// waking the existing ones.
1771+
Poll::Ready(false) => Poll::Ready(Err(anyhow!(
1772+
"deadlock detected: event loop cannot \
1773+
make further progress"
1774+
))),
17501775
Poll::Pending => Poll::Pending,
17511776
};
17521777
} else {
@@ -2602,6 +2627,20 @@ impl Instance {
26022627
/// # Ok(())
26032628
/// # }
26042629
/// ```
2630+
///
2631+
/// Note that this function will return a "deadlock" error in either of the
2632+
/// following scenarios:
2633+
///
2634+
/// - One or more guest tasks are still pending (i.e. have not yet returned,
2635+
/// or, in the case of async-lifted exports with callbacks, have not yet
2636+
/// returned `CALLBACK_CODE_EXIT`) even though all host tasks have completed
2637+
/// all host-owned stream and future handles have been closed, etc.
2638+
///
2639+
/// - Any and all guest tasks complete normally, but the future passed to
2640+
/// this function continues to return `Pending` when polled. In that case,
2641+
/// the future presumably does not depend on any guest task making further
2642+
/// progress (since no futher progress can be made) and thus is not an
2643+
/// appropriate future to poll using this function.
26052644
pub async fn run<U: Send, V: Send + Sync + 'static>(
26062645
&self,
26072646
mut store: impl AsContextMut<Data = U>,
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
;;! component_model_async = true
2+
;;! reference_types = true
3+
;;! gc_types = true
4+
;;! multi_memory = true
5+
6+
;; - Component B asks component A to enable backpressure
7+
;; - Component B makes an async call to component A
8+
;; - Component B asserts this subtask is in the "STARTING" state
9+
;; - Component B adds the subtask to a waitable set and calls waitable-set.wait
10+
;;
11+
;; This leaves both tasks in a deadlock situation, which, as of this writing,
12+
;; Wasmtime will handle by trapping. In the future, once there's a host API for
13+
;; cancelling tasks, that behavior may change, in which case this test will need
14+
;; to be updated.
15+
(component
16+
17+
(component $A
18+
(core func $backpressure.set (canon backpressure.set))
19+
(core module $m
20+
(import "" "backpressure.set" (func $backpressure.set (param i32)))
21+
22+
(func (export "f") (result i32) unreachable)
23+
(func (export "callback") (param i32 i32 i32) (result i32) unreachable)
24+
25+
(func (export "turn-on-backpressure")
26+
(call $backpressure.set (i32.const 1)))
27+
)
28+
29+
(core instance $i (instantiate $m
30+
(with "" (instance
31+
(export "backpressure.set" (func $backpressure.set))
32+
))
33+
))
34+
35+
(func (export "turn-on-backpressure") (canon lift (core func $i "turn-on-backpressure")))
36+
(func (export "f")
37+
(canon lift (core func $i "f") async (callback (func $i "callback"))))
38+
)
39+
(instance $A (instantiate $A))
40+
41+
(core module $libc (memory (export "mem") 1))
42+
(core instance $libc (instantiate $libc))
43+
44+
(core func $f (canon lower (func $A "f") async (memory $libc "mem")))
45+
(core func $turn-on-backpressure (canon lower (func $A "turn-on-backpressure")))
46+
(core func $waitable-set.new (canon waitable-set.new))
47+
(core func $waitable.join (canon waitable.join))
48+
(core func $waitable-set.wait (canon waitable-set.wait (memory $libc "mem")))
49+
50+
(core module $m
51+
(import "" "f" (func $f (param i32 i32) (result i32)))
52+
(import "" "turn-on-backpressure" (func $turn-on-backpressure))
53+
(import "" "waitable-set.new" (func $waitable-set.new (result i32)))
54+
(import "" "waitable.join" (func $waitable.join (param i32 i32)))
55+
(import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32)))
56+
57+
(func (export "f")
58+
(local $status i32)
59+
(local $set i32)
60+
call $turn-on-backpressure
61+
62+
(local.set $status (call $f (i32.const 0) (i32.const 0)))
63+
64+
;; low 4 bits should be "STARTING == 0"
65+
(i32.ne
66+
(i32.const 0)
67+
(i32.and
68+
(local.get $status)
69+
(i32.const 0xf)))
70+
if unreachable end
71+
72+
;; make a new waitable set and join our subtask into it
73+
(local.set $set (call $waitable-set.new))
74+
(call $waitable.join
75+
(i32.shr_u (local.get $status) (i32.const 4))
76+
(local.get $set))
77+
78+
;; block waiting for our task, which should deadlock (?)
79+
(call $waitable-set.wait (local.get $set) (i32.const 0))
80+
unreachable
81+
)
82+
)
83+
84+
(core instance $i (instantiate $m
85+
(with "" (instance
86+
(export "f" (func $f))
87+
(export "turn-on-backpressure" (func $turn-on-backpressure))
88+
(export "waitable-set.new" (func $waitable-set.new))
89+
(export "waitable.join" (func $waitable.join))
90+
(export "waitable-set.wait" (func $waitable-set.wait))
91+
))
92+
))
93+
94+
(func (export "f") (canon lift (core func $i "f")))
95+
)
96+
97+
(assert_trap (invoke "f") "deadlock detected")

tests/misc_testsuite/component-model-async/wait-forever.wast

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,4 @@
5353
(canon lift (core func $i "run")))
5454
)
5555

56-
(assert_trap (invoke "run") "async-lifted export failed to produce a result")
56+
(assert_trap (invoke "run") "deadlock detected")

0 commit comments

Comments
 (0)