Skip to main content

nidus_core/lifecycle/
mod.rs

1//! Application lifecycle hooks.
2
3use async_trait::async_trait;
4
5use crate::{NidusError, Result};
6
7/// Application lifecycle hook.
8#[async_trait]
9pub trait LifecycleHook: Send + Sync + 'static {
10    /// Runs during application startup.
11    async fn on_startup(&self) -> Result<()> {
12        Ok(())
13    }
14
15    /// Runs during application shutdown.
16    async fn on_shutdown(&self) -> Result<()> {
17        Ok(())
18    }
19}
20
21/// Ordered lifecycle hook runner.
22#[derive(Default)]
23pub struct LifecycleRunner {
24    hooks: Vec<Box<dyn LifecycleHook>>,
25}
26
27impl LifecycleRunner {
28    /// Creates an empty lifecycle runner.
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    /// Registers a lifecycle hook.
34    pub fn hook<H>(mut self, hook: H) -> Self
35    where
36        H: LifecycleHook,
37    {
38        self.hooks.push(Box::new(hook));
39        self
40    }
41
42    /// Runs startup hooks in registration order.
43    pub async fn startup(&self) -> Result<()> {
44        let span = tracing::info_span!("lifecycle.startup", hook_count = self.hooks.len());
45        let _entered = span.enter();
46        let mut started: Vec<usize> = Vec::new();
47
48        tracing::debug!(hook_count = self.hooks.len(), "lifecycle startup begin");
49        for (index, hook) in self.hooks.iter().enumerate() {
50            tracing::debug!(hook_index = index, "lifecycle startup hook begin");
51            if let Err(source) = hook.on_startup().await {
52                tracing::error!(
53                    hook_index = index,
54                    error = %source,
55                    "lifecycle startup hook failed"
56                );
57                let mut rollback_errors = Vec::new();
58                for started_index in started.into_iter().rev() {
59                    tracing::debug!(
60                        hook_index = started_index,
61                        "lifecycle startup rollback hook begin"
62                    );
63                    if let Err(error) = self.hooks[started_index].on_shutdown().await {
64                        tracing::error!(
65                            hook_index = started_index,
66                            error = %error,
67                            "lifecycle startup rollback hook failed"
68                        );
69                        rollback_errors.push(error);
70                    } else {
71                        tracing::debug!(
72                            hook_index = started_index,
73                            "lifecycle startup rollback hook complete"
74                        );
75                    }
76                }
77
78                return Err(NidusError::LifecycleStartup {
79                    source: Box::new(source),
80                    rollback_errors,
81                });
82            }
83            tracing::debug!(hook_index = index, "lifecycle startup hook complete");
84            started.push(index);
85        }
86        tracing::debug!(hook_count = self.hooks.len(), "lifecycle startup complete");
87        Ok(())
88    }
89
90    /// Runs shutdown hooks in reverse registration order.
91    pub async fn shutdown(&self) -> Result<()> {
92        let span = tracing::info_span!("lifecycle.shutdown", hook_count = self.hooks.len());
93        let _entered = span.enter();
94        tracing::debug!(hook_count = self.hooks.len(), "lifecycle shutdown begin");
95        for (index, hook) in self.hooks.iter().enumerate().rev() {
96            tracing::debug!(hook_index = index, "lifecycle shutdown hook begin");
97            hook.on_shutdown().await?;
98            tracing::debug!(hook_index = index, "lifecycle shutdown hook complete");
99        }
100        tracing::debug!(hook_count = self.hooks.len(), "lifecycle shutdown complete");
101        Ok(())
102    }
103
104    pub(crate) fn empty() -> Self {
105        Self::new()
106    }
107}