Skip to main content

dynamo_mocker/live/
handoff.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::sync::{Arc, Mutex, Weak};
5
6use anyhow::{anyhow, bail};
7use dashmap::DashMap;
8use dashmap::mapref::entry::Entry;
9use tokio::sync::mpsc;
10use tokio_util::sync::CancellationToken;
11use uuid::Uuid;
12
13use crate::common::handoff::{HandoffId, HandoffTransferTiming};
14use crate::scheduler::{SchedulerCommand, SchedulerCommandResult, SchedulerLifecycleEvent};
15
16use super::request::{Routes, shutdown_routes};
17use super::{
18    LiveEngine, LiveEngineInner, LiveRequestRegistration, PreparedSubmission, send_command,
19};
20
21const HANDOFF_EVENT_CAPACITY: usize = 8;
22
23#[derive(Default)]
24pub(super) struct HandoffRoutes {
25    by_id: DashMap<HandoffId, Arc<HandoffRoute>>,
26    last_by_id: DashMap<HandoffId, (Uuid, Weak<HandoffRoute>)>,
27}
28
29pub(super) type SharedHandoffRoutes = Arc<HandoffRoutes>;
30
31impl HandoffRoutes {
32    pub(super) fn is_empty(&self) -> bool {
33        self.by_id.is_empty()
34    }
35}
36
37struct HandoffRoute {
38    handoff_id: HandoffId,
39    generation: Uuid,
40    routes: Weak<HandoffRoutes>,
41    event_tx: Mutex<Option<mpsc::Sender<LiveHandoffEvent>>>,
42    command_lock: Arc<tokio::sync::Mutex<()>>,
43}
44
45impl HandoffRoute {
46    fn new(
47        handoff_id: HandoffId,
48        routes: Weak<HandoffRoutes>,
49        event_tx: mpsc::Sender<LiveHandoffEvent>,
50    ) -> Self {
51        Self {
52            handoff_id,
53            generation: Uuid::new_v4(),
54            routes,
55            event_tx: Mutex::new(Some(event_tx)),
56            command_lock: Arc::new(tokio::sync::Mutex::new(())),
57        }
58    }
59
60    async fn send(&self, event: LiveHandoffEvent, cancel: &CancellationToken) -> bool {
61        let event_tx = self.event_tx.lock().unwrap().as_ref().cloned();
62        let Some(event_tx) = event_tx else {
63            return false;
64        };
65        let delivered = tokio::select! {
66            biased;
67            _ = cancel.cancelled() => false,
68            result = event_tx.send(event) => result.is_ok(),
69        };
70        if !delivered {
71            self.shutdown();
72        }
73        delivered
74    }
75
76    fn shutdown(&self) {
77        self.event_tx.lock().unwrap().take();
78    }
79
80    fn is_latest_generation(&self, routes: &HandoffRoutes) -> bool {
81        match routes.last_by_id.get(&self.handoff_id) {
82            Some(current) => current.value().0 == self.generation,
83            None => false,
84        }
85    }
86}
87
88impl Drop for HandoffRoute {
89    fn drop(&mut self) {
90        let Some(routes) = self.routes.upgrade() else {
91            return;
92        };
93        routes
94            .last_by_id
95            .remove_if(&self.handoff_id, |_, (generation, _)| {
96                *generation == self.generation
97            });
98    }
99}
100
101/// Scheduler lifecycle events normalized for a registered disaggregated handoff.
102#[derive(Clone, Copy, Debug, PartialEq)]
103pub enum LiveHandoffEvent {
104    SourceHeld {
105        transfer_timing: HandoffTransferTiming,
106    },
107    DestinationReserved {
108        transferable_prompt_tokens: usize,
109    },
110}
111
112impl LiveEngine {
113    /// Register one disaggregated handoff and its normalized lifecycle stream.
114    pub fn register_handoff(
115        &self,
116        handoff_id: HandoffId,
117    ) -> anyhow::Result<(LiveHandoffControl, LiveHandoffEvents)> {
118        anyhow::ensure!(
119            !self.inner.cancel.is_cancelled(),
120            "live Mocker engine is not running"
121        );
122        let (event_tx, event_rx) = mpsc::channel(HANDOFF_EVENT_CAPACITY);
123        let previous_route = self
124            .inner
125            .handoff_routes
126            .last_by_id
127            .get(&handoff_id)
128            .and_then(|entry| entry.value().1.upgrade());
129        let _previous_command = match previous_route {
130            Some(route) => Some(route.command_lock.clone().try_lock_owned().map_err(|_| {
131                anyhow!("handoff {handoff_id:?} still has a scheduler command in progress")
132            })?),
133            None => None,
134        };
135        let route = Arc::new(HandoffRoute::new(
136            handoff_id,
137            Arc::downgrade(&self.inner.handoff_routes),
138            event_tx,
139        ));
140        match self.inner.handoff_routes.by_id.entry(handoff_id) {
141            Entry::Occupied(_) => {
142                bail!("handoff {handoff_id:?} already has a lifecycle route")
143            }
144            Entry::Vacant(entry) => {
145                entry.insert(Arc::clone(&route));
146            }
147        }
148        self.inner
149            .handoff_routes
150            .last_by_id
151            .insert(handoff_id, (route.generation, Arc::downgrade(&route)));
152        if self.inner.cancel.is_cancelled() {
153            route.shutdown();
154            remove_handoff_route(&self.inner.handoff_routes, &route);
155            bail!("live Mocker engine is not running");
156        }
157        Ok((
158            LiveHandoffControl {
159                engine: Arc::downgrade(&self.inner),
160                route: Arc::clone(&route),
161            },
162            LiveHandoffEvents {
163                route,
164                routes: Arc::clone(&self.inner.handoff_routes),
165                event_rx,
166            },
167        ))
168    }
169}
170
171/// Typed scheduler controls for one disaggregated handoff.
172#[derive(Clone)]
173pub struct LiveHandoffControl {
174    engine: Weak<LiveEngineInner>,
175    route: Arc<HandoffRoute>,
176}
177
178impl LiveHandoffControl {
179    pub fn handoff_id(&self) -> HandoffId {
180        self.route.handoff_id
181    }
182
183    pub async fn submit_prefill(
184        &self,
185        registration: LiveRequestRegistration,
186    ) -> anyhow::Result<()> {
187        let command_guard = self.route.command_lock.clone().lock_owned().await;
188        let engine = self.engine()?;
189        self.ensure_generation(&engine, false)?;
190        LiveEngine { inner: engine }
191            .submit_prepared(
192                registration,
193                PreparedSubmission::Source(self.route.handoff_id),
194                Some(command_guard),
195            )
196            .await
197    }
198
199    pub async fn reserve_destination(
200        &self,
201        registration: LiveRequestRegistration,
202    ) -> anyhow::Result<()> {
203        let command_guard = self.route.command_lock.clone().lock_owned().await;
204        let engine = self.engine()?;
205        self.ensure_generation(&engine, false)?;
206        LiveEngine { inner: engine }
207            .submit_prepared(
208                registration,
209                PreparedSubmission::Destination(DestinationCancellation {
210                    route: Arc::clone(&self.route),
211                }),
212                Some(command_guard),
213            )
214            .await
215    }
216
217    pub async fn release_source(&self) -> anyhow::Result<()> {
218        self.send_handoff_command(
219            SchedulerCommand::ReleaseSource {
220                handoff_id: self.route.handoff_id,
221            },
222            HandoffCommandResult::AppliedOrNoop,
223        )
224        .await
225    }
226
227    pub async fn cancel_source(&self) -> anyhow::Result<()> {
228        self.send_handoff_command(
229            SchedulerCommand::CancelSource {
230                handoff_id: self.route.handoff_id,
231            },
232            HandoffCommandResult::AppliedOrNoop,
233        )
234        .await
235    }
236
237    pub async fn activate_destination(&self) -> anyhow::Result<()> {
238        self.send_handoff_command(
239            SchedulerCommand::ActivateDestination {
240                handoff_id: self.route.handoff_id,
241            },
242            HandoffCommandResult::Applied,
243        )
244        .await
245    }
246
247    pub async fn cancel_destination(&self) -> anyhow::Result<()> {
248        self.send_handoff_command(
249            SchedulerCommand::CancelDestination {
250                handoff_id: self.route.handoff_id,
251            },
252            HandoffCommandResult::AppliedOrNoop,
253        )
254        .await
255    }
256
257    async fn send_handoff_command(
258        &self,
259        command: SchedulerCommand,
260        expected: HandoffCommandResult,
261    ) -> anyhow::Result<()> {
262        let _command_guard = self.route.command_lock.clone().lock_owned().await;
263        let engine = self.engine()?;
264        self.ensure_generation(&engine, true)?;
265        anyhow::ensure!(
266            !engine.cancel.is_cancelled(),
267            "live Mocker engine is not running"
268        );
269        let result = send_command(&engine.command_tx, command).await?;
270        match (expected, result) {
271            (HandoffCommandResult::Applied, SchedulerCommandResult::Applied)
272            | (
273                HandoffCommandResult::AppliedOrNoop,
274                SchedulerCommandResult::Applied | SchedulerCommandResult::Noop,
275            ) => Ok(()),
276            (_, result) => Err(anyhow!(
277                "unexpected scheduler handoff result for {:?}: {result:?}",
278                self.route.handoff_id
279            )),
280        }
281    }
282
283    fn engine(&self) -> anyhow::Result<Arc<LiveEngineInner>> {
284        self.engine
285            .upgrade()
286            .ok_or_else(|| anyhow!("live Mocker engine no longer exists"))
287    }
288
289    fn ensure_generation(
290        &self,
291        engine: &LiveEngineInner,
292        allow_unregistered: bool,
293    ) -> anyhow::Result<()> {
294        match engine.handoff_routes.by_id.get(&self.route.handoff_id) {
295            Some(current) if Arc::ptr_eq(current.value(), &self.route) => Ok(()),
296            Some(_) => bail!(
297                "handoff {:?} control belongs to an earlier registration",
298                self.route.handoff_id
299            ),
300            None if allow_unregistered
301                && self.route.is_latest_generation(&engine.handoff_routes) =>
302            {
303                Ok(())
304            }
305            None if allow_unregistered => bail!(
306                "handoff {:?} control belongs to an earlier registration",
307                self.route.handoff_id
308            ),
309            None => bail!(
310                "handoff {:?} lifecycle route is no longer registered",
311                self.route.handoff_id
312            ),
313        }
314    }
315}
316
317#[derive(Clone)]
318pub(super) struct DestinationCancellation {
319    route: Arc<HandoffRoute>,
320}
321
322impl DestinationCancellation {
323    pub(super) fn handoff_id(&self) -> HandoffId {
324        self.route.handoff_id
325    }
326
327    pub(super) async fn cancel(
328        &self,
329        command_tx: &mpsc::Sender<crate::scheduler::SchedulerCommandEnvelope>,
330    ) -> anyhow::Result<bool> {
331        let _command_guard = self.route.command_lock.clone().lock_owned().await;
332        let Some(routes) = self.route.routes.upgrade() else {
333            return Ok(false);
334        };
335        if !self.route.is_latest_generation(&routes) {
336            return Ok(false);
337        }
338        super::cancel_destination(command_tx, self.route.handoff_id).await
339    }
340}
341
342#[derive(Clone, Copy)]
343enum HandoffCommandResult {
344    Applied,
345    AppliedOrNoop,
346}
347
348/// Request-owned stream of normalized handoff lifecycle events.
349pub struct LiveHandoffEvents {
350    route: Arc<HandoffRoute>,
351    routes: SharedHandoffRoutes,
352    event_rx: mpsc::Receiver<LiveHandoffEvent>,
353}
354
355impl LiveHandoffEvents {
356    pub fn handoff_id(&self) -> HandoffId {
357        self.route.handoff_id
358    }
359
360    pub async fn recv(&mut self) -> Option<LiveHandoffEvent> {
361        self.event_rx.recv().await
362    }
363}
364
365impl Drop for LiveHandoffEvents {
366    fn drop(&mut self) {
367        self.route.shutdown();
368        remove_handoff_route(&self.routes, &self.route);
369    }
370}
371
372fn remove_handoff_route(routes: &HandoffRoutes, route: &Arc<HandoffRoute>) -> bool {
373    routes
374        .by_id
375        .remove_if(&route.handoff_id, |_, current| Arc::ptr_eq(current, route))
376        .is_some()
377}
378
379pub(super) async fn run_lifecycle_dispatcher(
380    mut lifecycle_rx: mpsc::Receiver<SchedulerLifecycleEvent>,
381    routes: SharedHandoffRoutes,
382    cancel: CancellationToken,
383) -> anyhow::Result<()> {
384    loop {
385        let event = tokio::select! {
386            biased;
387            _ = cancel.cancelled() => return Ok(()),
388            event = lifecycle_rx.recv() => {
389                let Some(event) = event else {
390                    if cancel.is_cancelled() {
391                        return Ok(());
392                    }
393                    bail!("live Mocker lifecycle lane closed unexpectedly");
394                };
395                event
396            }
397        };
398        let (handoff_id, event) = match event {
399            SchedulerLifecycleEvent::SourceHeld {
400                handoff_id,
401                transfer_timing,
402                ..
403            } => (handoff_id, LiveHandoffEvent::SourceHeld { transfer_timing }),
404            SchedulerLifecycleEvent::DestinationReserved {
405                handoff_id,
406                transferable_prompt_tokens,
407                ..
408            } => (
409                handoff_id,
410                LiveHandoffEvent::DestinationReserved {
411                    transferable_prompt_tokens,
412                },
413            ),
414        };
415        let route = routes
416            .by_id
417            .get(&handoff_id)
418            .map(|entry| Arc::clone(entry.value()));
419        if let Some(route) = route
420            && !route.send(event, &cancel).await
421        {
422            remove_handoff_route(&routes, &route);
423        }
424    }
425}
426
427pub(super) async fn supervise_lifecycle_dispatcher(
428    dispatcher: tokio::task::JoinHandle<anyhow::Result<()>>,
429    routes: Routes,
430    handoff_routes: SharedHandoffRoutes,
431    cancel: CancellationToken,
432) -> anyhow::Result<()> {
433    let result = match dispatcher.await {
434        Ok(Ok(())) => Ok(()),
435        Ok(Err(error)) => Err(error),
436        Err(error) => Err(anyhow!(
437            "live Mocker lifecycle dispatcher task failed: {error}"
438        )),
439    };
440    if let Err(error) = &result {
441        tracing::error!(%error, "live Mocker lifecycle dispatcher failed");
442    } else if !cancel.is_cancelled() {
443        tracing::error!("live Mocker lifecycle dispatcher exited unexpectedly");
444    }
445    cancel.cancel();
446    shutdown_routes(&routes);
447    shutdown_handoff_routes(&handoff_routes);
448    result
449}
450
451pub(super) fn shutdown_handoff_routes(routes: &HandoffRoutes) {
452    let active_routes = routes
453        .by_id
454        .iter()
455        .map(|entry| Arc::clone(entry.value()))
456        .collect::<Vec<_>>();
457    for route in active_routes {
458        route.shutdown();
459    }
460    routes.by_id.clear();
461}