neptune_job_queue/errors.rs
1use std::sync::Mutex;
2
3/// a job-handle error.
4#[derive(Debug, thiserror::Error)]
5pub enum JobHandleError {
6 #[error("the job was cancelled")]
7 JobCancelled,
8
9 // see comment for PanicInfo below
10 #[error("the job panicked during processing: {0:?}")]
11 JobPanicked(PanicInfo),
12
13 #[error("channel send error cancelling job")]
14 CancelJobError(#[from] tokio::sync::watch::error::SendError<()>),
15
16 #[error("channel recv error waiting for job results: {0}")]
17 JobResultError(#[from] tokio::sync::oneshot::error::RecvError),
18
19 #[error("downcast failed converting '{from}' to '{to}'")]
20 JobResultWrapperError {
21 from: &'static str,
22 to: &'static str,
23 },
24}
25
26impl From<Box<dyn std::any::Any + Send + 'static>> for JobHandleError {
27 fn from(panic_info: Box<dyn std::any::Any + Send + 'static>) -> Self {
28 Self::JobPanicked(PanicInfo(Mutex::new(panic_info)))
29 }
30}
31
32impl JobHandleError {
33 /// Returns true if the error was caused by the task panicking.
34 ///
35 /// ```
36 /// use neptune_job_queue::JobQueue;
37 /// use neptune_job_queue::traits::*;
38 ///
39 /// struct PanicJob;
40 ///
41 /// #[async_trait::async_trait]
42 /// impl Job for PanicJob {
43 /// fn is_async(&self) -> bool {
44 /// true
45 /// }
46 /// async fn run_async(&self) -> Box<dyn JobResult> {
47 /// panic!("{}", "boom");
48 /// }
49 /// }
50 ///
51 /// #[tokio::main]
52 /// async fn main() -> anyhow::Result<()> {
53 /// let job_queue = JobQueue::start();
54 /// let job_handle = job_queue.add_job(PanicJob, 1)?;
55 ///
56 /// let err = job_handle.await?.unwrap_err();
57 /// assert!(err.is_panic());
58 ///
59 /// Ok(())
60 /// }
61 /// ```
62 pub fn is_panic(&self) -> bool {
63 matches!(self, Self::JobPanicked(_))
64 }
65
66 /// into_panic() panics if the Error does not represent the underlying task terminating with a panic. Use is_panic to check the error reason or try_into_panic for a variant that does not panic.
67 ///
68 /// ```should_panic(expected = "boom")
69 /// use neptune_job_queue::JobQueue;
70 /// use neptune_job_queue::traits::*;
71 ///
72 /// struct PanicJob;
73 ///
74 /// #[async_trait::async_trait]
75 /// impl Job for PanicJob {
76 /// fn is_async(&self) -> bool {
77 /// true
78 /// }
79 /// async fn run_async(&self) -> Box<dyn JobResult> {
80 /// panic!("{}", "boom");
81 /// }
82 /// }
83 ///
84 /// #[tokio::main]
85 /// async fn main() -> anyhow::Result<()> {
86 /// let job_queue = JobQueue::start();
87 /// let job_handle = job_queue.add_job(PanicJob, 1)?;
88 ///
89 /// let err = job_handle.await?.unwrap_err();
90 ///
91 /// // Resume the panic on the main task
92 /// std::panic::resume_unwind(err.into_panic());
93 ///
94 /// Ok(())
95 /// }
96 /// ```
97 pub fn into_panic(self) -> Box<dyn std::any::Any + Send + 'static> {
98 self.try_into_panic()
99 .expect("should be JobPanicked variant")
100 }
101
102 /// Consumes the `JobHandleError`, returning the object with which the task panicked if the task terminated due to a panic. Otherwise, self is returned.
103 ///
104 /// ```should_panic(expected = "boom")
105 /// use neptune_job_queue::JobQueue;
106 /// use neptune_job_queue::traits::*;
107 ///
108 /// struct PanicJob;
109 ///
110 /// #[async_trait::async_trait]
111 /// impl Job for PanicJob {
112 /// fn is_async(&self) -> bool {
113 /// true
114 /// }
115 /// async fn run_async(&self) -> Box<dyn JobResult> {
116 /// panic!("{}", "boom");
117 /// }
118 /// }
119 ///
120 /// #[tokio::main]
121 /// async fn main() -> anyhow::Result<()> {
122 /// let job_queue = JobQueue::start();
123 /// let job_handle = job_queue.add_job(PanicJob, 1)?;
124 ///
125 /// let err = job_handle.await?.unwrap_err();
126 ///
127 /// // Resume the panic on the main task
128 /// let panic = err.try_into_panic()?;
129 /// std::panic::resume_unwind(panic);
130 ///
131 /// Ok(())
132 /// }
133 /// ```
134 pub fn try_into_panic(self) -> Result<Box<dyn std::any::Any + Send + 'static>, Self> {
135 match self {
136 // Consume self here
137 Self::JobPanicked(panic_mutex) => {
138 // The unwrap() cannot fail. see PanicInfo comment.
139 Ok(panic_mutex.0.into_inner().unwrap())
140 }
141 other => Err(other), // For other variants, just return them
142 }
143 }
144
145 /// returns panic message, if available
146 ///
147 /// returns None if Error variant is not `JobPanicked` or panic info cannot
148 /// be downcast to a string representation
149 ///
150 /// ```
151 /// use neptune_job_queue::JobQueue;
152 /// use neptune_job_queue::traits::*;
153 ///
154 /// struct PanicJob;
155 ///
156 /// #[async_trait::async_trait]
157 /// impl Job for PanicJob {
158 /// fn is_async(&self) -> bool {
159 /// true
160 /// }
161 /// async fn run_async(&self) -> Box<dyn JobResult> {
162 /// panic!("{}", "boom");
163 /// }
164 /// }
165 ///
166 /// #[tokio::main]
167 /// async fn main() -> anyhow::Result<()> {
168 /// let job_queue = JobQueue::start();
169 /// let job_handle = job_queue.add_job(PanicJob, 1)?;
170 ///
171 /// let err = job_handle.await?.unwrap_err();
172 /// assert_eq!( Some("boom".to_string()), err.panic_message());
173 ///
174 /// Ok(())
175 /// }
176 /// ```
177 pub fn panic_message(&self) -> Option<String> {
178 match self {
179 JobHandleError::JobPanicked(panic_mutex) => {
180 let guard = panic_mutex.0.lock().unwrap();
181 if let Some(s) = guard.downcast_ref::<&'static str>() {
182 Some((*s).to_string())
183 } else {
184 guard.downcast_ref::<String>().cloned()
185 }
186 }
187 _ => None,
188 }
189 }
190}
191
192/// Holds panic information
193//
194// 1. The Box holds panic info as returned from tokio JoinError::into_panic()
195// 2. The Mutex makes the panic info `Sync`.
196// 3. PanicInfo makes the mutex private to guarantee lock() can never be called
197// on it by code outside this module.
198// 4. Inside this module the lock is only held by `panic_message()` and the
199// `Debug` impl, neither of which can panic while holding the guard.
200// 5. Since no thread can panic while holding the lock:
201// a. the mutex can never be poisoned.
202// b. Mutex::into_inner() is guaranteed to succeed.
203//
204// possible alternatives to Mutex:
205//
206// Mutex is only being used for `Sync` not for locking.
207//
208// other possible candidates are discussed here:
209// https://github.com/Neptune-Crypto/neptune-core/pull/584#discussion_r2086163632
210//
211// std::sync::Exclusive seems a better fit, but is not yet in stable rust as
212// of rust 1.86.0.
213pub struct PanicInfo(Mutex<Box<dyn std::any::Any + Send + 'static>>);
214
215impl std::fmt::Debug for PanicInfo {
216 /// Shows the panic message when the payload is a string (the common case),
217 /// so that `unwrap()`s and logs of [`JobHandleError`] surface the original
218 /// panic message instead of an opaque `Any`.
219 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220 let message = self.0.try_lock().ok().and_then(|guard| {
221 guard
222 .downcast_ref::<&'static str>()
223 .map(|s| (*s).to_string())
224 .or_else(|| guard.downcast_ref::<String>().cloned())
225 });
226 match message {
227 Some(message) => write!(f, "PanicInfo({message:?})"),
228 None => write!(f, "PanicInfo(<non-string panic payload>)"),
229 }
230 }
231}
232
233#[derive(Debug, Clone, thiserror::Error)]
234#[non_exhaustive]
235pub enum AddJobError {
236 #[error("channel send error adding job. error: {0}")]
237 SendError(#[from] tokio::sync::mpsc::error::SendError<()>),
238}
239
240#[derive(Debug, thiserror::Error)]
241#[non_exhaustive]
242pub enum StopQueueError {
243 #[error("channel send error adding job. error: {0}")]
244 SendError(#[from] tokio::sync::watch::error::SendError<()>),
245
246 #[error("join error while waiting for job-queue to stop. error: {0}")]
247 JoinError(#[from] tokio::task::JoinError),
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[tokio::test]
255 async fn job_handle_error_into_panic() {
256 let join_err = tokio::spawn(async { panic!("boom") }).await.unwrap_err();
257 let err: JobHandleError = join_err.into_panic().into();
258 // just execute, to ensure it does not panic.
259 let _ = err.into_panic();
260 }
261
262 #[tokio::test]
263 async fn job_handle_error_try_into_panic() {
264 let join_err = tokio::spawn(async { panic!("boom") }).await.unwrap_err();
265 let err: JobHandleError = join_err.into_panic().into();
266 assert!(err.try_into_panic().is_ok());
267 }
268
269 #[tokio::test]
270 async fn job_handle_error_panic_message() {
271 let join_err = tokio::spawn(async { panic!("boom") }).await.unwrap_err();
272 let err: JobHandleError = join_err.into_panic().into();
273 assert_eq!(Some("boom"), err.panic_message().as_deref());
274 }
275}