use std::collections::BTreeSet;
use crafty_proto::{Membership, NodeId};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct Configuration {
voters: BTreeSet<NodeId>,
outgoing: BTreeSet<NodeId>,
learners: BTreeSet<NodeId>,
}
impl Configuration {
#[must_use]
pub fn from_membership(m: &Membership) -> Self {
Self {
voters: m.voters.iter().copied().collect(),
outgoing: m.voters_outgoing.iter().copied().collect(),
learners: m.learners.iter().copied().collect(),
}
}
#[must_use]
pub fn to_membership(&self) -> Membership {
Membership {
voters: self.voters.iter().copied().collect(),
voters_outgoing: self.outgoing.iter().copied().collect(),
learners: self.learners.iter().copied().collect(),
}
}
#[must_use]
pub fn is_joint(&self) -> bool {
!self.outgoing.is_empty()
}
#[must_use]
pub fn is_voter(&self, id: NodeId) -> bool {
self.voters.contains(&id) || self.outgoing.contains(&id)
}
#[must_use]
pub fn voters(&self) -> Vec<NodeId> {
self.voters.iter().copied().collect()
}
#[must_use]
pub fn members(&self) -> BTreeSet<NodeId> {
self.voters
.iter()
.chain(self.outgoing.iter())
.chain(self.learners.iter())
.copied()
.collect()
}
#[must_use]
pub fn peers(&self, me: NodeId) -> Vec<NodeId> {
let mut m = self.members();
m.remove(&me);
m.into_iter().collect()
}
#[must_use]
pub fn voter_peers(&self, me: NodeId) -> Vec<NodeId> {
self.voters
.iter()
.chain(self.outgoing.iter())
.copied()
.filter(|id| *id != me)
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
#[must_use]
pub fn has_quorum(&self, acked: &BTreeSet<NodeId>) -> bool {
majority(&self.voters, acked) && majority(&self.outgoing, acked)
}
}
fn majority(set: &BTreeSet<NodeId>, acked: &BTreeSet<NodeId>) -> bool {
if set.is_empty() {
return true;
}
let need = set.len() / 2 + 1;
acked.iter().filter(|id| set.contains(id)).count() >= need
}
#[cfg(test)]
mod tests {
use super::*;
fn ids(v: &[u64]) -> BTreeSet<NodeId> {
v.iter().copied().map(NodeId).collect()
}
fn membership(voters: &[u64], outgoing: &[u64], learners: &[u64]) -> Membership {
Membership {
voters: voters.iter().copied().map(NodeId).collect(),
voters_outgoing: outgoing.iter().copied().map(NodeId).collect(),
learners: learners.iter().copied().map(NodeId).collect(),
}
}
#[test]
fn stable_majority() {
let c = Configuration::from_membership(&membership(&[1, 2, 3], &[], &[]));
assert!(!c.is_joint());
assert!(c.has_quorum(&ids(&[1, 2])));
assert!(!c.has_quorum(&ids(&[1])));
}
#[test]
fn joint_needs_both_halves() {
let c = Configuration::from_membership(&membership(&[1, 2, 3], &[1, 4, 5], &[]));
assert!(c.is_joint());
assert!(
!c.has_quorum(&ids(&[1, 2])),
"majority of new but not of old"
);
assert!(c.has_quorum(&ids(&[1, 2, 4])), "majority of both");
assert!(
!c.has_quorum(&ids(&[4, 5, 1])),
"majority of old but not new"
);
}
#[test]
fn learners_do_not_vote_but_are_peers() {
let c = Configuration::from_membership(&membership(&[1, 2, 3], &[], &[9]));
assert!(!c.is_voter(NodeId(9)));
assert!(c.peers(NodeId(1)).contains(&NodeId(9)));
assert!(!c.voter_peers(NodeId(1)).contains(&NodeId(9)));
assert!(!c.has_quorum(&ids(&[1, 9])));
}
#[test]
fn single_node_is_its_own_quorum() {
let c = Configuration::from_membership(&membership(&[1], &[], &[]));
assert!(c.has_quorum(&ids(&[1])));
assert!(c.peers(NodeId(1)).is_empty());
}
#[test]
fn roundtrips_through_membership() {
let m = membership(&[3, 1, 2], &[], &[7]);
let c = Configuration::from_membership(&m);
let back = c.to_membership();
assert_eq!(back.voters, vec![NodeId(1), NodeId(2), NodeId(3)]);
assert_eq!(back.learners, vec![NodeId(7)]);
}
}