Skip to main content

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}