swactor/crates/distribution/src/swim/dissemination.rs

142 lines
4.5 KiB
Rust

//! SWIM piggybacked dissemination queue.
//!
//! Membership updates are piggybacked on existing protocol messages (pings, acks,
//! ping-reqs). Each update is transmitted `Λ * ceil(log2(n))` times before eviction,
//! where Λ is the dissemination multiplier and n is the cluster size.
//!
//! Priority ordering: Dead > Suspect > Alive (most urgent first).
use crate::messages::MembershipUpdate;
use crate::types::{MemberState, NodeId};
/// A queued membership update with a remaining transmit budget.
#[derive(Debug, Clone)]
struct DisseminationEntry {
update: MembershipUpdate,
/// Remaining number of times to piggyback this update.
remaining: usize,
}
/// The dissemination queue.
pub struct DisseminationQueue {
entries: Vec<DisseminationEntry>,
/// Λ multiplier — how many times log(n) to transmit each update.
lambda: usize,
}
impl DisseminationQueue {
pub fn new(lambda: usize) -> Self {
Self {
entries: Vec::new(),
lambda,
}
}
/// Enqueue a membership update for dissemination.
///
/// If an update for the same node already exists, it's replaced if the new
/// update has higher priority (higher incarnation, or same incarnation with
/// higher-priority state).
pub fn enqueue(&mut self, update: MembershipUpdate, cluster_size: usize) {
let budget = self.transmit_budget(cluster_size);
// Check for existing entry for this node
if let Some(existing) = self
.entries
.iter_mut()
.find(|e| e.update.node_id == update.node_id)
{
let dominated = update.incarnation > existing.update.incarnation
|| (update.incarnation == existing.update.incarnation
&& update.state > existing.update.state);
if dominated {
existing.update = update;
existing.remaining = budget;
}
return;
}
self.entries.push(DisseminationEntry {
update,
remaining: budget,
});
}
/// Take up to `max_count` updates to piggyback on an outgoing message.
///
/// Returns the updates sorted by priority (Dead first), and decrements
/// their remaining transmit count. Entries with zero remaining are evicted.
pub fn take(&mut self, max_count: usize) -> Vec<MembershipUpdate> {
// Sort by priority: Dead (2) > Suspect (1) > Alive (0), descending
self.entries
.sort_by(|a, b| b.update.state.priority().cmp(&a.update.state.priority()));
let count = max_count.min(self.entries.len());
let mut result = Vec::with_capacity(count);
for entry in self.entries.iter_mut().take(count) {
result.push(entry.update.clone());
entry.remaining = entry.remaining.saturating_sub(1);
}
// Evict exhausted entries
self.entries.retain(|e| e.remaining > 0);
result
}
/// Serialize piggyback data for inclusion in a wire envelope.
pub fn pack_piggyback(&mut self, max_updates: usize) -> Vec<u8> {
let updates = self.take(max_updates);
if updates.is_empty() {
return Vec::new();
}
serde_json::to_vec(&updates).unwrap_or_default()
}
/// Deserialize piggybacked membership updates from a wire envelope.
pub fn unpack_piggyback(bytes: &[u8]) -> Vec<MembershipUpdate> {
if bytes.is_empty() {
return Vec::new();
}
serde_json::from_slice(bytes).unwrap_or_default()
}
/// Number of queued entries.
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
/// Remove all pending updates for a given node.
///
/// Called when clearing a Dead member before re-peering, so stale
/// `(node_id, Dead, incarnation)` gossip doesn't leak out and re-infect
/// the cluster.
pub fn purge_node(&mut self, node_id: &NodeId) {
self.entries.retain(|e| e.update.node_id != *node_id);
}
/// Compute the transmit budget: `Λ * ceil(log2(max(n, 2)))`.
fn transmit_budget(&self, cluster_size: usize) -> usize {
let n = cluster_size.max(2) as f64;
let log_n = n.log2().ceil() as usize;
self.lambda * log_n.max(1)
}
}
/// Convenience: create a `MembershipUpdate` from components.
pub fn membership_update(
node_id: NodeId,
state: MemberState,
incarnation: u64,
) -> MembershipUpdate {
MembershipUpdate {
node_id,
state,
incarnation,
}
}