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}