Skip to main content

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}