Skip to main content

nautilus_common/live/
task.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Async task handle storage and lifecycle operations.
17
18use parking_lot::Mutex;
19
20use super::dst::task::JoinHandle;
21
22/// Stores async task handles without imposing spawn or join policy.
23#[derive(Debug, Default)]
24pub struct TaskHandles {
25    handles: Mutex<Vec<JoinHandle<()>>>,
26}
27
28impl TaskHandles {
29    /// Stores `handle` after removing handles for completed tasks.
30    pub fn push(&self, handle: JoinHandle<()>) {
31        let mut handles = self.handles.lock();
32        handles.retain(|handle| !handle.is_finished());
33        handles.push(handle);
34    }
35
36    /// Drains and aborts all stored task handles.
37    pub fn abort_all(&self) {
38        for handle in self.take_all() {
39            handle.abort();
40        }
41    }
42
43    /// Aborts all stored task handles without draining them, so callers can await their
44    /// termination later through [`Self::all_finished`].
45    pub fn abort_all_retained(&self) {
46        for handle in self.handles.lock().iter() {
47            handle.abort();
48        }
49    }
50
51    /// Removes and returns all stored task handles.
52    #[must_use]
53    pub fn take_all(&self) -> Vec<JoinHandle<()>> {
54        let mut handles = self.handles.lock();
55        std::mem::take(&mut *handles)
56    }
57
58    /// Returns whether every stored task handle has finished.
59    #[must_use]
60    pub fn all_finished(&self) -> bool {
61        self.handles.lock().iter().all(JoinHandle::is_finished)
62    }
63
64    /// Returns whether no task handles are stored.
65    #[must_use]
66    pub fn is_empty(&self) -> bool {
67        self.handles.lock().is_empty()
68    }
69
70    /// Returns the number of stored task handles.
71    #[must_use]
72    pub fn len(&self) -> usize {
73        self.handles.lock().len()
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use std::time::Duration;
80
81    use rstest::rstest;
82
83    use super::*;
84    use crate::live::dst::{task, time};
85
86    #[rstest]
87    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
88    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
89    async fn test_push_prunes_finished_handles() {
90        let tasks = TaskHandles::default();
91
92        let finished = task::spawn(async {});
93
94        time::timeout(Duration::from_secs(1), async {
95            while !finished.is_finished() {
96                task::yield_now().await;
97            }
98        })
99        .await
100        .expect("task should finish");
101
102        tasks.push(finished);
103        tasks.push(task::spawn(std::future::pending()));
104
105        assert_eq!(tasks.len(), 1);
106        tasks.abort_all();
107    }
108
109    #[rstest]
110    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
111    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
112    async fn test_abort_all_drains_before_aborting() {
113        let tasks = TaskHandles::default();
114        let mut drop_receivers = Vec::new();
115
116        for _ in 0..2 {
117            let (drop_tx, drop_rx) = tokio::sync::oneshot::channel();
118            let signal = DropSignal { tx: Some(drop_tx) };
119            tasks.push(task::spawn(async move {
120                let _signal = signal;
121                std::future::pending::<()>().await;
122            }));
123            drop_receivers.push(drop_rx);
124        }
125
126        tasks.abort_all();
127
128        assert!(tasks.is_empty());
129
130        for drop_rx in drop_receivers {
131            time::timeout(Duration::from_secs(1), drop_rx)
132                .await
133                .expect("aborted task should drop its future")
134                .expect("drop signal should be sent");
135        }
136    }
137
138    #[rstest]
139    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
140    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
141    async fn test_abort_all_retained_keeps_finished_handles_observable() {
142        let tasks = TaskHandles::default();
143        let mut drop_receivers = Vec::new();
144
145        for _ in 0..2 {
146            let (drop_tx, drop_rx) = tokio::sync::oneshot::channel();
147            let signal = DropSignal { tx: Some(drop_tx) };
148            tasks.push(task::spawn(async move {
149                let _signal = signal;
150                std::future::pending::<()>().await;
151            }));
152            drop_receivers.push(drop_rx);
153        }
154
155        tasks.abort_all_retained();
156
157        assert!(!tasks.is_empty());
158
159        for drop_rx in drop_receivers {
160            time::timeout(Duration::from_secs(1), drop_rx)
161                .await
162                .expect("aborted task should drop its future")
163                .expect("drop signal should be sent");
164        }
165
166        let _ = time::timeout(Duration::from_secs(1), async {
167            while !tasks.all_finished() {
168                task::yield_now().await;
169            }
170        })
171        .await;
172        assert!(tasks.all_finished());
173    }
174
175    #[rstest]
176    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
177    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
178    async fn test_take_all_extracts_handles() {
179        let tasks = TaskHandles::default();
180        tasks.push(task::spawn(std::future::pending()));
181        tasks.push(task::spawn(std::future::pending()));
182
183        let handles = tasks.take_all();
184
185        assert!(tasks.is_empty());
186        assert_eq!(handles.len(), 2);
187        for handle in handles {
188            handle.abort();
189        }
190    }
191
192    #[rstest]
193    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
194    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
195    async fn test_all_finished_preserves_handles() {
196        let tasks = TaskHandles::default();
197        assert!(tasks.all_finished());
198
199        let (release_tx, release_rx) = tokio::sync::oneshot::channel();
200        tasks.push(task::spawn(async move {
201            let _ = release_rx.await;
202        }));
203
204        assert!(!tasks.all_finished());
205        assert_eq!(tasks.len(), 1);
206
207        release_tx.send(()).expect("task should still be waiting");
208        time::timeout(Duration::from_secs(1), async {
209            while !tasks.all_finished() {
210                task::yield_now().await;
211            }
212        })
213        .await
214        .expect("task should finish");
215
216        assert!(tasks.all_finished());
217        assert_eq!(tasks.len(), 1);
218    }
219
220    #[rstest]
221    #[cfg_attr(not(all(feature = "simulation", madsim)), tokio::test)]
222    #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
223    async fn test_drop_detaches_tasks() {
224        let tasks = TaskHandles::default();
225        let (release_tx, release_rx) = tokio::sync::oneshot::channel();
226        let (done_tx, done_rx) = tokio::sync::oneshot::channel();
227
228        tasks.push(task::spawn(async move {
229            let _ = release_rx.await;
230            let _ = done_tx.send(());
231        }));
232        drop(tasks);
233        let _ = release_tx.send(());
234
235        time::timeout(Duration::from_secs(1), done_rx)
236            .await
237            .expect("detached task should complete")
238            .expect("completion signal should be sent");
239    }
240
241    struct DropSignal {
242        tx: Option<tokio::sync::oneshot::Sender<()>>,
243    }
244
245    impl Drop for DropSignal {
246        fn drop(&mut self) {
247            if let Some(tx) = self.tx.take() {
248                let _ = tx.send(());
249            }
250        }
251    }
252}