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}