Skip to main content

nidus_core/lifecycle/
mod.rs

1//! Application lifecycle hooks.
2
3use crate::{NidusError, Result};
4use async_trait::async_trait;
5
6/// Application lifecycle hook.
7#[async_trait]
8pub trait LifecycleHook: Send + Sync + 'static {
9    /// Runs during application startup.
10    async fn on_startup(&self) -> Result<()> {
11        Ok(())
12    }
13
14    /// Runs during application shutdown.
15    async fn on_shutdown(&self) -> Result<()> {
16        Ok(())
17    }
18}
19
20/// Ordered lifecycle hook runner.
21#[derive(Default)]
22pub struct LifecycleRunner {
23    hooks: Vec<Box<dyn LifecycleHook>>,
24}
25
26impl LifecycleRunner {
27    /// Creates an empty lifecycle runner.
28    pub fn new() -> Self {
29        Self::default()
30    }
31
32    /// Registers a lifecycle hook.
33    pub fn hook<H>(mut self, hook: H) -> Self
34    where
35        H: LifecycleHook,
36    {
37        self.hooks.push(Box::new(hook));
38        self
39    }
40
41    /// Runs startup hooks in registration order.
42    #[tracing::instrument(
43        name = "lifecycle.startup",
44        skip_all,
45        fields(hook_count = self.hooks.len())
46    )]
47    pub async fn startup(&self) -> Result<()> {
48        let mut started: Vec<usize> = Vec::new();
49
50        tracing::debug!(hook_count = self.hooks.len(), "lifecycle startup begin");
51        for (index, hook) in self.hooks.iter().enumerate() {
52            tracing::debug!(hook_index = index, "lifecycle startup hook begin");
53            if let Err(source) = hook.on_startup().await {
54                tracing::error!(
55                    hook_index = index,
56                    error = %source,
57                    "lifecycle startup hook failed"
58                );
59                let mut rollback_errors = Vec::new();
60                for started_index in started.into_iter().rev() {
61                    tracing::debug!(
62                        hook_index = started_index,
63                        "lifecycle startup rollback hook begin"
64                    );
65                    if let Err(error) = self.hooks[started_index].on_shutdown().await {
66                        tracing::error!(
67                            hook_index = started_index,
68                            error = %error,
69                            "lifecycle startup rollback hook failed"
70                        );
71                        rollback_errors.push(error);
72                    } else {
73                        tracing::debug!(
74                            hook_index = started_index,
75                            "lifecycle startup rollback hook complete"
76                        );
77                    }
78                }
79
80                return Err(NidusError::LifecycleStartup {
81                    source: Box::new(source),
82                    rollback_errors,
83                });
84            }
85            tracing::debug!(hook_index = index, "lifecycle startup hook complete");
86            started.push(index);
87        }
88        tracing::debug!(hook_count = self.hooks.len(), "lifecycle startup complete");
89        Ok(())
90    }
91
92    /// Runs shutdown hooks in reverse registration order.
93    ///
94    /// Every hook is attempted even when an earlier hook fails. The first
95    /// failure in shutdown order is returned after the remaining hooks run.
96    #[tracing::instrument(
97        name = "lifecycle.shutdown",
98        skip_all,
99        fields(hook_count = self.hooks.len())
100    )]
101    pub async fn shutdown(&self) -> Result<()> {
102        tracing::debug!(hook_count = self.hooks.len(), "lifecycle shutdown begin");
103        let mut first_error = None;
104        let mut error_count = 0_usize;
105        for (index, hook) in self.hooks.iter().enumerate().rev() {
106            tracing::debug!(hook_index = index, "lifecycle shutdown hook begin");
107            if let Err(error) = hook.on_shutdown().await {
108                error_count += 1;
109                tracing::error!(
110                    hook_index = index,
111                    error = %error,
112                    "lifecycle shutdown hook failed"
113                );
114                if first_error.is_none() {
115                    first_error = Some(error);
116                }
117            } else {
118                tracing::debug!(hook_index = index, "lifecycle shutdown hook complete");
119            }
120        }
121        if let Some(error) = first_error {
122            tracing::error!(
123                hook_count = self.hooks.len(),
124                error_count,
125                "lifecycle shutdown completed with errors"
126            );
127            return Err(error);
128        }
129        tracing::debug!(hook_count = self.hooks.len(), "lifecycle shutdown complete");
130        Ok(())
131    }
132
133    pub(crate) fn empty() -> Self {
134        Self::new()
135    }
136}