Skip to main content

dynomite/cluster/
peer.rs

1//! Cluster peer state.
2//!
3//! A [`Peer`] is the in-memory record for one dynomite node in the
4//! ring. Each peer carries its endpoint, rack, datacenter, token list,
5//! liveness state, and a handle to the outbound connection pool used
6//! when the dispatcher routes a request to it.
7//!
8//! The data shape carries the per-peer node record
9//! (rack, dc, secure flag, same-DC flag, token list, state). Peers are
10//! held by [`crate::cluster::ServerPool`] in an `Arc<RwLock<_>>` so
11//! gossip and dispatch can both observe the table without taking out
12//! exclusive locks for read.
13//!
14//! # Examples
15//!
16//! ```
17//! use dynomite::cluster::peer::{Peer, PeerEndpoint, PeerState};
18//! use dynomite::hashkit::DynToken;
19//!
20//! let p = Peer::new(
21//!     0,
22//!     PeerEndpoint::tcp("127.0.0.1".into(), 8101),
23//!     "rack1".into(),
24//!     "dc1".into(),
25//!     vec![DynToken::from_u32(101_134_286)],
26//!     true,
27//!     true,
28//!     false,
29//! );
30//! assert_eq!(p.rack(), "rack1");
31//! assert_eq!(p.state(), PeerState::Joining);
32//! ```
33
34use crate::hashkit::DynToken;
35
36/// Lifecycle state of a peer in the gossip view.
37///
38/// Numeric values are the stable per-node state
39/// constants (`UNKNOWN`, `JOINING`, `NORMAL`, `STANDBY`, `DOWN`,
40/// `RESET`, `LEAVING`).
41#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Default)]
42#[repr(u8)]
43pub enum PeerState {
44    /// Initial state before the first observation.
45    #[default]
46    Unknown = 0,
47    /// Peer is bootstrapping into the ring.
48    Joining = 1,
49    /// Peer is healthy and serving traffic.
50    Normal = 2,
51    /// Peer is on warm standby; requests are not routed there.
52    Standby = 3,
53    /// Failure detector marked the peer down.
54    Down = 4,
55    /// Peer is being re-established (connection pool reset).
56    Reset = 5,
57    /// Peer is preparing to leave the ring.
58    Leaving = 6,
59}
60
61impl PeerState {
62    /// Stable string label.
63    ///
64    /// # Examples
65    ///
66    /// ```
67    /// use dynomite::cluster::peer::PeerState;
68    /// assert_eq!(PeerState::Normal.name(), "NORMAL");
69    /// ```
70    #[must_use]
71    pub fn name(self) -> &'static str {
72        match self {
73            PeerState::Unknown => "UNKNOWN",
74            PeerState::Joining => "JOINING",
75            PeerState::Normal => "NORMAL",
76            PeerState::Standby => "STANDBY",
77            PeerState::Down => "DOWN",
78            PeerState::Reset => "RESET",
79            PeerState::Leaving => "LEAVING",
80        }
81    }
82
83    /// True when the dispatcher should consider this peer for
84    /// routing decisions. `Joining` peers are accepted because they
85    /// remain in the continuum until they
86    /// transition to `Down` or `Leaving`.
87    ///
88    /// # Examples
89    ///
90    /// ```
91    /// use dynomite::cluster::peer::PeerState;
92    /// assert!(PeerState::Normal.is_routable());
93    /// assert!(!PeerState::Down.is_routable());
94    /// ```
95    #[must_use]
96    pub fn is_routable(self) -> bool {
97        matches!(self, PeerState::Normal | PeerState::Joining)
98    }
99}
100
101/// Network endpoint of a peer.
102#[derive(Clone, Debug, Eq, PartialEq, Hash)]
103pub struct PeerEndpoint {
104    host: String,
105    port: u16,
106}
107
108impl PeerEndpoint {
109    /// Construct a TCP endpoint.
110    ///
111    /// # Examples
112    ///
113    /// ```
114    /// use dynomite::cluster::peer::PeerEndpoint;
115    /// let ep = PeerEndpoint::tcp("10.0.0.1".into(), 8101);
116    /// assert_eq!(ep.host(), "10.0.0.1");
117    /// assert_eq!(ep.port(), 8101);
118    /// assert_eq!(ep.pname(), "10.0.0.1:8101");
119    /// ```
120    #[must_use]
121    pub fn tcp(host: String, port: u16) -> Self {
122        Self { host, port }
123    }
124
125    /// Hostname or numeric IP.
126    #[must_use]
127    pub fn host(&self) -> &str {
128        &self.host
129    }
130
131    /// TCP port.
132    #[must_use]
133    pub fn port(&self) -> u16 {
134        self.port
135    }
136
137    /// Colon-joined `host:port` string.
138    #[must_use]
139    pub fn pname(&self) -> String {
140        format!("{}:{}", self.host, self.port)
141    }
142}
143
144/// One peer in the cluster ring.
145#[derive(Clone, Debug)]
146pub struct Peer {
147    idx: u32,
148    endpoint: PeerEndpoint,
149    rack: String,
150    dc: String,
151    tokens: Vec<DynToken>,
152    is_local: bool,
153    is_same_dc: bool,
154    is_secure: bool,
155    state: PeerState,
156    failure_count: u32,
157    last_state_ts_secs: u64,
158    /// Phi-accrual failure detector for this peer. Fed by
159    /// gossip heartbeats; queried by the gossip task to decide
160    /// when to transition `state` to [`PeerState::Down`].
161    /// Initialised lazily on the first `record_heartbeat` call.
162    fd: crate::cluster::failure_detector::PhiAccrual,
163}
164
165impl Peer {
166    /// Build a new peer record.
167    ///
168    /// `idx` is the peer's index in the pool's peer array (0 is
169    /// always the local node). The initial state is
170    /// [`PeerState::Joining`] for the local node and
171    /// [`PeerState::Down`] for a remote one (a remote peer is
172    /// promoted only after the first gossip ack).
173    ///
174    /// # Examples
175    ///
176    /// ```
177    /// use dynomite::cluster::peer::{Peer, PeerEndpoint, PeerState};
178    /// use dynomite::hashkit::DynToken;
179    /// let p = Peer::new(
180    ///     1,
181    ///     PeerEndpoint::tcp("h".into(), 1),
182    ///     "r".into(),
183    ///     "d".into(),
184    ///     vec![DynToken::from_u32(0)],
185    ///     false,
186    ///     true,
187    ///     false,
188    /// );
189    /// assert_eq!(p.idx(), 1);
190    /// assert_eq!(p.state(), PeerState::Down);
191    /// ```
192    #[must_use]
193    #[allow(clippy::too_many_arguments)]
194    pub fn new(
195        idx: u32,
196        endpoint: PeerEndpoint,
197        rack: String,
198        dc: String,
199        tokens: Vec<DynToken>,
200        is_local: bool,
201        is_same_dc: bool,
202        is_secure: bool,
203    ) -> Self {
204        let state = if is_local {
205            PeerState::Joining
206        } else {
207            PeerState::Down
208        };
209        Self {
210            idx,
211            endpoint,
212            rack,
213            dc,
214            tokens,
215            is_local,
216            is_same_dc,
217            is_secure,
218            state,
219            failure_count: 0,
220            last_state_ts_secs: 0,
221            fd: crate::cluster::failure_detector::PhiAccrual::default(),
222        }
223    }
224
225    /// Index of the peer in the pool's array.
226    #[must_use]
227    pub fn idx(&self) -> u32 {
228        self.idx
229    }
230
231    /// Endpoint reference.
232    #[must_use]
233    pub fn endpoint(&self) -> &PeerEndpoint {
234        &self.endpoint
235    }
236
237    /// Rack name.
238    #[must_use]
239    pub fn rack(&self) -> &str {
240        &self.rack
241    }
242
243    /// Datacenter name.
244    #[must_use]
245    pub fn dc(&self) -> &str {
246        &self.dc
247    }
248
249    /// Token list.
250    #[must_use]
251    pub fn tokens(&self) -> &[DynToken] {
252        &self.tokens
253    }
254
255    /// True for the local node.
256    #[must_use]
257    pub fn is_local(&self) -> bool {
258        self.is_local
259    }
260
261    /// True when the peer shares the local datacenter.
262    #[must_use]
263    pub fn is_same_dc(&self) -> bool {
264        self.is_same_dc
265    }
266
267    /// True when the peer expects an encrypted dnode link.
268    #[must_use]
269    pub fn is_secure(&self) -> bool {
270        self.is_secure
271    }
272
273    /// Current lifecycle state.
274    #[must_use]
275    pub fn state(&self) -> PeerState {
276        self.state
277    }
278
279    /// Update the lifecycle state. The supplied `ts_secs` is the
280    /// last-observed gossip timestamp, used by the
281    /// failure detector to age the peer out.
282    ///
283    /// # Examples
284    ///
285    /// ```
286    /// use dynomite::cluster::peer::{Peer, PeerEndpoint, PeerState};
287    /// use dynomite::hashkit::DynToken;
288    /// let mut p = Peer::new(
289    ///     0, PeerEndpoint::tcp("h".into(), 1), "r".into(), "d".into(),
290    ///     vec![DynToken::from_u32(0)], true, true, false,
291    /// );
292    /// p.set_state(PeerState::Normal, 42);
293    /// assert_eq!(p.state(), PeerState::Normal);
294    /// assert_eq!(p.last_state_ts_secs(), 42);
295    /// ```
296    pub fn set_state(&mut self, state: PeerState, ts_secs: u64) {
297        self.state = state;
298        self.last_state_ts_secs = ts_secs;
299    }
300
301    /// Last observed gossip timestamp (epoch seconds).
302    #[must_use]
303    pub fn last_state_ts_secs(&self) -> u64 {
304        self.last_state_ts_secs
305    }
306
307    /// Increment the consecutive-failure counter.
308    pub fn record_failure(&mut self) {
309        self.failure_count = self.failure_count.saturating_add(1);
310    }
311
312    /// Reset the consecutive-failure counter.
313    pub fn record_success(&mut self) {
314        self.failure_count = 0;
315    }
316
317    /// Current consecutive-failure count.
318    #[must_use]
319    pub fn failure_count(&self) -> u32 {
320        self.failure_count
321    }
322
323    /// Borrow the phi-accrual failure detector for this peer.
324    /// Used by the gossip task to record heartbeat arrivals and
325    /// to query the suspicion level on every tick.
326    #[must_use]
327    pub fn failure_detector(&self) -> &crate::cluster::failure_detector::PhiAccrual {
328        &self.fd
329    }
330
331    /// Mutably borrow the phi-accrual failure detector. Use
332    /// from the gossip / heartbeat task.
333    pub fn failure_detector_mut(&mut self) -> &mut crate::cluster::failure_detector::PhiAccrual {
334        &mut self.fd
335    }
336
337    /// First (primary) token of this peer, if any.
338    ///
339    /// # Examples
340    ///
341    /// ```
342    /// use dynomite::cluster::peer::{Peer, PeerEndpoint};
343    /// use dynomite::hashkit::DynToken;
344    /// let p = Peer::new(
345    ///     0, PeerEndpoint::tcp("h".into(), 1), "r".into(), "d".into(),
346    ///     vec![DynToken::from_u32(7)], true, true, false,
347    /// );
348    /// assert_eq!(p.primary_token().unwrap().get_int(), 7);
349    /// ```
350    #[must_use]
351    pub fn primary_token(&self) -> Option<&DynToken> {
352        self.tokens.first()
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359
360    fn mk(rack: &str, dc: &str, is_local: bool, is_same_dc: bool) -> Peer {
361        Peer::new(
362            0,
363            PeerEndpoint::tcp("127.0.0.1".into(), 8101),
364            rack.into(),
365            dc.into(),
366            vec![DynToken::from_u32(1)],
367            is_local,
368            is_same_dc,
369            false,
370        )
371    }
372
373    #[test]
374    fn local_peer_starts_joining() {
375        let p = mk("r", "d", true, true);
376        assert_eq!(p.state(), PeerState::Joining);
377    }
378
379    #[test]
380    fn remote_peer_starts_down() {
381        let p = mk("r", "d", false, true);
382        assert_eq!(p.state(), PeerState::Down);
383    }
384
385    #[test]
386    fn state_names_round_trip() {
387        for s in [
388            PeerState::Unknown,
389            PeerState::Joining,
390            PeerState::Normal,
391            PeerState::Standby,
392            PeerState::Down,
393            PeerState::Reset,
394            PeerState::Leaving,
395        ] {
396            assert!(!s.name().is_empty());
397        }
398    }
399
400    #[test]
401    fn failure_counter_works() {
402        let mut p = mk("r", "d", false, true);
403        p.record_failure();
404        p.record_failure();
405        assert_eq!(p.failure_count(), 2);
406        p.record_success();
407        assert_eq!(p.failure_count(), 0);
408    }
409
410    #[test]
411    fn routable_states() {
412        assert!(PeerState::Normal.is_routable());
413        assert!(PeerState::Joining.is_routable());
414        assert!(!PeerState::Down.is_routable());
415        assert!(!PeerState::Leaving.is_routable());
416    }
417}