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}