use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use foca::{BincodeCodec, Foca, Notification, Runtime, Timer};
use rand::SeedableRng;
use tokio::sync::{broadcast, mpsc};
use crate::instrument;
use crate::runtime::Runtime as SeamRuntime;
use super::config::NodeIdentity;
#[derive(Debug, Clone)]
pub enum ClusterEvent {
NodeJoined(NodeIdentity),
NodeLeft(NodeIdentity),
NodeFailed(NodeIdentity),
NodePruned(NodeIdentity),
ActorRegistered {
label: String,
node_id: String,
actor_type: String,
},
ActorDeregistered {
label: String,
node_id: String,
},
SpawnAckOk {
request_id: u64,
label: String,
},
SpawnAckErr {
request_id: u64,
error: String,
},
SingletonStopped {
label: String,
stopped_generation: u64,
},
}
#[derive(Debug)]
pub struct TimerEvent {
pub timer: Timer<NodeIdentity>,
pub timer_id: u64,
}
pub(crate) enum TimerCommand {
Insert {
timer: Timer<NodeIdentity>,
timer_id: u64,
delay: Duration,
},
CancelAll,
}
struct Scheduled {
deadline: Duration,
seq: u64,
event: TimerEvent,
}
impl PartialEq for Scheduled {
fn eq(&self, other: &Self) -> bool {
(self.deadline, self.seq) == (other.deadline, other.seq)
}
}
impl Eq for Scheduled {}
impl Ord for Scheduled {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
(self.deadline, self.seq).cmp(&(other.deadline, other.seq))
}
}
impl PartialOrd for Scheduled {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
pub(crate) fn spawn_timer_manager(
runtime: Arc<dyn SeamRuntime>,
timer_tx: mpsc::UnboundedSender<TimerEvent>,
) -> mpsc::UnboundedSender<TimerCommand> {
use std::cmp::Reverse;
use std::collections::BinaryHeap;
let (cmd_tx, mut cmd_rx) = mpsc::unbounded_channel::<TimerCommand>();
let sleep_rt = Arc::clone(&runtime);
runtime.spawn(Box::pin(async move {
let mut heap: BinaryHeap<Reverse<Scheduled>> = BinaryHeap::new();
let mut seq: u64 = 0;
loop {
let now = sleep_rt.now();
let next_deadline = heap.peek().map(|Reverse(s)| s.deadline);
tokio::select! {
cmd = cmd_rx.recv() => {
match cmd {
Some(TimerCommand::Insert { timer, timer_id, delay }) => {
heap.push(Reverse(Scheduled {
deadline: now + delay,
seq,
event: TimerEvent { timer, timer_id },
}));
seq += 1;
}
Some(TimerCommand::CancelAll) => heap.clear(),
None => break, }
}
_ = sleep_rt.sleep(next_deadline.unwrap_or(Duration::ZERO).saturating_sub(now)),
if next_deadline.is_some() =>
{
let now = sleep_rt.now();
while let Some(Reverse(s)) = heap.peek() {
if s.deadline <= now {
let Reverse(s) = heap.pop().unwrap();
let _ = timer_tx.send(s.event);
} else {
break;
}
}
}
}
}
}));
cmd_tx
}
pub struct ClusterMembership {
pub foca: Foca<
NodeIdentity,
BincodeCodec<bincode::config::Configuration>,
rand::rngs::StdRng,
foca::NoCustomBroadcast,
>,
pub event_tx: broadcast::Sender<ClusterEvent>,
}
impl ClusterMembership {
pub fn new(
identity: NodeIdentity,
event_tx: broadcast::Sender<ClusterEvent>,
foca_seed: u64,
) -> Self {
let config = foca::Config::simple();
let rng = rand::rngs::StdRng::seed_from_u64(foca_seed);
let foca = Foca::new(
identity,
config,
rng,
BincodeCodec(bincode::config::standard()),
);
Self { foca, event_tx }
}
}
pub struct FocaRuntime {
event_tx: broadcast::Sender<ClusterEvent>,
local_identity: NodeIdentity,
swim_tx: mpsc::UnboundedSender<(NodeIdentity, Vec<u8>)>,
timer_cmd_tx: mpsc::UnboundedSender<TimerCommand>,
timer_id_counter: AtomicU64,
}
impl FocaRuntime {
pub(crate) fn new(
local_identity: NodeIdentity,
event_tx: broadcast::Sender<ClusterEvent>,
swim_tx: mpsc::UnboundedSender<(NodeIdentity, Vec<u8>)>,
timer_cmd_tx: mpsc::UnboundedSender<TimerCommand>,
) -> Self {
Self {
event_tx,
local_identity,
swim_tx,
timer_cmd_tx,
timer_id_counter: AtomicU64::new(0),
}
}
pub fn cancel_all_timers(&mut self) {
let _ = self.timer_cmd_tx.send(TimerCommand::CancelAll);
}
}
impl Runtime<NodeIdentity> for FocaRuntime {
fn notify(&mut self, notification: Notification<'_, NodeIdentity>) {
match notification {
Notification::MemberUp(identity) => {
if identity.socket_addr() != self.local_identity.socket_addr() {
tracing::info!("SWIM: node joined: {identity}");
instrument::cluster_node_joined();
let _ = self
.event_tx
.send(ClusterEvent::NodeJoined(identity.clone()));
}
}
Notification::MemberDown(identity) => {
tracing::info!("SWIM: node failed (detected by failure detector): {identity}");
instrument::cluster_node_failed();
let _ = self
.event_tx
.send(ClusterEvent::NodeFailed(identity.clone()));
}
Notification::Defunct => {
tracing::warn!("SWIM: local node declared defunct");
}
Notification::Active => {
tracing::info!("SWIM: local node is active");
}
Notification::Idle => {
tracing::trace!("SWIM: cluster idle");
}
_ => {
tracing::trace!("SWIM: unhandled notification: {notification:?}");
}
}
}
fn send_to(&mut self, to: NodeIdentity, data: &[u8]) {
tracing::trace!("SWIM: sending {} bytes to {to}", data.len());
let _ = self.swim_tx.send((to, data.to_vec()));
}
fn submit_after(&mut self, event: Timer<NodeIdentity>, after: Duration) {
let timer_id = self.timer_id_counter.fetch_add(1, Ordering::SeqCst);
let _ = self.timer_cmd_tx.send(TimerCommand::Insert {
timer: event,
timer_id,
delay: after,
});
}
}
impl Drop for FocaRuntime {
fn drop(&mut self) {
self.cancel_all_timers();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::runtime::TokioRuntime;
#[tokio::test]
async fn timer_manager_fires_timer() {
let (timer_tx, mut timer_rx) = mpsc::unbounded_channel::<TimerEvent>();
let cmd_tx = spawn_timer_manager(Arc::new(TokioRuntime), timer_tx);
cmd_tx
.send(TimerCommand::Insert {
timer: Timer::ProbeRandomMember(0),
timer_id: 1,
delay: Duration::from_millis(10),
})
.unwrap();
let event = tokio::time::timeout(Duration::from_secs(1), timer_rx.recv())
.await
.expect("timer should fire within 1s")
.expect("channel should not close");
assert_eq!(event.timer_id, 1);
}
#[tokio::test]
async fn timer_manager_cancel_all_prevents_firing() {
let (timer_tx, mut timer_rx) = mpsc::unbounded_channel::<TimerEvent>();
let cmd_tx = spawn_timer_manager(Arc::new(TokioRuntime), timer_tx);
cmd_tx
.send(TimerCommand::Insert {
timer: Timer::ProbeRandomMember(0),
timer_id: 1,
delay: Duration::from_millis(200),
})
.unwrap();
cmd_tx.send(TimerCommand::CancelAll).unwrap();
let result = tokio::time::timeout(Duration::from_millis(400), timer_rx.recv()).await;
assert!(result.is_err(), "timer must NOT fire after CancelAll");
}
#[tokio::test]
async fn timer_manager_fires_in_deadline_order() {
let (timer_tx, mut timer_rx) = mpsc::unbounded_channel::<TimerEvent>();
let cmd_tx = spawn_timer_manager(Arc::new(TokioRuntime), timer_tx);
for (timer_id, ms) in [(1u64, 60u64), (2, 20), (3, 40)] {
cmd_tx
.send(TimerCommand::Insert {
timer: Timer::ProbeRandomMember(0),
timer_id,
delay: Duration::from_millis(ms),
})
.unwrap();
}
let mut order = Vec::new();
for _ in 0..3 {
let ev = tokio::time::timeout(Duration::from_secs(1), timer_rx.recv())
.await
.expect("timer fires")
.expect("channel open");
order.push(ev.timer_id);
}
assert_eq!(order, vec![2, 3, 1], "earliest deadline first");
}
#[cfg(feature = "sim")]
#[test]
fn apply_many_emits_member_up_synchronously_under_sim() {
use crate::cluster::net::NodeId;
use crate::sim::SimRuntime;
let rt: Arc<dyn SeamRuntime> = Arc::new(SimRuntime::new(1));
let (event_tx, mut event_rx) = broadcast::channel(16);
let local =
NodeIdentity::new_seeded("local", NodeId("local-id".into()), "127.0.0.1", 7001, 1);
let mut membership =
ClusterMembership::new(local.clone(), event_tx.clone(), rt.derive_seed("foca-prng"));
let (swim_tx, _swim_rx) = mpsc::unbounded_channel();
let (timer_tx, _timer_rx) = mpsc::unbounded_channel();
let timer_cmd_tx = spawn_timer_manager(Arc::clone(&rt), timer_tx);
let mut foca_rt = FocaRuntime::new(local, event_tx, swim_tx, timer_cmd_tx);
let other =
NodeIdentity::new_seeded("other", NodeId("other-id".into()), "127.0.0.1", 7002, 1);
membership
.foca
.apply_many(
std::iter::once(foca::Member::alive(other.clone())),
false,
&mut foca_rt,
)
.expect("apply_many succeeds");
match event_rx.try_recv() {
Ok(ClusterEvent::NodeJoined(id)) => {
assert_eq!(id.endpoint_id, other.endpoint_id, "joined peer is `other`");
}
got => panic!("expected NodeJoined synchronously, got {got:?}"),
}
}
}