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        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                // Startup is sequential, so every earlier index completed successfully.
59                for started_index in (0..index).rev() {
60                    tracing::debug!(
61                        hook_index = started_index,
62                        "lifecycle startup rollback hook begin"
63                    );
64                    if let Err(error) = self.hooks[started_index].on_shutdown().await {
65                        tracing::error!(
66                            hook_index = started_index,
67                            error = %error,
68                            "lifecycle startup rollback hook failed"
69                        );
70                        rollback_errors.push(error);
71                    } else {
72                        tracing::debug!(
73                            hook_index = started_index,
74                            "lifecycle startup rollback hook complete"
75                        );
76                    }
77                }
78
79                return Err(NidusError::LifecycleStartup {
80                    source: Box::new(source),
81                    rollback_errors,
82                });
83            }
84            tracing::debug!(hook_index = index, "lifecycle startup hook complete");
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    ///
92    /// Every hook is attempted even when an earlier hook fails. The first
93    /// failure in shutdown order is returned after the remaining hooks run.
94    #[tracing::instrument(
95        name = "lifecycle.shutdown",
96        skip_all,
97        fields(hook_count = self.hooks.len())
98    )]
99    pub async fn shutdown(&self) -> Result<()> {
100        tracing::debug!(hook_count = self.hooks.len(), "lifecycle shutdown begin");
101        let mut first_error = None;
102        let mut error_count = 0_usize;
103        for (index, hook) in self.hooks.iter().enumerate().rev() {
104            tracing::debug!(hook_index = index, "lifecycle shutdown hook begin");
105            if let Err(error) = hook.on_shutdown().await {
106                error_count += 1;
107                tracing::error!(
108                    hook_index = index,
109                    error = %error,
110                    "lifecycle shutdown hook failed"
111                );
112                if first_error.is_none() {
113                    first_error = Some(error);
114                }
115            } else {
116                tracing::debug!(hook_index = index, "lifecycle shutdown hook complete");
117            }
118        }
119        if let Some(error) = first_error {
120            tracing::error!(
121                hook_count = self.hooks.len(),
122                error_count,
123                "lifecycle shutdown completed with errors"
124            );
125            return Err(error);
126        }
127        tracing::debug!(hook_count = self.hooks.len(), "lifecycle shutdown complete");
128        Ok(())
129    }
130
131    pub(crate) fn empty() -> Self {
132        Self::new()
133    }
134}