Skip to main content

harmos_runtime/runtime/
lifecycle.rs

1/* ***
2 *
3 * Harmos
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * ***
8 */
9
10/* === Modules === */
11
12/* === Re-Exports === */
13
14/* === Imports === */
15
16// Standard Library Imports
17use std::future::Future;
18use std::pin::Pin;
19use std::sync::atomic::Ordering;
20use std::sync::{Arc, OnceLock, Weak};
21use std::task::Poll;
22use std::time::Duration;
23// Third Party Imports
24use tokio::sync::watch;
25use tokio::task::JoinHandle;
26// Local Imports
27use super::{Handle, Participants, Runtime, STOP_GRACE, Stopping};
28use crate::storage::StorageManager;
29use crate::supervision::cleanup::{Cleanup, Finalize, Resource};
30use crate::{Application, Error, Result, state::Position};
31
32/* === Constants === */
33
34/* === Macros === */
35
36/* === Type Aliases === */
37
38pub(super) type Outcome = std::result::Result<Position, Failure>;
39
40/* === Structs === */
41
42pub(super) struct Shutdown<A: Application> {
43    handles: Handle<A>,
44    participants: Participants,
45    journal_handles: Weak<()>,
46    notices: watch::Sender<Option<String>>,
47    state: Drain,
48    outcome: Arc<OnceLock<Outcome>>,
49    progress: watch::Sender<&'static str>,
50}
51
52#[derive(Clone)]
53pub(super) struct Failure {
54    domain: &'static str,
55    source: Arc<dyn std::error::Error + Send + Sync>,
56}
57
58/* === Enums === */
59
60enum Drain {
61    Running(JoinHandle<Position>),
62    Drained(Outcome),
63}
64
65/* === Traits === */
66
67/* === Implementations === */
68
69impl<A: Application> Runtime<A> {
70    pub async fn start_storage<C>(&self, storage: StorageManager<C>) -> Result<()> {
71        let storage = storage.erased();
72        let storage = {
73            let _stopping = self.stopping.lock().expect("runtime transition is healthy");
74            if self.closing.load(Ordering::Acquire) {
75                return Err(Error::Closing { domain: "runtime" });
76            }
77            if let Some(current) = self.storage.get()
78                && !current.same_instance(&storage)
79            {
80                return Err(Error::AlreadyRegistered {
81                    domain: "runtime storage",
82                    name: "primary".to_owned(),
83                });
84            }
85            storage.claim_owner(self.operations.resident_identity())?;
86            self.storage.get_or_init(|| storage)
87        };
88        storage.start_on::<A>(&self.operations).await
89    }
90
91    pub fn storage(&self) -> Option<&StorageManager<crate::storage::ErasedCatalog>> {
92        self.storage.get()
93    }
94
95    pub async fn stop_with_timeout(&self, timeout: Duration) -> Result<Position> {
96        let deadline = tokio::time::Instant::now().checked_add(timeout).filter(|_| !timeout.is_zero()).ok_or(Error::InvalidInput {
97            field: "runtime shutdown timeout",
98            reason: "must be positive and representable",
99        })?;
100        match tokio::time::timeout_at(deadline, self.stop()).await {
101            Ok(result) => result,
102            Err(_) => {
103                let stopping = self.stopping.lock().expect("runtime transition is healthy");
104                let phase = match &*stopping {
105                    Stopping::Running(_) => "admission",
106                    Stopping::Closing { progress, .. } => *progress.borrow(),
107                };
108                tracing::error!(phase, ?timeout, "runtime shutdown budget exhausted; original cleanup retained");
109                Err(Error::ShutdownTimeout { phase })
110            }
111        }
112    }
113
114    pub async fn stop(&self) -> Result<Position> {
115        self.closing.store(true, Ordering::Release);
116        #[cfg(feature = "grpc")]
117        self.grpc.close_admission();
118        #[cfg(all(feature = "plugin", not(target_arch = "wasm32")))]
119        self.plugin.close_admission();
120        #[cfg(not(all(feature = "plugin", not(target_arch = "wasm32"))))]
121        {
122            // Notify Residents Before Cancelling Their In-Flight Calls
123            self.operations.close_resident_admission();
124            self.operations.close_admission();
125            #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
126            {
127                self.records.stop_listeners();
128                self.streams.stop_listeners();
129            }
130        }
131        let (cleanup, completed) = {
132            let mut stopping = self.stopping.lock().expect("runtime transition is healthy");
133            if let Stopping::Running(supervisor) = &mut *stopping {
134                let outcome = Arc::new(OnceLock::new());
135                let progress = watch::channel("producers and listeners").0;
136                let original = Shutdown {
137                    handles: self.handle(),
138                    participants: self.participants.clone(),
139                    journal_handles: self.journal_handles.clone(),
140                    notices: self.notices.clone(),
141                    state: Drain::Running(supervisor.take().expect("runtime owns its supervisor")),
142                    outcome: outcome.clone(),
143                    progress: progress.clone(),
144                };
145                *stopping = Stopping::Closing {
146                    cleanup: Arc::new(Cleanup::new(original)),
147                    outcome,
148                    progress,
149                };
150            }
151            let Stopping::Closing { cleanup, outcome, .. } = &*stopping else {
152                unreachable!("shutdown retains its driver before awaiting it");
153            };
154            (cleanup.clone(), outcome.clone())
155        };
156        cleanup
157            .settle()
158            .await
159            .map_err(|source| Arc::try_unwrap(source).unwrap_or_else(|source| Error::TaskFailed { domain: "runtime cleanup", source }))?;
160        outcome(completed.get().expect("physical shutdown published its outcome"))
161    }
162}
163
164impl<A: Application> Resource for Shutdown<A> {
165    fn finalize(&mut self) -> Finalize<'_> {
166        Box::pin(async move { self.finish().await.map_err(Arc::new) })
167    }
168}
169
170impl<A: Application> Shutdown<A> {
171    async fn finish(&mut self) -> Result<()> {
172        if let Drain::Running(supervisor) = &mut self.state {
173            #[cfg(all(feature = "plugin", not(target_arch = "wasm32")))]
174            {
175                self.progress.send_replace("plugins");
176                self.handles.plugin.shutdown().await?;
177            }
178            self.handles.operations.close_resident_admission();
179            self.handles.operations.close_admission();
180            #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
181            {
182                self.handles.records.stop_listeners();
183                self.handles.streams.stop_listeners();
184            }
185            self.progress.send_replace("observers and listeners");
186            let observers = self.handles.operations.shutdown_observers();
187            #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
188            let (observers, records, streams) =
189                tokio::join!(observers, self.handles.records.shutdown_listeners(), self.handles.streams.shutdown_listeners(),);
190            #[cfg(not(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar")))]
191            let observers = observers.await;
192            if self.handles.operations.observer_cleanup_pending() {
193                return Err(observers.err().unwrap_or(Error::NotReady { domain: "observer cleanup" }));
194            }
195            #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
196            records?;
197            #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
198            streams?;
199            self.progress.send_replace("application services");
200            let services = self.handles.operations.shutdown_application_services().await;
201            if self.handles.operations.service_cleanup_pending(false) {
202                return Err(services.err().unwrap_or(Error::NotReady { domain: "service cleanup" }));
203            }
204            // Original SDK Shutdown Keeps Its Dispatcher Until Application Cleanup Settles.
205            self.progress.send_replace("operations");
206            self.handles.operations.shutdown().await?;
207            self.progress.send_replace("participants");
208            self.participants.stop().await;
209            self.handles.state.seal();
210            self.progress.send_replace("journal");
211            let mut settled = joined(supervisor, &self.journal_handles, &self.notices).await;
212            if let Err(source) = observers
213                && settled.is_ok()
214            {
215                settled = Err(Failure {
216                    domain: "observer",
217                    source: Arc::new(source),
218                });
219            }
220            if let Err(source) = services
221                && settled.is_ok()
222            {
223                settled = Err(Failure {
224                    domain: "service",
225                    source: Arc::new(source),
226                });
227            }
228            self.handles.streams.close();
229            self.state = Drain::Drained(settled);
230        }
231        let Drain::Drained(settled) = &self.state else {
232            unreachable!("the journal is drained before storage closes");
233        };
234        let mut settled = settled.clone();
235        if let Some(storage) = self.handles.storage() {
236            self.progress.send_replace("storage");
237            storage.close().await?;
238        }
239        self.progress.send_replace("infrastructure services");
240        let services = self.handles.operations.shutdown_residents().await;
241        if self.handles.operations.service_cleanup_pending(true) {
242            return Err(services.err().unwrap_or(Error::NotReady { domain: "service cleanup" }));
243        }
244        if let Err(source) = services
245            && settled.is_ok()
246        {
247            settled = Err(Failure {
248                domain: "service",
249                source: Arc::new(source),
250            });
251        }
252        self.outcome.set(settled).unwrap_or_else(|_| unreachable!("shutdown publishes its outcome once"));
253        Ok(())
254    }
255}
256
257impl<A: Application> Handle<A> {
258    pub fn storage(&self) -> Option<&StorageManager<crate::storage::ErasedCatalog>> {
259        self.storage.get()
260    }
261}
262
263/* === Public Functions === */
264
265/* === Private Functions === */
266
267async fn joined(supervisor: &mut JoinHandle<Position>, journal_handles: &Weak<()>, notices: &watch::Sender<Option<String>>) -> Outcome {
268    let mut grace = Box::pin(tokio::time::sleep(STOP_GRACE));
269    let joined = std::future::poll_fn(|context| {
270        if let Poll::Ready(settled) = Pin::new(&mut *supervisor).poll(context) {
271            return Poll::Ready(Some(settled));
272        }
273        if grace.as_mut().poll(context).is_ready() {
274            return Poll::Ready(None);
275        }
276
277        Poll::Pending
278    })
279    .await;
280
281    match joined {
282        Some(settled) => settled,
283        None => {
284            let live = journal_handles.strong_count();
285            notices.send_replace(Some(format!(
286                "the journal stop is still draining; {live} StateManager clone{} outstanding, none \
287                     of which holds it open",
288                if live == 1 { "" } else { "s" }
289            )));
290
291            supervisor.await
292        }
293    }
294    .map_err(|source| Failure {
295        domain: "journal supervisor",
296        source: Arc::new(source),
297    })
298}
299
300fn outcome(settled: &Outcome) -> Result<Position> {
301    match settled {
302        Ok(position) => Ok(*position),
303        Err(failure) => Err(Error::TaskFailed {
304            domain: failure.domain,
305            source: failure.source.clone(),
306        }),
307    }
308}
309
310/* === Tests === */