Skip to content

use reference counting for guest tasks - #14548

Open
dicej wants to merge 2 commits into
bytecodealliance:mainfrom
dicej:guest-task-ref-counts
Open

dicej wants to merge 2 commits into
bytecodealliance:mainfrom
dicej:guest-task-ref-counts

Conversation

@dicej

@dicej dicej commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Previously, we relied on GuestTask::ready_to_delete to determine when all references to the task have been released and thus when it can be safely removed from the table and dropped. However, that function did not properly take into account any subtask handle owned by the guest and not yet dropped.

This commit removes GuestTask::ready_to_delete in favor of a reference count field which is incremented for each new reference (e.g. host handles, guest handles, implicit and explicit threads, etc.) and decremented as those references are released.

Fixes #14459

@dicej
dicej requested a review from alexcrichton October 5, 2026 18:34
@dicej
dicej requested a review from a team as a code owner October 5, 2026 18:34

@alexcrichton alexcrichton left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall I'm definitely feeling like this is cleaning up a lot of the internals 👍

This diff has two panicking tests, both related to the new Drop impl which I also commented on below, however:

diff --git a/tests/all/component_model/async.rs b/tests/all/component_model/async.rs
index eae7ca0012..b210019bb2 100644
--- a/tests/all/component_model/async.rs
+++ b/tests/all/component_model/async.rs
@@ -1612,3 +1612,73 @@ fn inter_component_stream_is_not_intra_component() -> Result<()> {
 
     Ok(())
 }
+
+const THUNK_COMPONENT: &str = r#"
+    (component
+        (core module $m
+            (func (export "thunk"))
+        )
+        (core instance $i (instantiate $m))
+        (func (export "thunk") async
+            (canon lift (core func $i "thunk"))
+        )
+    )
+"#;
+
+/// Dropping the result of `start_call_concurrent` outside of the store's event
+/// loop should not panic.
+#[tokio::test]
+#[cfg_attr(miri, ignore)]
+async fn drop_start_call_concurrent_outside_event_loop() -> Result<()> {
+    let engine = super::async_engine();
+    let component = Component::new(&engine, THUNK_COMPONENT)?;
+    let mut store = Store::new(&engine, ());
+    let instance = Linker::new(&engine)
+        .instantiate_async(&mut store, &component)
+        .await?;
+    let thunk = instance.get_typed_func::<(), ()>(&mut store, "thunk")?;
+
+    let call = thunk.start_call_concurrent(&mut store, ())?;
+    drop(call);
+
+    Ok(())
+}
+
+/// An error returned from `TaskGroupHook::handle_finish` for a
+/// `call_concurrent` should not panic.
+#[tokio::test]
+#[cfg_attr(miri, ignore)]
+async fn task_group_hook_finish_error_call_concurrent() -> Result<()> {
+    struct FailFinish;
+
+    impl TaskGroupHook for FailFinish {
+        fn handle_start(&mut self, _: TaskGroupId) -> Result<()> {
+            Ok(())
+        }
+        fn handle_enter(&mut self, _: TaskGroupId) -> Result<()> {
+            Ok(())
+        }
+        fn handle_exit(&mut self, _: TaskGroupId) -> Result<()> {
+            Ok(())
+        }
+        fn handle_finish(&mut self, _: TaskGroupId) -> Result<()> {
+            wasmtime::bail!("handle_finish failed")
+        }
+    }
+
+    let engine = super::async_engine();
+    let component = Component::new(&engine, THUNK_COMPONENT)?;
+    let mut store = Store::new(&engine, ());
+    let instance = Linker::new(&engine)
+        .instantiate_async(&mut store, &component)
+        .await?;
+    let thunk = instance.get_typed_func::<(), ()>(&mut store, "thunk")?;
+    store.task_group_hook(FailFinish);
+
+    let result = store
+        .run_concurrent(async |store| thunk.call_concurrent(store, ()).await)
+        .await;
+    assert!(result.is_err() || result.unwrap().is_err());
+
+    Ok(())
+}

Comment thread crates/wasmtime/src/runtime/component/concurrent.rs Outdated
Comment thread crates/wasmtime/src/runtime/component/concurrent.rs Outdated
Comment on lines +6490 to +6495
impl<R> Drop for StagedCall<R> {
fn drop(&mut self) {
check_ambient_store(self.store);
tls::get(|store| store.decrement_task_ref_count(self.task)).unwrap();
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels extremely dangerous to me because there's nothing keeping this scoped to some sort of Accessor-based closure (e.g. a lifetime or similar). I feel like we've had a lot of historical issues where code like this (with external requirements) was satisfied when first-written but later broken without notice which led to various issues.

Can this be shuffled around or similar to somewhere that's more guaranteed to have the store?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically, the issue here is that {Typed}FuncCallConcurrent (based internally on StagedCall) needs to keep the task alive until it is finished or dropped, at which point it must decrement the ref count. So we either need to document that responsibility (e.g. "you must either call finish_call_concurrent or dispose_call_concurrent before dropping this or the task will leak") or rig up some kind of "deadman switch" based e.g. on a oneshot channel to ensure that the ref count is decremented on the next turn of the event loop.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't personally feel too strongly about how to fix things. I'd prioritize not using channels since everything is already too slow, and otherwise beyond that IMO it should basically just not be possible to panic the API. We already require explicit deletion for things like futures/streams/resources, so if this is one more that seems not super consequential

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I just pushed an update; let me know what you think.

Comment thread crates/wasmtime/src/runtime/component/concurrent.rs Outdated
@github-actions github-actions Bot added the wasmtime:api Related to the API of the `wasmtime` crate itself label Oct 5, 2026
Previously, we relied on `GuestTask::ready_to_delete` to determine when all
references to the task have been released and thus when it can be safely removed
from the table and dropped.  However, that function did not properly take into
account any subtask handle owned by the guest and not yet dropped.

This commit removes `GuestTask::ready_to_delete` in favor of a reference count
field which is incremented for each new reference (e.g. host handles, guest
handles, implicit and explicit threads, etc.) and decremented as those
references are released.

Note that, as with `TaskGroup`s, the refcounting mechanism used here is pretty
ad-hoc and primitive; if you forget to decrement in all the correct places, the
task will silently leak.  Ideally we'd be using something like `std::rc::Rc` or
equivalent, but since we need access to the store in order to decrement and
delete (and because Rust doesn't yet have compile-time-checked linear types), we
can't match the ergonomics of `Rc`.  Perhaps the next best thing would be a
"runtime-checked linear type" similar to `Fiber` which panics if dropped without
having been explicitly disposed?

Fixes bytecodealliance#14459
@dicej
dicej force-pushed the guest-task-ref-counts branch from 7430818 to 2a40222 Compare October 9, 2026 22:12
Rather than dispose of the underlying guest task in `StagedCall::drop`, require
that it be disposed of explicitly by the caller.

Co-Authored-By: Alex Crichton <alex@alexcrichton.com>
@dicej
dicej force-pushed the guest-task-ref-counts branch from 2a40222 to dcc9d0f Compare October 9, 2026 22:29

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

wasmtime:api Related to the API of the `wasmtime` crate itself

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Component Model async runtime may delete still-live subtask handles

2 participants