1#![allow(dead_code)]
2use std::collections::HashMap;
9use std::time::{Duration, Instant};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ElectionState {
14 Idle,
16 Candidate,
18 Follower,
20 Leader,
22}
23
24impl ElectionState {
25 #[must_use]
27 pub fn is_candidate(self) -> bool {
28 self == ElectionState::Candidate
29 }
30
31 #[must_use]
33 pub fn is_leader(self) -> bool {
34 self == ElectionState::Leader
35 }
36
37 #[must_use]
39 pub fn label(self) -> &'static str {
40 match self {
41 ElectionState::Idle => "idle",
42 ElectionState::Candidate => "candidate",
43 ElectionState::Follower => "follower",
44 ElectionState::Leader => "leader",
45 }
46 }
47}
48
49#[derive(Debug, Clone)]
51pub struct NodeVote {
52 pub voter_id: String,
54 pub candidate_id: String,
56 pub term: u64,
58 pub cast_at: Instant,
60 pub reason: Option<String>,
62}
63
64impl NodeVote {
65 #[must_use]
67 pub fn new(voter_id: impl Into<String>, candidate_id: impl Into<String>, term: u64) -> Self {
68 Self {
69 voter_id: voter_id.into(),
70 candidate_id: candidate_id.into(),
71 term,
72 cast_at: Instant::now(),
73 reason: None,
74 }
75 }
76
77 #[must_use]
80 pub fn is_valid(&self, expected_term: u64) -> bool {
81 self.term == expected_term
82 && !self.voter_id.is_empty()
83 && !self.candidate_id.is_empty()
84 && self.voter_id != self.candidate_id
85 }
86
87 #[must_use]
89 pub fn age_ms(&self, now: Instant) -> u64 {
90 now.saturating_duration_since(self.cast_at).as_millis() as u64
91 }
92}
93
94#[derive(Debug)]
96pub struct ElectionManager {
97 pub node_id: String,
99 pub term: u64,
101 pub state: ElectionState,
103 votes: HashMap<String, NodeVote>,
105 cluster_size: usize,
107 election_started_at: Option<Instant>,
109 election_timeout: Duration,
111}
112
113impl ElectionManager {
114 #[must_use]
116 pub fn new(
117 node_id: impl Into<String>,
118 cluster_size: usize,
119 election_timeout: Duration,
120 ) -> Self {
121 Self {
122 node_id: node_id.into(),
123 term: 0,
124 state: ElectionState::Idle,
125 votes: HashMap::new(),
126 cluster_size,
127 election_started_at: None,
128 election_timeout,
129 }
130 }
131
132 pub fn start_election(&mut self) {
134 self.term += 1;
135 self.state = ElectionState::Candidate;
136 self.votes.clear();
137 self.election_started_at = Some(Instant::now());
138 }
139
140 pub fn record_vote(&mut self, vote: NodeVote) -> bool {
145 if !vote.is_valid(self.term) {
146 return false;
147 }
148 if self.votes.contains_key(&vote.voter_id) {
150 return false;
151 }
152 self.votes.insert(vote.voter_id.clone(), vote);
153 self.update_state();
154 true
155 }
156
157 #[must_use]
159 pub fn winner(&self) -> Option<&str> {
160 let mut tally: HashMap<&str, usize> = HashMap::new();
161 for vote in self.votes.values() {
162 *tally.entry(vote.candidate_id.as_str()).or_insert(0) += 1;
163 }
164 tally
165 .into_iter()
166 .max_by_key(|(_, count)| *count)
167 .map(|(id, _)| id)
168 }
169
170 #[must_use]
172 pub fn quorum(&self) -> usize {
173 self.cluster_size / 2 + 1
174 }
175
176 #[must_use]
178 pub fn vote_count(&self) -> usize {
179 self.votes.len()
180 }
181
182 #[must_use]
184 pub fn is_timed_out(&self, now: Instant) -> bool {
185 match self.election_started_at {
186 None => false,
187 Some(started) => now.saturating_duration_since(started) >= self.election_timeout,
188 }
189 }
190
191 pub fn become_follower(&mut self) {
193 self.state = ElectionState::Follower;
194 self.votes.clear();
195 self.election_started_at = None;
196 }
197
198 pub fn become_leader(&mut self) {
200 self.state = ElectionState::Leader;
201 }
202
203 fn update_state(&mut self) {
204 let my_id = self.node_id.clone();
206 let my_votes = self
207 .votes
208 .values()
209 .filter(|v| v.candidate_id == my_id)
210 .count();
211 if my_votes >= self.quorum() {
212 self.state = ElectionState::Leader;
213 }
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use std::time::Duration;
221
222 fn make_manager(node_id: &str, cluster_size: usize) -> ElectionManager {
223 ElectionManager::new(node_id, cluster_size, Duration::from_secs(5))
224 }
225
226 fn cast_vote(manager: &mut ElectionManager, voter: &str, candidate: &str) -> bool {
227 let vote = NodeVote::new(voter, candidate, manager.term);
228 manager.record_vote(vote)
229 }
230
231 #[test]
232 fn test_election_state_labels() {
233 assert_eq!(ElectionState::Idle.label(), "idle");
234 assert_eq!(ElectionState::Candidate.label(), "candidate");
235 assert_eq!(ElectionState::Follower.label(), "follower");
236 assert_eq!(ElectionState::Leader.label(), "leader");
237 }
238
239 #[test]
240 fn test_is_candidate() {
241 assert!(ElectionState::Candidate.is_candidate());
242 assert!(!ElectionState::Leader.is_candidate());
243 assert!(!ElectionState::Idle.is_candidate());
244 }
245
246 #[test]
247 fn test_is_leader() {
248 assert!(ElectionState::Leader.is_leader());
249 assert!(!ElectionState::Candidate.is_leader());
250 }
251
252 #[test]
253 fn test_node_vote_is_valid() {
254 let vote = NodeVote::new("voter1", "node2", 3);
255 assert!(vote.is_valid(3));
256 assert!(!vote.is_valid(2)); }
258
259 #[test]
260 fn test_node_vote_invalid_self_vote() {
261 let vote = NodeVote::new("node1", "node1", 1);
262 assert!(!vote.is_valid(1)); }
264
265 #[test]
266 fn test_node_vote_invalid_empty_ids() {
267 let vote = NodeVote::new("", "node2", 1);
268 assert!(!vote.is_valid(1));
269 }
270
271 #[test]
272 fn test_start_election_increments_term() {
273 let mut mgr = make_manager("n1", 5);
274 assert_eq!(mgr.term, 0);
275 mgr.start_election();
276 assert_eq!(mgr.term, 1);
277 assert!(mgr.state.is_candidate());
278 }
279
280 #[test]
281 fn test_start_election_clears_votes() {
282 let mut mgr = make_manager("n1", 3);
283 mgr.start_election();
284 cast_vote(&mut mgr, "n2", "n1");
285 mgr.start_election(); assert_eq!(mgr.vote_count(), 0);
287 }
288
289 #[test]
290 fn test_record_vote_accepted() {
291 let mut mgr = make_manager("n1", 3);
292 mgr.start_election();
293 assert!(cast_vote(&mut mgr, "n2", "n1"));
294 assert_eq!(mgr.vote_count(), 1);
295 }
296
297 #[test]
298 fn test_record_vote_duplicate_rejected() {
299 let mut mgr = make_manager("n1", 5);
300 mgr.start_election();
301 assert!(cast_vote(&mut mgr, "n2", "n1"));
302 assert!(!cast_vote(&mut mgr, "n2", "n1")); assert_eq!(mgr.vote_count(), 1);
304 }
305
306 #[test]
307 fn test_winner_after_majority() {
308 let mut mgr = make_manager("n1", 3);
309 mgr.start_election();
310 cast_vote(&mut mgr, "n2", "n1");
311 cast_vote(&mut mgr, "n3", "n1");
312 assert_eq!(mgr.winner(), Some("n1"));
313 assert!(mgr.state.is_leader());
314 }
315
316 #[test]
317 fn test_quorum_calculation() {
318 let mgr3 = make_manager("n1", 3);
319 assert_eq!(mgr3.quorum(), 2);
320 let mgr5 = make_manager("n1", 5);
321 assert_eq!(mgr5.quorum(), 3);
322 }
323
324 #[test]
325 fn test_become_follower() {
326 let mut mgr = make_manager("n1", 3);
327 mgr.start_election();
328 mgr.become_follower();
329 assert_eq!(mgr.state, ElectionState::Follower);
330 assert_eq!(mgr.vote_count(), 0);
331 }
332
333 #[test]
334 fn test_is_timed_out() {
335 let mut mgr = ElectionManager::new("n1", 3, Duration::from_millis(1));
336 mgr.start_election();
337 std::thread::sleep(Duration::from_millis(5));
338 assert!(mgr.is_timed_out(Instant::now()));
339 }
340
341 #[test]
342 fn test_not_timed_out_before_deadline() {
343 let mut mgr = ElectionManager::new("n1", 3, Duration::from_secs(60));
344 mgr.start_election();
345 assert!(!mgr.is_timed_out(Instant::now()));
346 }
347}