use std::collections::HashMap;
use std::time::Instant;
use serde::Serialize;
use tokio::sync::mpsc;
use tokio::time::{Duration, interval};
use crate::containerisation::traits::HyperionHeartbeatMessage;
use crate::heartbeat::handler::HeartbeatMissedHandler;
use crate::messages::heartbeat::HeartbeatResponse;
use crate::utilities::time::{current_epoch_ms, fmt_ms};
pub struct HeartbeatSender<T> {
interval_ms: u64,
response_timeout_ms: u64,
container_name: String,
target_senders: HashMap<String, mpsc::Sender<T>>,
response_rx: mpsc::Receiver<HeartbeatResponse>,
next_id: u64,
pending: HashMap<u64, (String, Instant)>,
missed_handler: Option<Box<dyn HeartbeatMissedHandler>>,
}
impl<T> HeartbeatSender<T>
where
T: HyperionHeartbeatMessage + Send + Clone + Serialize + 'static,
{
pub fn new(
interval_ms: u64,
response_timeout_ms: u64,
container_name: String,
all_senders: HashMap<String, mpsc::Sender<T>>,
targets: &[String],
missed_handler: Option<Box<dyn HeartbeatMissedHandler>>,
) -> (Self, mpsc::Sender<HeartbeatResponse>) {
let target_senders: HashMap<String, mpsc::Sender<T>> = targets
.iter()
.filter_map(|name| {
if let Some(s) = all_senders.get(name) {
Some((name.clone(), s.clone()))
} else {
log::warn!(
"HeartbeatSender: target '{}' not found in client senders — skipping",
name
);
None
}
})
.collect();
let (response_tx, response_rx) = mpsc::channel(32);
(
Self {
interval_ms,
response_timeout_ms,
container_name,
target_senders,
response_rx,
next_id: 0,
pending: HashMap::new(),
missed_handler,
},
response_tx,
)
}
pub async fn run(mut self) {
let mut ticker = interval(Duration::from_millis(self.interval_ms));
ticker.tick().await; log::info!(
"HeartbeatSender running — targets: {:?}",
self.target_senders.keys().collect::<Vec<_>>()
);
loop {
tokio::select! {
_ = ticker.tick() => {
let now_ms = current_epoch_ms();
for (name, sender) in &self.target_senders {
self.next_id += 1;
let id = self.next_id;
if let Some(msg) = T::make_heartbeat_request(id, self.container_name.clone(), now_ms) {
if sender.try_send(msg).is_err() {
log::warn!("HeartbeatSender: failed to queue request to '{}'", name);
}
self.pending.insert(id, (name.clone(), Instant::now()));
}
}
let timeout = Duration::from_millis(self.response_timeout_ms);
let missed_handler = &self.missed_handler;
self.pending.retain(|id, (target, sent_at)| {
if sent_at.elapsed() > timeout {
log::warn!("HeartbeatSender: no response from '{}' for request #{}", target, id);
if let Some(handler) = missed_handler {
handler.on_missed(target);
}
false
} else {
true
}
});
}
Some(response) = self.response_rx.recv() => {
if let Some((target, sent_at)) = self.pending.remove(&response.request_id) {
let latency_ms = sent_at.elapsed().as_millis();
if response.component_alive {
log::debug!(
"Heartbeat report ✓ {} | {} latency | activity {} ago | state {}",
target.to_ascii_uppercase(), fmt_ms(latency_ms), fmt_ms(response.ms_since_last_activity as u128), response.container_state_val
);
} else {
log::warn!(
"Heartbeat report ✓ {} | {} latency | COMPONENT NOT ALIVE | activity {} ago",
target.to_ascii_uppercase(), fmt_ms(latency_ms), fmt_ms(response.ms_since_last_activity as u128)
);
}
}
}
}
}
}
}