pax_runtime_api/
application_shutdown.rs1use crate::{
3 application::{AppInner, ApplicationInstance},
4 async_runtime::lock,
5};
6use std::{
7 cell::RefCell,
8 collections::HashMap,
9 rc::Rc,
10 sync::{Arc, Mutex},
11 time::Duration,
12};
13
14type Wake = Arc<dyn Fn() + Send + Sync>;
15struct TicketState {
16 result: Option<Result<(), String>>,
17 wake: Wake,
18}
19pub struct ShutdownTicket(Arc<Mutex<TicketState>>);
24pub struct ShutdownComplete(Option<Arc<Mutex<TicketState>>>);
26impl ShutdownTicket {
27 pub fn pending() -> (Self, ShutdownComplete) {
28 let state = Arc::new(Mutex::new(TicketState {
29 result: None,
30 wake: Arc::new(|| {}),
31 }));
32 (Self(state.clone()), ShutdownComplete(Some(state)))
33 }
34 pub fn ready(result: Result<(), String>) -> Self {
35 Self(Arc::new(Mutex::new(TicketState {
36 result: Some(result),
37 wake: Arc::new(|| {}),
38 })))
39 }
40 pub(crate) fn set_waker(&self, wake: Wake) {
41 let ready = {
42 let mut state = lock(&self.0);
43 state.wake = wake.clone();
44 state.result.is_some()
45 };
46 if ready {
47 wake();
48 }
49 }
50 pub(crate) fn unacknowledged() -> Self {
51 Self(Arc::new(Mutex::new(TicketState {
52 result: None,
53 wake: Arc::new(|| {}),
54 })))
55 }
56 pub fn result(&self) -> Option<Result<(), String>> {
57 lock(&self.0).result.clone()
58 }
59}
60impl ShutdownComplete {
61 pub fn complete(mut self, result: Result<(), String>) {
62 if let Some(state) = self.0.take() {
63 let wake = {
64 let mut state = lock(&state);
65 state.result = Some(result);
66 state.wake.clone()
67 };
68 wake();
69 }
70 }
71}
72impl Drop for ShutdownComplete {
73 fn drop(&mut self) {
74 if let Some(state) = self.0.take() {
75 let wake = lock(&state).wake.clone();
76 log::error!("shutdown signal dropped without acknowledgement; retaining dependencies");
77 wake();
78 }
79 }
80}
81#[doc(hidden)]
86pub trait Cleanup: 'static {
87 fn begin_shutdown(&self) -> ShutdownTicket;
88}
89
90thread_local! {
91 static RETIRED: RefCell<HashMap<u64, Rc<AppInner>>> = RefCell::new(HashMap::new());
92 static HOST_WAKE: RefCell<Wake> = RefCell::new(Arc::new(|| {}));
93}
94pub(crate) fn retire(inner: Rc<AppInner>) {
95 RETIRED.with(|retired| retired.borrow_mut().insert(inner.graph.id(), inner));
96}
97pub(crate) fn wake_route() -> Wake {
98 HOST_WAKE.with(|wake| wake.borrow().clone())
99}
100
101#[doc(hidden)]
105pub fn set_shutdown_waker(wake: impl Fn() + Send + Sync + 'static) {
106 let wake: Wake = Arc::new(wake);
107 HOST_WAKE.with(|host| *host.borrow_mut() = wake.clone());
108 let retired: Vec<_> = RETIRED.with(|retired| retired.borrow().values().cloned().collect());
109 for inner in retired {
110 if let Some(ticket) = inner.shutdown_ticket.borrow().as_ref() {
111 ticket.set_waker(wake.clone());
112 }
113 }
114 if pending_shutdowns() != 0 {
115 wake();
116 }
117}
118#[doc(hidden)]
121pub fn pump_shutdowns() -> usize {
122 let retired: Vec<_> = RETIRED.with(|retired| retired.borrow().values().cloned().collect());
123 for inner in retired {
124 let app = ApplicationInstance { inner };
125 app.advance_shutdown();
126 if app.inner.phase.get() == crate::AppPhase::Closed {
127 RETIRED.with(|retired| retired.borrow_mut().remove(&app.inner.graph.id()));
128 }
129 }
130 pending_shutdowns()
131}
132#[doc(hidden)]
133pub fn pending_shutdowns() -> usize {
134 RETIRED.with(|retired| retired.borrow().len())
135}
136
137pub(crate) fn deadline_wake(delay: Duration) {
140 let wake = wake_route();
141 #[cfg(not(target_arch = "wasm32"))]
142 if let Err(error) = std::thread::Builder::new()
143 .name("pax-shutdown-deadline".into())
144 .spawn(move || {
145 std::thread::sleep(delay);
146 wake();
147 })
148 {
149 log::error!("could not schedule shutdown deadline: {error}");
150 }
151 #[cfg(target_arch = "wasm32")]
152 {
153 use wasm_bindgen::prelude::*;
154 #[wasm_bindgen]
155 extern "C" {
156 #[wasm_bindgen(js_namespace = globalThis, js_name = setTimeout)]
157 fn set_timeout(callback: &JsValue, millis: i32);
158 }
159 let callback = wasm_bindgen::closure::Closure::once_into_js(move || wake());
160 set_timeout(&callback, delay.as_millis().min(i32::MAX as u128) as i32);
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use crate::{AppOptions, AppPhase, StopReason, TargetInfo};
168 thread_local! {
169 static EVENTS: RefCell<Vec<&'static str>> = const { RefCell::new(Vec::new()) };
170 static SIGNAL: RefCell<Option<ShutdownComplete>> = const { RefCell::new(None) };
171 }
172 struct Dependency;
173 impl Cleanup for Dependency {
174 fn begin_shutdown(&self) -> ShutdownTicket {
175 EVENTS.with(|events| events.borrow_mut().push("dependency shutdown"));
176 ShutdownTicket::ready(Ok(()))
177 }
178 }
179 impl Drop for Dependency {
180 fn drop(&mut self) {
181 EVENTS.with(|events| events.borrow_mut().push("dependency drop"));
182 }
183 }
184 struct Dependent;
185 impl Cleanup for Dependent {
186 fn begin_shutdown(&self) -> ShutdownTicket {
187 EVENTS.with(|events| events.borrow_mut().push("dependent shutdown"));
188 let (ticket, signal) = ShutdownTicket::pending();
189 SIGNAL.with(|slot| *slot.borrow_mut() = Some(signal));
190 ticket
191 }
192 }
193 fn prepared() -> ApplicationInstance {
194 let app = ApplicationInstance::prepare(
195 TargetInfo::default(),
196 AppOptions {
197 shutdown_deadline: Duration::ZERO,
198 ..AppOptions::default()
199 },
200 )
201 .unwrap();
202 app.context().retain_cleanup(Rc::new(Dependency)).unwrap();
203 app.context().retain_cleanup(Rc::new(Dependent)).unwrap();
204 app
205 }
206 #[test]
207 fn shutdown_is_sequential_and_retains_dependencies_after_deadline_and_engine_drop() {
208 EVENTS.with(|events| events.borrow_mut().clear());
209 let app = prepared();
210 let context = app.context();
211 app.activate();
212 app.begin_close();
213 app.finish_close(StopReason::HostClosed);
214 drop(app);
215 assert_eq!(context.phase(), AppPhase::Closing);
216 EVENTS.with(|events| assert_eq!(*events.borrow(), ["dependent shutdown"]));
217 assert_eq!(pump_shutdowns(), 1);
218 SIGNAL.with(|signal| signal.borrow_mut().take().unwrap().complete(Ok(())));
219 assert_eq!(pump_shutdowns(), 0);
220 assert_eq!(context.phase(), AppPhase::Closed);
221 EVENTS.with(|events| {
222 assert_eq!(
223 *events.borrow(),
224 [
225 "dependent shutdown",
226 "dependency shutdown",
227 "dependency drop"
228 ]
229 )
230 });
231 }
232 #[test]
233 fn retired_app_reports_late_task_panics_through_the_independent_host_wake() {
234 let wakes = Arc::new(std::sync::atomic::AtomicUsize::new(0));
235 let woke = wakes.clone();
236 set_shutdown_waker(move || {
237 woke.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
238 });
239 let app = prepared();
240 let errors = Rc::new(RefCell::new(Vec::new()));
241 let observed = errors.clone();
242 app.context()
243 .set_task_error_sink(move |error| observed.borrow_mut().push(error))
244 .unwrap();
245 app.activate();
246 let reporter = Rc::new(RefCell::new(None));
247 let capture = reporter.clone();
248 let task = app
249 .context()
250 .async_scope()
251 .unwrap()
252 .register_task(move |signal| {
253 *capture.borrow_mut() = Some(signal);
254 Box::new(|| {})
255 })
256 .unwrap();
257 drop(task);
258 app.begin_close();
259 app.finish_close(StopReason::HostClosed);
260 drop(app);
261 let before = wakes.load(std::sync::atomic::Ordering::SeqCst);
262 let reporter = reporter.borrow_mut().take().unwrap();
263 std::thread::spawn(move || {
264 reporter.finish(crate::TaskStatus::Panicked("late task failure".into()))
265 })
266 .join()
267 .unwrap();
268 assert!(wakes.load(std::sync::atomic::Ordering::SeqCst) > before);
269 SIGNAL.with(|signal| signal.borrow_mut().take().unwrap().complete(Ok(())));
270 assert_eq!(pump_shutdowns(), 0);
271 assert_eq!(*errors.borrow(), ["late task failure"]);
272 set_shutdown_waker(|| {});
273 }
274 #[test]
275 fn failed_initialization_uses_the_same_retained_cleanup_order() {
276 EVENTS.with(|events| events.borrow_mut().clear());
277 let app =
278 ApplicationInstance::prepare(TargetInfo::default(), AppOptions::default()).unwrap();
279 assert!(app
280 .initialize::<()>(|cx| {
281 cx.lifetime().retain_cleanup(Rc::new(Dependency))?;
282 cx.lifetime().retain_cleanup(Rc::new(Dependent))?;
283 Err("expected initialization failure".into())
284 })
285 .is_err());
286 drop(app);
287 EVENTS.with(|events| assert_eq!(*events.borrow(), ["dependent shutdown"]));
288 SIGNAL.with(|signal| signal.borrow_mut().take().unwrap().complete(Ok(())));
289 assert_eq!(pump_shutdowns(), 0);
290 EVENTS.with(|events| {
291 assert_eq!(
292 *events.borrow(),
293 [
294 "dependent shutdown",
295 "dependency shutdown",
296 "dependency drop"
297 ]
298 )
299 });
300 }
301}