Skip to main content

raft_io/
error.rs

1//! The crate error type.
2//!
3//! Every fallible operation in `raft-io` returns [`Result<T>`], whose error is
4//! [`Error`]. The type integrates with the portfolio's `error-forge` framework
5//! — it implements [`error_forge::ForgeError`], so callers get the stable
6//! `kind` / `caption` / severity metadata other crates rely on — while still
7//! behaving as an ordinary [`std::error::Error`].
8
9use core::fmt;
10
11use error_forge::ForgeError;
12
13use crate::types::NodeId;
14
15/// A specialised [`Result`](core::result::Result) for `raft-io` operations.
16///
17/// Defaults its error to [`Error`], so most signatures read `Result<T>`.
18///
19/// # Examples
20///
21/// ```
22/// use raft_io::{Error, Result};
23///
24/// fn leader_only() -> Result<()> {
25///     Err(Error::NotLeader { leader: Some(2) })
26/// }
27/// assert!(leader_only().is_err());
28/// ```
29pub type Result<T, E = Error> = core::result::Result<T, E>;
30
31/// Everything that can go wrong while driving a [`RaftNode`](crate::RaftNode).
32///
33/// The type is `#[non_exhaustive]`: later phases (persistence, snapshots) add
34/// variants without a major bump, so a `match` over it must include a wildcard
35/// arm.
36///
37/// # Examples
38///
39/// ```
40/// use raft_io::Error;
41///
42/// // A proposal sent to a follower is rejected with a hint to the leader.
43/// let err = Error::NotLeader { leader: Some(3) };
44/// assert_eq!(err.to_string(), "not the leader; current leader is node 3");
45/// ```
46#[non_exhaustive]
47#[derive(Debug)]
48pub enum Error {
49    /// A client proposal was made to a node that is not the leader.
50    ///
51    /// Only the leader may accept proposals. `leader` carries the node's best
52    /// knowledge of who the current leader is, so the caller can redirect the
53    /// request; it is `None` when no leader is known (for example during an
54    /// election). This is a normal, recoverable condition — retry against the
55    /// indicated leader.
56    NotLeader {
57        /// The node believed to be the current leader, if known.
58        leader: Option<NodeId>,
59    },
60
61    /// A [`RaftLog`](crate::RaftLog) backend operation failed.
62    ///
63    /// The in-memory log never produces this, but a durable backend (the
64    /// `wal-db`-backed log arriving in `v0.4`) can fail to read, append, or
65    /// flush. `context` names the operation that was attempted (for example
66    /// `"append entries"` or `"sync log"`) so the message is actionable, and
67    /// `detail` carries the backend's own description. The caller should treat
68    /// a storage failure on the durability path as fatal to the node: a node
69    /// that cannot persist its state must not continue participating.
70    Storage {
71        /// What the log was trying to do when the failure occurred.
72        context: &'static str,
73        /// The underlying backend error, rendered as text.
74        detail: String,
75    },
76
77    /// A message failed to encode to or decode from its wire form.
78    ///
79    /// Produced by the `framing` module (the `framing` feature) when
80    /// `pack-io` cannot serialize a message or a received byte string does not
81    /// decode into a valid one. `context` names the operation and `detail`
82    /// carries the codec's description. A decode failure is not fatal — the
83    /// transport should drop the malformed message and carry on, exactly as Raft
84    /// tolerates a lost one.
85    Encoding {
86        /// What the framing layer was doing when it failed.
87        context: &'static str,
88        /// The underlying codec error, rendered as text.
89        detail: String,
90    },
91
92    /// A membership change was requested while a previous one is still in flight.
93    ///
94    /// Raft changes the configuration one server at a time, and the leader must
95    /// not begin a new change until the previous configuration entry has
96    /// committed. Retry once the in-flight change completes. This is a routine,
97    /// retryable condition.
98    ConfigInProgress,
99}
100
101impl fmt::Display for Error {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        match self {
104            Self::NotLeader { leader: Some(id) } => {
105                write!(f, "not the leader; current leader is node {id}")
106            }
107            Self::NotLeader { leader: None } => {
108                write!(f, "not the leader; no leader is currently known")
109            }
110            Self::Storage { context, detail } => {
111                write!(f, "log storage error while {context}: {detail}")
112            }
113            Self::Encoding { context, detail } => {
114                write!(f, "message framing error while {context}: {detail}")
115            }
116            Self::ConfigInProgress => {
117                write!(f, "a configuration change is already in progress")
118            }
119        }
120    }
121}
122
123impl std::error::Error for Error {}
124
125impl ForgeError for Error {
126    fn kind(&self) -> &'static str {
127        match self {
128            Self::NotLeader { .. } => "NotLeader",
129            Self::Storage { .. } => "Storage",
130            Self::Encoding { .. } => "Encoding",
131            Self::ConfigInProgress => "ConfigInProgress",
132        }
133    }
134
135    fn caption(&self) -> &'static str {
136        match self {
137            Self::NotLeader { .. } => "Not the leader",
138            Self::Storage { .. } => "Log storage failure",
139            Self::Encoding { .. } => "Message framing failure",
140            Self::ConfigInProgress => "Configuration change in progress",
141        }
142    }
143
144    /// A `NotLeader` rejection is retryable against the indicated leader and a
145    /// `ConfigInProgress` rejection is retryable once the change completes; a
146    /// storage failure on the durability path is not.
147    fn is_retryable(&self) -> bool {
148        matches!(self, Self::NotLeader { .. } | Self::ConfigInProgress)
149    }
150
151    /// A storage failure means the node can no longer guarantee durability and
152    /// should stop; a `NotLeader` rejection is a routine redirect.
153    fn is_fatal(&self) -> bool {
154        matches!(self, Self::Storage { .. })
155    }
156}
157
158impl Error {
159    /// Builds a [`Storage`](Error::Storage) error from any displayable backend
160    /// error.
161    ///
162    /// Backends implementing [`RaftLog`](crate::RaftLog) use this to map their
163    /// own error type into the crate's error without naming its fields.
164    ///
165    /// # Examples
166    ///
167    /// ```
168    /// use raft_io::Error;
169    ///
170    /// let io = std::io::Error::new(std::io::ErrorKind::Other, "disk full");
171    /// let err = Error::storage("append entries", io);
172    /// assert!(err.to_string().contains("disk full"));
173    /// ```
174    #[must_use]
175    pub fn storage(context: &'static str, source: impl fmt::Display) -> Self {
176        Self::Storage {
177            context,
178            detail: source.to_string(),
179        }
180    }
181
182    /// Builds an [`Encoding`](Error::Encoding) error from any displayable codec
183    /// error. Used by the `framing` layer.
184    ///
185    /// # Examples
186    ///
187    /// ```
188    /// use raft_io::Error;
189    ///
190    /// let err = Error::encoding("decode message", "unexpected end of input");
191    /// assert!(err.to_string().contains("unexpected end of input"));
192    /// ```
193    #[must_use]
194    pub fn encoding(context: &'static str, source: impl fmt::Display) -> Self {
195        Self::Encoding {
196            context,
197            detail: source.to_string(),
198        }
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn test_not_leader_display_with_known_leader() {
208        let e = Error::NotLeader { leader: Some(7) };
209        assert_eq!(e.to_string(), "not the leader; current leader is node 7");
210    }
211
212    #[test]
213    fn test_not_leader_display_without_leader() {
214        let e = Error::NotLeader { leader: None };
215        assert_eq!(
216            e.to_string(),
217            "not the leader; no leader is currently known"
218        );
219    }
220
221    #[test]
222    fn test_storage_constructor_captures_detail() {
223        let e = Error::storage("sync log", "device busy");
224        assert_eq!(
225            e.to_string(),
226            "log storage error while sync log: device busy"
227        );
228    }
229
230    #[test]
231    fn test_forge_metadata_distinguishes_variants() {
232        let not_leader = Error::NotLeader { leader: None };
233        let storage = Error::storage("append entries", "x");
234        assert_eq!(not_leader.kind(), "NotLeader");
235        assert_eq!(storage.kind(), "Storage");
236        assert!(not_leader.is_retryable());
237        assert!(!not_leader.is_fatal());
238        assert!(!storage.is_retryable());
239        assert!(storage.is_fatal());
240    }
241}