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
24use crate::{tasks::Runtime, tracing::init_tracing};
25
26/// Global list of Ctrl+C subscribers
27static CTRL_C_SUBSCRIBERS: OnceLock<Mutex<Vec<std_mpsc::Sender<()>>>> = OnceLock::new();
28
29/// Initialize tracing and run the given function.
30pub fn run(f: fn()) {
31    init_tracing();
32
33    f()
34}
35
36/// Create a temporary tokio runtime and block on the given future.
37pub fn block_on<F: Future>(future: F) -> F::Output {
38    #[cfg(feature = "backend-experiments")]
39    if std::env::var("SPAWNED_EXPERIMENT_RUNTIME").as_deref() == Ok("managed") {
40        // Experiment-only bounded shared pool; process lifetime, no caller dependency.
41        static POOL: OnceLock<Runtime> = OnceLock::new();
42        return POOL
43            .get_or_init(|| {
44                tokio::runtime::Builder::new_multi_thread()
45                    .worker_threads(2)
46                    .enable_all()
47                    .build()
48                    .unwrap()
49            })
50            .block_on(future);
51    }
52    #[cfg(feature = "backend-experiments")]
53    let rt = match std::env::var("SPAWNED_EXPERIMENT_RUNTIME").as_deref() {
54        Ok("current") => tokio::runtime::Builder::new_current_thread()
55            .enable_all()
56            .build()
57            .unwrap(),
58        Ok("one-worker") => tokio::runtime::Builder::new_multi_thread()
59            .worker_threads(1)
60            .enable_all()
61            .build()
62            .unwrap(),
63        _ => Runtime::new().unwrap(),
64    };
65    #[cfg(not(feature = "backend-experiments"))]
66    let rt = Runtime::new().unwrap();
67    rt.block_on(future)
68}
69
70/// Spawn blocking is the same as spawn for pure threaded usage.
71pub fn spawn_blocking<F, R>(f: F) -> JoinHandle<R>
72where
73    F: FnOnce() -> R + Send + 'static,
74    R: Send + 'static,
75{
76    spawn(f)
77}
78
79type CancelCallback = Box<dyn FnOnce() + Send>;
80
81/// A token that can be used to signal cancellation.
82///
83/// Supports registering callbacks via `on_cancel()` that fire when
84/// the token is cancelled, enabling efficient waiting patterns.
85#[derive(Clone)]
86pub struct CancellationToken {
87    is_cancelled: Arc<AtomicBool>,
88    callbacks: Arc<Mutex<Vec<CancelCallback>>>,
89}
90
91impl std::fmt::Debug for CancellationToken {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        f.debug_struct("CancellationToken")
94            .field("is_cancelled", &self.is_cancelled())
95            .finish()
96    }
97}
98
99impl Default for CancellationToken {
100    fn default() -> Self {
101        Self::new()
102    }
103}
104
105impl CancellationToken {
106    pub fn new() -> Self {
107        CancellationToken {
108            is_cancelled: Arc::new(false.into()),
109            callbacks: Arc::new(Mutex::new(Vec::new())),
110        }
111    }
112
113    pub fn is_cancelled(&self) -> bool {
114        self.is_cancelled.load(Ordering::SeqCst)
115    }
116
117    pub fn cancel(&self) {
118        self.is_cancelled.store(true, Ordering::SeqCst);
119        // Fire all registered callbacks
120        let callbacks: Vec<_> = self
121            .callbacks
122            .lock()
123            .unwrap_or_else(|e| e.into_inner())
124            .drain(..)
125            .collect();
126        for cb in callbacks {
127            cb();
128        }
129    }
130
131    /// Register a callback to be invoked when this token is cancelled.
132    /// If already cancelled, the callback fires immediately.
133    ///
134    /// This method is thread-safe: the callback is guaranteed to fire exactly
135    /// once, either immediately (if already cancelled) or when `cancel()` is called.
136    pub fn on_cancel(&self, callback: CancelCallback) {
137        // Hold the lock while checking is_cancelled to avoid a race with cancel().
138        // cancel() sets the flag BEFORE acquiring the lock, so if we see
139        // is_cancelled=false while holding the lock, cancel() hasn't drained
140        // callbacks yet and will drain ours after we release the lock.
141        let mut callbacks = self.callbacks.lock().unwrap_or_else(|e| e.into_inner());
142        if self.is_cancelled() {
143            drop(callbacks);
144            callback();
145        } else {
146            callbacks.push(callback);
147        }
148    }
149}
150
151/// Returns a closure that blocks until Ctrl+C is received.
152///
153/// Multiple calls to this function are supported - each returns a closure that
154/// will be notified when Ctrl+C is pressed. This allows multiple actors to
155/// react to the same signal.
156///
157/// The signal handler is registered on the first call. Subsequent calls simply
158/// add new subscribers to the broadcast list.
159///
160/// # Example
161///
162/// ```ignore
163/// // Both actors will be notified on Ctrl+C
164/// send_message_on(actor1.clone(), rt::ctrl_c(), Msg::Shutdown);
165/// send_message_on(actor2.clone(), rt::ctrl_c(), Msg::Shutdown);
166/// ```
167pub fn ctrl_c() -> impl FnOnce() + Send + 'static {
168    // Initialize subscribers list and register handler on first call
169    let subscribers = CTRL_C_SUBSCRIBERS.get_or_init(|| {
170        ctrlc::set_handler(|| {
171            if let Some(subs) = CTRL_C_SUBSCRIBERS.get() {
172                let mut guard = subs.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
173                // Notify all subscribers and remove dead ones (where receiver was dropped)
174                guard.retain(|tx| tx.send(()).is_ok());
175            }
176        })
177        .expect("Ctrl+C handler already set. Use ctrl_c() instead of ctrlc::set_handler()");
178        Mutex::new(Vec::new())
179    });
180
181    // Create a new subscriber channel
182    let (tx, rx) = std_mpsc::channel();
183    subscribers
184        .lock()
185        .unwrap_or_else(|poisoned| poisoned.into_inner())
186        .push(tx);
187
188    move || {
189        let _ = rx.recv();
190    }
191}