Skip to main content

kvbm_physical/transfer/notifications/
notification.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Transfer completion notification handle.
5
6use anyhow::Result;
7use futures::future::{Either, Ready, ready};
8use std::{
9    pin::Pin,
10    sync::Arc,
11    task::{Context, Poll},
12};
13use velo::{Event, EventAwaiter, EventManager};
14
15pub enum TransferAwaiter {
16    Local(EventAwaiter),
17    // Sync(SyncResult),
18}
19
20impl std::future::Future for TransferAwaiter {
21    type Output = Result<()>;
22
23    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
24        match self.get_mut() {
25            Self::Local(waiter) => Pin::new(waiter).poll(cx),
26            // Self::Sync(sync) => Pin::new(sync).poll(cx),
27        }
28    }
29}
30
31/// Notification handle for an in-progress transfer.
32///
33/// This object can be awaited to block until the transfer completes.
34/// The transfer is tracked by a background handler that polls for completion
35/// or processes notification events.
36///
37/// Uses `futures::Either` to avoid event system overhead for synchronous completions.
38/// Pending transfers use `LocalEventWaiter` which avoids heap allocation and repeated
39/// DashMap lookups when awaiting.
40pub struct TransferCompleteNotification {
41    awaiter: Either<Ready<Result<()>>, TransferAwaiter>,
42}
43
44impl TransferCompleteNotification {
45    /// Create a notification that is already completed (for synchronous transfers).
46    ///
47    /// This is useful for transfers that complete immediately without needing
48    /// background polling, such as memcpy operations.
49    ///
50    /// This is extremely efficient - no allocations, locks, or event system overhead.
51    pub fn completed() -> Self {
52        Self {
53            awaiter: Either::Left(ready(Ok(()))),
54        }
55    }
56
57    /// Create a notification from a `LocalEventWaiter`.
58    ///
59    /// This is the primary way to construct a notification when you already
60    /// have an event waiter from the event system.
61    pub fn from_awaiter(awaiter: EventAwaiter) -> Self {
62        Self {
63            awaiter: Either::Right(TransferAwaiter::Local(awaiter)),
64        }
65    }
66
67    // /// Create a notification from a synchronous active message result.
68    // pub fn from_sync_result(sync: SyncResult) -> Self {
69    //     Self {
70    //         awaiter: Either::Right(TransferAwaiter::Sync(sync)),
71    //     }
72    // }
73
74    /// Check if the notification can yield the current task.
75    ///
76    /// The internal ::Left arm is guaranteed to be ready, while the ::Right arm is not.
77    pub fn could_yield(&self) -> bool {
78        matches!(self.awaiter, Either::Right(_))
79    }
80
81    /// Aggregate multiple notifications into one that completes when all are done.
82    ///
83    /// This is useful when a transfer is split across multiple workers and you want
84    /// to wait for all of them to complete.
85    ///
86    /// # Arguments
87    /// * `notifications` - The notifications to aggregate
88    /// * `events` - The event system to create the aggregate event
89    /// * `runtime` - The tokio runtime handle to spawn the aggregation task
90    ///
91    /// # Behavior
92    /// - If the list is empty, returns an already-completed notification
93    /// - If there's only one, returns it directly
94    /// - Otherwise, creates a new event and spawns a task to await all notifications
95    pub fn aggregate(
96        notifications: Vec<Self>,
97        events: &Arc<EventManager>,
98        runtime: &tokio::runtime::Handle,
99    ) -> Result<Self> {
100        if notifications.is_empty() {
101            return Ok(Self::completed());
102        }
103        if notifications.len() == 1 {
104            return Ok(notifications.into_iter().next().unwrap());
105        }
106
107        // Check if all notifications are already complete (no yielding needed)
108        if notifications.iter().all(|n| !n.could_yield()) {
109            return Ok(Self::completed());
110        }
111
112        // Create a new event for the aggregate completion
113        let event = events.new_event()?;
114        let awaiter = events.awaiter(event.handle())?;
115
116        // Spawn task that awaits all notifications and triggers/poisons the event
117        runtime.spawn(await_all_notifications(notifications, event));
118
119        Ok(Self::from_awaiter(awaiter))
120    }
121}
122
123/// Awaits all transfer notifications and signals completion via the event.
124///
125/// This function awaits ALL notifications regardless of individual failures,
126/// then triggers the event on success or poisons it with error details on failure.
127async fn await_all_notifications(
128    notifications: Vec<TransferCompleteNotification>,
129    local_event: Event,
130) {
131    // Await all notifications, collecting results
132    let results: Vec<Result<()>> =
133        futures::future::join_all(notifications.into_iter().map(|n| n.into_future())).await;
134
135    // Check for any failures
136    let errors: Vec<_> = results.into_iter().filter_map(|r| r.err()).collect();
137
138    if errors.is_empty() {
139        // Ignore trigger error - if event system is shutdown, nothing to do
140        let _ = local_event.trigger();
141    } else {
142        let error_msg = errors
143            .iter()
144            .map(|e| e.to_string())
145            .collect::<Vec<_>>()
146            .join("; ");
147        // Ignore poison error - if event system is shutdown, nothing to do
148        let _ = local_event.poison(error_msg);
149    }
150}
151
152impl std::future::IntoFuture for TransferCompleteNotification {
153    type Output = Result<()>;
154    type IntoFuture = Either<Ready<Result<()>>, TransferAwaiter>;
155
156    fn into_future(self) -> Self::IntoFuture {
157        self.awaiter
158    }
159}