Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions crates/js-component-bindgen/src/intrinsics/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,21 @@ impl ComponentIntrinsic {
if (unresolvedRoots.size === 0) {{ return; }}

const err = new {runtime_error_class}('wasm trap: deadlock detected: event loop cannot make further progress');
// Which tasks were waiting, and on whose behalf. The message stays
// exactly what the Canonical ABI calls for, so this rides alongside
// it: a deadlock reported from a real program is otherwise a bare
// sentence, and the state that produced it is gone by the time
// anyone reads the failure.
err.deadlockDetail = {{
pendingHostOperations: {store_async_state}.pendingHostOperations,
suspendedTasks: [...suspendedTasks].map((task) => ({{
taskID: task.id(),
componentIdx: task.componentIdx(),
state: task.state(),
rootTaskID: task.getRootTask().id(),
}})),
unresolvedRootTaskIDs: [...unresolvedRoots].map((root) => root.id()),
}};
{store_trap}.error = err;
for (const root of unresolvedRoots) {{
root.setErrored(err);
Expand Down
15 changes: 15 additions & 0 deletions crates/js-component-bindgen/src/intrinsics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1659,6 +1659,21 @@ mod tests {
assert!(check.contains("task.reject(err);"));
}

#[test]
fn detected_deadlocks_carry_the_state_that_produced_them() {
let check =
render_intrinsic_body(Intrinsic::Component(ComponentIntrinsic::CheckForDeadlock));

// The message is what the Canonical ABI asks for and is asserted on elsewhere, so
// the state that produced the deadlock rides beside it rather than inside it.
assert!(check.contains("err.deadlockDetail = {"));
assert!(check.contains("pendingHostOperations: STORE_ASYNC_STATE.pendingHostOperations,"));
assert!(check.contains("taskID: task.id(),"));
assert!(check.contains("componentIdx: task.componentIdx(),"));
assert!(check.contains("rootTaskID: task.getRootTask().id(),"));
assert!(check.contains("unresolvedRootTaskIDs:"));
}

#[test]
fn host_async_operations_suppress_deadlock_detection() {
let tracker =
Expand Down
6 changes: 6 additions & 0 deletions packages/jco-transpile/test/p3/cli-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ const WASI_CLI_RUN_EXPORT = 'wasi:cli/run@0.3.0';

function reportUnhandled(error) {
console.error(error?.stack ?? error);
// A detected deadlock says only that the event loop stopped. What was waiting is
// attached to the error, and this is the last chance to print it: the run ends here,
// and on CI the captured output is all anyone gets.
if (error?.deadlockDetail) {
console.error(`deadlock detail: ${JSON.stringify(error.deadlockDetail, null, 2)}`);
}
process.exit(1);
}

Expand Down
Loading