1use std::time::Duration;
11
12use tokio::task::JoinHandle;
13use tokio_util::sync::CancellationToken;
14
15#[derive(Debug)]
22pub struct SpawnedTask {
23 cancel: CancellationToken,
24 handle: JoinHandle<()>,
25}
26
27impl SpawnedTask {
28 pub fn spawn<F, Fut>(body: F) -> Self
32 where
33 F: FnOnce(CancellationToken) -> Fut,
34 Fut: Future<Output = ()> + Send + 'static,
35 {
36 let cancel = CancellationToken::new();
37 let handle = tokio::spawn(body(cancel.clone()));
38 Self { cancel, handle }
39 }
40
41 pub fn spawn_with<Fut>(cancel: CancellationToken, body: Fut) -> Self
43 where
44 Fut: Future<Output = ()> + Send + 'static,
45 {
46 let handle = tokio::spawn(body);
47 Self { cancel, handle }
48 }
49
50 #[must_use]
53 pub const fn from_parts(cancel: CancellationToken, handle: JoinHandle<()>) -> Self {
54 Self { cancel, handle }
55 }
56
57 pub fn cancel(&self) {
59 self.cancel.cancel();
60 }
61
62 #[must_use]
64 pub fn is_finished(&self) -> bool {
65 self.handle.is_finished()
66 }
67
68 #[must_use]
70 pub fn cancellation(&self) -> CancellationToken {
71 self.cancel.clone()
72 }
73
74 pub async fn shutdown(self, grace: Duration) {
77 self.cancel.cancel();
78 let mut handle = self.handle;
79 if tokio::time::timeout(grace, &mut handle).await.is_err() {
80 handle.abort();
81 let _ = handle.await;
82 }
83 }
84
85 pub fn abort(self) {
87 self.handle.abort();
88 }
89
90 pub async fn join(self) {
92 let _ = self.handle.await;
93 }
94}
95
96#[derive(Debug, Default)]
102pub struct TaskGroup {
103 tasks: Vec<SpawnedTask>,
104}
105
106impl TaskGroup {
107 #[must_use]
109 pub const fn new() -> Self {
110 Self { tasks: Vec::new() }
111 }
112
113 pub fn push(&mut self, task: SpawnedTask) {
115 self.tasks.push(task);
116 }
117
118 #[must_use]
120 pub const fn len(&self) -> usize {
121 self.tasks.len()
122 }
123
124 #[must_use]
126 pub const fn is_empty(&self) -> bool {
127 self.tasks.is_empty()
128 }
129
130 pub fn cancel_all(&self) {
132 for task in &self.tasks {
133 task.cancel();
134 }
135 }
136
137 pub async fn shutdown(&mut self, grace: Duration) {
139 let tasks = std::mem::take(&mut self.tasks);
140 for task in &tasks {
141 task.cancel();
142 }
143 for task in tasks {
144 task.shutdown(grace).await;
145 }
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use std::sync::Arc;
153 use std::sync::atomic::{AtomicBool, Ordering};
154
155 #[tokio::test]
156 async fn shutdown_cancels_cooperative_task() {
157 let ran = Arc::new(AtomicBool::new(false));
158 let r = ran.clone();
159 let task = SpawnedTask::spawn(move |cancel| async move {
160 cancel.cancelled().await;
161 r.store(true, Ordering::SeqCst);
162 });
163 task.shutdown(Duration::from_secs(1)).await;
164 assert!(ran.load(Ordering::SeqCst));
165 }
166
167 #[tokio::test(start_paused = true)]
168 async fn shutdown_aborts_wedged_task() {
169 let task = SpawnedTask::spawn(|_cancel| async move {
170 std::future::pending::<()>().await;
171 });
172 task.shutdown(Duration::from_millis(50)).await;
174 }
175
176 #[tokio::test]
177 async fn group_shuts_down_all() {
178 let mut group = TaskGroup::new();
179 for _ in 0..3 {
180 group.push(SpawnedTask::spawn(|cancel| async move {
181 cancel.cancelled().await;
182 }));
183 }
184 assert_eq!(group.len(), 3);
185 group.shutdown(Duration::from_secs(1)).await;
186 assert!(group.is_empty());
187 }
188
189 #[tokio::test]
190 async fn task_constructors_cancel_abort_and_join_paths() {
191 let token = CancellationToken::new();
192 let finished = Arc::new(AtomicBool::new(false));
193 let done = Arc::clone(&finished);
194 let task = SpawnedTask::spawn_with(token.clone(), async move {
195 token.cancelled().await;
196 done.store(true, Ordering::SeqCst);
197 });
198 assert!(!task.is_finished());
199 let cancellation = task.cancellation();
200 task.cancel();
201 task.join().await;
202 assert!(cancellation.is_cancelled());
203 assert!(finished.load(Ordering::SeqCst));
204
205 let token = CancellationToken::new();
206 let handle = tokio::spawn(async move {
207 std::future::pending::<()>().await;
208 });
209 let task = SpawnedTask::from_parts(token, handle);
210 task.abort();
211 }
212
213 #[tokio::test]
214 async fn cancel_all_cancels_without_waiting() {
215 let mut group = TaskGroup::new();
216 let token = CancellationToken::new();
217 let task = SpawnedTask::from_parts(
218 token.clone(),
219 tokio::spawn(async move {
220 std::future::pending::<()>().await;
221 }),
222 );
223 group.push(task);
224
225 group.cancel_all();
226
227 assert!(token.is_cancelled());
228 group.shutdown(Duration::from_millis(1)).await;
229 }
230}