Skip to main content

powdb_sync/
error.rs

1//! Typed errors for the sync crate.
2//!
3//! The public API keeps returning `io::Result` (embedded hosts and the
4//! server thread sync failures through I/O plumbing), but every refusal this
5//! crate raises now carries a [`SyncError`] as the `io::Error`'s source, so a
6//! host can BRANCH on the refusal instead of substring-matching the rendered
7//! message — the same boundary contract `powdb-storage` ships. Display is
8//! byte-identical to the historical strings and the `io::ErrorKind` of every
9//! site is unchanged, so nothing on the wire or in logs moves.
10//!
11//! The semantic variants cover the resume/repair protocol, the refusals an
12//! embedded replica host must act on (resume from the recovered boundary,
13//! repair, or rebootstrap). The two catch-alls carry the long tail; promote a
14//! message to its own variant when a caller needs to branch on it.
15
16use std::io;
17
18/// A refusal raised by the sync crate. See the module docs for the contract.
19#[derive(Debug, thiserror::Error)]
20pub enum SyncError {
21    /// The replica's sync identity does not descend from the history it was
22    /// asked to apply. Resuming is impossible; the host must rebootstrap.
23    #[error("{0}")]
24    IdentityMismatch(String),
25    /// A stranded in-progress apply intent covers a different range than the
26    /// caller's, and the catalog LSN does not prove it void. Resume exactly
27    /// from the recovered catalog boundary, or repair.
28    #[error("{0}")]
29    ApplyInProgress(String),
30    /// The local apply state and the catalog disagree in a way no crash of a
31    /// well-behaved apply produces. Manual repair required before retry.
32    #[error("{0}")]
33    ApplyStateRequiresRepair(String),
34    /// The requested chunk start is not a boundary the local apply state
35    /// vouches for. Re-derive the resume point from the catalog LSN.
36    #[error("{0}")]
37    UntrustedApplyBoundary(String),
38    /// Catch-all for refusals of the caller's request
39    /// (`io::ErrorKind::InvalidInput`).
40    #[error("{0}")]
41    InvalidRequest(String),
42    /// Catch-all for on-disk or protocol state this crate refuses to trust
43    /// (`io::ErrorKind::InvalidData`).
44    #[error("{0}")]
45    CorruptState(String),
46}
47
48impl SyncError {
49    /// The `io::ErrorKind` each variant has always surfaced as.
50    fn kind(&self) -> io::ErrorKind {
51        match self {
52            SyncError::IdentityMismatch(_) | SyncError::InvalidRequest(_) => {
53                io::ErrorKind::InvalidInput
54            }
55            SyncError::ApplyInProgress(_)
56            | SyncError::ApplyStateRequiresRepair(_)
57            | SyncError::UntrustedApplyBoundary(_)
58            | SyncError::CorruptState(_) => io::ErrorKind::InvalidData,
59        }
60    }
61}
62
63/// Source-preserving: the rendered text is unchanged (io::Error's Display
64/// delegates to the payload) and the typed error stays recoverable via
65/// `err.get_ref().and_then(|e| e.downcast_ref::<SyncError>())`.
66impl From<SyncError> for io::Error {
67    fn from(err: SyncError) -> io::Error {
68        io::Error::new(err.kind(), err)
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    fn all_variants() -> Vec<SyncError> {
77        vec![
78            SyncError::IdentityMismatch("m".into()),
79            SyncError::ApplyInProgress("m".into()),
80            SyncError::ApplyStateRequiresRepair("m".into()),
81            SyncError::UntrustedApplyBoundary("m".into()),
82            SyncError::InvalidRequest("m".into()),
83            SyncError::CorruptState("m".into()),
84        ]
85    }
86
87    #[test]
88    fn conversion_preserves_kind_and_text_and_type() {
89        for variant in all_variants() {
90            let kind = variant.kind();
91            let text = variant.to_string();
92            let io_err: io::Error = variant.into();
93            assert_eq!(io_err.kind(), kind);
94            assert_eq!(io_err.to_string(), text, "Display must not change");
95            assert!(
96                io_err
97                    .get_ref()
98                    .and_then(|e| e.downcast_ref::<SyncError>())
99                    .is_some(),
100                "the typed error must survive the io::Error boundary"
101            );
102        }
103    }
104
105    #[test]
106    fn kinds_match_the_historical_sites() {
107        use io::ErrorKind::{InvalidData, InvalidInput};
108        let expect = [
109            (SyncError::IdentityMismatch("m".into()), InvalidInput),
110            (SyncError::ApplyInProgress("m".into()), InvalidData),
111            (SyncError::ApplyStateRequiresRepair("m".into()), InvalidData),
112            (SyncError::UntrustedApplyBoundary("m".into()), InvalidData),
113            (SyncError::InvalidRequest("m".into()), InvalidInput),
114            (SyncError::CorruptState("m".into()), InvalidData),
115        ];
116        for (variant, kind) in expect {
117            let io_err: io::Error = variant.into();
118            assert_eq!(io_err.kind(), kind);
119        }
120    }
121}