Skip to main content

rylv_pool/
error.rs

1use std::{
2    any::Any,
3    fmt,
4    panic::{AssertUnwindSafe, catch_unwind},
5    sync::Mutex,
6};
7
8/// A factory failure or an unwinding panic during a pool operation.
9#[derive(Debug)]
10pub enum PoolError<E> {
11    /// The factory returned an error without panicking.
12    Factory(E),
13    /// The operation panicked and its payload was captured.
14    Panic(PanicError),
15}
16
17impl<E: fmt::Display> fmt::Display for PoolError<E> {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        match self {
20            Self::Factory(error) => write!(f, "pool factory failed: {error}"),
21            Self::Panic(error) => write!(f, "pool operation panicked: {error}"),
22        }
23    }
24}
25
26impl<E: std::error::Error + 'static> std::error::Error for PoolError<E> {
27    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
28        match self {
29            Self::Factory(error) => Some(error),
30            Self::Panic(error) => Some(error),
31        }
32    }
33}
34
35impl<E> PoolError<E> {
36    /// Run an operation, preserving factory errors and capturing unwind panics.
37    ///
38    /// The operation must leave any externally observable state valid when it
39    /// unwinds. Panics configured to abort and panics in the panic hook cannot
40    /// be captured.
41    ///
42    /// # Errors
43    ///
44    /// Returns [`Self::Factory`] for a returned error or [`Self::Panic`] for an
45    /// unwinding panic. Successful work completed before a panic is retained.
46    pub fn catch<T>(operation: impl FnOnce() -> Result<T, E>) -> Result<T, Self> {
47        catch_operation(operation)
48            .map_err(Self::Panic)?
49            .map_err(Self::Factory)
50    }
51}
52
53/// The original payload of a captured unwind panic.
54///
55/// A mutex allows this error to be shared even when the payload is only Send.
56pub struct PanicError {
57    payload: Mutex<Box<dyn Any + Send>>,
58}
59
60impl PanicError {
61    /// Recover the original payload for inspection by the caller.
62    #[must_use]
63    pub fn into_payload(self) -> Box<dyn Any + Send> {
64        self.payload
65            .into_inner()
66            .unwrap_or_else(std::sync::PoisonError::into_inner)
67    }
68}
69
70impl fmt::Display for PanicError {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        let payload = self
73            .payload
74            .lock()
75            .unwrap_or_else(std::sync::PoisonError::into_inner);
76        if let Some(message) = payload.downcast_ref::<String>() {
77            f.write_str(message)
78        } else if let Some(message) = payload.downcast_ref::<&str>() {
79            f.write_str(message)
80        } else {
81            f.write_str("non-string panic payload")
82        }
83    }
84}
85
86impl fmt::Debug for PanicError {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        f.debug_struct("PanicError")
89            .field("message", &format_args!("{self}"))
90            .finish_non_exhaustive()
91    }
92}
93
94impl std::error::Error for PanicError {}
95
96// Keep this helper internal even if its containing module becomes public.
97#[allow(clippy::redundant_pub_crate)]
98pub(super) fn catch_operation<T>(operation: impl FnOnce() -> T) -> Result<T, PanicError> {
99    // Providers run client code outside storage borrows and must preserve
100    // their invariants on unwind. Callers own mutable factories exclusively
101    // for the operation; a panicking factory is never retried here.
102    catch_unwind(AssertUnwindSafe(operation)).map_err(|payload| PanicError {
103        payload: Mutex::new(payload),
104    })
105}
106
107#[cfg(test)]
108mod tests;