use std::collections::{HashMap, HashSet};
use crate::adapter::net::behavior::fold::{IslandId, NodeId};
pub type Epoch = u64;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReplicaSet {
members: Vec<NodeId>,
}
impl ReplicaSet {
pub fn new(members: impl IntoIterator<Item = NodeId>) -> Self {
let mut members: Vec<NodeId> = members.into_iter().collect();
members.sort_unstable();
members.dedup();
Self { members }
}
pub fn len(&self) -> usize {
self.members.len()
}
pub fn is_empty(&self) -> bool {
self.members.is_empty()
}
pub fn members(&self) -> &[NodeId] {
&self.members
}
pub fn contains(&self, node: NodeId) -> bool {
self.members.binary_search(&node).is_ok()
}
pub fn quorum_threshold(&self) -> usize {
self.members.len() / 2 + 1
}
pub fn has_quorum(&self, ack_count: usize) -> bool {
!self.members.is_empty() && ack_count >= self.quorum_threshold()
}
}
#[derive(Debug, Clone)]
pub struct QuorumWitness<'a> {
set: &'a ReplicaSet,
acked: HashSet<NodeId>,
}
impl<'a> QuorumWitness<'a> {
pub fn new(set: &'a ReplicaSet) -> Self {
Self {
set,
acked: HashSet::new(),
}
}
pub fn record_ack(&mut self, node: NodeId) -> bool {
if !self.set.contains(node) {
return false;
}
self.acked.insert(node)
}
pub fn ack_count(&self) -> usize {
self.acked.len()
}
pub fn has_quorum(&self) -> bool {
self.set.has_quorum(self.acked.len())
}
}
#[derive(Debug, Clone, Default)]
pub struct FenceLedger {
highest: HashMap<IslandId, Epoch>,
}
impl FenceLedger {
pub fn new() -> Self {
Self::default()
}
pub fn highest_witnessed(&self, island: IslandId) -> Epoch {
self.highest.get(&island).copied().unwrap_or(0)
}
pub fn accepts(&self, island: IslandId, epoch: Epoch) -> bool {
epoch >= self.highest_witnessed(island)
}
pub fn witness(&mut self, island: IslandId, epoch: Epoch) {
let slot = self.highest.entry(island).or_insert(0);
if epoch > *slot {
*slot = epoch;
}
}
pub fn accept_active(&mut self, island: IslandId, epoch: Epoch) -> bool {
if !self.accepts(island, epoch) {
return false;
}
self.witness(island, epoch);
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strict_majority_threshold() {
assert_eq!(ReplicaSet::new([1, 2, 3]).quorum_threshold(), 2);
assert_eq!(ReplicaSet::new([1, 2, 3, 4, 5]).quorum_threshold(), 3);
assert_eq!(ReplicaSet::new([1]).quorum_threshold(), 1);
assert_eq!(ReplicaSet::new([1, 2, 3, 4]).quorum_threshold(), 3);
}
#[test]
fn has_quorum_is_strict_majority() {
let set = ReplicaSet::new([1, 2, 3]);
assert!(!set.has_quorum(1));
assert!(set.has_quorum(2));
assert!(set.has_quorum(3));
assert!(!ReplicaSet::new([]).has_quorum(0));
assert!(!ReplicaSet::new([]).has_quorum(5));
}
#[test]
fn witness_counts_only_distinct_members() {
let set = ReplicaSet::new([1, 2, 3]);
let mut w = QuorumWitness::new(&set);
assert!(w.record_ack(1));
assert!(!w.record_ack(1), "duplicate ack does not count");
assert!(!w.record_ack(99), "non-member ack does not count");
assert_eq!(w.ack_count(), 1);
assert!(!w.has_quorum());
assert!(w.record_ack(2));
assert!(w.has_quorum(), "2/3 distinct members is a majority");
}
#[test]
fn minority_side_of_a_split_never_reaches_quorum() {
let set = ReplicaSet::new([1, 2, 3, 4, 5]);
let mut minority = QuorumWitness::new(&set);
minority.record_ack(1);
minority.record_ack(2);
assert_eq!(minority.ack_count(), 2);
assert!(
!minority.has_quorum(),
"minority side must NOT be able to commit Active",
);
let mut majority = QuorumWitness::new(&set);
majority.record_ack(3);
majority.record_ack(4);
majority.record_ack(5);
assert!(majority.has_quorum(), "majority side commits");
}
#[test]
fn fence_rejects_a_stale_ex_leaders_active() {
let mut ledger = FenceLedger::new();
let island: IslandId = 0xA0;
assert!(ledger.accept_active(island, 5), "current term accepted");
assert_eq!(ledger.highest_witnessed(island), 5);
assert!(
!ledger.accept_active(island, 4),
"stale ex-leader (lower epoch) must be fenced",
);
assert_eq!(ledger.highest_witnessed(island), 5, "fence unchanged");
assert!(
ledger.accept_active(island, 5),
"an equal-epoch retry from the live leader still passes",
);
assert!(
ledger.accept_active(island, 6),
"a newer term advances the fence"
);
assert_eq!(ledger.highest_witnessed(island), 6);
assert!(!ledger.accept_active(island, 5), "now epoch 5 is stale too");
}
#[test]
fn fence_is_per_island() {
let mut ledger = FenceLedger::new();
ledger.accept_active(0xA0, 7);
assert!(ledger.accept_active(0xB0, 1));
assert!(
!ledger.accept_active(0xA0, 6),
"island A0 still fenced at 7"
);
}
#[test]
fn leadership_change_minority_leader_cannot_commit() {
let set = ReplicaSet::new([1, 2, 3, 4, 5]);
let island: IslandId = 0xC0;
let mut ledger = FenceLedger::new();
let mut l2 = QuorumWitness::new(&set);
for r in [3, 4, 5] {
l2.record_ack(r);
}
assert!(l2.has_quorum());
assert!(ledger.accept_active(island, 6), "new leader commits");
let mut l1 = QuorumWitness::new(&set);
for r in [1, 2] {
l1.record_ack(r);
}
assert!(!l1.has_quorum(), "minority leader can't reach quorum");
assert!(!ledger.accepts(island, 5), "and the fence rejects epoch 5");
}
}