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}