mem_isolate/errors.rs
1//! Error handling is an important part of the `mem-isolate` crate. If something
2//! went wrong, we want to give the caller as much context as possible about how
3//! that error affected their `callable`, so they are well-equipped to know what
4//! to do about it.
5//!
6//! The primary error type is [`MemIsolateError`], which is returned by
7//! [`crate::execute_in_isolated_process`].
8//!
9//! Philosophically, error handling in `mem-isolate` is organized into three
10//! levels of error wrapping:
11//!
12//! 1. The first level describes the effect of the error on the `callable` (e.g.
13//! did your callable function execute or not)
14//! 2. The second level describes what `mem-isolate` operation caused the error
15//! (e.g. did serialization fail)
16//! 3. The third level is the underlying OS error if it is available (e.g. an
17//! `io::Error`)
18//!
19//! For most applications, you'll care only about the first level. For an
20//! example of common error handling dealing only with first level errors, see
21//! [`examples/error-handling-basic.rs`](https://github.com/brannondorsey/mem-isolate/blob/main/examples/error-handling-basic.rs).
22//!
23//! Levels two and three are useful if you want to know more about what
24//! **exactly** went wrong and expose internals about how `mem-isolate` works.
25//!
26//! Note: These errors all describe things that went wrong with a `mem-isolate`
27//! operation. They have nothing to do with the `callable` you passed to
28//! [`crate::execute_in_isolated_process`], which can define its own errors and
29//! maybe values by returning a [`Result`] or [`Option`] type.
30
31use serde::Deserialize;
32use serde::Serialize;
33use std::io;
34use thiserror::Error;
35
36/// [`MemIsolateError`] is the **primary error type returned by the crate**. The
37/// goal is to give the caller context about what happened to their callable if
38/// something went wrong.
39///
40/// For basic usage, and an introduction of how you should think about error
41/// handling with this crate, see
42/// [`examples/error-handling-basic.rs`](https://github.com/brannondorsey/mem-isolate/blob/main/examples/error-handling-basic.rs)
43///
44/// For an exhaustive look of all possible error variants, see [`examples/error-handling-complete.rs`](https://github.com/brannondorsey/mem-isolate/blob/main/examples/error-handling-complete.rs)
45// TODO: Consider making this Send + Sync
46#[derive(Error, Debug, Serialize, Deserialize)]
47pub enum MemIsolateError {
48 /// Indicates something went wrong before the callable was executed. Because
49 /// the callable never executed, it should be safe to naively retry the
50 /// callable with or without mem-isolate, even if the function is not
51 /// idempotent.
52 #[error("an error occurred before the callable was executed: {0}")]
53 CallableDidNotExecute(#[source] CallableDidNotExecuteError),
54
55 /// Indicates something went wrong after the callable was executed. **Do not**
56 /// retry execution of the callable unless it is idempotent.
57 #[error("an error occurred after the callable was executed: {0}")]
58 CallableExecuted(#[source] CallableExecutedError),
59
60 /// Indicates something went wrong, but it is unknown whether the callable was
61 /// executed. **You should retry the callable only if it is idempotent.**
62 #[error("the callable process exited with an unknown status: {0}")]
63 CallableStatusUnknown(#[source] CallableStatusUnknownError),
64}
65
66/// An error indicating something went wrong **after** the user-supplied callable was executed
67///
68/// You should only retry the callable if it is idempotent.
69#[derive(Error, Debug, Serialize, Deserialize)]
70pub enum CallableExecutedError {
71 /// An error occurred while serializing the result of the callable inside the child process
72 #[error(
73 "an error occurred while serializing the result of the callable inside the child process: {0}"
74 )]
75 SerializationFailed(String),
76
77 /// An error occurred while deserializing the result of the callable in the parent process
78 #[error(
79 "an error occurred while deserializing the result of the callable in the parent process: {0}"
80 )]
81 DeserializationFailed(String),
82
83 /// A system error occurred while writing the child process's result to the pipe.
84 #[serde(
85 serialize_with = "serialize_option_os_error",
86 deserialize_with = "deserialize_option_os_error"
87 )]
88 #[error("system error encountered writing the child process's result to the pipe: {}", format_option_error(.0))]
89 ChildPipeWriteFailed(#[source] Option<io::Error>),
90}
91
92/// An error indicating something went wrong **before** the user-supplied
93/// callable was executed
94///
95/// It is harmless to retry the callable.
96#[derive(Error, Debug, Serialize, Deserialize)]
97pub enum CallableDidNotExecuteError {
98 // TODO: Consider making these io::Errors be RawOsError typedefs instead.
99 // That rules out a ton of overloaded io::Error posibilities. It's more
100 // precise. WARNING: Serialization will fail if this is not an OS error.
101 //
102 /// A system error occurred while creating the pipe used to communicate with
103 /// the child process
104 #[serde(
105 serialize_with = "serialize_os_error",
106 deserialize_with = "deserialize_os_error"
107 )]
108 #[error(
109 "system error encountered creating the pipe used to communicate with the child process: {0}"
110 )]
111 PipeCreationFailed(#[source] io::Error),
112
113 /// A system error occurred while closing the child process's copy of the
114 /// pipe's read end
115 #[serde(
116 serialize_with = "serialize_option_os_error",
117 deserialize_with = "deserialize_option_os_error"
118 )]
119 #[error("system error encountered closing the child's copy of the pipe's read end: {}", format_option_error(.0))]
120 ChildPipeCloseFailed(#[source] Option<io::Error>),
121
122 /// A system error occurred while forking the child process which is used to
123 /// execute user-supplied callable
124 #[serde(
125 serialize_with = "serialize_os_error",
126 deserialize_with = "deserialize_os_error"
127 )]
128 #[error("system error encountered forking the child process: {0}")]
129 ForkFailed(#[source] io::Error),
130}
131
132/// An error indicating that something went wrong in a way where it is difficult
133/// or impossible to determine whether the user-supplied callable was executed
134/// `¯\_(ツ)_/¯`
135///
136/// You should only retry the callable if it is idempotent.
137#[derive(Error, Debug, Serialize, Deserialize)]
138pub enum CallableStatusUnknownError {
139 /// A system error occurred while closing the parent's copy of the pipe's
140 /// write end
141 #[serde(
142 serialize_with = "serialize_os_error",
143 deserialize_with = "deserialize_os_error"
144 )]
145 #[error("system error encountered closing the parent's copy of the pipe's write end: {0}")]
146 ParentPipeCloseFailed(#[source] io::Error),
147
148 /// A system error occurred while waiting for the child process to exit
149 #[serde(
150 serialize_with = "serialize_os_error",
151 deserialize_with = "deserialize_os_error"
152 )]
153 #[error("system error encountered waiting for the child process: {0}")]
154 WaitFailed(#[source] io::Error),
155
156 /// A system error occurred while reading the child's result from the pipe
157 #[serde(
158 serialize_with = "serialize_os_error",
159 deserialize_with = "deserialize_os_error"
160 )]
161 #[error("system error encountered reading the child's result from the pipe: {0}")]
162 ParentPipeReadFailed(#[source] io::Error),
163
164 /// The callable process died while executing the user-supplied callable
165 #[error("the callable process died during execution")]
166 CallableProcessDiedDuringExecution,
167
168 /// The child process responsible for executing the user-supplied callable
169 /// exited with an unexpected status
170 ///
171 /// Note this does not represent some sort of exit code or return value
172 /// indicating the success or failure of the user-supplied callable itself.
173 #[error("the callable process exited with an unexpected status: {0}")]
174 UnexpectedChildExitStatus(i32),
175
176 /// The child process responsible for executing the user-supplied callable
177 /// was killed by a signal
178 #[error("the callable process was killed by a signal: {0}")]
179 ChildProcessKilledBySignal(i32),
180
181 /// Waitpid returned an unexpected value
182 #[error("waitpid returned an unexpected value: {0}")]
183 UnexpectedWaitpidReturnValue(i32),
184}
185
186fn serialize_os_error<S>(error: &io::Error, serializer: S) -> Result<S::Ok, S::Error>
187where
188 S: serde::Serializer,
189{
190 if let Some(raw_os_error) = error.raw_os_error() {
191 serializer.serialize_i32(raw_os_error)
192 } else {
193 Err(serde::ser::Error::custom("not an os error"))
194 }
195}
196
197fn deserialize_os_error<'de, D>(deserializer: D) -> Result<io::Error, D::Error>
198where
199 D: serde::Deserializer<'de>,
200{
201 let s: i32 = i32::deserialize(deserializer)?;
202 Ok(io::Error::from_raw_os_error(s))
203}
204
205#[allow(clippy::ref_option)]
206fn serialize_option_os_error<S>(error: &Option<io::Error>, serializer: S) -> Result<S::Ok, S::Error>
207where
208 S: serde::Serializer,
209{
210 if let Some(error) = error {
211 serialize_os_error(error, serializer)
212 } else {
213 serializer.serialize_none()
214 }
215}
216
217fn deserialize_option_os_error<'de, D>(deserializer: D) -> Result<Option<io::Error>, D::Error>
218where
219 D: serde::Deserializer<'de>,
220{
221 let s: Option<i32> = Option::deserialize(deserializer)?;
222 match s {
223 Some(s) => Ok(Some(io::Error::from_raw_os_error(s))),
224 None => Ok(None),
225 }
226}
227
228#[allow(clippy::ref_option)]
229fn format_option_error(err: &Option<io::Error>) -> String {
230 match err {
231 Some(e) => e.to_string(),
232 None => "None".to_string(),
233 }
234}