1use std::cmp::Ordering;
25use std::time::{SystemTime, UNIX_EPOCH};
26
27#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct Proposal {
34 pub node_name: String,
36 pub dtvlsn: u64,
42 pub vlsn: u64,
44 pub priority: u32,
46 pub term: u64,
48 pub timestamp_ms: u64,
50}
51
52impl Proposal {
53 pub fn new(node_name: String, vlsn: u64, priority: u32, term: u64) -> Self {
55 let timestamp_ms = SystemTime::now()
56 .duration_since(UNIX_EPOCH)
57 .unwrap_or_default()
58 .as_millis() as u64;
59
60 Self { node_name, dtvlsn: 0, vlsn, priority, term, timestamp_ms }
61 }
62
63 pub fn with_timestamp(
66 node_name: String,
67 vlsn: u64,
68 priority: u32,
69 term: u64,
70 timestamp_ms: u64,
71 ) -> Self {
72 Self { node_name, dtvlsn: 0, vlsn, priority, term, timestamp_ms }
73 }
74
75 pub fn is_better_than(&self, other: &Proposal) -> bool {
78 self.cmp(other) == Ordering::Greater
79 }
80
81 pub fn with_dtvlsn(mut self, dtvlsn: u64) -> Self {
85 self.dtvlsn = dtvlsn;
86 self
87 }
88}
89
90impl Ord for Proposal {
91 fn cmp(&self, other: &Self) -> Ordering {
92 self.dtvlsn
98 .cmp(&other.dtvlsn)
99 .then_with(|| self.vlsn.cmp(&other.vlsn))
101 .then_with(|| self.priority.cmp(&other.priority))
103 .then_with(|| self.term.cmp(&other.term))
105 .then_with(|| self.node_name.cmp(&other.node_name))
107 }
108}
109
110impl PartialOrd for Proposal {
111 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
112 Some(self.cmp(other))
113 }
114}
115
116impl std::fmt::Display for Proposal {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 write!(
119 f,
120 "Proposal(node={}, vlsn={}, priority={}, term={}, ts={})",
121 self.node_name,
122 self.vlsn,
123 self.priority,
124 self.term,
125 self.timestamp_ms
126 )
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn test_new_sets_timestamp() {
136 let p = Proposal::new("node1".into(), 100, 1, 1);
137 assert!(p.timestamp_ms > 0);
138 }
139
140 #[test]
141 fn test_with_timestamp() {
142 let p = Proposal::with_timestamp("n".into(), 1, 1, 1, 42);
143 assert_eq!(p.timestamp_ms, 42);
144 }
145
146 #[test]
149 fn test_higher_dtvlsn_wins_over_higher_vlsn() {
150 let durable = Proposal::with_timestamp("durable".into(), 100, 1, 1, 0)
153 .with_dtvlsn(90);
154 let laggard_tail =
155 Proposal::with_timestamp("laggard".into(), 200, 1, 1, 0)
156 .with_dtvlsn(50);
157 assert!(
158 durable.is_better_than(&laggard_tail),
159 "higher DTVLSN must win over higher raw VLSN"
160 );
161 assert!(!laggard_tail.is_better_than(&durable));
162 }
163
164 #[test]
165 fn test_dtvlsn_tie_falls_back_to_vlsn() {
166 let a =
169 Proposal::with_timestamp("a".into(), 200, 1, 1, 0).with_dtvlsn(0);
170 let b =
171 Proposal::with_timestamp("b".into(), 100, 1, 1, 0).with_dtvlsn(0);
172 assert!(a.is_better_than(&b), "dtvlsn tie -> higher vlsn wins");
173 let c =
175 Proposal::with_timestamp("c".into(), 200, 1, 1, 0).with_dtvlsn(50);
176 let d =
177 Proposal::with_timestamp("d".into(), 100, 1, 1, 0).with_dtvlsn(50);
178 assert!(c.is_better_than(&d));
179 }
180
181 #[test]
182 fn test_higher_vlsn_wins() {
183 let a = Proposal::with_timestamp("node1".into(), 200, 1, 1, 0);
184 let b = Proposal::with_timestamp("node2".into(), 100, 1, 1, 0);
185 assert!(a.is_better_than(&b));
186 assert!(!b.is_better_than(&a));
187 }
188
189 #[test]
190 fn test_higher_vlsn_wins_regardless_of_priority() {
191 let a = Proposal::with_timestamp("node1".into(), 200, 1, 1, 0);
192 let b = Proposal::with_timestamp("node2".into(), 100, 999, 1, 0);
193 assert!(a.is_better_than(&b));
194 }
195
196 #[test]
199 fn test_higher_priority_wins_same_vlsn() {
200 let a = Proposal::with_timestamp("node1".into(), 100, 10, 1, 0);
201 let b = Proposal::with_timestamp("node2".into(), 100, 5, 1, 0);
202 assert!(a.is_better_than(&b));
203 assert!(!b.is_better_than(&a));
204 }
205
206 #[test]
209 fn test_higher_term_wins_same_vlsn_priority() {
210 let a = Proposal::with_timestamp("node1".into(), 100, 5, 3, 0);
211 let b = Proposal::with_timestamp("node2".into(), 100, 5, 1, 0);
212 assert!(a.is_better_than(&b));
213 assert!(!b.is_better_than(&a));
214 }
215
216 #[test]
219 fn test_name_tiebreaker() {
220 let a = Proposal::with_timestamp("node_b".into(), 100, 5, 1, 0);
221 let b = Proposal::with_timestamp("node_a".into(), 100, 5, 1, 0);
222 assert!(a.is_better_than(&b));
224 assert!(!b.is_better_than(&a));
225 }
226
227 #[test]
228 fn test_equal_proposals() {
229 let a = Proposal::with_timestamp("node1".into(), 100, 5, 1, 0);
230 let b = Proposal::with_timestamp("node1".into(), 100, 5, 1, 0);
231 assert!(!a.is_better_than(&b));
232 assert!(!b.is_better_than(&a));
233 assert_eq!(a, b);
234 }
235
236 #[test]
239 fn test_sort_picks_best_proposal() {
240 let proposals = [
241 Proposal::with_timestamp("low".into(), 50, 1, 1, 0),
242 Proposal::with_timestamp("high_vlsn".into(), 200, 1, 1, 0),
243 Proposal::with_timestamp("high_prio".into(), 100, 99, 1, 0),
244 ];
245 let best = proposals.iter().max().unwrap();
246 assert_eq!(best.node_name, "high_vlsn");
247 }
248
249 #[test]
250 fn test_sort_tiebreaker_chain() {
251 let mut proposals = [
252 Proposal::with_timestamp("c".into(), 100, 5, 1, 0),
253 Proposal::with_timestamp("a".into(), 100, 5, 1, 0),
254 Proposal::with_timestamp("b".into(), 100, 5, 1, 0),
255 ];
256 proposals.sort();
257 assert_eq!(proposals[0].node_name, "a");
259 assert_eq!(proposals[1].node_name, "b");
260 assert_eq!(proposals[2].node_name, "c");
261 }
262
263 #[test]
264 fn test_display() {
265 let p = Proposal::with_timestamp("n1".into(), 42, 3, 7, 1000);
266 let s = format!("{}", p);
267 assert!(s.contains("n1"));
268 assert!(s.contains("42"));
269 assert!(s.contains("term=7"));
270 }
271
272 #[test]
273 fn test_is_better_than_symmetry() {
274 let a = Proposal::with_timestamp("x".into(), 10, 1, 1, 0);
275 let b = Proposal::with_timestamp("y".into(), 20, 1, 1, 0);
276 assert!(b.is_better_than(&a));
278 assert!(!a.is_better_than(&b));
279 }
280
281 #[test]
282 fn test_zero_priority_loses() {
283 let zero = Proposal::with_timestamp("node1".into(), 100, 0, 1, 0);
284 let one = Proposal::with_timestamp("node2".into(), 100, 1, 1, 0);
285 assert!(one.is_better_than(&zero));
286 }
287}