Skip to main content

nexus_rt/
shutdown.rs

1//! Cooperative shutdown for event loops.
2//!
3//! [`Shutdown`] is a handler parameter that accesses the world's shutdown
4//! flag directly — no resource registration needed. Handlers trigger
5//! shutdown via [`Shutdown::trigger`]:
6//!
7//! ```
8//! use nexus_rt::{WorldBuilder, IntoHandler, Handler};
9//! use nexus_rt::shutdown::Shutdown;
10//!
11//! fn on_fatal(shutdown: Shutdown, _event: ()) {
12//!     shutdown.trigger();
13//! }
14//!
15//! let mut world = WorldBuilder::new().build();
16//! let mut handler = on_fatal.into_handler(world.registry());
17//! handler.run(&mut world, ());
18//! assert!(world.shutdown_handle().is_shutdown());
19//! ```
20//!
21//! The event loop owns a [`ShutdownHandle`] obtained from
22//! [`World::shutdown_handle`](crate::World::shutdown_handle) and checks it
23//! each iteration:
24//!
25//! ```
26//! use nexus_rt::WorldBuilder;
27//!
28//! let mut world = WorldBuilder::new().build();
29//! let shutdown = world.shutdown_handle();
30//!
31//! // typical event loop
32//! while !shutdown.is_shutdown() {
33//!     // poll drivers ...
34//!     # break;
35//! }
36//! ```
37//!
38//! # Signal Support (Linux only)
39//!
40//! With the `signals` feature, `ShutdownHandle::enable_signals` registers
41//! SIGINT and SIGTERM handlers that flip the shutdown flag automatically.
42//! Targets Linux infrastructure — not supported on Windows.
43
44use std::sync::Arc;
45use std::sync::atomic::{AtomicBool, Ordering};
46
47/// Handler parameter for cooperative shutdown.
48///
49/// Accesses the world's shutdown flag directly — not a resource.
50/// Uses [`Relaxed`](Ordering::Relaxed) ordering — the flag is checked
51/// once per poll iteration, not on a hot path requiring memory fencing.
52/// Holds a reference to World's `AtomicBool` shutdown flag.
53/// Lifetime-bound to the World borrow — cannot escape the dispatch frame.
54pub struct Shutdown<'w>(pub(crate) &'w AtomicBool);
55
56impl Shutdown<'_> {
57    /// Returns `true` if shutdown has been triggered.
58    #[inline(always)]
59    pub fn is_shutdown(&self) -> bool {
60        self.0.load(Ordering::Relaxed)
61    }
62
63    /// Trigger shutdown. The event loop will exit after the current
64    /// dispatch completes.
65    pub fn trigger(&self) {
66        self.0.store(true, Ordering::Relaxed);
67    }
68}
69
70impl std::fmt::Debug for Shutdown<'_> {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        f.debug_tuple("Shutdown")
73            .field(&self.is_shutdown())
74            .finish()
75    }
76}
77
78/// External handle for the event loop to check shutdown status.
79///
80/// Shares the same [`AtomicBool`] as the world's shutdown flag.
81/// Obtained via [`World::shutdown_handle`](crate::World::shutdown_handle).
82pub struct ShutdownHandle {
83    flag: Arc<AtomicBool>,
84}
85
86impl ShutdownHandle {
87    pub(crate) fn new(flag: Arc<AtomicBool>) -> Self {
88        Self { flag }
89    }
90
91    /// Returns `true` if shutdown has been triggered.
92    pub fn is_shutdown(&self) -> bool {
93        self.flag.load(Ordering::Relaxed)
94    }
95
96    /// Trigger shutdown from outside the event loop.
97    pub fn shutdown(&self) {
98        self.flag.store(true, Ordering::Relaxed);
99    }
100
101    /// Register SIGINT and SIGTERM handlers that trigger shutdown.
102    ///
103    /// Unix/Linux only. Uses [`signal_hook::flag::register`] — the signal
104    /// handler simply flips the shared [`AtomicBool`]. Safe to call
105    /// multiple times (subsequent calls are no-ops at the OS level for
106    /// the same signal).
107    ///
108    /// # Errors
109    ///
110    /// Returns an error if the OS rejects the signal registration.
111    ///
112    /// # Platform Support
113    ///
114    /// Targets Linux infrastructure. Not supported on Windows.
115    #[cfg(feature = "signals")]
116    pub fn enable_signals(&self) -> std::io::Result<()> {
117        signal_hook::flag::register(signal_hook::consts::SIGINT, Arc::clone(&self.flag))?;
118        signal_hook::flag::register(signal_hook::consts::SIGTERM, Arc::clone(&self.flag))?;
119        Ok(())
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn handle_not_shutdown_by_default() {
129        let world = crate::WorldBuilder::new().build();
130        let handle = world.shutdown_handle();
131        assert!(!handle.is_shutdown());
132    }
133
134    #[test]
135    fn shutdown_param_triggers() {
136        let world = crate::WorldBuilder::new().build();
137        let handle = world.shutdown_handle();
138        let shutdown = Shutdown(world.shutdown_flag());
139
140        assert!(!handle.is_shutdown());
141        shutdown.trigger();
142        assert!(handle.is_shutdown());
143    }
144
145    #[test]
146    fn handle_can_trigger_shutdown() {
147        let world = crate::WorldBuilder::new().build();
148        let handle = world.shutdown_handle();
149        assert!(!handle.is_shutdown());
150        handle.shutdown();
151        assert!(handle.is_shutdown());
152    }
153
154    #[test]
155    fn shutdown_in_handler() {
156        use crate::{Handler, IntoHandler};
157
158        fn trigger_shutdown(shutdown: Shutdown, _event: ()) {
159            shutdown.trigger();
160        }
161
162        let mut world = crate::WorldBuilder::new().build();
163        let handle = world.shutdown_handle();
164
165        let mut handler = trigger_shutdown.into_handler(world.registry());
166        assert!(!handle.is_shutdown());
167        handler.run(&mut world, ());
168        assert!(handle.is_shutdown());
169    }
170}