use hashbrown::HashSet;
use quoracle::{Expr, Node, QuorumSystem, choose, majority};
#[derive(Debug, Clone, Default)]
pub enum QuorumPolicy {
#[default]
SimpleMajority,
Flexible { phase1: usize, phase2: usize },
Expression(QuorumSystem<String>),
}
impl QuorumPolicy {
pub fn phase1_quorum(&self, electable: usize) -> usize {
match self {
Self::SimpleMajority => majority_size(electable),
Self::Flexible { phase1, .. } => *phase1,
Self::Expression(qs) => {
qs.read_quorums().map(|q| q.len()).min().unwrap_or(electable)
}
}
}
pub fn phase2_quorum(&self, electable: usize) -> usize {
match self {
Self::SimpleMajority => majority_size(electable),
Self::Flexible { phase2, .. } => *phase2,
Self::Expression(qs) => {
qs.write_quorums().map(|q| q.len()).min().unwrap_or(electable)
}
}
}
pub fn is_valid_phase2_quorum(
&self,
voters: &HashSet<&str>,
electable: usize,
) -> bool {
voters.len() >= self.phase2_quorum(electable)
}
pub fn validate(&self, n: usize) -> Result<(), String> {
match self {
Self::SimpleMajority => Ok(()),
Self::Flexible { phase1, phase2 } => {
if phase1 + phase2 > n {
Ok(())
} else {
Err(format!(
"Flexible quorum unsafe: phase1({phase1}) + phase2({phase2}) \
= {} which is NOT > n={n}. \
Safety requires phase1 + phase2 > n.",
phase1 + phase2
))
}
}
Self::Expression(_) => Ok(()),
}
}
pub fn build_expression(
node_names: &[String],
phase1_k: usize,
phase2_k: usize,
) -> Result<Self, quoracle::Error> {
let nodes: Vec<Expr<String>> = node_names
.iter()
.map(|n| Expr::Node(Node::new(n.clone())))
.collect();
let reads = choose(phase1_k, nodes.clone())?;
let writes = choose(phase2_k, nodes)?;
let qs = QuorumSystem::new(reads, writes)?;
Ok(Self::Expression(qs))
}
pub fn build_majority_expression(
node_names: &[String],
) -> Result<Self, quoracle::Error> {
let nodes: Vec<Expr<String>> = node_names
.iter()
.map(|n| Expr::Node(Node::new(n.clone())))
.collect();
let reads = majority(nodes)?;
let qs = QuorumSystem::from_reads(reads);
Ok(Self::Expression(qs))
}
}
pub(crate) fn majority_size(n: usize) -> usize {
if n == 0 { 0 } else { (n / 2) + 1 }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_majority_sizes() {
let p = QuorumPolicy::SimpleMajority;
assert_eq!(p.phase1_quorum(3), 2);
assert_eq!(p.phase2_quorum(3), 2);
assert_eq!(p.phase1_quorum(5), 3);
assert_eq!(p.phase2_quorum(5), 3);
assert_eq!(p.phase1_quorum(7), 4);
assert_eq!(p.phase2_quorum(7), 4);
}
#[test]
fn test_simple_majority_zero_nodes() {
let p = QuorumPolicy::SimpleMajority;
assert_eq!(p.phase1_quorum(0), 0);
assert_eq!(p.phase2_quorum(0), 0);
}
#[test]
fn test_simple_majority_validates() {
assert!(QuorumPolicy::SimpleMajority.validate(5).is_ok());
}
#[test]
fn test_flexible_5node_phase1_4_phase2_2() {
let p = QuorumPolicy::Flexible { phase1: 4, phase2: 2 };
assert_eq!(p.phase1_quorum(5), 4);
assert_eq!(p.phase2_quorum(5), 2);
assert!(p.validate(5).is_ok(), "4+2=6 > 5 should be safe");
}
#[test]
fn test_flexible_invalid_rejected() {
let p = QuorumPolicy::Flexible { phase1: 1, phase2: 1 };
assert!(p.validate(3).is_err());
}
#[test]
fn test_flexible_boundary_equal_rejected() {
let p = QuorumPolicy::Flexible { phase1: 2, phase2: 3 };
assert!(p.validate(5).is_err(), "2+3=5 == 5, not strictly greater");
}
#[test]
fn test_flexible_classic_majority_is_valid() {
let p = QuorumPolicy::Flexible { phase1: 3, phase2: 3 };
assert!(p.validate(5).is_ok(), "3+3=6 > 5");
}
#[test]
fn test_build_expression_choose_quorum() {
let names: Vec<String> = (0..5).map(|i| format!("node{i}")).collect();
let policy = QuorumPolicy::build_expression(&names, 4, 2)
.expect("4-of-5 reads and 2-of-5 writes must intersect");
assert_eq!(policy.phase1_quorum(5), 4);
assert_eq!(policy.phase2_quorum(5), 2);
assert!(policy.validate(5).is_ok());
}
#[test]
fn test_build_expression_non_intersecting_rejected() {
let names: Vec<String> = (0..3).map(|i| format!("node{i}")).collect();
let result = QuorumPolicy::build_expression(&names, 1, 1);
assert!(
result.is_err(),
"choose(1) reads and choose(1) writes over 3 nodes may not intersect"
);
}
#[test]
fn test_build_majority_expression() {
let names: Vec<String> = (0..5).map(|i| format!("node{i}")).collect();
let policy = QuorumPolicy::build_majority_expression(&names)
.expect("majority expression should always be valid");
assert_eq!(policy.phase1_quorum(5), 3);
assert_eq!(policy.phase2_quorum(5), 3);
}
#[test]
fn test_is_valid_phase2_quorum_simple() {
let p = QuorumPolicy::SimpleMajority;
let three: HashSet<&str> = ["a", "b", "c"].iter().copied().collect();
let two: HashSet<&str> = ["a", "b"].iter().copied().collect();
let one: HashSet<&str> = ["a"].iter().copied().collect();
assert!(p.is_valid_phase2_quorum(&three, 5));
assert!(p.is_valid_phase2_quorum(&two, 3)); assert!(!p.is_valid_phase2_quorum(&one, 3));
}
}