Skip to main content

geph5_rt/
reaper.rs

1//! A task "reaper" that cancels everything it owns when dropped, yet does not
2//! leak handles to tasks that have already finished. A faithful reimplementation
3//! of `smolscale::reaper` on top of [`crate::Task`].
4//!
5//! Note: a bare `tokio::task::JoinSet` is *not* a drop-in substitute — it needs
6//! `&mut self` to spawn (this `attach` takes `&self`) and never reaps finished
7//! tasks without active `join_next` calls.
8
9use async_channel::{Receiver, Sender};
10use futures_concurrency::future::Race;
11use futures_util::StreamExt;
12use futures_util::stream::FuturesUnordered;
13
14use crate::Task;
15
16/// Owns a set of detached tasks: finished tasks are dropped as they complete,
17/// and all still-running tasks are cancelled when the reaper is dropped.
18pub struct TaskReaper<T> {
19    send_task: Sender<Task<T>>,
20    _reaper: Task<()>,
21}
22
23impl<T: Send + 'static> Default for TaskReaper<T> {
24    fn default() -> Self {
25        Self::new()
26    }
27}
28
29impl<T: Send + 'static> TaskReaper<T> {
30    /// Creates a new reaper with its background driver task.
31    pub fn new() -> Self {
32        let (send_task, recv_task) = async_channel::unbounded();
33        let _reaper = crate::spawn(reaper_loop(recv_task));
34        Self { send_task, _reaper }
35    }
36
37    /// Attaches a task to this reaper, transferring ownership of its handle.
38    pub fn attach(&self, task: Task<T>) {
39        let _ = self.send_task.try_send(task);
40    }
41}
42
43async fn reaper_loop<T: Send + 'static>(recv_task: Receiver<Task<T>>) {
44    let mut inner: FuturesUnordered<Task<T>> = FuturesUnordered::new();
45    loop {
46        // Race receiving a new task against draining finished tasks. The drain
47        // arm never resolves (it parks on `pending` after polling the set); its
48        // only purpose is to drive attached tasks so completed ones are removed.
49        let next = (async { recv_task.recv().await }, async {
50            inner.next().await;
51            std::future::pending::<Result<Task<T>, async_channel::RecvError>>().await
52        })
53            .race()
54            .await;
55        match next {
56            Ok(task) => inner.push(task),
57            // Channel closed: the reaper was dropped. Returning drops `inner`,
58            // which aborts all still-running attached tasks.
59            Err(_) => return,
60        }
61    }
62}