vtcode_commons/
task_guard.rs1use tokio::task::JoinHandle;
25
26#[must_use = "TaskGuard aborts the task when dropped; bind it to a local"]
31pub struct TaskGuard {
32 handle: Option<JoinHandle<()>>,
33 label: &'static str,
34}
35
36impl TaskGuard {
37 pub fn new(handle: JoinHandle<()>) -> Self {
39 Self::with_label(handle, "task")
40 }
41
42 pub fn with_label(handle: JoinHandle<()>, label: &'static str) -> Self {
44 Self { handle: Some(handle), label }
45 }
46
47 pub fn disarm(&mut self) -> Option<JoinHandle<()>> {
51 self.handle.take()
52 }
53
54 pub fn is_finished(&self) -> bool {
59 self.handle.as_ref().is_some_and(JoinHandle::is_finished)
60 }
61
62 pub fn label(&self) -> &'static str {
64 self.label
65 }
66}
67
68impl std::fmt::Debug for TaskGuard {
69 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 formatter
71 .debug_struct("TaskGuard")
72 .field("label", &self.label)
73 .field("finished", &self.is_finished())
74 .finish_non_exhaustive()
75 }
76}
77
78impl Drop for TaskGuard {
79 fn drop(&mut self) {
80 if let Some(handle) = self.handle.take() {
81 handle.abort();
82 }
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use std::sync::Arc;
90 use std::sync::atomic::{AtomicBool, Ordering};
91 use std::time::Duration;
92
93 #[tokio::test]
94 async fn abort_on_drop_prevents_completion() {
95 let completed = Arc::new(AtomicBool::new(false));
96 let flag = Arc::clone(&completed);
97 let guard = TaskGuard::with_label(
98 tokio::spawn(async move {
99 tokio::time::sleep(Duration::from_millis(200)).await;
100 flag.store(true, Ordering::SeqCst);
101 }),
102 "abort-on-drop",
103 );
104 drop(guard);
105 tokio::time::sleep(Duration::from_millis(300)).await;
106 assert!(!completed.load(Ordering::SeqCst));
107 }
108
109 #[tokio::test]
110 async fn disarm_lets_task_complete() {
111 let completed = Arc::new(AtomicBool::new(false));
112 let flag = Arc::clone(&completed);
113 let mut guard = TaskGuard::with_label(
114 tokio::spawn(async move {
115 flag.store(true, Ordering::SeqCst);
116 }),
117 "disarm",
118 );
119 let handle = guard.disarm().expect("fresh guard holds a handle");
120 assert!(guard.disarm().is_none(), "second disarm releases nothing");
121 assert!(!guard.is_finished(), "disarmed guard owns nothing");
122 handle.await.expect("disarmed task runs to completion");
123 assert!(completed.load(Ordering::SeqCst));
124 }
125
126 #[tokio::test]
127 async fn is_finished_distinguishes_completed_and_pending_tasks() {
128 let completed = TaskGuard::new(tokio::spawn(async {}));
129 for _ in 0..100 {
131 if completed.is_finished() {
132 break;
133 }
134 tokio::task::yield_now().await;
135 }
136 assert!(completed.is_finished());
137
138 let pending = TaskGuard::new(tokio::spawn(async {
139 tokio::time::sleep(Duration::from_secs(60)).await;
140 }));
141 assert!(!pending.is_finished());
142 }
143
144 #[tokio::test]
145 async fn label_defaults_and_debug_render() {
146 let guard = TaskGuard::new(tokio::spawn(async {}));
147 assert_eq!(guard.label(), "task");
148 let rendered = format!("{:?}", guard);
149 assert!(rendered.contains("TaskGuard"));
150 assert!(rendered.contains("task"));
151 }
152}