Skip to main content

pulsedb/sync/
error.rs

1//! Sync-specific error types.
2//!
3//! [`SyncError`] covers all failure modes in the sync protocol:
4//! transport failures, handshake rejections, serialization issues,
5//! and shutdown coordination.
6
7use thiserror::Error;
8
9use super::types::InstanceId;
10
11/// Errors specific to the sync protocol.
12///
13/// These are wrapped into [`crate::PulseDBError::Sync`] when propagated
14/// through the public API.
15#[derive(Debug, Error)]
16pub enum SyncError {
17    /// Transport-level failure (network I/O, connection refused, etc.).
18    #[error("Sync transport error: {0}")]
19    Transport(String),
20
21    /// Handshake was rejected by the remote peer.
22    #[error("Sync handshake failed: {0}")]
23    Handshake(String),
24
25    /// Failed to serialize or deserialize a sync message.
26    #[error("Sync serialization error: {0}")]
27    Serialization(String),
28
29    /// Operation timed out waiting for a response.
30    #[error("Sync operation timed out")]
31    Timeout,
32
33    /// Connection to the remote peer was lost.
34    #[error("Connection to sync peer lost")]
35    ConnectionLost,
36
37    /// Protocol version mismatch between peers.
38    #[error("Sync protocol version mismatch: local v{local}, remote v{remote}")]
39    ProtocolVersion {
40        /// Local protocol version.
41        local: u32,
42        /// Remote protocol version.
43        remote: u32,
44    },
45
46    /// Wire-format preamble mismatch — caught by raw-byte inspection of the
47    /// 3-byte preamble *before* any deserialize, so a serializer mismatch
48    /// (e.g. a bincode-era peer vs a postcard-era peer) fails loud with a
49    /// typed error instead of yielding garbage through the decoder.
50    ///
51    /// This is distinct from [`SyncError::ProtocolVersion`]: that variant is
52    /// protocol-*semantics* (negotiated in-band after a successful decode);
53    /// this variant is wire-*format* (the bytes can't be trusted to decode at
54    /// all). Two failure shapes feed it:
55    ///
56    /// - **bad magic** (`got == None`): the leading bytes are not a PulseDB
57    ///   sync preamble at all (truncated body, a non-PulseDB POST, or a
58    ///   pre-4.0 no-preamble peer's body) — reported with `got: None`.
59    /// - **wrong version** (`got == Some(v)`): a valid magic but a
60    ///   `wire_format_version` byte this peer does not speak.
61    #[error("Sync wire-format mismatch: expected wire format v{expected}, got {}", match got { Some(g) => format!("v{g}"), None => "bad/absent magic".to_string() })]
62    WireFormatMismatch {
63        /// The wire-format version this peer speaks.
64        expected: u8,
65        /// The wire-format version observed in the preamble, or `None` when the
66        /// magic bytes were absent/wrong (so no trustworthy version was read).
67        got: Option<u8>,
68    },
69
70    /// Received an invalid or unrecognized payload.
71    #[error("Invalid sync payload: {0}")]
72    InvalidPayload(String),
73
74    /// No cursor found for the specified peer instance.
75    #[error("No sync cursor found for instance {instance}")]
76    CursorNotFound {
77        /// The peer instance whose cursor was not found.
78        instance: InstanceId,
79    },
80
81    /// The sync system is shutting down.
82    #[error("Sync system is shutting down")]
83    Shutdown,
84}
85
86impl SyncError {
87    /// Creates a transport error with the given message.
88    pub fn transport(msg: impl Into<String>) -> Self {
89        Self::Transport(msg.into())
90    }
91
92    /// Creates a handshake error with the given message.
93    pub fn handshake(msg: impl Into<String>) -> Self {
94        Self::Handshake(msg.into())
95    }
96
97    /// Creates a serialization error with the given message.
98    pub fn serialization(msg: impl Into<String>) -> Self {
99        Self::Serialization(msg.into())
100    }
101
102    /// Creates an invalid payload error with the given message.
103    pub fn invalid_payload(msg: impl Into<String>) -> Self {
104        Self::InvalidPayload(msg.into())
105    }
106
107    /// Creates a wire-format mismatch for a **bad / absent magic** preamble.
108    ///
109    /// Used when the leading bytes are not a recognizable PulseDB sync
110    /// preamble (truncated body, wrong magic, or a pre-preamble peer's body).
111    pub fn wire_format_bad_magic(expected: u8) -> Self {
112        Self::WireFormatMismatch {
113            expected,
114            got: None,
115        }
116    }
117
118    /// Creates a wire-format mismatch for a **wrong wire-format version**.
119    ///
120    /// Used when the magic is valid but the `wire_format_version` byte names a
121    /// version this peer does not speak.
122    pub fn wire_format_version(expected: u8, got: u8) -> Self {
123        Self::WireFormatMismatch {
124            expected,
125            got: Some(got),
126        }
127    }
128
129    /// Returns true if this is a transport error.
130    pub fn is_transport(&self) -> bool {
131        matches!(self, Self::Transport(_))
132    }
133
134    /// Returns true if this is a timeout error.
135    pub fn is_timeout(&self) -> bool {
136        matches!(self, Self::Timeout)
137    }
138
139    /// Returns true if this is a connection lost error.
140    pub fn is_connection_lost(&self) -> bool {
141        matches!(self, Self::ConnectionLost)
142    }
143
144    /// Returns true if this is a shutdown error.
145    pub fn is_shutdown(&self) -> bool {
146        matches!(self, Self::Shutdown)
147    }
148
149    /// Returns true if this is a wire-format mismatch (bad magic OR wrong
150    /// wire-format version) — the typed fail-loud signal for cross-version
151    /// sync, distinct from a generic [`SyncError::Serialization`].
152    pub fn is_wire_format_mismatch(&self) -> bool {
153        matches!(self, Self::WireFormatMismatch { .. })
154    }
155}
156
157impl From<postcard::Error> for SyncError {
158    fn from(err: postcard::Error) -> Self {
159        SyncError::Serialization(err.to_string())
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn test_sync_error_display() {
169        let err = SyncError::transport("connection refused");
170        assert_eq!(err.to_string(), "Sync transport error: connection refused");
171    }
172
173    #[test]
174    fn test_protocol_version_display() {
175        let err = SyncError::ProtocolVersion {
176            local: 1,
177            remote: 2,
178        };
179        assert_eq!(
180            err.to_string(),
181            "Sync protocol version mismatch: local v1, remote v2"
182        );
183    }
184
185    #[test]
186    fn test_sync_error_is_checks() {
187        assert!(SyncError::transport("x").is_transport());
188        assert!(SyncError::Timeout.is_timeout());
189        assert!(SyncError::ConnectionLost.is_connection_lost());
190        assert!(SyncError::Shutdown.is_shutdown());
191    }
192
193    #[test]
194    fn test_postcard_error_conversion() {
195        // Deserializing truncated bytes triggers a postcard error.
196        let bad_bytes = vec![0u8; 0]; // too short for a (u64, u64)
197        let postcard_err = postcard::from_bytes::<(u64, u64)>(&bad_bytes).unwrap_err();
198        let sync_err: SyncError = postcard_err.into();
199        assert!(matches!(sync_err, SyncError::Serialization(_)));
200    }
201
202    #[test]
203    fn test_wire_format_mismatch_typed_and_distinct() {
204        let bad_magic = SyncError::wire_format_bad_magic(3);
205        let wrong_ver = SyncError::wire_format_version(3, 2);
206
207        // Both are the typed wire-format signal, NOT a generic Serialization.
208        assert!(bad_magic.is_wire_format_mismatch());
209        assert!(wrong_ver.is_wire_format_mismatch());
210        assert!(!SyncError::serialization("x").is_wire_format_mismatch());
211
212        // Distinct from protocol-semantics ProtocolVersion.
213        assert!(matches!(
214            bad_magic,
215            SyncError::WireFormatMismatch { got: None, .. }
216        ));
217        assert!(matches!(
218            wrong_ver,
219            SyncError::WireFormatMismatch {
220                expected: 3,
221                got: Some(2)
222            }
223        ));
224    }
225}