use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::{Duration, Instant};
use dynamo_memory::nixl::{Agent as NixlAgent, NotificationMap, XferRequest};
use tokio::sync::mpsc;
use tokio::time::interval;
use tracing::{error, warn};
use uuid::Uuid;
use velo::{EventHandle, EventManager};
pub struct RegisterNixlNotification {
pub uuid: Uuid,
pub xfer_req: XferRequest,
pub event_handle: EventHandle,
}
trait NotificationSource: Send {
fn poll_notifications(&self) -> anyhow::Result<HashMap<String, Vec<String>>>;
}
struct NixlNotificationSource {
agent: NixlAgent,
}
impl NotificationSource for NixlNotificationSource {
fn poll_notifications(&self) -> anyhow::Result<HashMap<String, Vec<String>>> {
let mut notif_map = NotificationMap::new()?;
self.agent.get_notifications(&mut notif_map, None)?;
Ok(notif_map.take_notifs()?)
}
}
trait Registration: Send {
type Payload: Send;
fn decompose(self) -> (Uuid, EventHandle, Self::Payload);
}
impl Registration for RegisterNixlNotification {
type Payload = XferRequest;
fn decompose(self) -> (Uuid, EventHandle, XferRequest) {
(self.uuid, self.event_handle, self.xfer_req)
}
}
struct OutstandingTransfer<T> {
#[allow(dead_code)] _payload: T,
event_handle: EventHandle,
arrived_at: Instant,
last_warned_at: Option<Instant>,
}
fn check_and_warn_slow_transfer(
uuid: &Uuid,
arrived_at: Instant,
last_warned_at: Option<Instant>,
) -> Option<Instant> {
let elapsed = arrived_at.elapsed();
if elapsed > Duration::from_secs(60) {
let should_warn = last_warned_at
.map(|last| last.elapsed() > Duration::from_secs(30))
.unwrap_or(true);
if should_warn {
warn!(
uuid = %uuid,
elapsed_secs = elapsed.as_secs(),
"Transfer has been pending for over 1 minute"
);
return Some(Instant::now());
}
}
last_warned_at
}
fn collect_completed<T>(
notifications: HashMap<String, Vec<String>>,
outstanding: &HashMap<Uuid, OutstandingTransfer<T>>,
mut early_arrivals: Option<&mut HashSet<Uuid>>,
) -> Vec<Uuid> {
let mut completed = Vec::new();
for (_agent_name, notif_strings) in notifications {
for notif_str in notif_strings {
if let Ok(notif_uuid) = Uuid::parse_str(¬if_str) {
if outstanding.contains_key(¬if_uuid) {
completed.push(notif_uuid);
} else if let Some(early) = early_arrivals.as_deref_mut() {
warn!(
uuid = %notif_uuid,
"Notification arrived for transfer not in outstanding map (early arrival)"
);
early.insert(notif_uuid);
#[cfg(all(not(test), debug_assertions))]
panic!(
"Notification arrived for transfer not in outstanding map (early arrival); ensure all transfers are registered the NIXL notification can be triggered"
);
}
}
}
}
completed
}
fn complete_transfers<T>(
completed: Vec<Uuid>,
outstanding: &mut HashMap<Uuid, OutstandingTransfer<T>>,
system: &EventManager,
) {
for uuid in completed {
if let Some(transfer) = outstanding.remove(&uuid)
&& let Err(e) = system.trigger(transfer.event_handle)
{
error!(
uuid = %uuid,
error = %e,
"Failed to trigger completion event"
);
}
}
}
async fn process_events_core<S: NotificationSource, R: Registration>(
source: S,
mut rx: mpsc::Receiver<R>,
system: Arc<EventManager>,
) {
let mut outstanding: HashMap<Uuid, OutstandingTransfer<R::Payload>> = HashMap::new();
let mut early_arrivals: HashSet<Uuid> = HashSet::new();
let mut last_early_arrival_warn: Option<Instant> = None;
let mut check_interval = interval(Duration::from_millis(1));
loop {
tokio::select! {
notification = rx.recv() => {
match notification {
Some(reg) => {
let (uuid, event_handle, payload) = reg.decompose();
if early_arrivals.remove(&uuid) {
drop(payload);
if let Err(e) = system.trigger(event_handle) {
error!(
uuid = %uuid,
error = %e,
"Failed to trigger completion event for early arrival"
);
}
} else {
outstanding.insert(uuid, OutstandingTransfer {
_payload: payload,
event_handle,
arrived_at: Instant::now(),
last_warned_at: None,
});
}
}
None => {
break;
}
}
}
_ = check_interval.tick(), if !outstanding.is_empty() => {
let notifications = match source.poll_notifications() {
Ok(notifs) => notifs,
Err(e) => {
warn!(error = %e, "Failed to fetch notifications");
continue;
}
};
let completed = collect_completed(
notifications,
&outstanding,
Some(&mut early_arrivals),
);
for (uuid, transfer) in outstanding.iter_mut() {
if !completed.contains(uuid) {
transfer.last_warned_at = check_and_warn_slow_transfer(
uuid,
transfer.arrived_at,
transfer.last_warned_at,
);
}
}
if !early_arrivals.is_empty() {
let should_warn = last_early_arrival_warn
.map(|t| t.elapsed() > Duration::from_secs(30))
.unwrap_or(true);
if should_warn {
warn!(
count = early_arrivals.len(),
"early_arrivals buffer has unmatched entries"
);
last_early_arrival_warn = Some(Instant::now());
}
}
complete_transfers(completed, &mut outstanding, &system);
}
}
}
while !outstanding.is_empty() {
check_interval.tick().await;
match source.poll_notifications() {
Ok(notifications) => {
let completed = collect_completed(notifications, &outstanding, None);
complete_transfers(completed, &mut outstanding, &system);
}
Err(e) => {
warn!(error = %e, "Failed to fetch notifications during shutdown drain");
}
}
}
}
pub async fn process_nixl_notification_events(
agent: NixlAgent,
rx: mpsc::Receiver<RegisterNixlNotification>,
system: Arc<EventManager>,
) {
process_events_core(NixlNotificationSource { agent }, rx, system).await
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::VecDeque;
use std::sync::Mutex;
use tokio::task::yield_now;
use velo::EventStatus;
type NotificationQueue = Arc<Mutex<VecDeque<anyhow::Result<HashMap<String, Vec<String>>>>>>;
struct MockNotificationSource {
queue: NotificationQueue,
}
struct MockControl {
queue: NotificationQueue,
}
fn mock_source() -> (MockNotificationSource, MockControl) {
let queue = Arc::new(Mutex::new(VecDeque::new()));
(
MockNotificationSource {
queue: queue.clone(),
},
MockControl { queue },
)
}
impl NotificationSource for MockNotificationSource {
fn poll_notifications(&self) -> anyhow::Result<HashMap<String, Vec<String>>> {
self.queue
.lock()
.unwrap()
.pop_front()
.unwrap_or(Ok(HashMap::new()))
}
}
impl MockControl {
fn push_notification(&self, uuid: Uuid) {
let mut map = HashMap::new();
map.insert("test_agent".to_string(), vec![uuid.to_string()]);
self.queue.lock().unwrap().push_back(Ok(map));
}
fn push_error(&self) {
self.queue
.lock()
.unwrap()
.push_back(Err(anyhow::anyhow!("mock poll error")));
}
}
struct TestRegistration {
uuid: Uuid,
event_handle: EventHandle,
}
impl Registration for TestRegistration {
type Payload = ();
fn decompose(self) -> (Uuid, EventHandle, ()) {
(self.uuid, self.event_handle, ())
}
}
async fn tick() {
tokio::time::advance(Duration::from_millis(2)).await;
yield_now().await;
}
#[test]
fn warn_under_threshold_returns_none() {
let result = check_and_warn_slow_transfer(
&Uuid::new_v4(),
Instant::now() - Duration::from_secs(30),
None,
);
assert!(result.is_none());
}
#[test]
fn warn_over_threshold_first_time() {
let before = Instant::now();
let result = check_and_warn_slow_transfer(
&Uuid::new_v4(),
Instant::now() - Duration::from_secs(61),
None,
);
let after = Instant::now();
let t = result.expect("should have warned");
assert!(t >= before && t <= after);
}
#[test]
fn warn_throttled_within_30s() {
let last = Instant::now() - Duration::from_secs(10);
let result = check_and_warn_slow_transfer(
&Uuid::new_v4(),
Instant::now() - Duration::from_secs(90),
Some(last),
);
assert_eq!(result, Some(last));
}
#[test]
fn warn_throttle_expired_after_30s() {
let before = Instant::now();
let result = check_and_warn_slow_transfer(
&Uuid::new_v4(),
Instant::now() - Duration::from_secs(120),
Some(Instant::now() - Duration::from_secs(35)),
);
let t = result.expect("should have re-warned");
assert!(t >= before);
}
#[tokio::test(start_paused = true)]
async fn normal_flow_register_then_notify() -> anyhow::Result<()> {
let (source, control) = mock_source();
let (tx, rx) = mpsc::channel(16);
let system = Arc::new(EventManager::local());
let uuid = Uuid::new_v4();
let event = system.new_event()?;
let handle = event.into_handle();
let task = tokio::spawn(process_events_core(source, rx, system.clone()));
tx.send(TestRegistration {
uuid,
event_handle: handle,
})
.await?;
yield_now().await;
control.push_notification(uuid);
tick().await;
assert_eq!(system.poll(handle)?, EventStatus::Ready);
drop(tx);
task.await?;
Ok(())
}
#[tokio::test(start_paused = true)]
async fn early_arrival_notify_before_register() -> anyhow::Result<()> {
let (source, control) = mock_source();
let (tx, rx) = mpsc::channel(16);
let system = Arc::new(EventManager::local());
let uuid_early = Uuid::new_v4();
let event_early = system.new_event()?;
let handle_early = event_early.into_handle();
let uuid_filler = Uuid::new_v4();
let event_filler = system.new_event()?;
let handle_filler = event_filler.into_handle();
let task = tokio::spawn(process_events_core(source, rx, system.clone()));
tx.send(TestRegistration {
uuid: uuid_filler,
event_handle: handle_filler,
})
.await?;
yield_now().await;
control.push_notification(uuid_early);
tick().await;
tx.send(TestRegistration {
uuid: uuid_early,
event_handle: handle_early,
})
.await?;
yield_now().await;
assert_eq!(system.poll(handle_early)?, EventStatus::Ready);
control.push_notification(uuid_filler);
tick().await;
drop(tx);
task.await?;
Ok(())
}
#[tokio::test(start_paused = true)]
async fn multiple_transfers_complete_independently() -> anyhow::Result<()> {
let (source, control) = mock_source();
let (tx, rx) = mpsc::channel(16);
let system = Arc::new(EventManager::local());
let uuid_a = Uuid::new_v4();
let event_a = system.new_event()?;
let handle_a = event_a.into_handle();
let uuid_b = Uuid::new_v4();
let event_b = system.new_event()?;
let handle_b = event_b.into_handle();
let task = tokio::spawn(process_events_core(source, rx, system.clone()));
tx.send(TestRegistration {
uuid: uuid_a,
event_handle: handle_a,
})
.await?;
tx.send(TestRegistration {
uuid: uuid_b,
event_handle: handle_b,
})
.await?;
yield_now().await;
control.push_notification(uuid_b);
tick().await;
assert_eq!(system.poll(handle_b)?, EventStatus::Ready);
assert_eq!(system.poll(handle_a)?, EventStatus::Pending);
control.push_notification(uuid_a);
tick().await;
assert_eq!(system.poll(handle_a)?, EventStatus::Ready);
drop(tx);
task.await?;
Ok(())
}
#[tokio::test(start_paused = true)]
async fn mixed_ordering_early_and_normal() -> anyhow::Result<()> {
let (source, control) = mock_source();
let (tx, rx) = mpsc::channel(16);
let system = Arc::new(EventManager::local());
let uuid_early = Uuid::new_v4();
let event_early = system.new_event()?;
let handle_early = event_early.into_handle();
let uuid_normal = Uuid::new_v4();
let event_normal = system.new_event()?;
let handle_normal = event_normal.into_handle();
let task = tokio::spawn(process_events_core(source, rx, system.clone()));
tx.send(TestRegistration {
uuid: uuid_normal,
event_handle: handle_normal,
})
.await?;
yield_now().await;
control.push_notification(uuid_early);
tick().await;
tx.send(TestRegistration {
uuid: uuid_early,
event_handle: handle_early,
})
.await?;
yield_now().await;
assert_eq!(system.poll(handle_early)?, EventStatus::Ready);
assert_eq!(system.poll(handle_normal)?, EventStatus::Pending);
control.push_notification(uuid_normal);
tick().await;
assert_eq!(system.poll(handle_normal)?, EventStatus::Ready);
drop(tx);
task.await?;
Ok(())
}
#[tokio::test(start_paused = true)]
async fn channel_close_drains_outstanding() -> anyhow::Result<()> {
let (source, control) = mock_source();
let (tx, rx) = mpsc::channel(16);
let system = Arc::new(EventManager::local());
let uuid = Uuid::new_v4();
let event = system.new_event()?;
let handle = event.into_handle();
let task = tokio::spawn(process_events_core(source, rx, system.clone()));
tx.send(TestRegistration {
uuid,
event_handle: handle,
})
.await?;
yield_now().await;
drop(tx);
yield_now().await;
control.push_notification(uuid);
tick().await;
assert_eq!(system.poll(handle)?, EventStatus::Ready);
task.await?;
Ok(())
}
#[tokio::test(start_paused = true)]
async fn duplicate_notification_is_harmless() -> anyhow::Result<()> {
let (source, control) = mock_source();
let (tx, rx) = mpsc::channel(16);
let system = Arc::new(EventManager::local());
let uuid = Uuid::new_v4();
let event = system.new_event()?;
let handle = event.into_handle();
let task = tokio::spawn(process_events_core(source, rx, system.clone()));
tx.send(TestRegistration {
uuid,
event_handle: handle,
})
.await?;
yield_now().await;
control.push_notification(uuid);
tick().await;
assert_eq!(system.poll(handle)?, EventStatus::Ready);
control.push_notification(uuid);
let uuid2 = Uuid::new_v4();
let event2 = system.new_event()?;
let handle2 = event2.into_handle();
tx.send(TestRegistration {
uuid: uuid2,
event_handle: handle2,
})
.await?;
yield_now().await;
tick().await;
assert_eq!(system.poll(handle)?, EventStatus::Ready);
control.push_notification(uuid2);
tick().await;
drop(tx);
task.await?;
Ok(())
}
#[tokio::test(start_paused = true)]
async fn poll_error_does_not_crash_loop() -> anyhow::Result<()> {
let (source, control) = mock_source();
let (tx, rx) = mpsc::channel(16);
let system = Arc::new(EventManager::local());
let uuid = Uuid::new_v4();
let event = system.new_event()?;
let handle = event.into_handle();
let task = tokio::spawn(process_events_core(source, rx, system.clone()));
tx.send(TestRegistration {
uuid,
event_handle: handle,
})
.await?;
yield_now().await;
control.push_error();
tick().await;
assert_eq!(system.poll(handle)?, EventStatus::Pending);
control.push_notification(uuid);
tick().await;
assert_eq!(system.poll(handle)?, EventStatus::Ready);
drop(tx);
task.await?;
Ok(())
}
#[tokio::test(start_paused = true)]
async fn unknown_notification_in_drain_is_ignored() -> anyhow::Result<()> {
let (source, control) = mock_source();
let (tx, rx) = mpsc::channel(16);
let system = Arc::new(EventManager::local());
let uuid = Uuid::new_v4();
let event = system.new_event()?;
let handle = event.into_handle();
let task = tokio::spawn(process_events_core(source, rx, system.clone()));
tx.send(TestRegistration {
uuid,
event_handle: handle,
})
.await?;
yield_now().await;
drop(tx);
yield_now().await;
let mut map = HashMap::new();
map.insert(
"test_agent".to_string(),
vec![uuid.to_string(), Uuid::new_v4().to_string()],
);
control.queue.lock().unwrap().push_back(Ok(map));
tick().await;
assert_eq!(system.poll(handle)?, EventStatus::Ready);
task.await?;
Ok(())
}
}