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}