Skip to main content

actr_protocol/
error.rs

1//! Top-level error types for the Actor-RTC framework.
2//!
3//! ## Design
4//!
5//! Two layers only:
6//!
7//! ```text
8//! NetworkError   (transport-internal, never exposed to users)
9//!      ↓  From
10//! ActrError      (public, flat enum — what callers see)
11//! ```
12//!
13//! `RuntimeError` and `ProtocolError` have been removed.
14//!
15//! ## Error classification
16//!
17//! Every error belongs to one fault domain (`ErrorKind`):
18//!
19//! | Kind      | Meaning                        | Retry? | DLQ? |
20//! |-----------|--------------------------------|--------|------|
21//! | Transient | Environmental fluctuation      | yes    | no   |
22//! | Client    | Caller error (bad request)     | no     | no   |
23//! | Internal  | Framework bug / panic          | no     | no   |
24//! | Corrupt   | Data corruption                | no     | yes  |
25//!
26//! Use the `Classify` trait to query classification from any error type.
27
28use thiserror::Error;
29
30// ── ActrError ────────────────────────────────────────────────────────────────
31
32/// Top-level framework error, returned to all callers.
33///
34/// Flat enum — no nested error wrapping. Each variant is self-describing.
35#[derive(Error, Debug, Clone)]
36pub enum ActrError {
37    // ── Transient ──────────────────────────────────────────────────────────
38    /// Peer temporarily unavailable: connection lost, overloaded, or reconnecting.
39    ///
40    /// `ErrorKind::Transient` — retry with backoff.
41    #[error("unavailable: {0}")]
42    Unavailable(String),
43
44    /// Request deadline exceeded.
45    ///
46    /// `ErrorKind::Transient` — may retry with a fresh deadline.
47    #[error("timed out")]
48    TimedOut,
49
50    // ── Client ─────────────────────────────────────────────────────────────
51    /// Target actor not found.
52    ///
53    /// `ErrorKind::Client` — do not retry; check service discovery first.
54    #[error("not found: {0}")]
55    NotFound(String),
56
57    /// Permission denied by ACL.
58    ///
59    /// `ErrorKind::Client` — do not retry; fix authorization.
60    #[error("permission denied: {0}")]
61    PermissionDenied(String),
62
63    /// Invalid argument or malformed request.
64    ///
65    /// `ErrorKind::Client` — do not retry; fix the request.
66    #[error("invalid argument: {0}")]
67    InvalidArgument(String),
68
69    /// No handler registered for the given route key.
70    ///
71    /// `ErrorKind::Client` — do not retry; check service definition.
72    #[error("unknown route: {0}")]
73    UnknownRoute(String),
74
75    /// Required dependency not found in the lock file.
76    ///
77    /// `ErrorKind::Client` — do not retry; fix the manifest.
78    #[error("dependency '{service_name}' not found: {message}")]
79    DependencyNotFound {
80        service_name: String,
81        message: String,
82    },
83
84    // ── Corrupt ────────────────────────────────────────────────────────────
85    /// Protobuf decode failure — message data is corrupted.
86    ///
87    /// `ErrorKind::Corrupt` — route to Dead Letter Queue; do not retry.
88    #[error("decode failure: {0}")]
89    DecodeFailure(String),
90
91    // ── Internal ───────────────────────────────────────────────────────────
92    /// Feature not yet implemented.
93    ///
94    /// `ErrorKind::Internal` — do not retry.
95    #[error("not implemented: {0}")]
96    NotImplemented(String),
97
98    /// Internal framework error: bug, panic, or unrecoverable state.
99    ///
100    /// `ErrorKind::Internal` — do not retry; investigate logs.
101    #[error("internal error: {0}")]
102    Internal(String),
103}
104
105// ── ErrorKind ────────────────────────────────────────────────────────────────
106
107/// Fault domain classification for any framework error.
108///
109/// All error types implement [`Classify`] to expose their `ErrorKind`.
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum ErrorKind {
112    /// Environmental fluctuation — retry with exponential backoff.
113    Transient,
114    /// Caller error — bad request or system state; do not retry.
115    Client,
116    /// Framework bug or panic — do not retry; alert.
117    Internal,
118    /// Data corruption — route to Dead Letter Queue; manual intervention required.
119    Corrupt,
120}
121
122// ── Classify trait ───────────────────────────────────────────────────────────
123
124/// Fault-domain classification for error types.
125///
126/// Implement `kind()` only; `is_retryable()` and `requires_dlq()` have
127/// correct default implementations derived from `kind()`.
128pub trait Classify {
129    /// Returns the fault domain this error belongs to.
130    fn kind(&self) -> ErrorKind;
131
132    /// Returns `true` if the operation may be retried.
133    ///
134    /// Only `ErrorKind::Transient` errors are retryable.
135    fn is_retryable(&self) -> bool {
136        matches!(self.kind(), ErrorKind::Transient)
137    }
138
139    /// Returns `true` if the message should be routed to the Dead Letter Queue.
140    ///
141    /// Only `ErrorKind::Corrupt` errors require DLQ routing.
142    fn requires_dlq(&self) -> bool {
143        matches!(self.kind(), ErrorKind::Corrupt)
144    }
145}
146
147impl Classify for ActrError {
148    fn kind(&self) -> ErrorKind {
149        match self {
150            ActrError::Unavailable(_) | ActrError::TimedOut => ErrorKind::Transient,
151
152            ActrError::NotFound(_)
153            | ActrError::PermissionDenied(_)
154            | ActrError::InvalidArgument(_)
155            | ActrError::UnknownRoute(_)
156            | ActrError::DependencyNotFound { .. } => ErrorKind::Client,
157
158            ActrError::DecodeFailure(_) => ErrorKind::Corrupt,
159
160            ActrError::NotImplemented(_) | ActrError::Internal(_) => ErrorKind::Internal,
161        }
162    }
163}
164
165// ── Convenience type aliases ──────────────────────────────────────────────────
166
167/// Result type for actor RPC calls.
168pub type ActorResult<T> = Result<T, ActrError>;
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    // ── ActrError::kind() classification ─────────────────────────────────────
175
176    #[test]
177    fn transient_variants_classify_correctly() {
178        assert_eq!(
179            ActrError::Unavailable("x".into()).kind(),
180            ErrorKind::Transient
181        );
182        assert_eq!(ActrError::TimedOut.kind(), ErrorKind::Transient);
183    }
184
185    #[test]
186    fn client_variants_classify_correctly() {
187        assert_eq!(ActrError::NotFound("x".into()).kind(), ErrorKind::Client);
188        assert_eq!(
189            ActrError::PermissionDenied("x".into()).kind(),
190            ErrorKind::Client
191        );
192        assert_eq!(
193            ActrError::InvalidArgument("x".into()).kind(),
194            ErrorKind::Client
195        );
196        assert_eq!(
197            ActrError::UnknownRoute("x".into()).kind(),
198            ErrorKind::Client
199        );
200        assert_eq!(
201            ActrError::DependencyNotFound {
202                service_name: "svc".into(),
203                message: "not found".into(),
204            }
205            .kind(),
206            ErrorKind::Client
207        );
208    }
209
210    #[test]
211    fn corrupt_variant_classifies_correctly() {
212        assert_eq!(
213            ActrError::DecodeFailure("x".into()).kind(),
214            ErrorKind::Corrupt
215        );
216    }
217
218    #[test]
219    fn internal_variants_classify_correctly() {
220        assert_eq!(
221            ActrError::NotImplemented("x".into()).kind(),
222            ErrorKind::Internal
223        );
224        assert_eq!(ActrError::Internal("x".into()).kind(), ErrorKind::Internal);
225    }
226
227    // ── Classify default impls ────────────────────────────────────────────────
228
229    #[test]
230    fn only_transient_is_retryable() {
231        assert!(ActrError::Unavailable("x".into()).is_retryable());
232        assert!(ActrError::TimedOut.is_retryable());
233
234        assert!(!ActrError::NotFound("x".into()).is_retryable());
235        assert!(!ActrError::DecodeFailure("x".into()).is_retryable());
236        assert!(!ActrError::Internal("x".into()).is_retryable());
237    }
238
239    #[test]
240    fn only_corrupt_requires_dlq() {
241        assert!(ActrError::DecodeFailure("x".into()).requires_dlq());
242
243        assert!(!ActrError::Unavailable("x".into()).requires_dlq());
244        assert!(!ActrError::TimedOut.requires_dlq());
245        assert!(!ActrError::NotFound("x".into()).requires_dlq());
246        assert!(!ActrError::Internal("x".into()).requires_dlq());
247    }
248
249    // ── Clone ─────────────────────────────────────────────────────────────────
250
251    #[test]
252    fn actr_error_is_clone() {
253        let e = ActrError::InvalidArgument("bad".into());
254        let cloned = e.clone();
255        assert_eq!(format!("{cloned}"), "invalid argument: bad");
256    }
257}