hyperi_rustlib/
shutdown.rs1use std::sync::OnceLock;
45use tokio_util::sync::CancellationToken;
46
47static TOKEN: OnceLock<CancellationToken> = OnceLock::new();
48
49pub fn token() -> CancellationToken {
56 TOKEN.get_or_init(CancellationToken::new).clone()
57}
58
59pub fn is_shutdown() -> bool {
61 TOKEN.get().is_some_and(CancellationToken::is_cancelled)
62}
63
64pub fn trigger() {
69 if let Some(t) = TOKEN.get() {
70 t.cancel();
71 }
72}
73
74#[must_use]
82pub fn install_signal_handler() -> CancellationToken {
83 let t = token();
84 let cancel = t.clone();
85
86 tokio::spawn(async move {
87 wait_for_signal().await;
88 cancel.cancel();
89
90 #[cfg(feature = "logger")]
91 tracing::info!("Shutdown signal received, cancelling all tasks");
92 });
93
94 t
95}
96
97async fn wait_for_signal() {
99 let ctrl_c = async {
100 if let Err(e) = tokio::signal::ctrl_c().await {
101 tracing::error!(error = %e, "Failed to install Ctrl+C handler");
102 std::future::pending::<()>().await;
103 }
104 };
105
106 #[cfg(unix)]
107 let terminate = async {
108 match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
109 Ok(mut sig) => {
110 sig.recv().await;
111 }
112 Err(e) => {
113 tracing::error!(
114 error = %e,
115 "Failed to install SIGTERM handler, only Ctrl+C will trigger shutdown"
116 );
117 std::future::pending::<()>().await;
118 }
119 }
120 };
121
122 #[cfg(unix)]
123 tokio::select! {
124 () = ctrl_c => {},
125 () = terminate => {},
126 }
127
128 #[cfg(not(unix))]
129 ctrl_c.await;
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn token_is_not_cancelled_initially() {
138 let t = CancellationToken::new();
140 assert!(!t.is_cancelled());
141 }
142
143 #[test]
144 fn trigger_cancels_token() {
145 let t = CancellationToken::new();
146 assert!(!t.is_cancelled());
147 t.cancel();
148 assert!(t.is_cancelled());
149 }
150
151 #[test]
152 fn token_is_cloneable_and_shared() {
153 let t = CancellationToken::new();
154 let c1 = t.clone();
155 let c2 = t.clone();
156
157 assert!(!c1.is_cancelled());
158 assert!(!c2.is_cancelled());
159
160 t.cancel();
161
162 assert!(c1.is_cancelled());
163 assert!(c2.is_cancelled());
164 }
165
166 #[test]
167 fn multiple_triggers_are_idempotent() {
168 let t = CancellationToken::new();
169 t.cancel();
170 t.cancel(); assert!(t.is_cancelled());
172 }
173
174 #[tokio::test]
175 async fn cancelled_future_resolves_after_cancel() {
176 let t = CancellationToken::new();
177 let c = t.clone();
178
179 tokio::spawn(async move {
180 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
181 c.cancel();
182 });
183
184 t.cancelled().await;
186 assert!(t.is_cancelled());
187 }
188
189 #[tokio::test]
190 async fn child_token_cancelled_by_parent() {
191 let parent = CancellationToken::new();
192 let child = parent.child_token();
193
194 assert!(!child.is_cancelled());
195 parent.cancel();
196 assert!(child.is_cancelled());
197 }
198}