Skip to main content

dynamo_mocker/
live.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Reusable live-request boundary for the Mocker schedulers.
5//!
6//! This module owns the common submit, output-demultiplexing, and cancellation
7//! mechanics needed by network-facing mock engines. Submission admission is an
8//! owned operation, while cancellation uses a separate bounded scheduler lane
9//! that remains responsive during a long modeled pass.
10
11use std::num::NonZeroUsize;
12use std::sync::{Arc, Mutex, Weak};
13
14use anyhow::{Context, anyhow, bail};
15use dashmap::mapref::entry::Entry;
16use futures::future::{BoxFuture, FutureExt, Shared};
17use tokio::runtime::Handle;
18use tokio::sync::{mpsc, oneshot, watch};
19use tokio_util::sync::CancellationToken;
20use uuid::Uuid;
21
22use crate::common::handoff::HandoffId;
23use crate::common::protocols::{
24    DirectRequest, FpmPublisher, KvEventPublishers, MockEngineArgs, OutputSignal,
25};
26use crate::engine::{LiveEngineScheduler, create_engine_with_event_sender};
27#[cfg(test)]
28use crate::grouped_scheduler::CompletionBoundaryTestControl;
29use crate::grouped_scheduler::{
30    CompletionBoundaryDrain, GroupedSchedulerRankEventSinks, GroupedSchedulers,
31    create_grouped_scheduler_with_event_senders,
32};
33use crate::scheduler::{
34    LiveEngineEvent, MockerMetrics, SchedulerCancellationEnvelope, SchedulerCommand,
35    SchedulerCommandEnvelope, SchedulerCommandResult, SchedulerEventSender, SchedulerHandle,
36};
37
38mod handoff;
39mod request;
40
41pub use handoff::{LiveHandoffControl, LiveHandoffEvent, LiveHandoffEvents};
42
43use handoff::{
44    DestinationCancellation, HandoffRoutes, SharedHandoffRoutes, run_lifecycle_dispatcher,
45    shutdown_handoff_routes, supervise_lifecycle_dispatcher,
46};
47use request::{
48    ObservedOutput, OutputDelivery, RequestCancellation, RequestRoute, RequestRoutes, Routes,
49    remove_route, route_is_registered, shutdown_routes,
50};
51
52const SCHEDULER_EVENT_CAPACITY: usize = 8;
53const DEFAULT_REQUEST_OUTPUT_CAPACITY: usize = 8;
54
55/// Controls how much output one live request may retain before it is consumed.
56#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57pub enum RequestOutputBuffering {
58    /// Cancel the request when its output queue reaches `capacity`.
59    CancelOnOverflow { capacity: NonZeroUsize },
60    /// Buffer the request's full declared response.
61    FullResponse,
62}
63
64impl Default for RequestOutputBuffering {
65    fn default() -> Self {
66        Self::CancelOnOverflow {
67            capacity: NonZeroUsize::new(DEFAULT_REQUEST_OUTPUT_CAPACITY).unwrap(),
68        }
69    }
70}
71
72impl RequestOutputBuffering {
73    fn capacity_for(self, output_length: usize) -> usize {
74        let output_length = output_length.max(1);
75        match self {
76            Self::CancelOnOverflow { capacity } => output_length.min(capacity.get()),
77            Self::FullResponse => output_length,
78        }
79    }
80}
81
82/// Runtime publishers used by one live Mocker scheduler.
83#[derive(Clone, Default)]
84pub struct LiveEngineConfig {
85    pub kv_event_publishers: KvEventPublishers,
86    pub fpm_publisher: FpmPublisher,
87}
88
89pub(crate) struct ObservedAdmission {
90    pub(crate) event: crate::scheduler::AdmissionEvent,
91    pub(crate) observed_at: tokio::time::Instant,
92}
93
94#[derive(Default)]
95pub(crate) struct LiveEngineOptions {
96    pub(crate) kv_event_publishers: KvEventPublishers,
97    pub(crate) admission_tx: Option<mpsc::UnboundedSender<ObservedAdmission>>,
98    pub(crate) fpm_publisher: FpmPublisher,
99    pub(crate) request_output_buffering: RequestOutputBuffering,
100    pub(crate) allow_zero_output: bool,
101}
102
103/// Map a wire-protocol request ID to a deterministic scheduler UUID.
104pub fn stable_request_uuid(seed: u64, request_id: &str) -> Uuid {
105    let mut hasher = blake3::Hasher::new();
106    hasher.update(&seed.to_le_bytes());
107    hasher.update(request_id.as_bytes());
108    let mut bytes = [0u8; 16];
109    bytes.copy_from_slice(&hasher.finalize().as_bytes()[..16]);
110    // Mark the digest as an RFC 4122 variant/version-4 UUID. The identifier
111    // remains deterministic; these bits only make diagnostics parse cleanly.
112    bytes[6] = (bytes[6] & 0x0f) | 0x40;
113    bytes[8] = (bytes[8] & 0x3f) | 0x80;
114    Uuid::from_bytes(bytes)
115}
116
117/// Produce deterministic, tokenizer-independent output token IDs.
118pub fn deterministic_output_tokens(seed: u64, request_id: &str, count: usize) -> Vec<u32> {
119    (0..count)
120        .map(|position| {
121            let mut hasher = blake3::Hasher::new();
122            hasher.update(&seed.to_le_bytes());
123            hasher.update(request_id.as_bytes());
124            hasher.update(&(position as u64).to_le_bytes());
125            let bytes = hasher.finalize();
126            1_000 + (u32::from_le_bytes(bytes.as_bytes()[..4].try_into().unwrap()) % 31_000)
127        })
128        .collect()
129}
130
131/// A running Mocker scheduler with request-scoped output streams.
132#[derive(Clone)]
133pub struct LiveEngine {
134    inner: Arc<LiveEngineInner>,
135}
136
137struct LiveEngineInner {
138    command_tx: mpsc::Sender<SchedulerCommandEnvelope>,
139    cancellation_tx: mpsc::Sender<SchedulerCancellationEnvelope>,
140    routes: Routes,
141    handoff_routes: SharedHandoffRoutes,
142    metrics_rx: tokio::sync::watch::Receiver<MockerMetrics>,
143    request_output_buffering: RequestOutputBuffering,
144    allow_zero_output: bool,
145    group: Arc<LiveEngineGroup>,
146    cancel: CancellationToken,
147    runtime: Handle,
148    tasks: Mutex<LiveEngineTasks>,
149    // The scheduler's drop guard owns its task lifetime.
150    #[allow(dead_code)]
151    scheduler: Box<dyn SchedulerHandle>,
152}
153
154struct LiveEngineTasks {
155    dispatcher_supervisor: Option<tokio::task::JoinHandle<anyhow::Result<()>>>,
156    lifecycle_supervisor: Option<tokio::task::JoinHandle<anyhow::Result<()>>>,
157    shutdown: Option<SharedShutdown>,
158}
159
160type SharedShutdown = Shared<BoxFuture<'static, Result<(), Arc<str>>>>;
161
162struct LiveEngineGroup {
163    cancel: CancellationToken,
164    actor: Mutex<Option<tokio::task::JoinHandle<anyhow::Result<()>>>>,
165    shutdown: Mutex<Option<SharedShutdown>>,
166    completion_drain: CompletionBoundaryDrain,
167}
168
169impl LiveEngineGroup {
170    fn new(
171        cancel: CancellationToken,
172        actor: tokio::task::JoinHandle<anyhow::Result<()>>,
173        completion_drain: CompletionBoundaryDrain,
174    ) -> Self {
175        Self {
176            cancel,
177            actor: Mutex::new(Some(actor)),
178            shutdown: Mutex::new(None),
179            completion_drain,
180        }
181    }
182
183    fn shutdown(&self) -> SharedShutdown {
184        self.cancel.cancel();
185        let mut shutdown = self.shutdown.lock().unwrap();
186        if let Some(shutdown) = shutdown.as_ref() {
187            return shutdown.clone();
188        }
189        let actor = self.actor.lock().unwrap().take();
190        let future = async move {
191            let Some(actor) = actor else {
192                return Ok(());
193            };
194            match actor.await {
195                Ok(Ok(())) => Ok(()),
196                Ok(Err(error)) => Err(Arc::from(format!(
197                    "live Mocker scheduler failed: {error:#}"
198                ))),
199                Err(error) => Err(Arc::from(format!(
200                    "live Mocker scheduler task failed: {error}"
201                ))),
202            }
203        }
204        .boxed()
205        .shared();
206        *shutdown = Some(future.clone());
207        future
208    }
209}
210
211impl Drop for LiveEngineGroup {
212    fn drop(&mut self) {
213        self.cancel.cancel();
214    }
215}
216
217impl LiveEngine {
218    /// Start one live scheduler at `dp_rank`.
219    pub fn start(args: MockEngineArgs, dp_rank: u32) -> anyhow::Result<Self> {
220        Self::start_internal(args, dp_rank, LiveEngineOptions::default(), None)
221    }
222
223    /// Start one live scheduler with runtime-owned KV and FPM publishers.
224    pub fn start_with_config(
225        args: MockEngineArgs,
226        dp_rank: u32,
227        config: LiveEngineConfig,
228    ) -> anyhow::Result<Self> {
229        Self::start_with_config_and_request_output_buffering(
230            args,
231            dp_rank,
232            config,
233            RequestOutputBuffering::default(),
234        )
235    }
236
237    /// Start one live scheduler with runtime-owned publishers and an explicit
238    /// per-request output buffering policy.
239    pub fn start_with_config_and_request_output_buffering(
240        args: MockEngineArgs,
241        dp_rank: u32,
242        config: LiveEngineConfig,
243        request_output_buffering: RequestOutputBuffering,
244    ) -> anyhow::Result<Self> {
245        Self::start_internal(
246            args,
247            dp_rank,
248            LiveEngineOptions {
249                kv_event_publishers: config.kv_event_publishers,
250                fpm_publisher: config.fpm_publisher,
251                request_output_buffering,
252                ..LiveEngineOptions::default()
253            },
254            None,
255        )
256    }
257
258    /// Start all attention-DP ranks as one logical grouped engine.
259    ///
260    /// Each returned [`LiveEngine`] retains the latest-main rank-scoped request
261    /// and handoff API, while all ranks share one scheduler actor and one
262    /// [`aisimulate_core::engine::generalized::GeneralizedMockerEngine`] barrier.
263    pub fn start_grouped_with_configs(
264        args: MockEngineArgs,
265        configs: Vec<LiveEngineConfig>,
266    ) -> anyhow::Result<Vec<Self>> {
267        Self::start_grouped_with_configs_and_request_output_buffering(
268            args,
269            configs,
270            RequestOutputBuffering::default(),
271        )
272    }
273
274    /// Start all attention-DP ranks with an explicit per-request output
275    /// buffering policy.
276    pub fn start_grouped_with_configs_and_request_output_buffering(
277        args: MockEngineArgs,
278        configs: Vec<LiveEngineConfig>,
279        request_output_buffering: RequestOutputBuffering,
280    ) -> anyhow::Result<Vec<Self>> {
281        let options = configs
282            .into_iter()
283            .map(|config| LiveEngineOptions {
284                kv_event_publishers: config.kv_event_publishers,
285                fpm_publisher: config.fpm_publisher,
286                request_output_buffering,
287                ..LiveEngineOptions::default()
288            })
289            .collect();
290        Self::start_grouped_with_options(args, options, None)
291    }
292
293    pub(crate) fn start_grouped_with_options(
294        args: MockEngineArgs,
295        options: Vec<LiveEngineOptions>,
296        output_gate: Option<watch::Receiver<bool>>,
297    ) -> anyhow::Result<Vec<Self>> {
298        let runtime = Handle::try_current()
299            .context("LiveEngine::start_grouped_with_options requires an active Tokio runtime")?;
300        let args = args
301            .normalized()
302            .context("invalid Mocker engine arguments")?;
303        anyhow::ensure!(
304            options.len() == args.dp_size as usize,
305            "grouped live Mocker requires one options value per DP rank: expected {}, got {}",
306            args.dp_size,
307            options.len()
308        );
309
310        let cancel = CancellationToken::new();
311        let mut event_receivers = Vec::with_capacity(options.len());
312        let mut rank_sinks = Vec::with_capacity(options.len());
313        for options_for_rank in &options {
314            let (event_tx, event_rx) = mpsc::channel(SCHEDULER_EVENT_CAPACITY);
315            rank_sinks.push(GroupedSchedulerRankEventSinks {
316                event_tx: Some(SchedulerEventSender::Ordered {
317                    tx: event_tx,
318                    forward_admissions: options_for_rank.admission_tx.is_some(),
319                    cancel: cancel.clone(),
320                }),
321                kv_event_publishers: options_for_rank.kv_event_publishers.clone(),
322                fpm_publisher: options_for_rank.fpm_publisher.clone(),
323            });
324            event_receivers.push(event_rx);
325        }
326
327        let GroupedSchedulers {
328            schedulers,
329            actor,
330            completion_drain,
331        } = create_grouped_scheduler_with_event_senders(args, rank_sinks, Some(cancel.clone()))?;
332        let group = Arc::new(LiveEngineGroup::new(cancel, actor, completion_drain));
333        schedulers
334            .into_iter()
335            .zip(event_receivers)
336            .zip(options)
337            .map(|((scheduler, event_rx), options)| {
338                Self::from_scheduler(
339                    runtime.clone(),
340                    scheduler,
341                    Arc::clone(&group),
342                    event_rx,
343                    options,
344                    output_gate.clone(),
345                )
346            })
347            .collect()
348    }
349
350    #[cfg(test)]
351    pub(crate) fn start_with_options(
352        args: MockEngineArgs,
353        dp_rank: u32,
354        options: LiveEngineOptions,
355    ) -> anyhow::Result<Self> {
356        Self::start_internal(args, dp_rank, options, None)
357    }
358
359    #[cfg(test)]
360    fn start_with_output_gate(
361        args: MockEngineArgs,
362        dp_rank: u32,
363        output_gate: Option<watch::Receiver<bool>>,
364        request_output_capacity: usize,
365    ) -> anyhow::Result<Self> {
366        let request_output_capacity = NonZeroUsize::new(request_output_capacity)
367            .ok_or_else(|| anyhow!("request output capacity must be greater than 0"))?;
368        Self::start_internal(
369            args,
370            dp_rank,
371            LiveEngineOptions {
372                request_output_buffering: RequestOutputBuffering::CancelOnOverflow {
373                    capacity: request_output_capacity,
374                },
375                ..LiveEngineOptions::default()
376            },
377            output_gate,
378        )
379    }
380
381    fn start_internal(
382        args: MockEngineArgs,
383        dp_rank: u32,
384        options: LiveEngineOptions,
385        output_gate: Option<watch::Receiver<bool>>,
386    ) -> anyhow::Result<Self> {
387        let runtime =
388            Handle::try_current().context("LiveEngine::start requires an active Tokio runtime")?;
389        let args = args
390            .normalized()
391            .context("invalid Mocker engine arguments")?;
392        let group_cancel = CancellationToken::new();
393        let (event_tx, event_rx) = mpsc::channel::<LiveEngineEvent>(SCHEDULER_EVENT_CAPACITY);
394        let forward_admissions = options.admission_tx.is_some();
395        let LiveEngineScheduler {
396            handle: scheduler,
397            actor: scheduler_actor,
398            completion_drain,
399        } = create_engine_with_event_sender(
400            args,
401            dp_rank,
402            Some(SchedulerEventSender::Ordered {
403                tx: event_tx,
404                forward_admissions,
405                cancel: group_cancel.clone(),
406            }),
407            options.kv_event_publishers.clone(),
408            Some(group_cancel.clone()),
409            options.fpm_publisher.clone(),
410        )?;
411        let group = Arc::new(LiveEngineGroup::new(
412            group_cancel,
413            scheduler_actor,
414            completion_drain,
415        ));
416        Self::from_scheduler(runtime, scheduler, group, event_rx, options, output_gate)
417    }
418
419    fn from_scheduler(
420        runtime: Handle,
421        mut scheduler: Box<dyn SchedulerHandle>,
422        group: Arc<LiveEngineGroup>,
423        event_rx: mpsc::Receiver<LiveEngineEvent>,
424        options: LiveEngineOptions,
425        output_gate: Option<watch::Receiver<bool>>,
426    ) -> anyhow::Result<Self> {
427        let cancel = group.cancel.child_token();
428        let command_tx = scheduler.command_sender();
429        let cancellation_tx = scheduler.cancellation_sender();
430        let metrics_rx = scheduler.metrics_receiver();
431        let lifecycle_rx = scheduler
432            .take_lifecycle_receiver()
433            .expect("new live scheduler must expose one lifecycle receiver");
434        let routes = Arc::new(RequestRoutes::default());
435        let handoff_routes = Arc::new(HandoffRoutes::default());
436        let dispatcher = runtime.spawn(run_event_dispatcher(
437            event_rx,
438            Arc::clone(&routes),
439            cancel.clone(),
440            output_gate,
441            options.admission_tx,
442        ));
443        let dispatcher_supervisor = runtime.spawn(supervise_event_dispatcher(
444            dispatcher,
445            Arc::clone(&routes),
446            Arc::clone(&handoff_routes),
447            cancel.clone(),
448        ));
449        let lifecycle_dispatcher = runtime.spawn(run_lifecycle_dispatcher(
450            lifecycle_rx,
451            Arc::clone(&handoff_routes),
452            cancel.clone(),
453        ));
454        let lifecycle_supervisor = runtime.spawn(supervise_lifecycle_dispatcher(
455            lifecycle_dispatcher,
456            Arc::clone(&routes),
457            Arc::clone(&handoff_routes),
458            cancel.clone(),
459        ));
460
461        Ok(Self {
462            inner: Arc::new(LiveEngineInner {
463                command_tx,
464                cancellation_tx,
465                routes,
466                handoff_routes,
467                metrics_rx,
468                request_output_buffering: options.request_output_buffering,
469                allow_zero_output: options.allow_zero_output,
470                group,
471                cancel,
472                runtime,
473                tasks: Mutex::new(LiveEngineTasks {
474                    dispatcher_supervisor: Some(dispatcher_supervisor),
475                    lifecycle_supervisor: Some(lifecycle_supervisor),
476                    shutdown: None,
477                }),
478                scheduler,
479            }),
480        })
481    }
482
483    /// Register a request route without submitting it to the scheduler.
484    ///
485    /// Disaggregated handoff commands consume the returned registration after
486    /// their bootstrap session is ready. Dropping an unused registration
487    /// removes the route and closes the paired response stream.
488    pub fn prepare_request(
489        &self,
490        mut request: DirectRequest,
491    ) -> anyhow::Result<(LiveRequestRegistration, LiveRequest)> {
492        anyhow::ensure!(
493            !self.inner.cancel.is_cancelled(),
494            "live Mocker engine is not running"
495        );
496        let output_length = request.effective_max_output_tokens();
497        anyhow::ensure!(
498            self.inner.allow_zero_output || output_length > 0,
499            "live requests must generate at least one output token"
500        );
501        request.max_output_tokens = output_length;
502        let client_id = request.uuid.unwrap_or_else(Uuid::new_v4);
503        let scheduler_id = Uuid::new_v4();
504        request.uuid = Some(scheduler_id);
505        let output_capacity = self
506            .inner
507            .request_output_buffering
508            .capacity_for(output_length);
509        let (tx, rx) = mpsc::channel(output_capacity);
510        let route = Arc::new(RequestRoute::new(client_id, scheduler_id, tx));
511        match self.inner.routes.by_client.entry(client_id) {
512            Entry::Occupied(_) => bail!("request {client_id} is already active"),
513            Entry::Vacant(entry) => {
514                entry.insert(Arc::clone(&route));
515            }
516        }
517        match self.inner.routes.by_scheduler.entry(scheduler_id) {
518            Entry::Occupied(_) => {
519                remove_route(&self.inner.routes, &route);
520                bail!("internal scheduler request ID collision");
521            }
522            Entry::Vacant(entry) => {
523                entry.insert(Arc::clone(&route));
524            }
525        }
526        if self.inner.cancel.is_cancelled() {
527            route.shutdown();
528            remove_route(&self.inner.routes, &route);
529            bail!("live Mocker engine is not running");
530        }
531
532        let live = LiveRequest {
533            client_id,
534            rx,
535            route: Arc::downgrade(&route),
536            routes: Arc::clone(&self.inner.routes),
537            command_tx: self.inner.command_tx.clone(),
538            cancellation_tx: self.inner.cancellation_tx.clone(),
539            runtime: self.inner.runtime.clone(),
540        };
541        let registration = LiveRequestRegistration {
542            engine: Arc::downgrade(&self.inner),
543            routes: Arc::clone(&self.inner.routes),
544            prepared: Some(PreparedRequest { request, route }),
545        };
546        Ok((registration, live))
547    }
548
549    /// Submit a request and return its scoped output receiver.
550    pub async fn submit(&self, request: DirectRequest) -> anyhow::Result<LiveRequest> {
551        let (registration, live) = self.prepare_request(request)?;
552        self.submit_prepared(registration, PreparedSubmission::Ordinary, None)
553            .await?;
554        Ok(live)
555    }
556
557    async fn submit_prepared(
558        &self,
559        mut registration: LiveRequestRegistration,
560        submission: PreparedSubmission,
561        command_guard: Option<tokio::sync::OwnedMutexGuard<()>>,
562    ) -> anyhow::Result<()> {
563        anyhow::ensure!(
564            !self.inner.cancel.is_cancelled(),
565            "live Mocker engine is not running"
566        );
567        let PreparedRequest { request, route } = registration.take_for(&self.inner)?;
568        let scheduler_id = route.scheduler_id;
569        let client_id = route.client_id;
570        let routes = Arc::clone(&self.inner.routes);
571        let submission_route = Arc::clone(&route);
572        let command_tx = self.inner.command_tx.clone();
573        let task = self.inner.runtime.spawn(async move {
574            let _command_guard = command_guard;
575            let command = submission.command(request);
576            let result = send_command(&command_tx, command).await;
577            let admission = submission.validate(result, client_id, scheduler_id);
578            if admission.is_ok() {
579                submission_route.activate(submission.cancellation());
580            } else {
581                submission_route.shutdown();
582                remove_route(&routes, &submission_route);
583            }
584            admission
585        });
586        match task.await {
587            Ok(result) => result?,
588            Err(error) => {
589                route.shutdown();
590                remove_route(&self.inner.routes, &route);
591                return Err(anyhow!("live Mocker submission task failed: {error}"));
592            }
593        }
594        if self.inner.cancel.is_cancelled() {
595            route.shutdown();
596            remove_route(&self.inner.routes, &route);
597            bail!("live Mocker engine stopped during submission");
598        }
599        Ok(())
600    }
601
602    /// Cancel an active request and wait until the scheduler applies it.
603    pub async fn cancel(&self, request_id: Uuid) -> anyhow::Result<bool> {
604        let Some(route) = self
605            .inner
606            .routes
607            .by_client
608            .get(&request_id)
609            .map(|entry| Arc::clone(entry.value()))
610        else {
611            return Ok(false);
612        };
613        // ID-based cancellation is an Abort boundary: stop forwarding the
614        // response immediately so a backpressured dispatcher cannot delay the
615        // scheduler cancellation acknowledgement.
616        route.abandon_stream();
617        await_cancellation(spawn_cancellation(
618            &self.inner.runtime,
619            self.inner.command_tx.clone(),
620            self.inner.cancellation_tx.clone(),
621            Arc::clone(&self.inner.routes),
622            route,
623            true,
624        ))
625        .await
626    }
627
628    /// Subscribe to live scheduler occupancy and KV metrics.
629    pub fn metrics_receiver(&self) -> tokio::sync::watch::Receiver<MockerMetrics> {
630        self.inner.metrics_rx.clone()
631    }
632
633    /// Number of response streams currently registered with the dispatcher.
634    pub fn active_request_count(&self) -> usize {
635        self.inner.routes.by_client.len()
636    }
637
638    pub(crate) async fn drain_completion_boundary(&self) -> anyhow::Result<()> {
639        self.inner.group.completion_drain.wait().await
640    }
641
642    #[cfg(test)]
643    pub(crate) fn pause_completion_boundary_before_finish(&self) -> CompletionBoundaryTestControl {
644        self.inner.group.completion_drain.pause_before_finish()
645    }
646
647    #[cfg(test)]
648    pub(crate) fn group_is_cancelled(&self) -> bool {
649        self.inner.group.cancel.is_cancelled()
650    }
651
652    pub async fn shutdown(&self) -> anyhow::Result<()> {
653        let group_shutdown = self.inner.group.shutdown();
654        self.inner.cancel.cancel();
655        shutdown_routes(&self.inner.routes);
656        shutdown_handoff_routes(&self.inner.handoff_routes);
657        let shutdown = {
658            let mut tasks = self.inner.tasks.lock().unwrap();
659            if let Some(shutdown) = tasks.shutdown.as_ref() {
660                shutdown.clone()
661            } else {
662                let shutdown = shutdown_engine(
663                    group_shutdown,
664                    tasks.dispatcher_supervisor.take(),
665                    tasks.lifecycle_supervisor.take(),
666                    Arc::clone(&self.inner.routes),
667                    Arc::clone(&self.inner.handoff_routes),
668                )
669                .boxed()
670                .shared();
671                tasks.shutdown = Some(shutdown.clone());
672                shutdown
673            }
674        };
675        shutdown.await.map_err(|error| anyhow!("{error}"))
676    }
677}
678
679async fn shutdown_engine(
680    group_shutdown: SharedShutdown,
681    dispatcher_supervisor: Option<tokio::task::JoinHandle<anyhow::Result<()>>>,
682    lifecycle_supervisor: Option<tokio::task::JoinHandle<anyhow::Result<()>>>,
683    routes: Routes,
684    handoff_routes: SharedHandoffRoutes,
685) -> Result<(), Arc<str>> {
686    let mut first_error = group_shutdown.await.err().map(|error| anyhow!("{error}"));
687    if let Some(dispatcher_supervisor) = dispatcher_supervisor {
688        match dispatcher_supervisor.await {
689            Ok(Ok(())) => {}
690            Ok(Err(error)) if first_error.is_none() => {
691                first_error = Some(error.context("live Mocker event dispatcher failed"))
692            }
693            Err(error) if first_error.is_none() => {
694                first_error = Some(anyhow!("live Mocker dispatcher supervisor failed: {error}"))
695            }
696            Ok(Err(_)) | Err(_) => {}
697        }
698    }
699    if let Some(lifecycle_supervisor) = lifecycle_supervisor {
700        match lifecycle_supervisor.await {
701            Ok(Ok(())) => {}
702            Ok(Err(error)) if first_error.is_none() => {
703                first_error = Some(error.context("live Mocker lifecycle dispatcher failed"))
704            }
705            Err(error) if first_error.is_none() => {
706                first_error = Some(anyhow!(
707                    "live Mocker lifecycle dispatcher supervisor failed: {error}"
708                ))
709            }
710            Ok(Err(_)) | Err(_) => {}
711        }
712    }
713    shutdown_routes(&routes);
714    shutdown_handoff_routes(&handoff_routes);
715    if let Some(error) = first_error {
716        return Err(Arc::from(format!("{error:#}")));
717    }
718    if !routes.by_client.is_empty() || !routes.by_scheduler.is_empty() {
719        return Err(Arc::from("live Mocker shutdown left active request routes"));
720    }
721    if !handoff_routes.is_empty() {
722        return Err(Arc::from("live Mocker shutdown left active handoff routes"));
723    }
724    Ok(())
725}
726
727impl Drop for LiveEngineInner {
728    fn drop(&mut self) {
729        self.cancel.cancel();
730    }
731}
732
733struct PreparedRequest {
734    request: DirectRequest,
735    route: Arc<RequestRoute>,
736}
737
738/// An output route prepared for ordinary or disaggregated scheduler admission.
739pub struct LiveRequestRegistration {
740    engine: Weak<LiveEngineInner>,
741    routes: Routes,
742    prepared: Option<PreparedRequest>,
743}
744
745impl LiveRequestRegistration {
746    fn take_for(&mut self, engine: &Arc<LiveEngineInner>) -> anyhow::Result<PreparedRequest> {
747        let Some(owner) = self.engine.upgrade() else {
748            bail!("live Mocker engine no longer exists");
749        };
750        anyhow::ensure!(
751            Arc::ptr_eq(&owner, engine),
752            "prepared request belongs to a different live Mocker engine"
753        );
754        self.prepared
755            .take()
756            .ok_or_else(|| anyhow!("prepared request was already consumed"))
757    }
758}
759
760impl Drop for LiveRequestRegistration {
761    fn drop(&mut self) {
762        if let Some(prepared) = self.prepared.take() {
763            prepared.route.shutdown();
764            remove_route(&self.routes, &prepared.route);
765        }
766    }
767}
768
769#[derive(Clone)]
770enum PreparedSubmission {
771    Ordinary,
772    Source(HandoffId),
773    Destination(DestinationCancellation),
774}
775
776impl PreparedSubmission {
777    fn cancellation(&self) -> RequestCancellation {
778        match self {
779            Self::Destination(cancellation) => {
780                RequestCancellation::Destination(cancellation.clone())
781            }
782            Self::Ordinary | Self::Source(_) => RequestCancellation::Request,
783        }
784    }
785
786    fn command(&self, request: DirectRequest) -> SchedulerCommand {
787        match self {
788            Self::Ordinary => SchedulerCommand::Submit(request),
789            Self::Source(handoff_id) => SchedulerCommand::SubmitHandoffPrefill {
790                handoff_id: *handoff_id,
791                request,
792            },
793            Self::Destination(cancellation) => SchedulerCommand::ReserveDestination {
794                handoff_id: cancellation.handoff_id(),
795                request,
796            },
797        }
798    }
799
800    fn validate(
801        &self,
802        result: anyhow::Result<SchedulerCommandResult>,
803        client_id: Uuid,
804        scheduler_id: Uuid,
805    ) -> anyhow::Result<()> {
806        match (self, result) {
807            (
808                Self::Ordinary | Self::Source(_),
809                Ok(SchedulerCommandResult::Submitted(submitted)),
810            ) if submitted == scheduler_id => Ok(()),
811            (
812                Self::Destination(_),
813                Ok(SchedulerCommandResult::DestinationAccepted { request_id }),
814            ) if request_id == scheduler_id => Ok(()),
815            (_, Ok(result)) => Err(anyhow!(
816                "unexpected scheduler submit result for {client_id}: {result:?}"
817            )),
818            (_, Err(error)) => Err(error),
819        }
820    }
821}
822
823/// Request-owned stream of Mocker output signals.
824pub struct LiveRequest {
825    client_id: Uuid,
826    rx: mpsc::Receiver<ObservedOutput>,
827    route: Weak<RequestRoute>,
828    routes: Routes,
829    command_tx: mpsc::Sender<SchedulerCommandEnvelope>,
830    cancellation_tx: mpsc::Sender<SchedulerCancellationEnvelope>,
831    runtime: Handle,
832}
833
834impl LiveRequest {
835    pub fn id(&self) -> Uuid {
836        self.client_id
837    }
838
839    pub async fn recv(&mut self) -> Option<OutputSignal> {
840        self.recv_observed().await.map(|output| output.event)
841    }
842
843    pub(crate) async fn recv_observed(&mut self) -> Option<ObservedOutput> {
844        self.rx.recv().await
845    }
846
847    /// Cancel this request and wait for scheduler-side cleanup.
848    pub async fn cancel(self) -> anyhow::Result<bool> {
849        let Some(route) = self.route.upgrade() else {
850            return Ok(false);
851        };
852        route.abandon_stream();
853        await_cancellation(spawn_cancellation(
854            &self.runtime,
855            self.command_tx.clone(),
856            self.cancellation_tx.clone(),
857            Arc::clone(&self.routes),
858            route,
859            true,
860        ))
861        .await
862    }
863}
864
865impl Drop for LiveRequest {
866    fn drop(&mut self) {
867        let Some(route) = self.route.upgrade() else {
868            return;
869        };
870        route.abandon_stream();
871        drop(spawn_cancellation(
872            &self.runtime,
873            self.command_tx.clone(),
874            self.cancellation_tx.clone(),
875            Arc::clone(&self.routes),
876            route,
877            true,
878        ));
879    }
880}
881
882async fn run_event_dispatcher(
883    mut event_rx: mpsc::Receiver<LiveEngineEvent>,
884    routes: Routes,
885    cancel: CancellationToken,
886    mut output_gate: Option<watch::Receiver<bool>>,
887    admission_tx: Option<mpsc::UnboundedSender<ObservedAdmission>>,
888) -> anyhow::Result<()> {
889    let mut pending_event = None;
890    loop {
891        if cancel.is_cancelled() {
892            drop(pending_event.take());
893            while event_rx.recv().await.is_some() {}
894            return Ok(());
895        }
896
897        if matches!(
898            pending_event.as_ref(),
899            Some(LiveEngineEvent::Outputs { .. })
900        ) && output_gate.as_ref().is_some_and(|gate| !*gate.borrow())
901        {
902            let Some(gate) = output_gate.as_mut() else {
903                unreachable!("the output gate was checked above");
904            };
905            tokio::select! {
906                biased;
907                _ = cancel.cancelled() => continue,
908                changed = gate.changed() => {
909                    if changed.is_err() {
910                        bail!("live Mocker output gate closed");
911                    }
912                }
913            }
914            continue;
915        }
916
917        let event = if let Some(event) = pending_event.take() {
918            event
919        } else {
920            tokio::select! {
921                biased;
922                _ = cancel.cancelled() => continue,
923                event = event_rx.recv() => {
924                    let Some(event) = event else {
925                        if cancel.is_cancelled() {
926                            return Ok(());
927                        }
928                        bail!("live Mocker ordered event lane closed unexpectedly");
929                    };
930                    event
931                }
932            }
933        };
934
935        match event {
936            LiveEngineEvent::Admissions(batch) => {
937                dispatch_admission_batch(batch, &routes, admission_tx.as_ref())?;
938            }
939            LiveEngineEvent::Outputs { signals, delivered }
940                if output_gate.as_ref().is_some_and(|gate| !*gate.borrow()) =>
941            {
942                pending_event = Some(LiveEngineEvent::Outputs { signals, delivered });
943            }
944            LiveEngineEvent::Outputs { signals, delivered } => {
945                let Some(failed) = dispatch_output_batch(signals, &routes, &cancel) else {
946                    return Ok(());
947                };
948                // This is the route-delivery boundary, not merely the
949                // scheduler-to-dispatcher enqueue boundary. Failed routes are
950                // returned to the grouped dispatcher, which applies native
951                // cleanup synchronously through `GroupedPassBoundary`.
952                let _ = delivered.send(failed);
953            }
954        }
955    }
956}
957
958fn dispatch_admission_batch(
959    batch: Vec<crate::scheduler::AdmissionEvent>,
960    routes: &Routes,
961    admission_tx: Option<&mpsc::UnboundedSender<ObservedAdmission>>,
962) -> anyhow::Result<()> {
963    let Some(admission_tx) = admission_tx else {
964        return Ok(());
965    };
966    let observed_at = tokio::time::Instant::now();
967    for mut admission in batch {
968        let scheduler_id = admission.uuid;
969        let Some(route) = routes
970            .by_scheduler
971            .get(&scheduler_id)
972            .map(|entry| Arc::clone(entry.value()))
973        else {
974            continue;
975        };
976        admission.uuid = route.client_id;
977        admission_tx
978            .send(ObservedAdmission {
979                event: admission,
980                observed_at,
981            })
982            .map_err(|_| anyhow!("live Mocker admission receiver closed"))?;
983    }
984    Ok(())
985}
986
987async fn supervise_event_dispatcher(
988    dispatcher: tokio::task::JoinHandle<anyhow::Result<()>>,
989    routes: Routes,
990    handoff_routes: SharedHandoffRoutes,
991    cancel: CancellationToken,
992) -> anyhow::Result<()> {
993    let result = match dispatcher.await {
994        Ok(Ok(())) => Ok(()),
995        Ok(Err(error)) => Err(error),
996        Err(error) => Err(anyhow!("live Mocker event dispatcher task failed: {error}")),
997    };
998    if let Err(error) = &result {
999        tracing::error!(%error, "live Mocker event dispatcher failed");
1000    } else if !cancel.is_cancelled() {
1001        tracing::error!("live Mocker event dispatcher exited unexpectedly");
1002    }
1003    cancel.cancel();
1004    shutdown_routes(&routes);
1005    shutdown_handoff_routes(&handoff_routes);
1006    result
1007}
1008
1009fn dispatch_output_batch(
1010    batch: Vec<OutputSignal>,
1011    routes: &Routes,
1012    cancel: &CancellationToken,
1013) -> Option<Vec<OutputSignal>> {
1014    let observed_at = tokio::time::Instant::now();
1015    let mut failed = Vec::new();
1016    for mut signal in batch {
1017        if cancel.is_cancelled() {
1018            return None;
1019        }
1020        let scheduler_signal = signal.clone();
1021        let scheduler_id = signal.uuid;
1022        let terminal = signal.completed;
1023        let Some(route) = routes
1024            .by_scheduler
1025            .get(&scheduler_id)
1026            .map(|entry| Arc::clone(entry.value()))
1027        else {
1028            continue;
1029        };
1030
1031        signal.uuid = route.client_id;
1032        let delivery = route.send_output(ObservedOutput {
1033            event: signal,
1034            observed_at,
1035        });
1036        if delivery != OutputDelivery::Delivered {
1037            let newly_abandoned = route.abandon_stream();
1038            if newly_abandoned && delivery == OutputDelivery::Full {
1039                tracing::debug!(
1040                    client_id = %route.client_id,
1041                    scheduler_id = %route.scheduler_id,
1042                    "cancelling live Mocker request with a full output stream"
1043                );
1044            }
1045            // The scheduler-side sender converts this acknowledgement into
1046            // `OutputClosed`; the grouped completion dispatcher cancels the
1047            // native request before releasing the pass boundary. Retire the
1048            // Dynamo route now so a replacement cannot receive stale output.
1049            route.shutdown();
1050            remove_route(routes, &route);
1051            failed.push(scheduler_signal);
1052            continue;
1053        }
1054        if terminal && route.observe_terminal() {
1055            remove_route(routes, &route);
1056        }
1057    }
1058    Some(failed)
1059}
1060
1061fn spawn_cancellation(
1062    runtime: &Handle,
1063    command_tx: mpsc::Sender<SchedulerCommandEnvelope>,
1064    cancellation_tx: mpsc::Sender<SchedulerCancellationEnvelope>,
1065    routes: Routes,
1066    route: Arc<RequestRoute>,
1067    abandon_stream: bool,
1068) -> tokio::task::JoinHandle<anyhow::Result<bool>> {
1069    runtime.spawn(async move {
1070        if !route.wait_for_admission().await {
1071            return Ok(false);
1072        }
1073
1074        let _cancel_guard = route.cancel_lock.lock().await;
1075        if !route_is_registered(&routes, &route) {
1076            return Ok(false);
1077        }
1078        if abandon_stream {
1079            route.abandon_stream();
1080        }
1081        let Some(cancellation) = route.begin_cancellation() else {
1082            return Ok(false);
1083        };
1084
1085        let result = match cancellation {
1086            RequestCancellation::Request => {
1087                cancel_request(
1088                    &cancellation_tx,
1089                    route.scheduler_id,
1090                    abandon_stream,
1091                )
1092                .await
1093            }
1094            RequestCancellation::Destination(cancellation) => cancellation.cancel(&command_tx).await,
1095        };
1096        if route.finish_cancellation(&result) {
1097            remove_route(&routes, &route);
1098        }
1099        if let Err(error) = &result {
1100            tracing::debug!(client_id = %route.client_id, scheduler_id = %route.scheduler_id, %error, "live Mocker request cancellation failed");
1101        }
1102        result
1103    })
1104}
1105
1106async fn await_cancellation(
1107    cancellation: tokio::task::JoinHandle<anyhow::Result<bool>>,
1108) -> anyhow::Result<bool> {
1109    match cancellation.await {
1110        Ok(result) => result,
1111        Err(error) => Err(anyhow!("live Mocker cancellation task failed: {error}")),
1112    }
1113}
1114
1115async fn cancel_request(
1116    cancellation_tx: &mpsc::Sender<SchedulerCancellationEnvelope>,
1117    request_id: Uuid,
1118    discard_pending_output: bool,
1119) -> anyhow::Result<bool> {
1120    let (reply, response) = oneshot::channel();
1121    cancellation_tx
1122        .send(SchedulerCancellationEnvelope {
1123            request_id,
1124            discard_pending_output,
1125            reply,
1126        })
1127        .await
1128        .map_err(|_| anyhow!("Mocker scheduler is not accepting cancellations"))?;
1129    let effects = response
1130        .await
1131        .map_err(|_| anyhow!("Mocker scheduler dropped a cancellation acknowledgement"))??;
1132    match effects.result {
1133        SchedulerCommandResult::Applied => Ok(true),
1134        SchedulerCommandResult::Noop => Ok(false),
1135        result => Err(anyhow!(
1136            "unexpected scheduler cancellation result for {request_id}: {result:?}"
1137        )),
1138    }
1139}
1140
1141async fn cancel_destination(
1142    command_tx: &mpsc::Sender<SchedulerCommandEnvelope>,
1143    handoff_id: HandoffId,
1144) -> anyhow::Result<bool> {
1145    match send_command(
1146        command_tx,
1147        SchedulerCommand::CancelDestination { handoff_id },
1148    )
1149    .await?
1150    {
1151        SchedulerCommandResult::Applied => Ok(true),
1152        SchedulerCommandResult::Noop => Ok(false),
1153        result => Err(anyhow!(
1154            "unexpected scheduler destination cancellation result for {handoff_id:?}: {result:?}"
1155        )),
1156    }
1157}
1158
1159async fn send_command(
1160    command_tx: &mpsc::Sender<SchedulerCommandEnvelope>,
1161    command: SchedulerCommand,
1162) -> anyhow::Result<SchedulerCommandResult> {
1163    let (reply, response) = oneshot::channel();
1164    command_tx
1165        .send(SchedulerCommandEnvelope { command, reply })
1166        .await
1167        .map_err(|_| anyhow!("Mocker scheduler is not accepting commands"))?;
1168    let effects = response
1169        .await
1170        .map_err(|_| anyhow!("Mocker scheduler dropped a command acknowledgement"))??;
1171    Ok(effects.result)
1172}
1173
1174#[cfg(test)]
1175mod tests;