use crate::registry::{SessionRegistry, SharedSession};
use crate::runtime::SessionLifecycleEvent;
use crate::session::Session;
use std::collections::VecDeque;
use tokio::sync::broadcast::error::TryRecvError;
pub(crate) const PENDING_EVENT_LIMIT: usize = 1024;
pub(crate) struct InitialSync {
handles: std::vec::IntoIter<SharedSession>,
}
impl InitialSync {
pub(crate) async fn begin(registry: &SessionRegistry) -> Self {
Self {
handles: registry.shared_sessions().await.into_iter(),
}
}
pub(crate) fn remaining(&self) -> usize {
self.handles.len()
}
pub(crate) async fn next_session(&mut self) -> Option<Session> {
let handle = self.handles.next()?;
let session = handle.lock().await;
Some(session.clone())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DrainError {
Lagged(u64),
Overflow { limit: usize },
}
impl DrainError {
pub(crate) fn message(self) -> String {
match self {
Self::Lagged(skipped) => {
format!("WatchSessions receiver fell behind by {skipped} events")
}
Self::Overflow { limit } => format!(
"WatchSessions initial sync buffered more than {limit} lifecycle events; \
reconnect and reconcile with ListSessions"
),
}
}
}
pub(crate) fn drain_lifecycle_events(
rx: &mut tokio::sync::broadcast::Receiver<SessionLifecycleEvent>,
pending: &mut VecDeque<SessionLifecycleEvent>,
limit: usize,
) -> Result<(), DrainError> {
loop {
match rx.try_recv() {
Ok(event) => {
if pending.len() >= limit {
return Err(DrainError::Overflow { limit });
}
pending.push_back(event);
}
Err(TryRecvError::Empty | TryRecvError::Closed) => return Ok(()),
Err(TryRecvError::Lagged(skipped)) => return Err(DrainError::Lagged(skipped)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
fn sample_session(id: &str) -> Session {
Session::builder(id, "macp.mode.decision.v1", "agent://alice")
.ttl_expiry(i64::MAX)
.ttl_ms(60_000)
.started_at_unix_ms(1)
.participants(vec!["agent://alice".into()])
.mode_version("1.0.0")
.configuration_version("cfg-1")
.build()
}
async fn registry_with(count: usize) -> SessionRegistry {
let registry = SessionRegistry::new();
for i in 0..count {
let id = format!("sess-{i:04}");
registry
.insert_recovered_session(id.clone(), sample_session(&id))
.await;
}
registry
}
fn created(session_id: &str) -> SessionLifecycleEvent {
SessionLifecycleEvent::Created {
session_id: session_id.to_string(),
}
}
#[tokio::test]
async fn initial_sync_visits_every_session_exactly_once() {
let registry = registry_with(64).await;
let mut sync = InitialSync::begin(®istry).await;
assert_eq!(sync.remaining(), 64);
let mut seen: HashSet<String> = HashSet::new();
while let Some(session) = sync.next_session().await {
assert!(
seen.insert(session.session_id.clone()),
"{} emitted twice",
session.session_id
);
}
assert_eq!(seen.len(), 64, "the traversal dropped sessions");
assert_eq!(sync.remaining(), 0);
assert!(sync.next_session().await.is_none(), "must stay exhausted");
}
#[tokio::test]
async fn initial_sync_of_an_empty_registry_is_empty() {
let registry = registry_with(0).await;
let mut sync = InitialSync::begin(®istry).await;
assert_eq!(sync.remaining(), 0);
assert!(sync.next_session().await.is_none());
}
#[tokio::test]
async fn eviction_mid_traversal_does_not_perturb_the_sync() {
let registry = registry_with(6).await;
let mut sync = InitialSync::begin(®istry).await;
let first = sync.next_session().await.expect("first session");
registry.sessions.write().await.clear();
assert!(registry.get_session(&first.session_id).await.is_none());
let mut seen: HashSet<String> = HashSet::new();
seen.insert(first.session_id);
while let Some(session) = sync.next_session().await {
assert!(seen.insert(session.session_id.clone()));
}
assert_eq!(
seen.len(),
6,
"the handle snapshot must survive registry eviction"
);
}
#[tokio::test]
async fn drain_preserves_more_events_than_the_bus_holds() {
let (tx, mut rx) = tokio::sync::broadcast::channel(64);
let mut pending = VecDeque::new();
for burst in 0..4 {
for i in 0..64 {
tx.send(created(&format!("s-{burst}-{i}"))).unwrap();
}
drain_lifecycle_events(&mut rx, &mut pending, PENDING_EVENT_LIMIT)
.expect("draining between sessions must neither lag nor overflow");
}
assert_eq!(pending.len(), 256, "every event must survive the sync");
assert!(pending.len() <= PENDING_EVENT_LIMIT);
tx.send(created("live")).unwrap();
assert!(matches!(
rx.recv().await,
Ok(SessionLifecycleEvent::Created { .. })
));
}
#[tokio::test]
async fn drain_never_grows_pending_past_the_limit() {
let (tx, mut rx) = tokio::sync::broadcast::channel(64);
let mut pending = VecDeque::new();
let limit = 10;
for i in 0..12 {
tx.send(created(&format!("s{i}"))).unwrap();
}
let err = drain_lifecycle_events(&mut rx, &mut pending, limit)
.expect_err("12 events into a limit of 10 must overflow");
assert_eq!(err, DrainError::Overflow { limit });
assert_eq!(
pending.len(),
limit,
"the buffer must stop at the limit, never grow past it"
);
assert!(err.message().contains("10"));
}
#[tokio::test]
async fn drain_reports_bus_lag() {
let (tx, mut rx) = tokio::sync::broadcast::channel(2);
for i in 0..5 {
tx.send(created(&format!("s{i}"))).unwrap();
}
let mut pending = VecDeque::new();
let err =
drain_lifecycle_events(&mut rx, &mut pending, 100).expect_err("the bus dropped events");
assert!(matches!(err, DrainError::Lagged(skipped) if skipped == 3));
assert!(err.message().contains("fell behind by 3"));
}
#[tokio::test]
async fn drain_tolerates_empty_and_closed_bus() {
let (tx, mut rx) = tokio::sync::broadcast::channel(64);
let mut pending = VecDeque::new();
assert!(drain_lifecycle_events(&mut rx, &mut pending, PENDING_EVENT_LIMIT).is_ok());
assert!(pending.is_empty());
tx.send(created("s1")).unwrap();
drop(tx);
assert!(drain_lifecycle_events(&mut rx, &mut pending, PENDING_EVENT_LIMIT).is_ok());
assert_eq!(pending.len(), 1);
assert!(drain_lifecycle_events(&mut rx, &mut pending, PENDING_EVENT_LIMIT).is_ok());
}
}