Skip to main content

starlang_core/
exit_reason.rs

1//! Process exit reasons.
2//!
3//! An [`ExitReason`] describes why a process terminated. This is used in
4//! exit signals, monitor DOWN messages, and supervisor restart decisions.
5
6use serde::{Deserialize, Serialize};
7use std::fmt;
8
9/// The reason a process exited.
10///
11/// Exit reasons determine how linked processes and supervisors respond to
12/// process termination.
13///
14/// # Normal vs Abnormal Exits
15///
16/// - **Normal exits** ([`ExitReason::Normal`], [`ExitReason::Shutdown`],
17///   [`ExitReason::ShutdownReason`]): Do not trigger restarts for `Transient`
18///   children in supervisors. Links with `trap_exit = false` do not propagate.
19///
20/// - **Abnormal exits** ([`ExitReason::Killed`], [`ExitReason::Error`]):
21///   Trigger restarts and propagate through links.
22///
23/// # Examples
24///
25/// ```
26/// use starlang_core::ExitReason;
27///
28/// let reason = ExitReason::Normal;
29/// assert!(reason.is_normal());
30///
31/// let reason = ExitReason::Error("connection lost".to_string());
32/// assert!(!reason.is_normal());
33/// ```
34#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
35pub enum ExitReason {
36    /// Process completed successfully.
37    ///
38    /// This is the standard exit reason when a process finishes its work
39    /// without errors.
40    #[default]
41    Normal,
42
43    /// Process was asked to shut down.
44    ///
45    /// Used when a supervisor or other controller requests graceful termination.
46    Shutdown,
47
48    /// Process was asked to shut down with a specific reason.
49    ///
50    /// Similar to `Shutdown` but includes additional context.
51    ShutdownReason(String),
52
53    /// Process was forcefully terminated.
54    ///
55    /// Used with `Process::exit(pid, ExitReason::Killed)` for unconditional
56    /// termination that cannot be trapped.
57    Killed,
58
59    /// Process terminated due to an error.
60    ///
61    /// This is an abnormal exit that will trigger supervisor restarts and
62    /// propagate through links.
63    Error(String),
64}
65
66impl ExitReason {
67    /// Returns `true` if this is a normal exit reason.
68    ///
69    /// Normal exits include `Normal`, `Shutdown`, and `ShutdownReason`.
70    /// These do not trigger supervisor restarts for `Transient` children.
71    ///
72    /// # Examples
73    ///
74    /// ```
75    /// use starlang_core::ExitReason;
76    ///
77    /// assert!(ExitReason::Normal.is_normal());
78    /// assert!(ExitReason::Shutdown.is_normal());
79    /// assert!(ExitReason::ShutdownReason("done".into()).is_normal());
80    /// assert!(!ExitReason::Killed.is_normal());
81    /// assert!(!ExitReason::Error("oops".into()).is_normal());
82    /// ```
83    pub fn is_normal(&self) -> bool {
84        matches!(
85            self,
86            ExitReason::Normal | ExitReason::Shutdown | ExitReason::ShutdownReason(_)
87        )
88    }
89
90    /// Returns `true` if this is an abnormal exit reason.
91    ///
92    /// Abnormal exits include `Killed` and `Error`. These trigger supervisor
93    /// restarts and propagate through links to non-trapping processes.
94    #[inline]
95    pub fn is_abnormal(&self) -> bool {
96        !self.is_normal()
97    }
98
99    /// Returns `true` if this is the `Killed` variant.
100    ///
101    /// The `Killed` reason is special: it cannot be trapped and always
102    /// terminates the target process.
103    #[inline]
104    pub fn is_killed(&self) -> bool {
105        matches!(self, ExitReason::Killed)
106    }
107
108    /// Creates an error exit reason from any displayable type.
109    ///
110    /// # Examples
111    ///
112    /// ```
113    /// use starlang_core::ExitReason;
114    ///
115    /// let reason = ExitReason::error("something went wrong");
116    /// assert_eq!(reason, ExitReason::Error("something went wrong".to_string()));
117    /// ```
118    pub fn error(msg: impl fmt::Display) -> Self {
119        ExitReason::Error(msg.to_string())
120    }
121
122    /// Creates a shutdown exit reason with a message.
123    ///
124    /// # Examples
125    ///
126    /// ```
127    /// use starlang_core::ExitReason;
128    ///
129    /// let reason = ExitReason::shutdown("maintenance");
130    /// assert!(reason.is_normal());
131    /// ```
132    pub fn shutdown(msg: impl fmt::Display) -> Self {
133        ExitReason::ShutdownReason(msg.to_string())
134    }
135}
136
137impl fmt::Display for ExitReason {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        match self {
140            ExitReason::Normal => write!(f, "normal"),
141            ExitReason::Shutdown => write!(f, "shutdown"),
142            ExitReason::ShutdownReason(reason) => write!(f, "shutdown: {}", reason),
143            ExitReason::Killed => write!(f, "killed"),
144            ExitReason::Error(msg) => write!(f, "error: {}", msg),
145        }
146    }
147}
148
149impl From<&str> for ExitReason {
150    fn from(s: &str) -> Self {
151        ExitReason::Error(s.to_string())
152    }
153}
154
155impl From<String> for ExitReason {
156    fn from(s: String) -> Self {
157        ExitReason::Error(s)
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn test_is_normal() {
167        assert!(ExitReason::Normal.is_normal());
168        assert!(ExitReason::Shutdown.is_normal());
169        assert!(ExitReason::ShutdownReason("test".into()).is_normal());
170        assert!(!ExitReason::Killed.is_normal());
171        assert!(!ExitReason::Error("test".into()).is_normal());
172    }
173
174    #[test]
175    fn test_is_abnormal() {
176        assert!(!ExitReason::Normal.is_abnormal());
177        assert!(ExitReason::Killed.is_abnormal());
178        assert!(ExitReason::Error("test".into()).is_abnormal());
179    }
180
181    #[test]
182    fn test_is_killed() {
183        assert!(ExitReason::Killed.is_killed());
184        assert!(!ExitReason::Normal.is_killed());
185        assert!(!ExitReason::Error("test".into()).is_killed());
186    }
187
188    #[test]
189    fn test_display() {
190        assert_eq!(format!("{}", ExitReason::Normal), "normal");
191        assert_eq!(format!("{}", ExitReason::Shutdown), "shutdown");
192        assert_eq!(
193            format!("{}", ExitReason::ShutdownReason("timeout".into())),
194            "shutdown: timeout"
195        );
196        assert_eq!(format!("{}", ExitReason::Killed), "killed");
197        assert_eq!(
198            format!("{}", ExitReason::Error("oops".into())),
199            "error: oops"
200        );
201    }
202
203    #[test]
204    fn test_from_str() {
205        let reason: ExitReason = "something failed".into();
206        assert_eq!(reason, ExitReason::Error("something failed".to_string()));
207    }
208
209    #[test]
210    fn test_serialization() {
211        let reasons = vec![
212            ExitReason::Normal,
213            ExitReason::Shutdown,
214            ExitReason::ShutdownReason("test".into()),
215            ExitReason::Killed,
216            ExitReason::Error("error".into()),
217        ];
218
219        for reason in reasons {
220            let bytes = postcard::to_allocvec(&reason).unwrap();
221            let decoded: ExitReason = postcard::from_bytes(&bytes).unwrap();
222            assert_eq!(reason, decoded);
223        }
224    }
225
226    #[test]
227    fn test_helper_constructors() {
228        let err = ExitReason::error("failed");
229        assert_eq!(err, ExitReason::Error("failed".to_string()));
230
231        let shut = ExitReason::shutdown("maintenance");
232        assert_eq!(shut, ExitReason::ShutdownReason("maintenance".to_string()));
233    }
234}