Skip to main content

boson_core/
error.rs

1//! Error types for Boson core operations.
2
3use std::error::Error as StdError;
4
5use thiserror::Error;
6
7/// Result type alias for Boson core operations.
8pub type Result<T> = std::result::Result<T, BosonError>;
9
10/// Errors that can occur in Boson operations.
11#[derive(Debug, Error)]
12pub enum BosonError {
13    /// Task not found in registry.
14    #[error("task not found: {0}")]
15    TaskNotFound(String),
16
17    /// Job not found.
18    #[error("job not found: {0}")]
19    JobNotFound(String),
20
21    /// Run not found.
22    #[error("run not found: {0}")]
23    RunNotFound(String),
24
25    /// Task config not found.
26    #[error("task config not found: {0}")]
27    TaskConfigNotFound(String),
28
29    /// Parameter serialization/deserialization error.
30    #[error("parameter error: {0}")]
31    ParamError(String),
32
33    /// Signature mismatch between job and current task.
34    #[error("signature mismatch: job expects {expected}, task has {actual}")]
35    SignatureMismatch {
36        /// Expected signature from the enqueued job.
37        expected: String,
38        /// Current task signature in the registry.
39        actual: String,
40    },
41
42    /// Invalid priority or pool.
43    #[error("invalid config: {0}")]
44    InvalidConfig(String),
45
46    /// Persistence / adapter backend failure.
47    #[error("backend error: {message}")]
48    Backend {
49        /// Operator-safe summary (stable for logs and HTTP bodies).
50        message: String,
51        /// Optional underlying adapter error for `Error::source` chains.
52        #[source]
53        source: Option<Box<dyn StdError + Send + Sync>>,
54    },
55
56    /// Internal error.
57    #[error("internal error: {message}")]
58    Internal {
59        /// Operator-safe summary.
60        message: String,
61        /// Optional underlying cause for `Error::source` chains.
62        #[source]
63        source: Option<Box<dyn StdError + Send + Sync>>,
64    },
65
66    /// Enqueue blocked by rate limit or in-flight cap; caller should retry after backoff.
67    #[error("enqueue rate limited for task: {0}")]
68    RateLimited(String),
69
70    /// Named queue backend not registered on the router.
71    #[error("unknown queue backend: {0}")]
72    UnknownBackend(String),
73}
74
75impl BosonError {
76    /// Backend failure without an underlying source.
77    #[must_use]
78    pub fn backend(message: impl Into<String>) -> Self {
79        Self::Backend {
80            message: message.into(),
81            source: None,
82        }
83    }
84
85    /// Backend failure wrapping an underlying error.
86    #[must_use]
87    pub fn backend_source(
88        message: impl Into<String>,
89        source: impl StdError + Send + Sync + 'static,
90    ) -> Self {
91        Self::Backend {
92            message: message.into(),
93            source: Some(Box::new(source)),
94        }
95    }
96
97    /// Internal failure without an underlying source.
98    #[must_use]
99    pub fn internal(message: impl Into<String>) -> Self {
100        Self::Internal {
101            message: message.into(),
102            source: None,
103        }
104    }
105
106    /// Internal failure wrapping an underlying error.
107    #[must_use]
108    pub fn internal_source(
109        message: impl Into<String>,
110        source: impl StdError + Send + Sync + 'static,
111    ) -> Self {
112        Self::Internal {
113            message: message.into(),
114            source: Some(Box::new(source)),
115        }
116    }
117
118    /// Message text for [`Self::Backend`], if this is a backend error.
119    #[must_use]
120    pub fn backend_message(&self) -> Option<&str> {
121        match self {
122            Self::Backend { message, .. } => Some(message.as_str()),
123            _ => None,
124        }
125    }
126
127    /// True when a backend adapter reported a unique / duplicate-key constraint failure.
128    #[must_use]
129    pub fn is_backend_unique_violation(&self) -> bool {
130        self.backend_message().is_some_and(|msg| {
131            msg.contains("UNIQUE") || msg.contains("unique") || msg.contains("Duplicate")
132        })
133    }
134}
135
136/// Identity reconstruction failure at handler boundary.
137#[derive(Debug, Error)]
138pub enum IdentityError {
139    /// Actor JSON could not be parsed or mapped.
140    #[error("invalid actor: {0}")]
141    InvalidActor(String),
142}
143
144impl From<serde_json::Error> for BosonError {
145    fn from(err: serde_json::Error) -> Self {
146        Self::ParamError(err.to_string())
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use std::error::Error as StdError;
153
154    use super::BosonError;
155
156    #[derive(Debug, thiserror::Error)]
157    #[error("dial failed")]
158    struct DialFailed;
159
160    #[test]
161    fn backend_source_is_reachable_via_std_error() {
162        let err = BosonError::backend_source("nats connect", DialFailed);
163        assert!(err
164            .backend_message()
165            .is_some_and(|m| m.contains("nats connect")));
166        assert!(err
167            .source()
168            .is_some_and(|s| s.to_string().contains("dial failed")));
169    }
170
171    #[test]
172    fn unique_violation_helper_matches_message() {
173        let err = BosonError::backend("sql backend: UNIQUE constraint failed");
174        assert!(err.is_backend_unique_violation());
175        assert!(!BosonError::backend("timeout").is_backend_unique_violation());
176    }
177}