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}