amaters_cluster/
types.rs

1//! Core types for Raft consensus
2
3use std::time::Duration;
4
5/// Node identifier
6pub type NodeId = u64;
7
8/// Raft term number
9pub type Term = u64;
10
11/// Log entry index (1-indexed, 0 means no entry)
12pub type LogIndex = u64;
13
14/// Raft node state
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum NodeState {
17    /// Follower state - passive, responds to RPCs
18    Follower,
19    /// Candidate state - requesting votes for leadership
20    Candidate,
21    /// Leader state - handles client requests and replicates log
22    Leader,
23}
24
25impl NodeState {
26    /// Get the state name as a string
27    pub fn as_str(&self) -> &'static str {
28        match self {
29            NodeState::Follower => "Follower",
30            NodeState::Candidate => "Candidate",
31            NodeState::Leader => "Leader",
32        }
33    }
34}
35
36/// Configuration for a Raft node
37#[derive(Debug, Clone)]
38pub struct RaftConfig {
39    /// This node's ID
40    pub node_id: NodeId,
41    /// List of all peer node IDs (including this node)
42    pub peers: Vec<NodeId>,
43    /// Election timeout range (min, max) in milliseconds
44    pub election_timeout_range: (u64, u64),
45    /// Heartbeat interval in milliseconds
46    pub heartbeat_interval: u64,
47    /// Maximum number of entries to send in a single AppendEntries RPC
48    pub max_entries_per_message: usize,
49    /// Whether to enable log compaction
50    pub enable_compaction: bool,
51    /// Snapshot threshold (number of log entries before triggering snapshot)
52    pub snapshot_threshold: usize,
53}
54
55impl RaftConfig {
56    /// Create a new Raft configuration with sensible defaults
57    pub fn new(node_id: NodeId, peers: Vec<NodeId>) -> Self {
58        Self {
59            node_id,
60            peers,
61            election_timeout_range: (150, 300),
62            heartbeat_interval: 50,
63            max_entries_per_message: 100,
64            enable_compaction: true,
65            snapshot_threshold: 10000,
66        }
67    }
68
69    /// Get a random election timeout within the configured range
70    pub fn random_election_timeout(&self) -> Duration {
71        use std::collections::hash_map::RandomState;
72        use std::hash::BuildHasher;
73
74        let (min, max) = self.election_timeout_range;
75        let range = max - min;
76
77        // Use current time as seed for randomization
78        let now = std::time::SystemTime::now()
79            .duration_since(std::time::UNIX_EPOCH)
80            .map(|d| d.as_nanos())
81            .unwrap_or(0);
82
83        let random_value = RandomState::new().hash_one(now);
84
85        let timeout_ms = min + (random_value % range);
86        Duration::from_millis(timeout_ms)
87    }
88
89    /// Get the heartbeat interval
90    pub fn heartbeat_interval(&self) -> Duration {
91        Duration::from_millis(self.heartbeat_interval)
92    }
93
94    /// Validate the configuration
95    pub fn validate(&self) -> Result<(), String> {
96        // Check that node_id is in peers list
97        if !self.peers.contains(&self.node_id) {
98            return Err(format!("Node ID {} not found in peers list", self.node_id));
99        }
100
101        // Check for odd number of nodes (for quorum)
102        if self.peers.len() % 2 == 0 {
103            return Err(format!(
104                "Raft requires odd number of nodes, got {}",
105                self.peers.len()
106            ));
107        }
108
109        // Check minimum nodes
110        if self.peers.len() < 3 {
111            return Err(format!(
112                "Raft requires at least 3 nodes for fault tolerance, got {}",
113                self.peers.len()
114            ));
115        }
116
117        // Check election timeout range
118        let (min, max) = self.election_timeout_range;
119        if min >= max {
120            return Err(format!(
121                "Election timeout min ({}) must be less than max ({})",
122                min, max
123            ));
124        }
125
126        // Check heartbeat interval vs election timeout
127        if self.heartbeat_interval >= min {
128            return Err(format!(
129                "Heartbeat interval ({}) must be less than election timeout min ({})",
130                self.heartbeat_interval, min
131            ));
132        }
133
134        Ok(())
135    }
136
137    /// Calculate the quorum size (majority)
138    pub fn quorum_size(&self) -> usize {
139        self.peers.len() / 2 + 1
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_node_state_as_str() {
149        assert_eq!(NodeState::Follower.as_str(), "Follower");
150        assert_eq!(NodeState::Candidate.as_str(), "Candidate");
151        assert_eq!(NodeState::Leader.as_str(), "Leader");
152    }
153
154    #[test]
155    fn test_raft_config_new() {
156        let config = RaftConfig::new(1, vec![1, 2, 3]);
157        assert_eq!(config.node_id, 1);
158        assert_eq!(config.peers, vec![1, 2, 3]);
159        assert_eq!(config.election_timeout_range, (150, 300));
160        assert_eq!(config.heartbeat_interval, 50);
161    }
162
163    #[test]
164    fn test_raft_config_validate_valid() {
165        let config = RaftConfig::new(1, vec![1, 2, 3]);
166        assert!(config.validate().is_ok());
167    }
168
169    #[test]
170    fn test_raft_config_validate_node_not_in_peers() {
171        let config = RaftConfig::new(4, vec![1, 2, 3]);
172        assert!(config.validate().is_err());
173    }
174
175    #[test]
176    fn test_raft_config_validate_even_number_of_nodes() {
177        let config = RaftConfig::new(1, vec![1, 2, 3, 4]);
178        assert!(config.validate().is_err());
179    }
180
181    #[test]
182    fn test_raft_config_validate_too_few_nodes() {
183        let config = RaftConfig::new(1, vec![1]);
184        assert!(config.validate().is_err());
185    }
186
187    #[test]
188    fn test_raft_config_quorum_size() {
189        let config = RaftConfig::new(1, vec![1, 2, 3]);
190        assert_eq!(config.quorum_size(), 2);
191
192        let config = RaftConfig::new(1, vec![1, 2, 3, 4, 5]);
193        assert_eq!(config.quorum_size(), 3);
194    }
195
196    #[test]
197    fn test_random_election_timeout() {
198        let config = RaftConfig::new(1, vec![1, 2, 3]);
199        let timeout1 = config.random_election_timeout();
200        let timeout2 = config.random_election_timeout();
201
202        // Both should be within range
203        assert!(timeout1.as_millis() >= 150);
204        assert!(timeout1.as_millis() <= 300);
205        assert!(timeout2.as_millis() >= 150);
206        assert!(timeout2.as_millis() <= 300);
207    }
208}