Skip to main content

klukai_types/
spawn.rs

1//! Keep track of how many futures of a certain kind are running (in an [AtomicUsize])
2
3use std::pin::Pin;
4use std::sync::atomic::{AtomicUsize, Ordering};
5use std::task::{Context, Poll};
6use std::time::Duration;
7
8use futures::Future;
9use pin_project_lite::pin_project;
10use tracing::{info, trace};
11
12/// Global counter for [spawn_counted] and [spawn_counted_w_handle]
13pub static PENDING_HANDLES: AtomicUsize = AtomicUsize::new(0);
14
15#[cfg(all(unix, debug_assertions))]
16static BACKTRACES: parking_lot::Mutex<Vec<Option<std::backtrace::Backtrace>>> =
17    parking_lot::Mutex::new(Vec::new());
18
19/// Spawn `fut` as a [CountedFut] (increments/decrements an [AtomicUsize])
20#[track_caller]
21pub fn spawn_counted<F>(fut: F) -> tokio::task::JoinHandle<F::Output>
22where
23    F: Future + Send + 'static,
24    F::Output: Send,
25{
26    tokio::spawn(CountedFut::new(fut, &PENDING_HANDLES))
27}
28
29/// Spawn `fut` as a [CountedFut] (increments/decrements an [AtomicUsize])
30/// on the given [tokio::runtime::Handle].
31#[track_caller]
32pub fn spawn_counted_w_handle<F>(
33    fut: F,
34    h: &tokio::runtime::Handle,
35) -> tokio::task::JoinHandle<F::Output>
36where
37    F: Future + Send + 'static,
38    F::Output: Send,
39{
40    h.spawn(CountedFut::new(fut, &PENDING_HANDLES))
41}
42
43/// Spawn blocking `fut` as a [CountedFut] (increments/decrements an [AtomicUsize])
44/// on the given [tokio::runtime::Handle].
45#[track_caller]
46pub fn spawn_blocking_counted_w_handle<F, R>(
47    func: F,
48    h: &tokio::runtime::Handle,
49) -> tokio::task::JoinHandle<R>
50where
51    F: FnOnce() -> R + Send + 'static,
52    R: Send + 'static,
53{
54    h.spawn_blocking(move || {
55        let _guard = CountedGuard::new(&PENDING_HANDLES);
56        func()
57    })
58}
59
60pub struct CountedGuard {
61    pendings: &'static AtomicUsize,
62}
63
64impl CountedGuard {
65    pub fn new(pendings: &'static AtomicUsize) -> Self {
66        pendings.fetch_add(1, Ordering::SeqCst);
67        Self { pendings }
68    }
69}
70
71impl Drop for CountedGuard {
72    fn drop(&mut self) {
73        let count = self.pendings.fetch_sub(1, Ordering::SeqCst);
74        trace!("dropping counted guard, count: {}", count - 1);
75    }
76}
77
78pin_project! {
79    /// An [AtomicUsize] is incremented when building this future, and decremented
80    /// again when the future completes.
81    #[project = CountedProj]
82    pub struct CountedFut<F> {
83        #[pin]
84        fut: F,
85        pendings: &'static AtomicUsize,
86        index: usize,
87    }
88
89    impl<F> PinnedDrop for CountedFut<F> {
90        fn drop(this: Pin<&mut Self>) {
91            let count = this.pendings.fetch_sub(1, Ordering::SeqCst);
92            trace!("dropping counted future, count: {}", count - 1);
93
94            #[cfg(all(unix, debug_assertions))]
95            {
96                let mut lock = BACKTRACES.lock();
97                lock[this.index] = None;
98            }
99        }
100    }
101}
102
103impl<F> CountedFut<F> {
104    /// Create a new [CountedFut], immediately incrementing `pendings`
105    pub fn new(fut: F, pendings: &'static AtomicUsize) -> Self {
106        pendings.fetch_add(1, Ordering::SeqCst);
107
108        #[cfg(all(unix, debug_assertions))]
109        let index = {
110            let mut lock = BACKTRACES.lock();
111            let i = lock.len();
112            lock.push(Some(std::backtrace::Backtrace::capture()));
113            i
114        };
115
116        #[cfg(not(all(unix, debug_assertions)))]
117        let index = 0;
118
119        Self {
120            fut,
121            pendings,
122            index,
123        }
124    }
125}
126
127impl<F> Future for CountedFut<F>
128where
129    F: Future,
130{
131    type Output = F::Output;
132
133    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<F::Output> {
134        trace!("polling counted future");
135        let this = self.project();
136        this.fut.poll(cx)
137    }
138}
139
140/// Waits for [PENDING_HANDLES] to reach zero. All counted futures must be
141/// [Tripwire]-aware, and the tripwire must've been tripped, otherwise this
142/// is just going to sleep for 600 seconds.
143pub async fn wait_for_all_pending_handles() {
144    #[cfg(all(unix, debug_assertions))]
145    {
146        let signals = signal_hook_tokio::Signals::new([signal_hook::consts::SIGTERM])
147            .expect("unable to create signals");
148        let handle = signals.handle();
149
150        let mut sig = tokio::spawn(handle_sigterm(signals));
151        let mut wait = tokio::spawn(wait_handles());
152
153        tokio::select! {
154            _ = &mut sig => {
155                eprintln!("received SIGTERM");
156            }
157            _ = &mut wait => {
158                handle.close();
159                let _ = sig.await;
160                return;
161            }
162        }
163
164        let _ = wait.await;
165    }
166
167    #[cfg(not(all(unix, debug_assertions)))]
168    {
169        wait_handles().await;
170    }
171}
172
173async fn wait_handles() {
174    let mut rounds = 0;
175
176    for _ in 0..600 {
177        match PENDING_HANDLES.load(Ordering::SeqCst) {
178            0 => {
179                info!("All spawned futures done!");
180                break;
181            }
182            n => {
183                rounds += 1;
184                if rounds % 10 == 0 {
185                    info!("Waiting on {n} spawned futures before exiting")
186                }
187            }
188        }
189        tokio::time::sleep(Duration::from_millis(100)).await;
190    }
191}
192
193/// cargo nextest sends a `SIGTERM` when a test times out, which gives us a
194/// chance to print the backtraces of the counted futures that are still running
195#[cfg(all(unix, debug_assertions))]
196async fn handle_sigterm(mut signals: signal_hook_tokio::SignalsInfo) {
197    use futures_util::StreamExt;
198    while let Some(signal) = signals.next().await {
199        if signal != signal_hook::consts::SIGTERM {
200            continue;
201        }
202
203        let lock = BACKTRACES.lock();
204
205        eprintln!("active counted futures:");
206        for (i, bt) in lock.iter().enumerate() {
207            if let Some(bt) = bt {
208                eprintln!("#{i}: {bt}");
209            }
210        }
211    }
212}