Skip to main content

oximedia_distributed/
consensus.rs

1//! Distributed consensus module (Raft-inspired).
2//!
3//! Provides a simplified Raft consensus implementation for leader election
4//! and distributed log replication in the distributed encoding cluster.
5
6#![allow(dead_code)]
7
8/// Unique identifier for a Raft node.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
10pub struct NodeId(pub u64);
11
12impl NodeId {
13    /// Create a new `NodeId`.
14    #[must_use]
15    pub fn new(id: u64) -> Self {
16        Self(id)
17    }
18
19    /// Get the inner u64 value.
20    #[must_use]
21    pub fn inner(self) -> u64 {
22        self.0
23    }
24}
25
26impl std::fmt::Display for NodeId {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        write!(f, "Node({})", self.0)
29    }
30}
31
32/// Raft term number.
33#[derive(
34    Debug,
35    Clone,
36    Copy,
37    PartialEq,
38    Eq,
39    PartialOrd,
40    Ord,
41    serde::Serialize,
42    serde::Deserialize,
43    Default,
44)]
45pub struct RaftTerm(pub u64);
46
47impl RaftTerm {
48    /// Create a new `RaftTerm`.
49    #[must_use]
50    pub fn new(term: u64) -> Self {
51        Self(term)
52    }
53
54    /// Get the inner u64 value.
55    #[must_use]
56    pub fn inner(self) -> u64 {
57        self.0
58    }
59
60    /// Increment the term.
61    #[must_use]
62    pub fn increment(self) -> Self {
63        Self(self.0 + 1)
64    }
65}
66
67impl std::fmt::Display for RaftTerm {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        write!(f, "Term({})", self.0)
70    }
71}
72
73/// Role of a Raft node.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum RaftRole {
76    /// Follower - the default state.
77    Follower,
78    /// Candidate - seeking election.
79    Candidate,
80    /// Leader - coordinating the cluster.
81    Leader,
82}
83
84impl RaftRole {
85    /// Returns true if the node can vote in leader elections.
86    #[must_use]
87    pub fn can_vote(self) -> bool {
88        matches!(self, RaftRole::Follower | RaftRole::Candidate)
89    }
90}
91
92impl std::fmt::Display for RaftRole {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        match self {
95            RaftRole::Follower => write!(f, "Follower"),
96            RaftRole::Candidate => write!(f, "Candidate"),
97            RaftRole::Leader => write!(f, "Leader"),
98        }
99    }
100}
101
102/// An entry in the Raft log.
103#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
104pub struct LogEntry {
105    /// The term when this entry was created.
106    pub term: RaftTerm,
107    /// Index of this entry in the log (1-based).
108    pub index: u64,
109    /// Serialized command (e.g., JSON-encoded operation).
110    pub command: String,
111}
112
113impl LogEntry {
114    /// Create a new log entry.
115    pub fn new(term: RaftTerm, index: u64, command: impl Into<String>) -> Self {
116        Self {
117            term,
118            index,
119            command: command.into(),
120        }
121    }
122}
123
124/// The Raft replicated log.
125#[derive(Debug, Default)]
126pub struct RaftLog {
127    /// All log entries (0-indexed internally, 1-indexed externally).
128    pub entries: Vec<LogEntry>,
129}
130
131impl RaftLog {
132    /// Create a new empty Raft log.
133    #[must_use]
134    pub fn new() -> Self {
135        Self {
136            entries: Vec::new(),
137        }
138    }
139
140    /// Append a new entry to the log.
141    pub fn append(&mut self, entry: LogEntry) {
142        self.entries.push(entry);
143    }
144
145    /// Get the entry at the given 1-based index.
146    #[must_use]
147    pub fn entry_at(&self, idx: u64) -> Option<&LogEntry> {
148        if idx == 0 {
149            return None;
150        }
151        self.entries.get((idx - 1) as usize)
152    }
153
154    /// Get the index of the last entry (0 if log is empty).
155    #[must_use]
156    pub fn last_index(&self) -> u64 {
157        self.entries.len() as u64
158    }
159
160    /// Get the term of the last entry (default term 0 if log is empty).
161    #[must_use]
162    pub fn last_term(&self) -> RaftTerm {
163        self.entries.last().map(|e| e.term).unwrap_or_default()
164    }
165}
166
167/// A request to vote for a candidate in a Raft election.
168#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
169pub struct VoteRequest {
170    /// The candidate's current term.
171    pub term: RaftTerm,
172    /// The candidate's node ID.
173    pub candidate_id: NodeId,
174    /// Index of the candidate's last log entry.
175    pub last_log_index: u64,
176    /// Term of the candidate's last log entry.
177    pub last_log_term: RaftTerm,
178}
179
180/// A response to a vote request.
181#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
182pub struct VoteResponse {
183    /// The current term, for the candidate to update itself.
184    pub term: RaftTerm,
185    /// True if the vote was granted.
186    pub vote_granted: bool,
187}
188
189/// A Raft consensus node.
190#[derive(Debug)]
191pub struct RaftNode {
192    /// This node's ID.
193    pub id: NodeId,
194    /// Current role.
195    pub role: RaftRole,
196    /// Latest term this node has seen.
197    pub current_term: RaftTerm,
198    /// `NodeId` this node voted for in the current term.
199    pub voted_for: Option<NodeId>,
200    /// The replicated log.
201    pub log: RaftLog,
202    /// Index of highest log entry known to be committed.
203    pub commit_index: u64,
204    /// Index of highest log entry applied to state machine.
205    pub last_applied: u64,
206}
207
208impl RaftNode {
209    /// Create a new Raft node in Follower state.
210    #[must_use]
211    pub fn new(id: NodeId) -> Self {
212        Self {
213            id,
214            role: RaftRole::Follower,
215            current_term: RaftTerm::default(),
216            voted_for: None,
217            log: RaftLog::new(),
218            commit_index: 0,
219            last_applied: 0,
220        }
221    }
222
223    /// Process an incoming vote request and return a response.
224    ///
225    /// Grant vote if:
226    /// 1. Candidate's term >= our current term.
227    /// 2. We haven't voted for anyone else this term.
228    /// 3. Candidate's log is at least as up-to-date as ours.
229    pub fn process_vote_request(&mut self, req: &VoteRequest) -> VoteResponse {
230        // If we see a higher term, update and step down
231        if req.term > self.current_term {
232            self.step_down(req.term);
233        }
234
235        if req.term < self.current_term {
236            return VoteResponse {
237                term: self.current_term,
238                vote_granted: false,
239            };
240        }
241
242        // Check if we can vote for this candidate
243        let already_voted_other = self.voted_for.is_some_and(|v| v != req.candidate_id);
244
245        if already_voted_other {
246            return VoteResponse {
247                term: self.current_term,
248                vote_granted: false,
249            };
250        }
251
252        // Check log up-to-date-ness
253        let our_last_term = self.log.last_term();
254        let our_last_index = self.log.last_index();
255
256        let log_ok = req.last_log_term > our_last_term
257            || (req.last_log_term == our_last_term && req.last_log_index >= our_last_index);
258
259        if log_ok {
260            self.voted_for = Some(req.candidate_id);
261            VoteResponse {
262                term: self.current_term,
263                vote_granted: true,
264            }
265        } else {
266            VoteResponse {
267                term: self.current_term,
268                vote_granted: false,
269            }
270        }
271    }
272
273    /// Transition this node to Candidate and start a new election.
274    pub fn become_candidate(&mut self) {
275        self.current_term = self.current_term.increment();
276        self.role = RaftRole::Candidate;
277        self.voted_for = Some(self.id); // Vote for self
278    }
279
280    /// Transition this node to Leader.
281    pub fn become_leader(&mut self) {
282        self.role = RaftRole::Leader;
283    }
284
285    /// Step down to Follower with a new term (e.g., after seeing higher term).
286    pub fn step_down(&mut self, new_term: RaftTerm) {
287        self.current_term = new_term;
288        self.role = RaftRole::Follower;
289        self.voted_for = None;
290    }
291}
292
293/// A timer that tracks election timeouts.
294#[derive(Debug, Clone)]
295pub struct ElectionTimer {
296    /// Timeout duration in milliseconds.
297    pub timeout_ms: u64,
298    /// Timestamp (ms) of the last reset.
299    pub last_reset_ms: u64,
300}
301
302impl ElectionTimer {
303    /// Create a new election timer.
304    #[must_use]
305    pub fn new(timeout_ms: u64, now_ms: u64) -> Self {
306        Self {
307            timeout_ms,
308            last_reset_ms: now_ms,
309        }
310    }
311
312    /// Returns true if the timer has expired at the given time.
313    #[must_use]
314    pub fn is_expired(&self, now_ms: u64) -> bool {
315        now_ms.saturating_sub(self.last_reset_ms) >= self.timeout_ms
316    }
317
318    /// Reset the timer to the current time.
319    pub fn reset(&mut self, now_ms: u64) {
320        self.last_reset_ms = now_ms;
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    fn make_node(id: u64) -> RaftNode {
329        RaftNode::new(NodeId::new(id))
330    }
331
332    #[test]
333    fn test_node_initial_state() {
334        let node = make_node(1);
335        assert_eq!(node.id, NodeId::new(1));
336        assert_eq!(node.role, RaftRole::Follower);
337        assert_eq!(node.current_term, RaftTerm::default());
338        assert!(node.voted_for.is_none());
339    }
340
341    #[test]
342    fn test_raft_role_can_vote() {
343        assert!(RaftRole::Follower.can_vote());
344        assert!(RaftRole::Candidate.can_vote());
345        assert!(!RaftRole::Leader.can_vote());
346    }
347
348    #[test]
349    fn test_raft_term_increment() {
350        let term = RaftTerm::new(5);
351        assert_eq!(term.increment().inner(), 6);
352    }
353
354    #[test]
355    fn test_raft_log_append_and_entry_at() {
356        let mut log = RaftLog::new();
357        assert_eq!(log.last_index(), 0);
358        assert_eq!(log.last_term(), RaftTerm::default());
359
360        log.append(LogEntry::new(RaftTerm::new(1), 1, "cmd1"));
361        log.append(LogEntry::new(RaftTerm::new(1), 2, "cmd2"));
362
363        assert_eq!(log.last_index(), 2);
364        assert_eq!(log.last_term(), RaftTerm::new(1));
365        assert!(log.entry_at(1).is_some());
366        assert_eq!(log.entry_at(1).expect("entry should exist").command, "cmd1");
367        assert!(log.entry_at(0).is_none());
368        assert!(log.entry_at(3).is_none());
369    }
370
371    #[test]
372    fn test_become_candidate() {
373        let mut node = make_node(1);
374        node.become_candidate();
375        assert_eq!(node.role, RaftRole::Candidate);
376        assert_eq!(node.current_term, RaftTerm::new(1));
377        assert_eq!(node.voted_for, Some(NodeId::new(1)));
378    }
379
380    #[test]
381    fn test_become_leader() {
382        let mut node = make_node(1);
383        node.become_candidate();
384        node.become_leader();
385        assert_eq!(node.role, RaftRole::Leader);
386    }
387
388    #[test]
389    fn test_step_down() {
390        let mut node = make_node(1);
391        node.become_candidate();
392        node.step_down(RaftTerm::new(5));
393        assert_eq!(node.role, RaftRole::Follower);
394        assert_eq!(node.current_term, RaftTerm::new(5));
395        assert!(node.voted_for.is_none());
396    }
397
398    #[test]
399    fn test_vote_request_grant() {
400        let mut node = make_node(2);
401        let req = VoteRequest {
402            term: RaftTerm::new(1),
403            candidate_id: NodeId::new(1),
404            last_log_index: 0,
405            last_log_term: RaftTerm::default(),
406        };
407        let resp = node.process_vote_request(&req);
408        assert!(resp.vote_granted);
409    }
410
411    #[test]
412    fn test_vote_request_deny_lower_term() {
413        let mut node = make_node(2);
414        node.step_down(RaftTerm::new(3));
415        let req = VoteRequest {
416            term: RaftTerm::new(2),
417            candidate_id: NodeId::new(1),
418            last_log_index: 0,
419            last_log_term: RaftTerm::default(),
420        };
421        let resp = node.process_vote_request(&req);
422        assert!(!resp.vote_granted);
423    }
424
425    #[test]
426    fn test_vote_request_deny_already_voted() {
427        let mut node = make_node(2);
428        let req1 = VoteRequest {
429            term: RaftTerm::new(1),
430            candidate_id: NodeId::new(1),
431            last_log_index: 0,
432            last_log_term: RaftTerm::default(),
433        };
434        let req2 = VoteRequest {
435            term: RaftTerm::new(1),
436            candidate_id: NodeId::new(3),
437            last_log_index: 0,
438            last_log_term: RaftTerm::default(),
439        };
440        node.process_vote_request(&req1);
441        let resp2 = node.process_vote_request(&req2);
442        assert!(!resp2.vote_granted);
443    }
444
445    #[test]
446    fn test_vote_deny_stale_log() {
447        let mut node = make_node(2);
448        // Node 2 has log entries at term 2
449        node.log.append(LogEntry::new(RaftTerm::new(2), 1, "x"));
450        let req = VoteRequest {
451            term: RaftTerm::new(3),
452            candidate_id: NodeId::new(1),
453            last_log_index: 0,
454            last_log_term: RaftTerm::new(1), // candidate's log is older
455        };
456        let resp = node.process_vote_request(&req);
457        assert!(!resp.vote_granted);
458    }
459
460    #[test]
461    fn test_election_timer_expired() {
462        let timer = ElectionTimer::new(150, 1000);
463        assert!(!timer.is_expired(1100));
464        assert!(timer.is_expired(1150));
465        assert!(timer.is_expired(1200));
466    }
467
468    #[test]
469    fn test_election_timer_reset() {
470        let mut timer = ElectionTimer::new(150, 1000);
471        timer.reset(1100);
472        assert!(!timer.is_expired(1200)); // only 100ms elapsed after reset
473        assert!(timer.is_expired(1250));
474    }
475
476    #[test]
477    fn test_node_id_display() {
478        let id = NodeId::new(42);
479        assert_eq!(format!("{}", id), "Node(42)");
480    }
481
482    #[test]
483    fn test_raft_term_ordering() {
484        assert!(RaftTerm::new(5) > RaftTerm::new(3));
485        assert!(RaftTerm::new(1) < RaftTerm::new(2));
486        assert_eq!(RaftTerm::new(4), RaftTerm::new(4));
487    }
488}