use std::sync::atomic::{AtomicU64, Ordering};
pub fn elect_leader(live_node_ids: &[String]) -> Option<&String> {
live_node_ids.iter().min()
}
#[derive(Debug, Default)]
pub struct EpochFencer {
highest: AtomicU64,
}
impl EpochFencer {
pub fn new() -> Self {
Self::default()
}
pub fn check_and_advance(&self, epoch: u64) -> bool {
let mut cur = self.highest.load(Ordering::Acquire);
loop {
if epoch < cur {
return false;
}
if epoch == cur {
return true;
}
match self.highest.compare_exchange(
cur,
epoch,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => return true,
Err(actual) => cur = actual,
}
}
}
pub fn highest(&self) -> u64 {
self.highest.load(Ordering::Acquire)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ids(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
#[test]
fn elects_lowest_live_node_id() {
assert_eq!(
elect_leader(&ids(&["node-c", "node-a", "node-b"])),
Some(&"node-a".to_string())
);
}
#[test]
fn no_leader_without_live_members() {
assert_eq!(elect_leader(&[]), None);
}
#[test]
fn failover_picks_next_node_when_leader_leaves() {
let full = ids(&["node-a", "node-b", "node-c"]);
assert_eq!(elect_leader(&full), Some(&"node-a".to_string()));
let after = ids(&["node-b", "node-c"]);
assert_eq!(elect_leader(&after), Some(&"node-b".to_string()));
}
#[test]
fn fencer_accepts_equal_and_higher_rejects_lower() {
let f = EpochFencer::new();
assert!(f.check_and_advance(5), "first record sets the gate");
assert!(f.check_and_advance(5), "same epoch (same leader) is accepted");
assert!(f.check_and_advance(7), "newer epoch advances the gate");
assert_eq!(f.highest(), 7);
assert!(
!f.check_and_advance(6),
"a deposed leader's older epoch is fenced out"
);
assert!(!f.check_and_advance(5));
assert_eq!(f.highest(), 7, "rejected records don't move the gate");
}
}