Skip to main content

atomr_remote/
error.rs

1//! Typed remote-system error.
2//!
3//! Phase 5 of `docs/full-port-plan.md`. The historical
4//! `TransportError::HandshakeRejected(String)` and ad-hoc `Result`
5//! shapes get normalized into this single enum. Each variant has a
6//! [`RemoteErrorKind`] discriminant suitable for matching and
7//! per-kind metrics, plus a free-form message for diagnostics.
8//!
9//! Existing callers continue to use `TransportError` directly; new
10//! code should prefer [`RemoteError`] and let `?` convert. Phase 13
11//! removes the legacy `HandshakeRejected(String)` once every
12//! call-site has migrated.
13
14use thiserror::Error;
15
16use crate::pdu::AkkaPdu;
17use crate::transport::TransportError;
18
19/// Stable discriminant for a [`RemoteError`].
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21#[non_exhaustive]
22pub enum RemoteErrorKind {
23    Handshake,
24    Quarantined,
25    Tombstoned,
26    UnknownPdu,
27    Codec,
28    Transport,
29    Closed,
30    Timeout,
31    /// Back-pressure: bounded send queue rejected the enqueue (Phase 5.G).
32    BackPressure,
33    /// Catch-all for less-frequent error sites.
34    Other,
35}
36
37impl RemoteErrorKind {
38    pub fn as_str(self) -> &'static str {
39        match self {
40            Self::Handshake => "handshake",
41            Self::Quarantined => "quarantined",
42            Self::Tombstoned => "tombstoned",
43            Self::UnknownPdu => "unknown_pdu",
44            Self::Codec => "codec",
45            Self::Transport => "transport",
46            Self::Closed => "closed",
47            Self::Timeout => "timeout",
48            Self::BackPressure => "back_pressure",
49            Self::Other => "other",
50        }
51    }
52}
53
54/// Typed remote-system error.
55///
56/// Construct via [`RemoteError::new`], or via `From<TransportError>`
57/// for transport-layer failures. Matches across `kind` are stable.
58#[derive(Debug, Error)]
59#[error("{kind:?}: {message}")]
60#[non_exhaustive]
61pub struct RemoteError {
62    pub kind: RemoteErrorKind,
63    pub message: String,
64}
65
66impl RemoteError {
67    pub fn new(kind: RemoteErrorKind, message: impl Into<String>) -> Self {
68        Self { kind, message: message.into() }
69    }
70
71    /// Construct an `UnknownPdu` error from a received PDU. Used in
72    /// place of `panic!("unexpected pdu …")` so production code never
73    /// crashes on a protocol mismatch.
74    pub fn unknown_pdu(pdu: &AkkaPdu) -> Self {
75        Self::new(RemoteErrorKind::UnknownPdu, format!("unexpected PDU: {pdu:?}"))
76    }
77
78    pub fn quarantined(target: impl std::fmt::Display) -> Self {
79        Self::new(RemoteErrorKind::Quarantined, format!("{target} is quarantined"))
80    }
81
82    pub fn tombstoned(target: impl std::fmt::Display) -> Self {
83        Self::new(RemoteErrorKind::Tombstoned, format!("{target} is tombstoned"))
84    }
85}
86
87impl From<TransportError> for RemoteError {
88    fn from(e: TransportError) -> Self {
89        let (kind, msg) = match &e {
90            TransportError::HandshakeRejected(s) => (RemoteErrorKind::Handshake, s.clone()),
91            TransportError::Closed => (RemoteErrorKind::Closed, "transport closed".into()),
92            other => (RemoteErrorKind::Transport, other.to_string()),
93        };
94        Self::new(kind, msg)
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn from_transport_handshake_rejected() {
104        let t = TransportError::HandshakeRejected("bad cookie".into());
105        let r: RemoteError = t.into();
106        assert_eq!(r.kind, RemoteErrorKind::Handshake);
107        assert!(r.message.contains("bad cookie"));
108    }
109
110    #[test]
111    fn quarantined_constructs() {
112        let r = RemoteError::quarantined("akka.tcp://Sys@host:7355");
113        assert_eq!(r.kind, RemoteErrorKind::Quarantined);
114        assert!(r.message.contains("host:7355"));
115    }
116
117    #[test]
118    fn kind_strings_stable() {
119        assert_eq!(RemoteErrorKind::Handshake.as_str(), "handshake");
120        assert_eq!(RemoteErrorKind::UnknownPdu.as_str(), "unknown_pdu");
121    }
122}