mermaid_model/utils/task.rs
1//! Task-ownership helpers.
2//!
3//! A sibling relay task must not outlive its parent future as a detached task.
4//! [`spawn_guarded`] wraps a spawned task in an [`AbortOnDrop`] guard, so if the
5//! parent is dropped (e.g. its turn is cancelled) before it `take()`s the handle
6//! to await it, the task is aborted rather than leaked. The effect runner's
7//! streaming relays and the provider stream bridge both own their relay tasks
8//! this way, making the "every task is owned" invariant hold structurally rather
9//! than only behaviorally (#F39, #58, #60).
10
11/// Await a sibling relay task's handle, logging a panic (but not a normal
12/// post-cancellation abort). Awaiting the handle keeps a stray panic from
13/// vanishing the way a bare `let _ = handle.await` would.
14pub async fn join_logged(handle: tokio::task::JoinHandle<()>, what: &str) {
15 if let Err(e) = handle.await
16 && !e.is_cancelled()
17 {
18 tracing::warn!(error = %e, task = what, "sibling relay task panicked");
19 }
20}
21
22/// Owns a sibling relay task so it cannot outlive its parent future as a
23/// detached task: if the parent is dropped before it `take()`s the handle for
24/// [`join_logged`], the guard aborts the task on drop. On the normal path the
25/// handle is taken and awaited, so the drop is a no-op.
26pub struct AbortOnDrop(Option<tokio::task::JoinHandle<()>>);
27
28impl AbortOnDrop {
29 pub fn take(mut self) -> tokio::task::JoinHandle<()> {
30 self.0.take().expect("AbortOnDrop::take called once")
31 }
32}
33
34impl Drop for AbortOnDrop {
35 fn drop(&mut self) {
36 if let Some(handle) = &self.0 {
37 handle.abort();
38 }
39 }
40}
41
42/// `tokio::spawn` a relay, wrapped in an [`AbortOnDrop`] so the parent owns it.
43pub fn spawn_guarded<F>(fut: F) -> AbortOnDrop
44where
45 F: std::future::Future<Output = ()> + Send + 'static,
46{
47 AbortOnDrop(Some(tokio::spawn(fut)))
48}