use super::lifeguard::Millis;
use super::swim::{MemberState, MembershipEvent, Outgoing, Packet, Swim, SwimConfig};
use crate::remote::NodeId;
use crate::util::det_rng::DetRng;
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ClusterConfig {
pub tick_ms: Millis,
pub latency_ms: Millis,
pub loss_permille: u16,
}
impl Default for ClusterConfig {
fn default() -> Self {
Self {
tick_ms: 100,
latency_ms: 20,
loss_permille: 0,
}
}
}
#[derive(Debug, Clone)]
struct InFlight {
deliver_at: Millis,
from: NodeId,
to: NodeId,
packet: Packet,
}
#[derive(Debug)]
pub struct VirtualCluster {
nodes: BTreeMap<NodeId, Swim>,
config: ClusterConfig,
now: Millis,
queue: Vec<InFlight>,
dead: BTreeSet<NodeId>,
partition: BTreeMap<NodeId, u8>,
events: BTreeMap<NodeId, Vec<MembershipEvent>>,
rng: DetRng,
slow_node: Option<(NodeId, Millis)>,
}
impl VirtualCluster {
#[must_use]
pub fn new(
node_ids: &[NodeId],
swim_config: SwimConfig,
config: ClusterConfig,
seed: u64,
) -> Self {
let mut nodes = BTreeMap::new();
let mut partition = BTreeMap::new();
let mut events = BTreeMap::new();
for (index, id) in node_ids.iter().enumerate() {
let mut swim = Swim::new(
id.clone(),
swim_config.clone(),
seed.wrapping_add(index as u64),
);
for other in node_ids {
if other != id {
swim.add_peer(0, other.clone());
}
}
let _ = swim.drain_events();
nodes.insert(id.clone(), swim);
partition.insert(id.clone(), 0);
events.insert(id.clone(), Vec::new());
}
Self {
nodes,
config,
now: 0,
queue: Vec::new(),
dead: BTreeSet::new(),
partition,
events,
rng: DetRng::new(seed ^ 0xA7C3_5E91_D24B_8F60),
slow_node: None,
}
}
pub fn set_slow_node(&mut self, node: &NodeId, extra_ms: Millis) {
self.slow_node = Some((node.clone(), extra_ms));
}
#[must_use]
pub const fn now(&self) -> Millis {
self.now
}
#[must_use]
pub fn view(&self, viewer: &NodeId, subject: &NodeId) -> Option<MemberState> {
self.nodes
.get(viewer)
.and_then(|swim| swim.state_of(subject))
}
#[must_use]
pub fn events_of(&self, node: &NodeId) -> &[MembershipEvent] {
self.events.get(node).map_or(&[], |log| log.as_slice())
}
pub fn kill(&mut self, node: &NodeId) {
self.dead.insert(node.clone());
self.queue.retain(|m| &m.from != node && &m.to != node);
}
pub fn partition_groups(&mut self, groups: &[&[NodeId]]) {
for (gid, group) in groups.iter().enumerate() {
for id in group.iter() {
self.partition.insert(id.clone(), gid as u8);
}
}
let partition = &self.partition;
self.queue
.retain(|m| partition.get(&m.from) == partition.get(&m.to));
}
pub fn heal(&mut self) {
for group in self.partition.values_mut() {
*group = 0;
}
}
#[must_use]
pub fn all_living_agree_dead(&self, dead: &[NodeId]) -> bool {
for (id, swim) in &self.nodes {
if self.dead.contains(id) || dead.iter().any(|d| d == id) {
continue;
}
for d in dead {
if swim.state_of(d) != Some(MemberState::Dead) {
return false;
}
}
}
true
}
#[must_use]
pub fn all_living_agree_alive(&self, subjects: &[NodeId]) -> bool {
for (id, swim) in &self.nodes {
if self.dead.contains(id) {
continue;
}
for subject in subjects {
if id == subject {
continue;
}
if swim.state_of(subject) != Some(MemberState::Alive) {
return false;
}
}
}
true
}
pub fn advance(&mut self, duration_ms: Millis) {
let target = self.now.saturating_add(duration_ms);
while self.now < target {
self.step();
}
}
fn step(&mut self) {
self.now = self.now.saturating_add(self.config.tick_ms);
self.deliver_due();
self.tick_nodes();
}
fn deliver_due(&mut self) {
let now = self.now;
let mut due = Vec::new();
let mut remaining = Vec::new();
for message in std::mem::take(&mut self.queue) {
if message.deliver_at <= now {
due.push(message);
} else {
remaining.push(message);
}
}
self.queue = remaining;
due.sort_by(|a, b| {
a.deliver_at
.cmp(&b.deliver_at)
.then_with(|| a.from.cmp(&b.from))
.then_with(|| a.to.cmp(&b.to))
});
for message in due {
if !self.reachable(&message.from, &message.to) {
continue;
}
if self.config.loss_permille > 0
&& (self.rng.next_usize(1000) as u16) < self.config.loss_permille
{
continue;
}
let outcome = if let Some(node) = self.nodes.get_mut(&message.to) {
let out = node.handle(now, message.from.clone(), message.packet);
let drained = node.drain_events();
Some((out, drained))
} else {
None
};
if let Some((out, drained)) = outcome {
self.collect_events(&message.to, drained);
self.enqueue(&message.to, out);
}
}
}
fn tick_nodes(&mut self) {
let now = self.now;
let live: Vec<NodeId> = self
.nodes
.keys()
.filter(|id| !self.dead.contains(*id))
.cloned()
.collect();
for id in live {
let outcome = if let Some(node) = self.nodes.get_mut(&id) {
let out = node.tick(now);
let drained = node.drain_events();
Some((out, drained))
} else {
None
};
if let Some((out, drained)) = outcome {
self.collect_events(&id, drained);
self.enqueue(&id, out);
}
}
}
fn enqueue(&mut self, from: &NodeId, outgoing: Vec<Outgoing>) {
for packet in outgoing {
if !self.reachable(from, &packet.to) {
continue;
}
let mut latency = self.config.latency_ms;
if let Some((slow, extra)) = self.slow_node.as_ref() {
if from == slow || &packet.to == slow {
latency = latency.saturating_add(*extra);
}
}
let deliver_at = self.now.saturating_add(latency);
self.queue.push(InFlight {
deliver_at,
from: from.clone(),
to: packet.to,
packet: packet.packet,
});
}
}
fn collect_events(&mut self, node: &NodeId, drained: Vec<MembershipEvent>) {
if let Some(log) = self.events.get_mut(node) {
log.extend(drained);
}
}
fn reachable(&self, from: &NodeId, to: &NodeId) -> bool {
!self.dead.contains(from)
&& !self.dead.contains(to)
&& self.partition.get(from) == self.partition.get(to)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ids(n: usize) -> Vec<NodeId> {
(0..n).map(|i| NodeId::new(format!("n{i}"))).collect()
}
#[test]
fn cluster_stabilizes_with_no_faults() {
let nodes = ids(5);
let mut cluster =
VirtualCluster::new(&nodes, SwimConfig::default(), ClusterConfig::default(), 7);
cluster.advance(10_000);
assert!(cluster.all_living_agree_alive(&nodes));
}
#[test]
fn seven_node_kill_two_converges_to_dead() {
let nodes = ids(7);
let mut cluster =
VirtualCluster::new(&nodes, SwimConfig::default(), ClusterConfig::default(), 42);
cluster.advance(3_000); cluster.kill(&nodes[5]);
cluster.kill(&nodes[6]);
cluster.advance(120_000);
assert!(cluster.all_living_agree_dead(&[nodes[5].clone(), nodes[6].clone()]));
}
#[test]
fn partition_heal_refutes_suspicion() {
let nodes = ids(5);
let mut cluster =
VirtualCluster::new(&nodes, SwimConfig::default(), ClusterConfig::default(), 11);
cluster.advance(3_000);
let majority = [nodes[0].clone(), nodes[1].clone(), nodes[2].clone()];
let minority = [nodes[3].clone(), nodes[4].clone()];
cluster.partition_groups(&[&majority, &minority]);
cluster.advance(3_000);
cluster.heal();
cluster.advance(15_000);
assert!(cluster.all_living_agree_alive(&nodes));
}
#[test]
fn killed_node_traffic_is_dropped() {
let nodes = ids(3);
let mut cluster =
VirtualCluster::new(&nodes, SwimConfig::default(), ClusterConfig::default(), 5);
cluster.advance(1_000);
cluster.kill(&nodes[2]);
cluster.advance(120_000);
assert_eq!(cluster.view(&nodes[0], &nodes[2]), Some(MemberState::Dead));
assert_eq!(cluster.view(&nodes[1], &nodes[2]), Some(MemberState::Dead));
}
}