Skip to main content

corpus_ipc/
error.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Backend error type.
4
5/// Errors returned by [`crate::IpcBackend`] and [`crate::HybridFlowBackend`].
6///
7/// These cover initialization, processing, model persistence, IPC communication,
8/// invalid caller input, and structured-message protocol rejection. Most
9/// variants carry a human-readable `String`. [`BackendError::Protocol`] carries
10/// a [`ProtocolError`] so a hybrid-flow receiver can reject an unexpected
11/// [`crate::IpcMessage`] variant without overloading a transport or validation
12/// failure.
13#[derive(Debug, thiserror::Error)]
14pub enum BackendError {
15    /// Backend failed to initialize (e.g. model load failure, ZMQ connect error).
16    #[error("Initialization failed: {0}")]
17    InitializationError(String),
18
19    /// Error during `process_batch` (e.g. shape mismatch, backend internal failure).
20    #[error("Processing failed: {0}")]
21    ProcessingError(String),
22
23    /// Model save/load or filesystem I/O error.
24    #[error("Model I/O error: {0}")]
25    ModelError(String),
26
27    /// IPC / transport communication error (ZMQ send/recv, framing, timeout).
28    #[error("Communication error: {0}")]
29    CommunicationError(String),
30
31    /// Caller supplied invalid input (e.g. empty batch where disallowed, bad config).
32    #[error("Invalid input: {0}")]
33    InvalidInput(String),
34
35    /// A structured message was valid on the wire but not acceptable for this call.
36    ///
37    /// Hybrid-flow receivers return this when an inbound [`crate::IpcMessage`]
38    /// decodes and validates, then turns out to be a different variant than the
39    /// method accepts. The frame is already consumed; it is not dropped silently
40    /// and it is not retried.
41    #[error(transparent)]
42    Protocol(#[from] ProtocolError),
43}
44
45/// Structured-message protocol rejection.
46///
47/// This is distinct from transport failure ([`BackendError::CommunicationError`])
48/// and from payload validation ([`BackendError::InvalidInput`]).
49#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
50pub enum ProtocolError {
51    /// The decoded message variant is not the one this call accepts.
52    #[error("unexpected IpcMessage variant `{found}`; expected {expected}")]
53    UnexpectedVariant {
54        /// Serde variant name that arrived (`Ping`, `Spikes`, …).
55        found: &'static str,
56        /// Serde variant name this call accepts.
57        expected: &'static str,
58    },
59}
60
61#[cfg(test)]
62mod tests {
63    use super::{BackendError, ProtocolError};
64    use std::error::Error;
65
66    fn protocol() -> ProtocolError {
67        ProtocolError::UnexpectedVariant {
68            found: "Ping",
69            expected: "Spikes",
70        }
71    }
72
73    fn variant_kind(err: &BackendError) -> &'static str {
74        match err {
75            BackendError::InitializationError(_) => "init",
76            BackendError::ProcessingError(_) => "process",
77            BackendError::ModelError(_) => "model",
78            BackendError::CommunicationError(_) => "comm",
79            BackendError::InvalidInput(_) => "input",
80            BackendError::Protocol(_) => "protocol",
81        }
82    }
83
84    #[test]
85    fn backend_error_string_variants_keep_display_source_and_match() {
86        let cases = [
87            (
88                BackendError::InitializationError("model missing".into()),
89                "Initialization failed: model missing",
90                "init",
91            ),
92            (
93                BackendError::ProcessingError("shape mismatch".into()),
94                "Processing failed: shape mismatch",
95                "process",
96            ),
97            (
98                BackendError::ModelError("disk full".into()),
99                "Model I/O error: disk full",
100                "model",
101            ),
102            (
103                BackendError::CommunicationError("timeout".into()),
104                "Communication error: timeout",
105                "comm",
106            ),
107            (
108                BackendError::InvalidInput("empty batch".into()),
109                "Invalid input: empty batch",
110                "input",
111            ),
112        ];
113        for (err, display, kind) in cases {
114            assert_eq!(err.to_string(), display);
115            assert!(err.source().is_none(), "{display}");
116            assert_eq!(variant_kind(&err), kind);
117        }
118    }
119
120    #[test]
121    fn backend_error_protocol_stays_transparent() {
122        let inner = protocol();
123        let err = BackendError::from(inner.clone());
124        assert_eq!(
125            inner.to_string(),
126            "unexpected IpcMessage variant `Ping`; expected Spikes"
127        );
128        assert_eq!(err.to_string(), inner.to_string());
129        assert!(inner.source().is_none());
130        assert!(err.source().is_none());
131        assert!(matches!(
132            err,
133            BackendError::Protocol(ProtocolError::UnexpectedVariant {
134                found: "Ping",
135                expected: "Spikes",
136            })
137        ));
138        assert_eq!(variant_kind(&err), "protocol");
139    }
140}