strut_core/
context.rs

1use std::sync::atomic::{AtomicBool, Ordering};
2use std::sync::OnceLock;
3use tokio_util::sync::CancellationToken;
4use tracing::{info, warn};
5
6// Global singleton token that represents the application context
7static TOKEN: OnceLock<CancellationToken> = OnceLock::new();
8
9/// Facade representing the global (singleton) application context.
10///
11/// The context starts in “alive” state, and can be
12/// [terminated](AppContext::terminate) at any time. It’s also possible to
13/// [auto-terminate](AppContext::auto_terminate) the context when an OS shutdown
14/// signal is intercepted. The context can be effectively terminated only once:
15/// repeated termination produces no additional effect.
16///
17/// Any number of asynchronous tasks may use the [`AppContext`] facade as a
18/// central reference for whether the application is still alive (not in the
19/// process of shutting down). A task may [wait](AppContext::terminated) for the
20/// context to be terminated.
21///
22/// ## Example
23///
24/// ```rust
25/// use strut_core::AppContext;
26/// use tokio::task;
27///
28/// #[tokio::main]
29/// async fn main() {
30///     // Spawn a task that waits for context termination
31///     let cleanup_task = task::spawn(async move {
32///         // Wait...
33///         AppContext::terminated().await;
34///
35///         // Perform some cleanup...
36///     });
37///
38///     // Terminate manually
39///     AppContext::terminate();
40///
41///     // Wait for cleanup to complete
42///     cleanup_task.await.unwrap();
43/// }
44/// ```
45pub struct AppContext;
46
47impl AppContext {
48    /// Internal chokepoint for accessing the global singleton [`TOKEN`].
49    fn token() -> &'static CancellationToken {
50        TOKEN.get_or_init(CancellationToken::new)
51    }
52
53    /// Blocks until the global application context is terminated.
54    ///
55    /// Any number of tasks may await on this method. If a task starts waiting
56    /// on this method after the context has been terminated, the returned
57    /// future completes immediately.
58    pub async fn terminated() {
59        Self::token().cancelled().await;
60    }
61
62    /// Terminates the global application context. If the context is already
63    /// terminated, no additional effect is produced beyond a `tracing` event.
64    ///
65    /// When the context is terminated, all tasks
66    /// [waiting](AppContext::terminated) on it will unblock.
67    pub fn terminate() {
68        info!("Terminating application context");
69
70        Self::token().cancel();
71    }
72
73    /// Schedules listening for the OS shutdown signals, which
74    /// [replaces](AppContext::listen_for_shutdown_signals) the default shutdown
75    /// behavior of this entire OS process. After this method returns, the first
76    /// intercepted OS shutdown signal will [terminate](AppContext::terminate)
77    /// this context.
78    ///
79    /// [Take note](AppContext::listen_for_shutdown_signals) of the consequences
80    /// of replacing the default process shutdown behavior.
81    ///
82    /// Repeated calls to this method produce no additional effect.
83    ///
84    /// This method must be awaited to ensure that signal listening has already
85    /// started by the time the returned future completes.
86    pub async fn auto_terminate() {
87        // Guard against multiple calls to this method
88        static CALLED: AtomicBool = AtomicBool::new(false);
89
90        // If already called, pull a no-op
91        if CALLED.swap(true, Ordering::Relaxed) {
92            return;
93        }
94
95        // Schedule listening for OS shutdown signals
96        tokio::spawn(Self::listen_for_shutdown_signals());
97
98        // Yield to runtime to ensure the task spawned above has time to start working
99        tokio::task::yield_now().await;
100    }
101
102    /// Reports whether the global application context has been terminated as of
103    /// this moment.
104    ///
105    /// This method is not suitable for waiting for it to be terminated. For
106    /// such purposes, use [`AppContext::terminated`].
107    pub fn is_terminated() -> bool {
108        Self::token().is_cancelled()
109    }
110
111    /// Reports whether the global application context has **not** yet been
112    /// terminated as of this moment.
113    ///
114    /// This method is not suitable for waiting for it to be terminated. For
115    /// such purposes, use [`AppContext::terminated`].
116    pub fn is_alive() -> bool {
117        !Self::token().is_cancelled()
118    }
119
120    /// **Replaces** the default shutdown behavior of this entire OS process by
121    /// subscribing to the OS shutdown signals. Upon receiving the _first_
122    /// shutdown signal, prevents normal process termination and instead cancels
123    /// the global application context.
124    ///
125    /// After the first shutdown signal is handled, this method starts listening
126    /// for repeated signals of the same kind. When such repeated signal is
127    /// intercepted, the process exits immediately with a non-zero status code.
128    ///
129    /// There is a minuscule delay between the initial signal is handled and
130    /// listening starts for repeated signals: within that delay it is possible
131    /// that a repeated signal may fly through un-handled.
132    ///
133    /// ## Shutdown signals
134    ///
135    /// This method hijacks both `SIGINT` and `SIGTERM` on Unix platforms, and
136    /// the `ctrl_c` action on non-Unix platforms.
137    ///
138    /// ## Usage notes
139    ///
140    /// Calling this method is a one-way street. After it starts executing,
141    /// there is no way to restore the original shutdown behavior for this
142    /// process.
143    ///
144    /// There is no benefit (and theoretically no harm) in calling this method
145    /// more than once in the same process (e.g., from multiple asynchronous
146    /// tasks).
147    ///
148    /// This method never returns. The future it generates will either get
149    /// aborted, or an [`exit`](std::process::exit) call will terminate the
150    /// whole process.
151    async fn listen_for_shutdown_signals() -> ! {
152        // Wait for first shutdown signal
153        Self::wait_for_shutdown_signal().await;
154
155        // Report
156        info!("Shutdown signal intercepted");
157
158        // On first shutdown signal, cancel the global token
159        Self::token().cancel();
160
161        // Wait for any subsequent shutdown signal
162        Self::wait_for_shutdown_signal().await;
163
164        // Report
165        warn!("Repeated shutdown signal intercepted; exiting");
166
167        // Exit forcibly
168        std::process::exit(1);
169    }
170
171    /// Waits for the next OS shutdown signal on a Unix platform.
172    #[cfg(unix)]
173    async fn wait_for_shutdown_signal() {
174        use tokio::signal::unix::{signal, SignalKind};
175
176        let mut sigint = signal(SignalKind::interrupt()).unwrap();
177        let mut sigterm = signal(SignalKind::terminate()).unwrap();
178
179        tokio::select! {
180            biased; // no need to pay for randomized branch checking
181            _ = sigint.recv() => {}
182            _ = sigterm.recv() => {}
183        }
184    }
185
186    /// Waits for the next `ctrl_c` action on a non-Unix platform.
187    #[cfg(not(unix))]
188    async fn wait_for_shutdown_signal() {
189        tokio::signal::ctrl_c().await.unwrap();
190    }
191}