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}