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}