use super::gossip::GossipBuffer;
use super::lifeguard::{
Awareness, Millis, max_suspicion_ms, min_suspicion_ms, suspicion_timeout_ms,
};
use crate::remote::NodeId;
use crate::util::det_rng::DetRng;
use std::collections::{BTreeMap, BTreeSet};
fn supersedes_prec(new: (bool, u64, u8), old: (bool, u64, u8)) -> bool {
let (nt, ninc, nrank) = new;
let (ot, oinc, orank) = old;
match (nt, ot) {
(true, false) => true,
(false, true) => false,
_ => (ninc, nrank) > (oinc, orank),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Rumor {
Alive {
node: NodeId,
incarnation: u64,
},
Suspect {
node: NodeId,
incarnation: u64,
from: NodeId,
},
Confirm {
node: NodeId,
incarnation: u64,
from: NodeId,
},
Leave {
node: NodeId,
incarnation: u64,
},
}
impl Rumor {
#[must_use]
pub fn alive(node: NodeId, incarnation: u64) -> Self {
Self::Alive { node, incarnation }
}
#[must_use]
pub fn suspect(node: NodeId, incarnation: u64, from: NodeId) -> Self {
Self::Suspect {
node,
incarnation,
from,
}
}
#[must_use]
pub fn confirm(node: NodeId, incarnation: u64, from: NodeId) -> Self {
Self::Confirm {
node,
incarnation,
from,
}
}
#[must_use]
pub fn leave(node: NodeId, incarnation: u64) -> Self {
Self::Leave { node, incarnation }
}
#[must_use]
pub fn node(&self) -> &NodeId {
match self {
Self::Alive { node, .. }
| Self::Suspect { node, .. }
| Self::Confirm { node, .. }
| Self::Leave { node, .. } => node,
}
}
#[must_use]
pub fn incarnation(&self) -> u64 {
match self {
Self::Alive { incarnation, .. }
| Self::Suspect { incarnation, .. }
| Self::Confirm { incarnation, .. }
| Self::Leave { incarnation, .. } => *incarnation,
}
}
fn rank_terminal(&self) -> (bool, u8) {
match self {
Self::Alive { .. } => (false, 0),
Self::Suspect { .. } => (false, 1),
Self::Leave { .. } => (true, 2),
Self::Confirm { .. } => (true, 3),
}
}
fn precedence(&self) -> (bool, u64, u8) {
let (terminal, rank) = self.rank_terminal();
(terminal, self.incarnation(), rank)
}
#[must_use]
pub fn supersedes(&self, other: &Self) -> bool {
supersedes_prec(self.precedence(), other.precedence())
}
fn accuser(&self) -> Option<NodeId> {
match self {
Self::Suspect { from, .. } | Self::Confirm { from, .. } => Some(from.clone()),
_ => None,
}
}
fn target_state(&self) -> MemberState {
match self {
Self::Alive { .. } => MemberState::Alive,
Self::Suspect { .. } => MemberState::Suspect,
Self::Confirm { .. } => MemberState::Dead,
Self::Leave { .. } => MemberState::Left,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MemberState {
Alive,
Suspect,
Dead,
Left,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MembershipKind {
Alive,
Suspect,
Dead,
Left,
}
impl MembershipKind {
fn from_state(state: MemberState) -> Self {
match state {
MemberState::Alive => Self::Alive,
MemberState::Suspect => Self::Suspect,
MemberState::Dead => Self::Dead,
MemberState::Left => Self::Left,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MembershipEvent {
pub node: NodeId,
pub kind: MembershipKind,
pub incarnation: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Payload {
Ping {
seq: u64,
},
Ack {
seq: u64,
},
PingReq {
seq: u64,
target: NodeId,
},
Nack {
seq: u64,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Packet {
pub payload: Payload,
pub gossip: Vec<Rumor>,
}
impl Packet {
#[must_use]
pub fn new(payload: Payload) -> Self {
Self {
payload,
gossip: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Outgoing {
pub to: NodeId,
pub packet: Packet,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SwimConfigError {
ZeroProbeInterval,
ZeroProbeTimeout,
ProbeTimeoutNotLessThanInterval,
ZeroSuspicionMult,
ZeroSuspicionMaxTimeoutMult,
ZeroGossipRetransmitMult,
ZeroMaxPiggyback,
AwarenessMaxTooSmall,
ZeroMaxMembers,
}
impl std::fmt::Display for SwimConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let msg = match self {
Self::ZeroProbeInterval => "probe_interval_ms must be >= 1",
Self::ZeroProbeTimeout => "probe_timeout_ms must be >= 1",
Self::ProbeTimeoutNotLessThanInterval => {
"probe_timeout_ms must be < probe_interval_ms (room for indirect probing)"
}
Self::ZeroSuspicionMult => "suspicion_mult must be >= 1",
Self::ZeroSuspicionMaxTimeoutMult => "suspicion_max_timeout_mult must be >= 1",
Self::ZeroGossipRetransmitMult => "gossip_retransmit_mult must be >= 1",
Self::ZeroMaxPiggyback => "max_piggyback must be >= 1",
Self::AwarenessMaxTooSmall => "awareness_max must be >= 1",
Self::ZeroMaxMembers => "max_members must be >= 1",
};
f.write_str(msg)
}
}
impl std::error::Error for SwimConfigError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SwimConfig {
pub probe_interval_ms: Millis,
pub probe_timeout_ms: Millis,
pub indirect_probe_count: u32,
pub suspicion_mult: u32,
pub suspicion_max_timeout_mult: u32,
pub gossip_retransmit_mult: u32,
pub max_piggyback: usize,
pub awareness_max: i32,
pub max_members: usize,
}
impl Default for SwimConfig {
fn default() -> Self {
Self {
probe_interval_ms: 1000,
probe_timeout_ms: 500,
indirect_probe_count: 3,
suspicion_mult: 4,
suspicion_max_timeout_mult: 6,
gossip_retransmit_mult: 4,
max_piggyback: 8,
awareness_max: 8,
max_members: 65_536,
}
}
}
impl SwimConfig {
pub fn validate(&self) -> Result<(), SwimConfigError> {
if self.probe_interval_ms == 0 {
return Err(SwimConfigError::ZeroProbeInterval);
}
if self.probe_timeout_ms == 0 {
return Err(SwimConfigError::ZeroProbeTimeout);
}
if self.probe_timeout_ms >= self.probe_interval_ms {
return Err(SwimConfigError::ProbeTimeoutNotLessThanInterval);
}
if self.suspicion_mult == 0 {
return Err(SwimConfigError::ZeroSuspicionMult);
}
if self.suspicion_max_timeout_mult == 0 {
return Err(SwimConfigError::ZeroSuspicionMaxTimeoutMult);
}
if self.gossip_retransmit_mult == 0 {
return Err(SwimConfigError::ZeroGossipRetransmitMult);
}
if self.max_piggyback == 0 {
return Err(SwimConfigError::ZeroMaxPiggyback);
}
if self.awareness_max < 1 {
return Err(SwimConfigError::AwarenessMaxTooSmall);
}
if self.max_members == 0 {
return Err(SwimConfigError::ZeroMaxMembers);
}
Ok(())
}
}
#[derive(Debug, Clone)]
struct Member {
state: MemberState,
incarnation: u64,
suspect_since: Millis,
suspect_from: BTreeSet<NodeId>,
}
impl Member {
fn new(state: MemberState, incarnation: u64, now: Millis) -> Self {
Self {
state,
incarnation,
suspect_since: now,
suspect_from: BTreeSet::new(),
}
}
fn precedence(&self) -> (bool, u64, u8) {
let (terminal, rank) = match self.state {
MemberState::Alive => (false, 0),
MemberState::Suspect => (false, 1),
MemberState::Left => (true, 2),
MemberState::Dead => (true, 3),
};
(terminal, self.incarnation, rank)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ProbePhase {
Direct,
Indirect,
}
#[derive(Debug, Clone)]
struct Probe {
target: NodeId,
seq: u64,
sent_at: Millis,
phase: ProbePhase,
indirect_helpers: u32,
nacks: u32,
acked: bool,
}
#[derive(Debug, Clone)]
struct Forward {
requester: NodeId,
requester_seq: u64,
helper_seq: u64,
sent_at: Millis,
}
#[derive(Debug, Clone)]
pub struct Swim {
local: NodeId,
incarnation: u64,
config: SwimConfig,
members: BTreeMap<NodeId, Member>,
awareness: Awareness,
gossip: GossipBuffer,
rng: DetRng,
seq: u64,
probe: Option<Probe>,
probe_order: Vec<NodeId>,
probe_idx: usize,
last_period_at: Millis,
forwards: Vec<Forward>,
started: bool,
events: Vec<MembershipEvent>,
}
impl Swim {
#[must_use]
pub fn new(local: NodeId, config: SwimConfig, seed: u64) -> Self {
let awareness = Awareness::new(config.awareness_max);
let mut gossip = GossipBuffer::new(config.gossip_retransmit_mult);
gossip.set_cluster_size(config.gossip_retransmit_mult, 1);
Self {
local,
incarnation: 0,
config,
members: BTreeMap::new(),
awareness,
gossip,
rng: DetRng::new(seed),
seq: 0,
probe: None,
probe_order: Vec::new(),
probe_idx: 0,
last_period_at: 0,
forwards: Vec::new(),
started: false,
events: Vec::new(),
}
}
#[must_use]
pub fn local(&self) -> &NodeId {
&self.local
}
#[must_use]
pub fn incarnation(&self) -> u64 {
self.incarnation
}
#[must_use]
pub fn config(&self) -> &SwimConfig {
&self.config
}
#[must_use]
pub fn state_of(&self, node: &NodeId) -> Option<MemberState> {
self.members.get(node).map(|m| m.state)
}
#[must_use]
pub fn member_count(&self) -> usize {
self.members.len()
}
#[must_use]
pub fn alive_peers(&self) -> Vec<NodeId> {
self.members
.iter()
.filter(|&(_, m)| m.state == MemberState::Alive)
.map(|(id, _)| id.clone())
.collect()
}
pub fn drain_events(&mut self) -> Vec<MembershipEvent> {
std::mem::take(&mut self.events)
}
pub fn add_peer(&mut self, now: Millis, peer: NodeId) {
if peer == self.local || self.members.contains_key(&peer) {
return;
}
if self.adopt(now, peer.clone(), MemberState::Alive, 0, None) {
self.gossip.queue(Rumor::alive(peer, 0));
}
}
pub fn declare_leave(&mut self) {
self.incarnation += 1;
self.gossip
.queue(Rumor::leave(self.local.clone(), self.incarnation));
}
pub fn tick(&mut self, now: Millis) -> Vec<Outgoing> {
let mut out = Vec::new();
self.expire_suspicions(now);
self.expire_forwards(now, &mut out);
self.advance_probe(now, &mut out);
self.maybe_start_period(now, &mut out);
out
}
pub fn handle(&mut self, now: Millis, from: NodeId, packet: Packet) -> Vec<Outgoing> {
let mut out = Vec::new();
self.observe_sender(now, &from);
let Packet { payload, gossip } = packet;
for rumor in &gossip {
self.apply_rumor(now, rumor);
}
match payload {
Payload::Ping { seq } => {
let pkt = self.packet_to(from, Payload::Ack { seq });
out.push(pkt);
}
Payload::Ack { seq } => self.handle_ack(seq, &mut out),
Payload::PingReq { seq, target } => {
self.handle_ping_req(now, &from, seq, target, &mut out);
}
Payload::Nack { seq } => self.handle_nack(seq),
}
out
}
fn cluster_size(&self) -> usize {
self.members.len() + 1
}
fn refresh_cluster_size(&mut self) {
self.gossip
.set_cluster_size(self.config.gossip_retransmit_mult, self.cluster_size());
}
fn next_seq(&mut self) -> u64 {
self.seq += 1;
self.seq
}
fn effective_period(&self) -> Millis {
self.awareness.scale(self.config.probe_interval_ms)
}
fn packet_to(&mut self, to: NodeId, payload: Payload) -> Outgoing {
let gossip = self.gossip.select(self.config.max_piggyback);
Outgoing {
to,
packet: Packet { payload, gossip },
}
}
fn adopt(
&mut self,
now: Millis,
node: NodeId,
state: MemberState,
incarnation: u64,
accuser: Option<NodeId>,
) -> bool {
let is_new = !self.members.contains_key(&node);
if is_new && self.members.len() >= self.config.max_members {
return false;
}
let prev_state = {
let member = self
.members
.entry(node.clone())
.or_insert_with(|| Member::new(MemberState::Alive, incarnation, now));
let prev = member.state;
member.state = state;
member.incarnation = incarnation;
if state == MemberState::Suspect {
if prev != MemberState::Suspect {
member.suspect_since = now;
member.suspect_from.clear();
}
if let Some(a) = accuser {
member.suspect_from.insert(a);
}
} else {
member.suspect_from.clear();
}
prev
};
if is_new {
self.refresh_cluster_size();
}
if is_new || prev_state != state {
self.events.push(MembershipEvent {
node,
kind: MembershipKind::from_state(state),
incarnation,
});
}
true
}
fn apply_rumor(&mut self, now: Millis, rumor: &Rumor) {
if rumor.incarnation() == u64::MAX {
return;
}
if rumor.node() == &self.local {
self.maybe_refute(rumor);
return;
}
let node = rumor.node().clone();
let supersede = match self.members.get(&node) {
Some(m) => supersedes_prec(rumor.precedence(), m.precedence()),
None => true,
};
if supersede {
if self.adopt(
now,
node,
rumor.target_state(),
rumor.incarnation(),
rumor.accuser(),
) {
self.gossip.queue(rumor.clone());
}
} else if let Rumor::Suspect {
incarnation, from, ..
} = rumor
{
if let Some(m) = self.members.get_mut(&node) {
if m.state == MemberState::Suspect && *incarnation == m.incarnation {
m.suspect_from.insert(from.clone());
}
}
}
}
fn maybe_refute(&mut self, rumor: &Rumor) {
match rumor {
Rumor::Suspect { incarnation, .. } | Rumor::Confirm { incarnation, .. } => {
if *incarnation >= self.incarnation {
self.incarnation = incarnation.saturating_add(1);
}
self.gossip
.queue(Rumor::alive(self.local.clone(), self.incarnation));
}
Rumor::Alive { incarnation, .. } => {
if *incarnation > self.incarnation {
self.incarnation = *incarnation;
}
}
Rumor::Leave { .. } => {}
}
}
fn observe_sender(&mut self, now: Millis, from: &NodeId) {
if from == &self.local || self.members.contains_key(from) {
return;
}
if self.adopt(now, from.clone(), MemberState::Alive, 0, None) {
self.gossip.queue(Rumor::alive(from.clone(), 0));
}
}
fn handle_ack(&mut self, seq: u64, out: &mut Vec<Outgoing>) {
let mut matched_probe = false;
if let Some(p) = self.probe.as_mut() {
if p.seq == seq && !p.acked {
p.acked = true;
matched_probe = true;
}
}
if matched_probe {
self.awareness.apply_delta(-1);
}
if let Some(pos) = self.forwards.iter().position(|f| f.helper_seq == seq) {
let f = self.forwards.remove(pos);
let pkt = self.packet_to(
f.requester,
Payload::Ack {
seq: f.requester_seq,
},
);
out.push(pkt);
}
}
fn handle_ping_req(
&mut self,
now: Millis,
from: &NodeId,
seq: u64,
target: NodeId,
out: &mut Vec<Outgoing>,
) {
if target == self.local {
let pkt = self.packet_to(from.clone(), Payload::Ack { seq });
out.push(pkt);
return;
}
if self.forwards.len() >= self.config.max_members {
return;
}
let helper_seq = self.next_seq();
self.forwards.push(Forward {
requester: from.clone(),
requester_seq: seq,
helper_seq,
sent_at: now,
});
let pkt = self.packet_to(target, Payload::Ping { seq: helper_seq });
out.push(pkt);
}
fn handle_nack(&mut self, seq: u64) {
if let Some(p) = self.probe.as_mut() {
if p.seq == seq {
p.nacks += 1;
}
}
}
fn expire_suspicions(&mut self, now: Millis) {
let n = self.cluster_size();
let min = min_suspicion_ms(self.config.suspicion_mult, n, self.config.probe_interval_ms);
let max = max_suspicion_ms(min, self.config.suspicion_max_timeout_mult);
let k = self.config.indirect_probe_count;
let mut to_kill = Vec::new();
for (id, m) in &self.members {
if m.state == MemberState::Suspect {
let confirmations = m.suspect_from.len().saturating_sub(1) as u32;
let timeout = suspicion_timeout_ms(min, max, confirmations, k);
if now.saturating_sub(m.suspect_since) >= timeout {
to_kill.push((id.clone(), m.incarnation));
}
}
}
for (id, inc) in to_kill {
self.adopt(now, id.clone(), MemberState::Dead, inc, None);
self.gossip
.queue(Rumor::confirm(id, inc, self.local.clone()));
}
}
fn expire_forwards(&mut self, now: Millis, out: &mut Vec<Outgoing>) {
let timeout = self.awareness.scale(self.config.probe_timeout_ms);
let mut expired = Vec::new();
let mut i = 0;
while i < self.forwards.len() {
if now.saturating_sub(self.forwards[i].sent_at) >= timeout {
expired.push(self.forwards.remove(i));
} else {
i += 1;
}
}
for f in expired {
let pkt = self.packet_to(
f.requester,
Payload::Nack {
seq: f.requester_seq,
},
);
out.push(pkt);
}
}
fn advance_probe(&mut self, now: Millis, out: &mut Vec<Outgoing>) {
let (sent_at, phase, target, seq, acked) = match self.probe.as_ref() {
Some(p) => (p.sent_at, p.phase, p.target.clone(), p.seq, p.acked),
None => return,
};
if acked {
return;
}
let period_deadline = sent_at + self.effective_period();
if now >= period_deadline {
self.conclude_failed_probe(now);
return;
}
let direct_deadline = sent_at + self.awareness.scale(self.config.probe_timeout_ms);
if phase == ProbePhase::Direct && now >= direct_deadline {
let helpers = self.pick_helpers(&target, self.config.indirect_probe_count as usize);
for h in &helpers {
let pkt = self.packet_to(
h.clone(),
Payload::PingReq {
seq,
target: target.clone(),
},
);
out.push(pkt);
}
if let Some(p) = self.probe.as_mut() {
p.phase = ProbePhase::Indirect;
p.indirect_helpers = helpers.len() as u32;
}
}
}
fn conclude_failed_probe(&mut self, now: Millis) {
let Some(probe) = self.probe.take() else {
return;
};
let mut delta = 1;
if probe.indirect_helpers > 0 && probe.nacks < probe.indirect_helpers {
delta += i32::try_from(probe.indirect_helpers - probe.nacks).unwrap_or(i32::MAX);
}
self.awareness.apply_delta(delta);
let target = probe.target;
let inc = match self.members.get(&target) {
Some(m) if m.state == MemberState::Alive => m.incarnation,
_ => return,
};
self.adopt(
now,
target.clone(),
MemberState::Suspect,
inc,
Some(self.local.clone()),
);
self.gossip
.queue(Rumor::suspect(target, inc, self.local.clone()));
}
fn maybe_start_period(&mut self, now: Millis, out: &mut Vec<Outgoing>) {
if self.started && now.saturating_sub(self.last_period_at) < self.effective_period() {
return;
}
let lingering = matches!(self.probe.as_ref(), Some(p) if !p.acked);
if lingering {
self.conclude_failed_probe(now);
}
self.probe = None;
let Some(target) = self.next_probe_target() else {
self.last_period_at = now;
self.started = true;
return;
};
let seq = self.next_seq();
let pkt = self.packet_to(target.clone(), Payload::Ping { seq });
self.probe = Some(Probe {
target,
seq,
sent_at: now,
phase: ProbePhase::Direct,
indirect_helpers: 0,
nacks: 0,
acked: false,
});
self.last_period_at = now;
self.started = true;
out.push(pkt);
}
fn next_probe_target(&mut self) -> Option<NodeId> {
loop {
if self.probe_idx >= self.probe_order.len() {
self.probe_order = self
.members
.iter()
.filter(|&(_, m)| m.state != MemberState::Dead && m.state != MemberState::Left)
.map(|(id, _)| id.clone())
.collect();
if self.probe_order.is_empty() {
return None;
}
self.rng.shuffle(&mut self.probe_order);
self.probe_idx = 0;
}
let cand = self.probe_order[self.probe_idx].clone();
self.probe_idx += 1;
if let Some(m) = self.members.get(&cand) {
if m.state != MemberState::Dead && m.state != MemberState::Left {
return Some(cand);
}
}
}
}
fn pick_helpers(&mut self, target: &NodeId, k: usize) -> Vec<NodeId> {
let mut cands: Vec<NodeId> = self
.members
.iter()
.filter(|&(id, m)| id != target && m.state == MemberState::Alive)
.map(|(id, _)| id.clone())
.collect();
self.rng.shuffle(&mut cands);
cands.truncate(k);
cands
}
}
#[cfg(test)]
mod tests {
use super::*;
fn node(s: &str) -> NodeId {
NodeId::new(s)
}
fn cfg() -> SwimConfig {
SwimConfig::default()
}
#[test]
fn default_config_is_valid_with_cited_defaults() {
let c = cfg();
assert!(c.validate().is_ok());
assert_eq!(c.probe_interval_ms, 1000);
assert_eq!(c.probe_timeout_ms, 500);
assert_eq!(c.indirect_probe_count, 3);
assert_eq!(c.suspicion_mult, 4);
assert_eq!(c.suspicion_max_timeout_mult, 6);
assert_eq!(c.gossip_retransmit_mult, 4);
assert_eq!(c.awareness_max, 8);
assert_eq!(c.max_members, 65_536);
}
#[test]
fn config_validation_rejects_each_bad_field() {
let bad = |f: &dyn Fn(&mut SwimConfig)| {
let mut c = cfg();
f(&mut c);
c.validate()
};
assert_eq!(
bad(&|c| c.probe_interval_ms = 0),
Err(SwimConfigError::ZeroProbeInterval)
);
assert_eq!(
bad(&|c| c.probe_timeout_ms = 0),
Err(SwimConfigError::ZeroProbeTimeout)
);
assert_eq!(
bad(&|c| c.probe_timeout_ms = c.probe_interval_ms),
Err(SwimConfigError::ProbeTimeoutNotLessThanInterval)
);
assert_eq!(
bad(&|c| c.suspicion_mult = 0),
Err(SwimConfigError::ZeroSuspicionMult)
);
assert_eq!(
bad(&|c| c.suspicion_max_timeout_mult = 0),
Err(SwimConfigError::ZeroSuspicionMaxTimeoutMult)
);
assert_eq!(
bad(&|c| c.gossip_retransmit_mult = 0),
Err(SwimConfigError::ZeroGossipRetransmitMult)
);
assert_eq!(
bad(&|c| c.max_piggyback = 0),
Err(SwimConfigError::ZeroMaxPiggyback)
);
assert_eq!(
bad(&|c| c.awareness_max = 0),
Err(SwimConfigError::AwarenessMaxTooSmall)
);
assert_eq!(
bad(&|c| c.max_members = 0),
Err(SwimConfigError::ZeroMaxMembers)
);
}
#[test]
fn members_capped_at_max_members_fail_closed() {
let mut config = cfg();
config.max_members = 3;
let mut s = Swim::new(node("local"), config, 1);
for p in ["a", "b", "c"] {
s.add_peer(0, node(p));
}
assert_eq!(s.member_count(), 3);
let _ = s.drain_events();
s.add_peer(0, node("d"));
assert_eq!(s.member_count(), 3, "join past the cap must be rejected");
assert!(
s.state_of(&node("d")).is_none(),
"rejected node must not be tracked",
);
s.apply_rumor(0, &Rumor::alive(node("evil"), 9));
assert_eq!(
s.member_count(),
3,
"gossip about a new node past the cap is dropped"
);
assert!(s.state_of(&node("evil")).is_none());
let events = s.drain_events();
assert!(
events
.iter()
.all(|e| e.node != node("d") && e.node != node("evil")),
"no membership event for a rejected node (nothing to amplify)",
);
s.apply_rumor(0, &Rumor::suspect(node("a"), 1, node("b")));
assert_eq!(
s.state_of(&node("a")),
Some(MemberState::Suspect),
"updates to existing members are unaffected by the cap",
);
assert_eq!(s.member_count(), 3);
}
#[test]
fn rumor_precedence_table() {
let n = node("x");
let a = node("a");
let alive1 = Rumor::alive(n.clone(), 1);
let alive2 = Rumor::alive(n.clone(), 2);
let suspect1 = Rumor::suspect(n.clone(), 1, a.clone());
let suspect2 = Rumor::suspect(n.clone(), 2, a.clone());
let dead1 = Rumor::confirm(n.clone(), 1, a.clone());
let leave1 = Rumor::leave(n.clone(), 1);
assert!(alive2.supersedes(&alive1));
assert!(!alive1.supersedes(&alive2));
assert!(alive2.supersedes(&suspect1));
assert!(!alive1.supersedes(&suspect1));
assert!(suspect1.supersedes(&alive1));
assert!(suspect2.supersedes(&suspect1));
assert!(!suspect1.supersedes(&suspect2));
assert!(dead1.supersedes(&alive2));
assert!(dead1.supersedes(&suspect2));
assert!(leave1.supersedes(&alive2));
assert!(!alive2.supersedes(&dead1));
assert!(!suspect2.supersedes(&dead1));
assert!(dead1.supersedes(&leave1));
assert!(!leave1.supersedes(&dead1));
}
#[test]
fn ping_is_acked_and_sender_is_learned() {
let mut s = Swim::new(node("self"), cfg(), 1);
let out = s.handle(0, node("peer"), Packet::new(Payload::Ping { seq: 42 }));
assert_eq!(out.len(), 1);
assert_eq!(out[0].to, node("peer"));
assert_eq!(out[0].packet.payload, Payload::Ack { seq: 42 });
assert_eq!(s.state_of(&node("peer")), Some(MemberState::Alive));
let events = s.drain_events();
assert!(
events
.iter()
.any(|e| e.node == node("peer") && e.kind == MembershipKind::Alive)
);
}
#[test]
fn tick_starts_a_probe_period() {
let mut s = Swim::new(node("self"), cfg(), 7);
s.add_peer(0, node("a"));
let _ = s.drain_events();
let out = s.tick(0);
assert_eq!(out.len(), 1);
assert_eq!(out[0].to, node("a"));
assert!(matches!(out[0].packet.payload, Payload::Ping { .. }));
}
#[test]
fn direct_ack_keeps_target_alive_and_improves_health() {
let mut s = Swim::new(node("self"), cfg(), 7);
s.add_peer(0, node("a"));
let _ = s.drain_events();
let out = s.tick(0);
let Payload::Ping { seq } = out[0].packet.payload else {
panic!("expected ping");
};
let _ = s.handle(100, node("a"), Packet::new(Payload::Ack { seq }));
let _ = s.tick(1000);
let events = s.drain_events();
assert!(!events.iter().any(|e| e.kind == MembershipKind::Suspect));
assert_eq!(s.state_of(&node("a")), Some(MemberState::Alive));
}
#[test]
fn unacked_probe_escalates_to_indirect_then_suspects() {
let mut s = Swim::new(node("self"), cfg(), 3);
for p in ["a", "b", "c"] {
s.add_peer(0, node(p));
}
let _ = s.drain_events();
let out = s.tick(0);
let target = out[0].to.clone();
let Payload::Ping { .. } = out[0].packet.payload else {
panic!("expected ping");
};
let out2 = s.tick(500);
assert!(!out2.is_empty(), "expected indirect ping-reqs");
for o in &out2 {
assert_ne!(o.to, target, "helper must not be the target");
match &o.packet.payload {
Payload::PingReq { target: t, .. } => assert_eq!(*t, target),
other => panic!("expected ping-req, got {other:?}"),
}
}
let _ = s.tick(1000);
assert_eq!(s.state_of(&target), Some(MemberState::Suspect));
let events = s.drain_events();
assert!(
events
.iter()
.any(|e| e.node == target && e.kind == MembershipKind::Suspect)
);
}
#[test]
fn suspect_escalates_to_dead_after_timeout() {
let mut s = Swim::new(node("self"), cfg(), 5);
s.add_peer(0, node("a"));
let pkt = Packet {
payload: Payload::Ping { seq: 1 },
gossip: vec![Rumor::suspect(node("a"), 0, node("acc"))],
};
let _ = s.handle(0, node("acc"), pkt);
assert_eq!(s.state_of(&node("a")), Some(MemberState::Suspect));
let _ = s.drain_events();
let _ = s.tick(10_000);
assert_eq!(s.state_of(&node("a")), Some(MemberState::Suspect));
let _ = s.tick(30_000);
assert_eq!(s.state_of(&node("a")), Some(MemberState::Dead));
let events = s.drain_events();
assert!(
events
.iter()
.any(|e| e.node == node("a") && e.kind == MembershipKind::Dead)
);
}
#[test]
fn independent_confirmations_shrink_the_window() {
let mut s = Swim::new(node("self"), cfg(), 5);
s.add_peer(0, node("a"));
for acc in ["acc1", "acc2", "acc3", "acc4"] {
let pkt = Packet {
payload: Payload::Ping { seq: 1 },
gossip: vec![Rumor::suspect(node("a"), 0, node(acc))],
};
let _ = s.handle(0, node(acc), pkt);
}
let _ = s.drain_events();
let _ = s.tick(5_000);
assert_eq!(s.state_of(&node("a")), Some(MemberState::Dead));
}
#[test]
fn alive_rumor_with_higher_incarnation_refutes_suspicion() {
let mut s = Swim::new(node("self"), cfg(), 5);
s.add_peer(0, node("a"));
let _ = s.handle(
0,
node("acc"),
Packet {
payload: Payload::Ping { seq: 1 },
gossip: vec![Rumor::suspect(node("a"), 0, node("acc"))],
},
);
assert_eq!(s.state_of(&node("a")), Some(MemberState::Suspect));
let _ = s.drain_events();
let _ = s.handle(
10,
node("a"),
Packet {
payload: Payload::Ping { seq: 2 },
gossip: vec![Rumor::alive(node("a"), 1)],
},
);
assert_eq!(s.state_of(&node("a")), Some(MemberState::Alive));
let events = s.drain_events();
assert!(
events
.iter()
.any(|e| e.node == node("a") && e.kind == MembershipKind::Alive)
);
}
#[test]
fn self_suspicion_triggers_incarnation_bump_and_alive_flood() {
let mut s = Swim::new(node("self"), cfg(), 9);
assert_eq!(s.incarnation(), 0);
let out = s.handle(
0,
node("acc"),
Packet {
payload: Payload::Ping { seq: 5 },
gossip: vec![Rumor::suspect(node("self"), 0, node("acc"))],
},
);
assert_eq!(s.incarnation(), 1);
let ack = &out[0];
assert!(ack.packet.gossip.iter().any(|r| matches!(
r,
Rumor::Alive { node: nn, incarnation: 1 } if nn == &node("self")
)));
assert_eq!(s.state_of(&node("self")), None);
}
#[test]
fn self_suspicion_with_max_incarnation_is_rejected_at_ingress() {
let mut s = Swim::new(node("self"), cfg(), 9);
let out = s.handle(
0,
node("acc"),
Packet {
payload: Payload::Ping { seq: 5 },
gossip: vec![Rumor::suspect(node("self"), u64::MAX, node("acc"))],
},
);
assert_eq!(s.incarnation(), 0);
let ack = &out[0];
assert!(!ack.packet.gossip.iter().any(|r| matches!(
r,
Rumor::Alive { node: nn, incarnation } if nn == &node("self") && *incarnation == u64::MAX
)));
}
#[test]
fn self_refutation_ceiling_stays_open_below_the_reserved_sentinel() {
let mut s = Swim::new(node("self"), cfg(), 9);
let out = s.handle(
0,
node("acc"),
Packet {
payload: Payload::Ping { seq: 7 },
gossip: vec![Rumor::suspect(node("self"), u64::MAX - 1, node("acc"))],
},
);
assert_eq!(s.incarnation(), u64::MAX);
let refutation = Rumor::alive(node("self"), u64::MAX);
let planted = Rumor::suspect(node("self"), u64::MAX - 1, node("acc"));
assert!(
refutation.supersedes(&planted),
"Alive at the ceiling must out-incarnate the highest plantable suspicion"
);
let ack = &out[0];
assert!(ack.packet.gossip.iter().any(|r| matches!(
r,
Rumor::Alive { node: nn, incarnation } if nn == &node("self") && *incarnation == u64::MAX
)));
}
#[test]
fn ping_req_relay_table_capped_fail_closed() {
let mut config = cfg();
config.max_members = 2;
let mut s = Swim::new(node("local"), config, 1);
let out1 = s.handle(
0,
node("req"),
Packet::new(Payload::PingReq {
seq: 1,
target: node("t1"),
}),
);
let out2 = s.handle(
0,
node("req"),
Packet::new(Payload::PingReq {
seq: 2,
target: node("t2"),
}),
);
assert!(
out1.iter()
.any(|o| matches!(o.packet.payload, Payload::Ping { .. }))
&& out2
.iter()
.any(|o| matches!(o.packet.payload, Payload::Ping { .. })),
"relays within the cap emit a Ping to the target"
);
let out3 = s.handle(
0,
node("req"),
Packet::new(Payload::PingReq {
seq: 3,
target: node("t3"),
}),
);
assert!(
!out3
.iter()
.any(|o| matches!(o.packet.payload, Payload::Ping { .. })),
"a PingReq past the relay cap must not emit a relayed Ping"
);
}
#[test]
fn suspicion_is_piggybacked_for_dissemination() {
let mut s = Swim::new(node("self"), cfg(), 3);
for p in ["a", "b"] {
s.add_peer(0, node(p));
}
let out = s.tick(0);
let target = out[0].to.clone();
let _ = s.tick(500);
let _ = s.tick(1000);
assert_eq!(s.state_of(&target), Some(MemberState::Suspect));
let later = s.tick(3000);
let carried = later.iter().any(|o| {
o.packet
.gossip
.iter()
.any(|r| matches!(r, Rumor::Suspect { node: nn, .. } if *nn == target))
});
assert!(
carried,
"suspicion must be piggybacked on outbound probe traffic"
);
}
#[test]
fn declare_leave_floods_leave_rumor() {
let mut s = Swim::new(node("self"), cfg(), 1);
s.add_peer(0, node("a"));
s.declare_leave();
assert_eq!(s.incarnation(), 1);
let out = s.handle(0, node("a"), Packet::new(Payload::Ping { seq: 1 }));
assert!(out[0].packet.gossip.iter().any(|r| matches!(
r,
Rumor::Leave { node: nn, .. } if nn == &node("self")
)));
}
#[test]
fn dead_is_terminal_and_not_resurrected_by_alive() {
let mut s = Swim::new(node("self"), cfg(), 1);
s.add_peer(0, node("a"));
let _ = s.handle(
0,
node("acc"),
Packet {
payload: Payload::Ping { seq: 1 },
gossip: vec![Rumor::confirm(node("a"), 5, node("acc"))],
},
);
assert_eq!(s.state_of(&node("a")), Some(MemberState::Dead));
let _ = s.handle(
1,
node("b"),
Packet {
payload: Payload::Ping { seq: 2 },
gossip: vec![Rumor::alive(node("a"), 99)],
},
);
assert_eq!(s.state_of(&node("a")), Some(MemberState::Dead));
}
#[test]
fn indirect_helper_relays_ack_to_requester() {
let mut s = Swim::new(node("helper"), cfg(), 1);
let out = s.handle(
0,
node("req"),
Packet::new(Payload::PingReq {
seq: 77,
target: node("tgt"),
}),
);
assert_eq!(out.len(), 1);
assert_eq!(out[0].to, node("tgt"));
let Payload::Ping { seq: helper_seq } = out[0].packet.payload else {
panic!("expected ping to target");
};
let relayed = s.handle(
10,
node("tgt"),
Packet::new(Payload::Ack { seq: helper_seq }),
);
assert_eq!(relayed.len(), 1);
assert_eq!(relayed[0].to, node("req"));
assert_eq!(relayed[0].packet.payload, Payload::Ack { seq: 77 });
}
#[test]
fn helper_nacks_requester_when_target_silent() {
let mut s = Swim::new(node("helper"), cfg(), 1);
let _ = s.handle(
0,
node("req"),
Packet::new(Payload::PingReq {
seq: 88,
target: node("tgt"),
}),
);
let out = s.tick(600);
let nack = out
.iter()
.find(|o| matches!(o.packet.payload, Payload::Nack { .. }))
.expect("helper should nack the requester");
assert_eq!(nack.to, node("req"));
assert_eq!(nack.packet.payload, Payload::Nack { seq: 88 });
}
}