Skip to main content

spawned_rt/threads/
mod.rs

1//! IO-threads based module to support shared behavior with task based version.
2
3pub mod mpsc;
4pub mod oneshot;
5
6// This cfg is only for cancellation models: instrumented tokens must be
7// constructed inside loom::model, not in ordinary actor or signal tests.
8#[cfg(spawned_loom)]
9use loom::sync::{
10    atomic::{AtomicBool, Ordering},
11    Arc, Mutex,
12};
13#[cfg(not(spawned_loom))]
14use std::sync::{
15    atomic::{AtomicBool, Ordering},
16    Arc, Mutex,
17};
18use std::sync::{mpsc as std_mpsc, OnceLock};
19pub use std::{
20    future::Future,
21    thread::{sleep, spawn, JoinHandle},
22};
23
24#[cfg(all(feature = "async", not(spawned_sim)))]
25use crate::tasks::Runtime;
26// Native actors keep real Tokio under simulation: they run on OS threads.
27use crate::tracing::init_tracing;
28#[cfg(all(feature = "async", spawned_sim))]
29use tokio::runtime::Runtime;
30
31/// Global list of Ctrl+C subscribers
32static CTRL_C_SUBSCRIBERS: OnceLock<Mutex<Vec<std_mpsc::Sender<()>>>> = OnceLock::new();
33
34/// Initialize tracing and run the given function.
35pub fn run(f: fn()) {
36    init_tracing();
37
38    f()
39}
40
41/// Block on a future using an isolated Tokio runtime with one async worker.
42///
43/// The future is driven on the calling thread. Spawned async helpers use one
44/// additional worker, so they can progress while that thread blocks. Tokio may
45/// create further threads for `spawn_blocking`, filesystem or DNS operations.
46/// The worker count is explicit and independent of `TOKIO_WORKER_THREADS`.
47#[cfg(feature = "async")]
48pub fn block_on<F: Future>(future: F) -> F::Output {
49    #[cfg(feature = "backend-experiments")]
50    if std::env::var("SPAWNED_EXPERIMENT_RUNTIME").as_deref() == Ok("managed") {
51        // Experiment-only bounded shared pool; process lifetime, no caller dependency.
52        static POOL: OnceLock<Runtime> = OnceLock::new();
53        return POOL
54            .get_or_init(|| {
55                tokio::runtime::Builder::new_multi_thread()
56                    .worker_threads(2)
57                    .enable_all()
58                    .build()
59                    .unwrap()
60            })
61            .block_on(future);
62    }
63    #[cfg(feature = "backend-experiments")]
64    let rt = match std::env::var("SPAWNED_EXPERIMENT_RUNTIME").as_deref() {
65        Ok("current") => tokio::runtime::Builder::new_current_thread()
66            .enable_all()
67            .build()
68            .unwrap(),
69        Ok("one-worker") => tokio::runtime::Builder::new_multi_thread()
70            .worker_threads(1)
71            .enable_all()
72            .build()
73            .unwrap(),
74        _ => isolated_runtime(),
75    };
76    #[cfg(not(feature = "backend-experiments"))]
77    let rt = isolated_runtime();
78    rt.block_on(future)
79}
80
81// Keep a multithread runtime for block_in_place and independently scheduled
82// helpers, but never allocate a machine-sized worker pool for each actor.
83#[cfg(feature = "async")]
84fn isolated_runtime() -> Runtime {
85    tokio::runtime::Builder::new_multi_thread()
86        .worker_threads(1)
87        .enable_all()
88        .build()
89        .unwrap()
90}
91
92/// Spawn blocking is the same as spawn for pure threaded usage.
93pub fn spawn_blocking<F, R>(f: F) -> JoinHandle<R>
94where
95    F: FnOnce() -> R + Send + 'static,
96    R: Send + 'static,
97{
98    spawn(f)
99}
100
101type CancelCallback = Box<dyn FnOnce() + Send>;
102
103/// A token that can be used to signal cancellation.
104///
105/// Supports registering callbacks via `on_cancel()` that fire when
106/// the token is cancelled, enabling efficient waiting patterns.
107#[derive(Clone)]
108pub struct CancellationToken {
109    is_cancelled: Arc<AtomicBool>,
110    callbacks: Arc<Mutex<Vec<CancelCallback>>>,
111}
112
113impl std::fmt::Debug for CancellationToken {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        f.debug_struct("CancellationToken")
116            .field("is_cancelled", &self.is_cancelled())
117            .finish()
118    }
119}
120
121impl Default for CancellationToken {
122    fn default() -> Self {
123        Self::new()
124    }
125}
126
127impl CancellationToken {
128    pub fn new() -> Self {
129        CancellationToken {
130            is_cancelled: Arc::new(false.into()),
131            callbacks: Arc::new(Mutex::new(Vec::new())),
132        }
133    }
134
135    pub fn is_cancelled(&self) -> bool {
136        self.is_cancelled.load(Ordering::SeqCst)
137    }
138
139    pub fn cancel(&self) {
140        self.is_cancelled.store(true, Ordering::SeqCst);
141        // Fire all registered callbacks
142        let callbacks: Vec<_> = self
143            .callbacks
144            .lock()
145            .unwrap_or_else(|e| e.into_inner())
146            .drain(..)
147            .collect();
148        for cb in callbacks {
149            cb();
150        }
151    }
152
153    /// Register a callback to be invoked when this token is cancelled.
154    /// If already cancelled, the callback fires immediately.
155    ///
156    /// This method is thread-safe: the callback is guaranteed to fire exactly
157    /// once, either immediately (if already cancelled) or when `cancel()` is called.
158    pub fn on_cancel(&self, callback: CancelCallback) {
159        // Hold the lock while checking is_cancelled to avoid a race with cancel().
160        // cancel() sets the flag BEFORE acquiring the lock, so if we see
161        // is_cancelled=false while holding the lock, cancel() hasn't drained
162        // callbacks yet and will drain ours after we release the lock.
163        let mut callbacks = self.callbacks.lock().unwrap_or_else(|e| e.into_inner());
164        if self.is_cancelled() {
165            drop(callbacks);
166            callback();
167        } else {
168            callbacks.push(callback);
169        }
170    }
171}
172
173/// Returns a closure that blocks until Ctrl+C is received.
174///
175/// Multiple calls to this function are supported - each returns a closure that
176/// will be notified when Ctrl+C is pressed. This allows multiple actors to
177/// react to the same signal.
178///
179/// The signal handler is registered on the first call. Subsequent calls simply
180/// add new subscribers to the broadcast list.
181///
182/// # Example
183///
184/// ```ignore
185/// // Both actors will be notified on Ctrl+C
186/// send_message_on(actor1.clone(), rt::ctrl_c(), Msg::Shutdown);
187/// send_message_on(actor2.clone(), rt::ctrl_c(), Msg::Shutdown);
188/// ```
189pub fn ctrl_c() -> impl FnOnce() + Send + 'static {
190    // Initialize subscribers list and register handler on first call
191    let subscribers = CTRL_C_SUBSCRIBERS.get_or_init(|| {
192        ctrlc::set_handler(|| {
193            if let Some(subs) = CTRL_C_SUBSCRIBERS.get() {
194                let mut guard = subs.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
195                // Notify all subscribers and remove dead ones (where receiver was dropped)
196                guard.retain(|tx| tx.send(()).is_ok());
197            }
198        })
199        .expect("Ctrl+C handler already set. Use ctrl_c() instead of ctrlc::set_handler()");
200        Mutex::new(Vec::new())
201    });
202
203    // Create a new subscriber channel
204    let (tx, rx) = std_mpsc::channel();
205    subscribers
206        .lock()
207        .unwrap_or_else(|poisoned| poisoned.into_inner())
208        .push(tx);
209
210    move || {
211        let _ = rx.recv();
212    }
213}
214
215/// Park the caller while polling an executor-independent future. Thread-only
216/// builds have no Tokio reactor, timer driver or implicit helper workers.
217#[cfg(not(feature = "async"))]
218pub fn block_on<F: Future>(future: F) -> F::Output {
219    use std::task::{Context, Poll, Wake, Waker};
220    struct Parker {
221        notified: std::sync::Mutex<bool>,
222        signal: std::sync::Condvar,
223    }
224    impl Parker {
225        fn notify(&self) {
226            *self.notified.lock().unwrap_or_else(|p| p.into_inner()) = true;
227            self.signal.notify_one();
228        }
229        fn wait(&self) {
230            let mut notified = self.notified.lock().unwrap_or_else(|p| p.into_inner());
231            while !*notified {
232                notified = self
233                    .signal
234                    .wait(notified)
235                    .unwrap_or_else(|p| p.into_inner());
236            }
237            *notified = false;
238        }
239    }
240    impl Wake for Parker {
241        fn wake(self: std::sync::Arc<Self>) {
242            self.notify();
243        }
244        fn wake_by_ref(self: &std::sync::Arc<Self>) {
245            self.notify();
246        }
247    }
248    // Each invocation owns its wake permit. Nested blocking waits must not
249    // consume a wake intended for their caller's still-pending future.
250    let parker = std::sync::Arc::new(Parker {
251        notified: std::sync::Mutex::new(false),
252        signal: std::sync::Condvar::new(),
253    });
254    let waker = Waker::from(parker.clone());
255    let mut context = Context::from_waker(&waker);
256    let mut future = std::pin::pin!(future);
257    loop {
258        match future.as_mut().poll(&mut context) {
259            Poll::Ready(value) => return value,
260            Poll::Pending => parker.wait(),
261        }
262    }
263}
264
265#[cfg(all(test, not(feature = "async")))]
266mod parking_tests {
267    #[test]
268    fn nested_block_on_does_not_consume_the_outer_futures_wakeup() {
269        let (tx, rx) = std::sync::mpsc::channel();
270        let worker = std::thread::spawn(move || {
271            let mut outer_polled = false;
272            let value = super::block_on(std::future::poll_fn(|cx| {
273                if outer_polled {
274                    return std::task::Poll::Ready(42);
275                }
276                outer_polled = true;
277                cx.waker().wake_by_ref();
278                let mut inner_polled = false;
279                super::block_on(std::future::poll_fn(|inner| {
280                    if inner_polled {
281                        std::task::Poll::Ready(())
282                    } else {
283                        inner_polled = true;
284                        inner.waker().wake_by_ref();
285                        std::task::Poll::Pending
286                    }
287                }));
288                std::task::Poll::Pending
289            }));
290            tx.send(value).unwrap();
291        });
292        let result = rx.recv_timeout(std::time::Duration::from_secs(1));
293        if result.is_err() {
294            // Rescue the old shared-thread-token executor so a regression is a
295            // finite assertion failure rather than a hanging test process.
296            worker.thread().unpark();
297            let _ = rx.recv_timeout(std::time::Duration::from_secs(1));
298        }
299        let value = result.expect("inner executor consumed the outer wake");
300        worker.join().unwrap();
301        assert_eq!(value, 42);
302    }
303}