use super::swim::Rumor;
use crate::remote::NodeId;
use std::collections::HashMap;
#[derive(Debug, Clone)]
struct Buffered {
rumor: Rumor,
transmits: u32,
}
#[derive(Debug, Clone)]
pub struct GossipBuffer {
rumors: HashMap<NodeId, Buffered>,
retransmit_limit: u32,
}
impl GossipBuffer {
#[must_use]
pub fn new(retransmit_limit: u32) -> Self {
Self {
rumors: HashMap::new(),
retransmit_limit: retransmit_limit.max(1),
}
}
pub fn set_cluster_size(&mut self, retransmit_mult: u32, n: usize) {
let scale = (((n + 1) as f64).log10().ceil() as u32).max(1);
self.retransmit_limit = retransmit_mult.saturating_mul(scale).max(1);
}
#[must_use]
pub fn retransmit_limit(&self) -> u32 {
self.retransmit_limit
}
pub fn queue(&mut self, rumor: Rumor) {
let node = rumor.node().clone();
match self.rumors.get_mut(&node) {
Some(existing) => {
if rumor.supersedes(&existing.rumor) {
existing.rumor = rumor;
existing.transmits = 0;
}
}
None => {
self.rumors.insert(
node,
Buffered {
rumor,
transmits: 0,
},
);
}
}
}
pub fn select(&mut self, max: usize) -> Vec<Rumor> {
if max == 0 || self.rumors.is_empty() {
return Vec::new();
}
let mut order: Vec<NodeId> = self.rumors.keys().cloned().collect();
order.sort_by(|a, b| {
let ta = self.rumors[a].transmits;
let tb = self.rumors[b].transmits;
ta.cmp(&tb).then_with(|| a.cmp(b))
});
let mut selected = Vec::with_capacity(max.min(order.len()));
for node in order.into_iter().take(max) {
if let Some(buffered) = self.rumors.get_mut(&node) {
buffered.transmits += 1;
selected.push(buffered.rumor.clone());
}
}
let limit = self.retransmit_limit;
self.rumors.retain(|_, b| b.transmits < limit);
selected
}
#[must_use]
pub fn len(&self) -> usize {
self.rumors.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.rumors.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn node(s: &str) -> NodeId {
NodeId::new(s)
}
#[test]
fn retransmit_limit_scales_with_cluster() {
let mut g = GossipBuffer::new(4);
g.set_cluster_size(4, 7); assert_eq!(g.retransmit_limit(), 4);
g.set_cluster_size(4, 100); assert_eq!(g.retransmit_limit(), 12);
g.set_cluster_size(4, 0); assert_eq!(g.retransmit_limit(), 4);
}
#[test]
fn fresher_rumor_supersedes_and_resets_counter() {
let mut g = GossipBuffer::new(3);
g.queue(Rumor::alive(node("a"), 1));
let _ = g.select(8);
g.queue(Rumor::suspect(node("a"), 1, node("b")));
assert_eq!(g.len(), 1);
let out = g.select(8);
assert_eq!(out.len(), 1);
assert!(matches!(out[0], Rumor::Suspect { .. }));
}
#[test]
fn stale_rumor_is_dropped() {
let mut g = GossipBuffer::new(3);
g.queue(Rumor::suspect(node("a"), 5, node("b")));
g.queue(Rumor::alive(node("a"), 4));
let out = g.select(8);
assert_eq!(out.len(), 1);
assert!(matches!(out[0], Rumor::Suspect { .. }));
}
#[test]
fn rumor_is_evicted_after_retransmit_limit() {
let mut g = GossipBuffer::new(2);
g.queue(Rumor::alive(node("a"), 1));
assert_eq!(g.select(8).len(), 1); assert_eq!(g.select(8).len(), 1); assert!(g.is_empty());
assert!(g.select(8).is_empty());
}
#[test]
fn select_prefers_fewest_transmits() {
let mut g = GossipBuffer::new(10);
g.queue(Rumor::alive(node("a"), 1));
let _ = g.select(8);
let _ = g.select(8);
g.queue(Rumor::alive(node("b"), 1));
let out = g.select(1);
assert_eq!(out.len(), 1);
assert_eq!(out[0].node().as_str(), "b");
}
#[test]
fn empty_and_zero_budget() {
let mut g = GossipBuffer::new(3);
assert!(g.select(8).is_empty());
g.queue(Rumor::alive(node("a"), 1));
assert!(g.select(0).is_empty());
assert_eq!(g.len(), 1);
}
}