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