oximedia_distributed/
replication.rs1use std::collections::VecDeque;
7
8#[allow(dead_code)]
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct LogEntry {
12 pub term: u64,
14 pub index: u64,
16 pub data: Vec<u8>,
18 pub command: String,
20}
21
22impl LogEntry {
23 #[allow(dead_code)]
25 pub fn new(term: u64, index: u64, command: impl Into<String>, data: Vec<u8>) -> Self {
26 Self {
27 term,
28 index,
29 data,
30 command: command.into(),
31 }
32 }
33}
34
35#[allow(dead_code)]
37pub struct ReplicatedLog {
38 entries: VecDeque<LogEntry>,
39 max_size: usize,
41 commit_index: u64,
43 last_applied: u64,
45}
46
47impl ReplicatedLog {
48 #[allow(dead_code)]
50 #[must_use]
51 pub fn new(max_size: usize) -> Self {
52 Self {
53 entries: VecDeque::new(),
54 max_size,
55 commit_index: 0,
56 last_applied: 0,
57 }
58 }
59
60 #[allow(dead_code)]
64 pub fn append(&mut self, term: u64, command: impl Into<String>, data: Vec<u8>) -> u64 {
65 let index = self.last_index() + 1;
66 let entry = LogEntry::new(term, index, command, data);
67 self.entries.push_back(entry);
68 if self.entries.len() > self.max_size {
69 self.entries.pop_front();
70 }
71 index
72 }
73
74 #[allow(dead_code)]
76 #[must_use]
77 pub fn last_index(&self) -> u64 {
78 self.entries.back().map_or(0, |e| e.index)
79 }
80
81 #[allow(dead_code)]
83 #[must_use]
84 pub fn last_term(&self) -> u64 {
85 self.entries.back().map_or(0, |e| e.term)
86 }
87
88 #[allow(dead_code)]
90 pub fn commit_up_to(&mut self, index: u64) {
91 if index > self.commit_index {
92 self.commit_index = index.min(self.last_index());
93 }
94 }
95
96 #[allow(dead_code)]
98 pub fn apply_committed(&mut self) -> Vec<LogEntry> {
99 let mut applied = Vec::new();
100 while self.last_applied < self.commit_index {
101 self.last_applied += 1;
102 if let Some(entry) = self.get(self.last_applied) {
103 applied.push(entry.clone());
104 }
105 }
106 applied
107 }
108
109 #[allow(dead_code)]
111 #[must_use]
112 pub fn get(&self, index: u64) -> Option<&LogEntry> {
113 self.entries.iter().find(|e| e.index == index)
114 }
115
116 #[allow(dead_code)]
118 #[must_use]
119 pub fn commit_index(&self) -> u64 {
120 self.commit_index
121 }
122
123 #[allow(dead_code)]
125 #[must_use]
126 pub fn len(&self) -> usize {
127 self.entries.len()
128 }
129
130 #[allow(dead_code)]
132 #[must_use]
133 pub fn is_empty(&self) -> bool {
134 self.entries.is_empty()
135 }
136}
137
138#[allow(dead_code)]
140pub struct TermTracker {
141 current_term: u64,
142 voted_for: Option<String>,
143}
144
145impl TermTracker {
146 #[allow(dead_code)]
148 #[must_use]
149 pub fn new() -> Self {
150 Self {
151 current_term: 0,
152 voted_for: None,
153 }
154 }
155
156 #[allow(dead_code)]
158 #[must_use]
159 pub fn current_term(&self) -> u64 {
160 self.current_term
161 }
162
163 #[allow(dead_code)]
167 pub fn advance_term(&mut self, new_term: u64) -> bool {
168 if new_term > self.current_term {
169 self.current_term = new_term;
170 self.voted_for = None;
171 true
172 } else {
173 false
174 }
175 }
176
177 #[allow(dead_code)]
181 pub fn grant_vote(&mut self, candidate_id: impl Into<String>) -> bool {
182 if self.voted_for.is_none() {
183 self.voted_for = Some(candidate_id.into());
184 true
185 } else {
186 false
187 }
188 }
189
190 #[allow(dead_code)]
192 #[must_use]
193 pub fn voted_for(&self) -> Option<&str> {
194 self.voted_for.as_deref()
195 }
196}
197
198impl Default for TermTracker {
199 fn default() -> Self {
200 Self::new()
201 }
202}
203
204#[allow(dead_code)]
206pub struct QuorumHelper;
207
208impl QuorumHelper {
209 #[allow(dead_code)]
211 #[must_use]
212 pub fn majority(cluster_size: usize) -> usize {
213 cluster_size / 2 + 1
214 }
215
216 #[allow(dead_code)]
218 #[must_use]
219 pub fn has_quorum(votes: usize, cluster_size: usize) -> bool {
220 votes >= Self::majority(cluster_size)
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn test_log_entry_new() {
230 let e = LogEntry::new(1, 1, "set", b"data".to_vec());
231 assert_eq!(e.term, 1);
232 assert_eq!(e.index, 1);
233 assert_eq!(e.command, "set");
234 }
235
236 #[test]
237 fn test_replicated_log_append_increments_index() {
238 let mut log = ReplicatedLog::new(100);
239 let i1 = log.append(1, "cmd1", vec![]);
240 let i2 = log.append(1, "cmd2", vec![]);
241 assert_eq!(i1, 1);
242 assert_eq!(i2, 2);
243 }
244
245 #[test]
246 fn test_replicated_log_last_index_and_term() {
247 let mut log = ReplicatedLog::new(100);
248 assert_eq!(log.last_index(), 0);
249 assert_eq!(log.last_term(), 0);
250 log.append(2, "cmd", vec![]);
251 assert_eq!(log.last_index(), 1);
252 assert_eq!(log.last_term(), 2);
253 }
254
255 #[test]
256 fn test_replicated_log_compaction() {
257 let mut log = ReplicatedLog::new(3);
258 for _ in 0..5 {
259 log.append(1, "x", vec![]);
260 }
261 assert_eq!(log.len(), 3);
262 }
263
264 #[test]
265 fn test_replicated_log_get_entry() {
266 let mut log = ReplicatedLog::new(10);
267 log.append(1, "cmd", b"hello".to_vec());
268 let entry = log.get(1).expect("get should return a value");
269 assert_eq!(entry.data, b"hello");
270 }
271
272 #[test]
273 fn test_replicated_log_get_missing() {
274 let log = ReplicatedLog::new(10);
275 assert!(log.get(99).is_none());
276 }
277
278 #[test]
279 fn test_commit_and_apply() {
280 let mut log = ReplicatedLog::new(100);
281 log.append(1, "a", vec![]);
282 log.append(1, "b", vec![]);
283 log.append(1, "c", vec![]);
284 log.commit_up_to(2);
285 let applied = log.apply_committed();
286 assert_eq!(applied.len(), 2);
287 assert_eq!(applied[0].command, "a");
288 assert_eq!(applied[1].command, "b");
289 }
290
291 #[test]
292 fn test_commit_up_to_capped_at_last_index() {
293 let mut log = ReplicatedLog::new(100);
294 log.append(1, "x", vec![]);
295 log.commit_up_to(999);
296 assert_eq!(log.commit_index(), 1);
297 }
298
299 #[test]
300 fn test_term_tracker_initial_state() {
301 let t = TermTracker::new();
302 assert_eq!(t.current_term(), 0);
303 assert!(t.voted_for().is_none());
304 }
305
306 #[test]
307 fn test_term_tracker_advance_term() {
308 let mut t = TermTracker::new();
309 assert!(t.advance_term(3));
310 assert_eq!(t.current_term(), 3);
311 assert!(!t.advance_term(2)); }
313
314 #[test]
315 fn test_term_tracker_grant_vote_once() {
316 let mut t = TermTracker::new();
317 t.advance_term(1);
318 assert!(t.grant_vote("node-a"));
319 assert!(!t.grant_vote("node-b")); assert_eq!(t.voted_for(), Some("node-a"));
321 }
322
323 #[test]
324 fn test_term_tracker_vote_cleared_on_term_advance() {
325 let mut t = TermTracker::new();
326 t.advance_term(1);
327 t.grant_vote("node-a");
328 t.advance_term(2);
329 assert!(t.voted_for().is_none());
330 }
331
332 #[test]
333 fn test_quorum_majority_odd_cluster() {
334 assert_eq!(QuorumHelper::majority(5), 3);
335 assert_eq!(QuorumHelper::majority(3), 2);
336 }
337
338 #[test]
339 fn test_quorum_majority_even_cluster() {
340 assert_eq!(QuorumHelper::majority(4), 3);
341 }
342
343 #[test]
344 fn test_has_quorum() {
345 assert!(QuorumHelper::has_quorum(3, 5));
346 assert!(!QuorumHelper::has_quorum(2, 5));
347 assert!(QuorumHelper::has_quorum(2, 3));
348 }
349}