use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use tokio::sync::broadcast;
use crate::snapshot::Snapshot;
pub const FRAME_BUFFER: usize = 16;
#[derive(Clone)]
pub struct FrameBus {
inner: Arc<Inner>,
}
struct Inner {
sender: broadcast::Sender<Arc<Snapshot>>,
latest: tokio::sync::RwLock<Option<LatestFrame>>,
next_seq: AtomicU64,
collection_interval: Duration,
fresh_collect_lock: tokio::sync::Mutex<()>,
}
#[derive(Clone)]
pub struct LatestFrame {
pub snapshot: Arc<Snapshot>,
pub published_at: Instant,
pub seq: u64,
}
impl FrameBus {
pub fn new(collection_interval: Duration) -> Self {
let (sender, _rx) = broadcast::channel(FRAME_BUFFER);
Self {
inner: Arc::new(Inner {
sender,
latest: tokio::sync::RwLock::new(None),
next_seq: AtomicU64::new(1),
collection_interval,
fresh_collect_lock: tokio::sync::Mutex::new(()),
}),
}
}
pub async fn publish(&self, snapshot: Snapshot) -> u64 {
let seq = self.inner.next_seq.fetch_add(1, Ordering::Relaxed);
let arc = Arc::new(snapshot);
{
let mut guard = self.inner.latest.write().await;
*guard = Some(LatestFrame {
snapshot: arc.clone(),
published_at: Instant::now(),
seq,
});
}
let _ = self.inner.sender.send(arc);
seq
}
pub fn subscribe(&self) -> broadcast::Receiver<Arc<Snapshot>> {
self.inner.sender.subscribe()
}
pub fn subscriber_count(&self) -> usize {
self.inner.sender.receiver_count()
}
pub async fn latest(&self) -> Option<LatestFrame> {
self.inner.latest.read().await.clone()
}
pub fn collection_interval(&self) -> Duration {
self.inner.collection_interval
}
pub async fn lock_fresh_collect(&self) -> tokio::sync::MutexGuard<'_, ()> {
self.inner.fresh_collect_lock.lock().await
}
#[cfg(test)]
pub fn next_seq(&self) -> u64 {
self.inner.next_seq.load(Ordering::Relaxed)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::snapshot::Snapshot;
fn make_snapshot(host: &str) -> Snapshot {
Snapshot {
schema: 1,
timestamp: "2026-04-20T00:00:00Z".to_string(),
hostname: host.to_string(),
gpus: None,
cpus: None,
memory: None,
chassis: None,
processes: None,
storage: None,
errors: Vec::new(),
}
}
#[tokio::test]
async fn publish_updates_latest_and_increments_seq() {
let bus = FrameBus::new(Duration::from_secs(3));
assert!(bus.latest().await.is_none());
let seq = bus.publish(make_snapshot("h1")).await;
assert_eq!(seq, 1);
let latest = bus.latest().await.expect("latest must be present");
assert_eq!(latest.snapshot.hostname, "h1");
assert_eq!(latest.seq, 1);
let seq2 = bus.publish(make_snapshot("h2")).await;
assert_eq!(seq2, 2);
}
#[tokio::test]
async fn subscribers_receive_published_frames() {
let bus = FrameBus::new(Duration::from_secs(3));
let mut rx = bus.subscribe();
assert_eq!(bus.subscriber_count(), 1);
bus.publish(make_snapshot("rx-host")).await;
let frame = rx.recv().await.expect("receiver must get the frame");
assert_eq!(frame.hostname, "rx-host");
}
#[tokio::test]
async fn dropping_receiver_releases_slot() {
let bus = FrameBus::new(Duration::from_secs(3));
let rx = bus.subscribe();
assert_eq!(bus.subscriber_count(), 1);
drop(rx);
assert_eq!(bus.subscriber_count(), 0);
}
#[tokio::test]
async fn fresh_collect_lock_serializes_callers() {
let bus = FrameBus::new(Duration::from_secs(3));
let guard = bus.lock_fresh_collect().await;
let bus2 = bus.clone();
let handle = tokio::spawn(async move {
let _g = bus2.lock_fresh_collect().await;
});
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
assert!(
!handle.is_finished(),
"second lock_fresh_collect caller should block while first guard is held"
);
drop(guard);
handle
.await
.expect("second caller must complete once the guard is released");
}
}