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
63impl From<SwimError> for crate::error::ClusterError {
64    fn from(err: SwimError) -> Self {
65        crate::error::ClusterError::Transport {
66            detail: err.to_string(),
67        }
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn display_contains_context() {
77        let err = SwimError::StaleIncarnation {
78            node_id: NodeId::new("n1"),
79            received: Incarnation::new(3),
80            local: Incarnation::new(5),
81        };
82        let msg = err.to_string();
83        assert!(msg.contains("n1"));
84        assert!(msg.contains('3'));
85        assert!(msg.contains('5'));
86    }
87
88    #[test]
89    fn invalid_config_display() {
90        let err = SwimError::InvalidConfig {
91            field: "probe_timeout",
92            reason: "must be strictly less than probe_interval",
93        };
94        assert!(err.to_string().contains("probe_timeout"));
95    }
96
97    #[test]
98    fn bridges_to_cluster_error() {
99        let err: crate::error::ClusterError = SwimError::UnknownMember {
100            node_id: NodeId::new("n42"),
101        }
102        .into();
103        assert!(matches!(err, crate::error::ClusterError::Transport { .. }));
104    }
105}