Skip to main content

pax_runtime_api/
application_shutdown.rs

1//! Retained asynchronous resource cleanup with UI-thread ownership and host wakeups.
2use 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}
19/// Nonblocking acknowledgement of retained cleanup. A pending ticket retains its
20/// owner and all earlier dependencies; a deadline never fabricates completion.
21/// An explicit Err acknowledges quiescence with a cleanup error. Dropping the
22/// signal is not acknowledgement and leaves dependencies retained.
23pub struct ShutdownTicket(Arc<Mutex<TicketState>>);
24/// Transferable signal. Dropping it without acknowledgement reports failure.
25pub 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/// An internal adapter owner whose teardown may wait. `begin_shutdown` runs once on the UI
82/// thread and must immediately reject new work, start cleanup without blocking,
83/// and return a ticket. Dependencies registered earlier remain allocated until
84/// all later owners acknowledge. Destruction itself must never block the UI.
85#[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/// Install a host-owned UI scheduler before configuring a cartridge. Its token
102/// remains valid for retired applications after engine/window disposal. The
103/// callback schedules `pump_shutdowns`; it must not call it synchronously.
104#[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/// Advance acknowledged services in reverse registration order on the owner
119/// thread. A deadline is diagnostic; unfinished applications remain retained.
120#[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
137// One deadline notification, never a periodic executor poll. Retired native
138// cartridges remain loaded while this closure or any service can still run.
139pub(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}