nodedb_cluster/swim/error.rs
1//! Typed error variants for the SWIM subsystem.
2//!
3//! `SwimError` is the single error type returned by every public function
4//! in `nodedb_cluster::swim`. It is wired into the cluster-wide
5//! [`ClusterError`] enum via a `From` impl in `crate::error`, which in turn
6//! bridges to `nodedb_types::NodeDbError` at the public API boundary.
7
8use thiserror::Error;
9
10use nodedb_types::NodeId;
11
12use super::incarnation::Incarnation;
13use super::member::MemberState;
14
15/// Errors produced by the SWIM failure detector and membership layer.
16#[derive(Debug, Error)]
17pub enum SwimError {
18 /// A message or update referenced a node id not present in the
19 /// membership list. This is non-fatal — the detector will request a
20 /// full sync from the sender.
21 #[error("swim: unknown member {node_id}")]
22 UnknownMember { node_id: NodeId },
23
24 /// Received update carries an incarnation strictly older than the
25 /// locally recorded value, so the update is refuted.
26 #[error("swim: stale incarnation for {node_id}: received {received:?} <= local {local:?}")]
27 StaleIncarnation {
28 node_id: NodeId,
29 received: Incarnation,
30 local: Incarnation,
31 },
32
33 /// Received a `Suspect` update targeting the local node. The failure
34 /// detector must bump its own incarnation and broadcast an `Alive`
35 /// refutation. Callers treat this as a signal, not a fatal error.
36 #[error("swim: local node suspected at incarnation {incarnation:?}")]
37 SelfSuspected { incarnation: Incarnation },
38
39 /// A state transition violated the SWIM state machine (e.g. attempting
40 /// to move a `Left` member back to `Alive`). Always a bug.
41 #[error("swim: invalid state transition {from:?} -> {to:?}")]
42 InvalidTransition { from: MemberState, to: MemberState },
43
44 /// Configuration validation failed. Returned by [`super::SwimConfig::validate`].
45 #[error("swim: invalid config field {field}: {reason}")]
46 InvalidConfig {
47 field: &'static str,
48 reason: &'static str,
49 },
50
51 /// zerompk failed to serialize a `SwimMessage`. In practice this is
52 /// infallible for the current message schema — the variant exists so
53 /// future additions to the wire format cannot silently panic.
54 #[error("swim: encode failure: {detail}")]
55 Encode { detail: String },
56
57 /// zerompk failed to parse incoming bytes as a `SwimMessage`. Common
58 /// causes: truncated datagram, version skew, random UDP noise.
59 #[error("swim: decode failure: {detail}")]
60 Decode { detail: String },
61
62 /// Transport backend has been closed; no further I/O is possible.
63 /// Returned by [`super::detector::Transport::recv`] on shutdown.
64 #[error("swim: transport closed")]
65 TransportClosed,
66
67 /// The in-flight probe map is full. Should never happen in practice —
68 /// the detector caps concurrent probes at a few tens — but the error
69 /// exists so a runaway bug cannot corrupt the detector state.
70 #[error("swim: probe inflight table overflow")]
71 ProbeInflightOverflow,
72}
73
74impl From<SwimError> for crate::error::ClusterError {
75 fn from(err: SwimError) -> Self {
76 crate::error::ClusterError::Transport {
77 detail: err.to_string(),
78 }
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn display_contains_context() {
88 let err = SwimError::StaleIncarnation {
89 node_id: NodeId::new("n1"),
90 received: Incarnation::new(3),
91 local: Incarnation::new(5),
92 };
93 let msg = err.to_string();
94 assert!(msg.contains("n1"));
95 assert!(msg.contains('3'));
96 assert!(msg.contains('5'));
97 }
98
99 #[test]
100 fn invalid_config_display() {
101 let err = SwimError::InvalidConfig {
102 field: "probe_timeout",
103 reason: "must be strictly less than probe_interval",
104 };
105 assert!(err.to_string().contains("probe_timeout"));
106 }
107
108 #[test]
109 fn bridges_to_cluster_error() {
110 let err: crate::error::ClusterError = SwimError::UnknownMember {
111 node_id: NodeId::new("n42"),
112 }
113 .into();
114 assert!(matches!(err, crate::error::ClusterError::Transport { .. }));
115 }
116}