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::DashMap;
16use dashmap::mapref::entry::Entry;
17use tokio::runtime::Handle;
18use tokio::sync::{mpsc, oneshot, watch};
19use tokio_util::sync::CancellationToken;
20use uuid::Uuid;
21
22use crate::common::protocols::{
23    DirectRequest, FpmPublisher, KvEventPublishers, MockEngineArgs, OutputSignal,
24};
25use crate::engine::{LiveEngineScheduler, create_engine_with_event_sender};
26use crate::scheduler::{
27    LiveEngineEvent, MockerMetrics, SchedulerCancellationEnvelope, SchedulerCommand,
28    SchedulerCommandEnvelope, SchedulerCommandResult, SchedulerEventSender, SchedulerHandle,
29};
30
31#[derive(Default)]
32struct RequestRoutes {
33    by_client: DashMap<Uuid, Arc<RequestRoute>>,
34    by_scheduler: DashMap<Uuid, Arc<RequestRoute>>,
35}
36
37type Routes = Arc<RequestRoutes>;
38
39const SCHEDULER_EVENT_CAPACITY: usize = 8;
40const DEFAULT_REQUEST_OUTPUT_CAPACITY: usize = 8;
41
42pub(crate) struct ObservedAdmission {
43    pub(crate) event: crate::scheduler::AdmissionEvent,
44    pub(crate) observed_at: tokio::time::Instant,
45}
46
47pub(crate) struct ObservedOutput {
48    pub(crate) event: OutputSignal,
49    pub(crate) observed_at: tokio::time::Instant,
50}
51
52pub(crate) struct LiveEngineOptions {
53    pub(crate) kv_event_publishers: KvEventPublishers,
54    pub(crate) admission_tx: Option<mpsc::UnboundedSender<ObservedAdmission>>,
55    pub(crate) fpm_publisher: FpmPublisher,
56    pub(crate) request_output_capacity: Option<NonZeroUsize>,
57    pub(crate) allow_zero_output: bool,
58}
59
60impl Default for LiveEngineOptions {
61    fn default() -> Self {
62        Self {
63            kv_event_publishers: KvEventPublishers::default(),
64            admission_tx: None,
65            fpm_publisher: FpmPublisher::default(),
66            request_output_capacity: NonZeroUsize::new(DEFAULT_REQUEST_OUTPUT_CAPACITY),
67            allow_zero_output: false,
68        }
69    }
70}
71
72#[derive(Clone, Copy, Debug, Eq, PartialEq)]
73enum RequestState {
74    Submitting,
75    Active,
76    Cancelling,
77    Closed,
78}
79
80#[derive(Clone, Copy, Debug, Eq, PartialEq)]
81struct RequestLifecycle {
82    state: RequestState,
83    stream_abandoned: bool,
84    terminal_seen: bool,
85}
86
87struct RequestRoute {
88    client_id: Uuid,
89    scheduler_id: Uuid,
90    output_tx: Mutex<Option<mpsc::Sender<ObservedOutput>>>,
91    lifecycle_tx: watch::Sender<RequestLifecycle>,
92    cancel_lock: tokio::sync::Mutex<()>,
93}
94
95impl RequestRoute {
96    fn new(client_id: Uuid, scheduler_id: Uuid, output_tx: mpsc::Sender<ObservedOutput>) -> Self {
97        let (lifecycle_tx, _) = watch::channel(RequestLifecycle {
98            state: RequestState::Submitting,
99            stream_abandoned: false,
100            terminal_seen: false,
101        });
102        Self {
103            client_id,
104            scheduler_id,
105            output_tx: Mutex::new(Some(output_tx)),
106            lifecycle_tx,
107            cancel_lock: tokio::sync::Mutex::new(()),
108        }
109    }
110
111    fn activate(&self) {
112        self.lifecycle_tx.send_if_modified(|lifecycle| {
113            if lifecycle.state != RequestState::Submitting {
114                return false;
115            }
116            lifecycle.state = RequestState::Active;
117            true
118        });
119    }
120
121    fn abandon_stream(&self) -> bool {
122        self.close_output();
123        let mut abandoned = false;
124        self.lifecycle_tx.send_if_modified(|lifecycle| {
125            if lifecycle.stream_abandoned {
126                return false;
127            }
128            lifecycle.stream_abandoned = true;
129            abandoned = true;
130            true
131        });
132        abandoned
133    }
134
135    async fn wait_for_admission(&self) -> bool {
136        let mut lifecycle_rx = self.lifecycle_tx.subscribe();
137        loop {
138            match lifecycle_rx.borrow_and_update().state {
139                RequestState::Submitting | RequestState::Cancelling => {}
140                RequestState::Active => return true,
141                RequestState::Closed => return false,
142            }
143            if lifecycle_rx.changed().await.is_err() {
144                return false;
145            }
146        }
147    }
148
149    fn begin_cancellation(&self) -> bool {
150        let mut started = false;
151        self.lifecycle_tx.send_if_modified(|lifecycle| {
152            if lifecycle.state == RequestState::Active {
153                lifecycle.state = RequestState::Cancelling;
154                started = true;
155                return true;
156            }
157            false
158        });
159        started
160    }
161
162    fn finish_cancellation(&self, result: &anyhow::Result<bool>) -> bool {
163        let mut remove = false;
164        self.lifecycle_tx.send_if_modified(|lifecycle| {
165            if lifecycle.state != RequestState::Cancelling {
166                return false;
167            }
168            remove = match result {
169                Ok(true) => true,
170                Ok(false) => lifecycle.stream_abandoned || lifecycle.terminal_seen,
171                Err(_) => lifecycle.terminal_seen,
172            };
173            lifecycle.state = if remove {
174                RequestState::Closed
175            } else {
176                RequestState::Active
177            };
178            true
179        });
180        if remove {
181            self.close_output();
182        }
183        remove
184    }
185
186    fn send_output(&self, output: ObservedOutput) -> OutputDelivery {
187        let output_tx = self.output_tx.lock().unwrap().as_ref().cloned();
188        let Some(output_tx) = output_tx else {
189            return OutputDelivery::Closed;
190        };
191        match output_tx.try_send(output) {
192            Ok(()) => OutputDelivery::Delivered,
193            Err(mpsc::error::TrySendError::Full(_)) => OutputDelivery::Full,
194            Err(mpsc::error::TrySendError::Closed(_)) => OutputDelivery::Closed,
195        }
196    }
197
198    /// Record a terminal signal and return whether the route can be removed.
199    /// An in-flight cancellation retains it until the scheduler acknowledges
200    /// cleanup; its scheduler ID is never reused by a replacement request.
201    fn observe_terminal(&self) -> bool {
202        self.close_output();
203        let mut remove = false;
204        self.lifecycle_tx.send_if_modified(|lifecycle| {
205            lifecycle.terminal_seen = true;
206            if lifecycle.state != RequestState::Cancelling {
207                lifecycle.state = RequestState::Closed;
208                remove = true;
209            }
210            true
211        });
212        remove
213    }
214
215    fn shutdown(&self) {
216        self.close_output();
217        self.lifecycle_tx.send_if_modified(|lifecycle| {
218            if lifecycle.state == RequestState::Closed {
219                return false;
220            }
221            lifecycle.state = RequestState::Closed;
222            true
223        });
224    }
225
226    fn close_output(&self) {
227        self.output_tx.lock().unwrap().take();
228    }
229}
230
231#[derive(Clone, Copy, Debug, Eq, PartialEq)]
232enum OutputDelivery {
233    Delivered,
234    Full,
235    Closed,
236}
237
238/// A running Mocker scheduler with request-scoped output streams.
239#[derive(Clone)]
240pub struct LiveEngine {
241    inner: Arc<LiveEngineInner>,
242}
243
244struct LiveEngineInner {
245    command_tx: mpsc::Sender<SchedulerCommandEnvelope>,
246    cancellation_tx: mpsc::Sender<SchedulerCancellationEnvelope>,
247    routes: Routes,
248    metrics_rx: tokio::sync::watch::Receiver<MockerMetrics>,
249    request_output_capacity: Option<NonZeroUsize>,
250    allow_zero_output: bool,
251    cancel: CancellationToken,
252    runtime: Handle,
253    tasks: Mutex<LiveEngineTasks>,
254    // The scheduler's drop guard owns its task lifetime.
255    #[allow(dead_code)]
256    scheduler: Box<dyn SchedulerHandle>,
257}
258
259struct LiveEngineTasks {
260    scheduler_actor: Option<tokio::task::JoinHandle<anyhow::Result<()>>>,
261    dispatcher_supervisor: Option<tokio::task::JoinHandle<anyhow::Result<()>>>,
262}
263
264impl LiveEngine {
265    /// Start one live scheduler at `dp_rank`.
266    pub fn start(args: MockEngineArgs, dp_rank: u32) -> anyhow::Result<Self> {
267        Self::start_internal(args, dp_rank, LiveEngineOptions::default(), None)
268    }
269
270    pub(crate) fn start_with_options(
271        args: MockEngineArgs,
272        dp_rank: u32,
273        options: LiveEngineOptions,
274    ) -> anyhow::Result<Self> {
275        Self::start_internal(args, dp_rank, options, None)
276    }
277
278    pub(crate) fn start_with_options_and_output_gate(
279        args: MockEngineArgs,
280        dp_rank: u32,
281        options: LiveEngineOptions,
282        output_gate: watch::Receiver<bool>,
283    ) -> anyhow::Result<Self> {
284        Self::start_internal(args, dp_rank, options, Some(output_gate))
285    }
286
287    #[cfg(test)]
288    fn start_with_output_gate(
289        args: MockEngineArgs,
290        dp_rank: u32,
291        output_gate: Option<watch::Receiver<bool>>,
292        request_output_capacity: usize,
293    ) -> anyhow::Result<Self> {
294        let request_output_capacity = NonZeroUsize::new(request_output_capacity)
295            .ok_or_else(|| anyhow!("request output capacity must be greater than 0"))?;
296        Self::start_internal(
297            args,
298            dp_rank,
299            LiveEngineOptions {
300                request_output_capacity: Some(request_output_capacity),
301                ..LiveEngineOptions::default()
302            },
303            output_gate,
304        )
305    }
306
307    fn start_internal(
308        args: MockEngineArgs,
309        dp_rank: u32,
310        options: LiveEngineOptions,
311        output_gate: Option<watch::Receiver<bool>>,
312    ) -> anyhow::Result<Self> {
313        let runtime =
314            Handle::try_current().context("LiveEngine::start requires an active Tokio runtime")?;
315        let args = args
316            .normalized()
317            .context("invalid Mocker engine arguments")?;
318        let cancel = CancellationToken::new();
319        let (event_tx, event_rx) = mpsc::channel::<LiveEngineEvent>(SCHEDULER_EVENT_CAPACITY);
320        let LiveEngineScheduler {
321            handle: scheduler,
322            actor: scheduler_actor,
323        } = create_engine_with_event_sender(
324            args,
325            dp_rank,
326            Some(SchedulerEventSender::Ordered(event_tx)),
327            options.kv_event_publishers,
328            Some(cancel.clone()),
329            options.fpm_publisher,
330        );
331        let command_tx = scheduler.command_sender();
332        let cancellation_tx = scheduler.cancellation_sender();
333        let metrics_rx = scheduler.metrics_receiver();
334        let routes = Arc::new(RequestRoutes::default());
335        let dispatcher = runtime.spawn(run_event_dispatcher(
336            event_rx,
337            Arc::clone(&routes),
338            cancellation_tx.clone(),
339            runtime.clone(),
340            cancel.clone(),
341            output_gate,
342            options.admission_tx,
343        ));
344        let dispatcher_supervisor = runtime.spawn(supervise_event_dispatcher(
345            dispatcher,
346            Arc::clone(&routes),
347            cancel.clone(),
348        ));
349
350        Ok(Self {
351            inner: Arc::new(LiveEngineInner {
352                command_tx,
353                cancellation_tx,
354                routes,
355                metrics_rx,
356                request_output_capacity: options.request_output_capacity,
357                allow_zero_output: options.allow_zero_output,
358                cancel,
359                runtime,
360                tasks: Mutex::new(LiveEngineTasks {
361                    scheduler_actor: Some(scheduler_actor),
362                    dispatcher_supervisor: Some(dispatcher_supervisor),
363                }),
364                scheduler,
365            }),
366        })
367    }
368
369    /// Submit a request and return its scoped output receiver.
370    pub async fn submit(&self, mut request: DirectRequest) -> anyhow::Result<LiveRequest> {
371        anyhow::ensure!(
372            !self.inner.cancel.is_cancelled(),
373            "live Mocker engine is not running"
374        );
375        // Both scheduler cores treat an explicit token plan as authoritative.
376        // Normalize the effective length while keeping delivery buffering
377        // independent of a caller's declared maximum response length.
378        let output_length = request
379            .output_token_ids
380            .as_ref()
381            .map_or(request.max_output_tokens, Vec::len);
382        anyhow::ensure!(
383            self.inner.allow_zero_output || output_length > 0,
384            "live requests must generate at least one output token"
385        );
386        request.max_output_tokens = output_length;
387        let client_id = request.uuid.unwrap_or_else(Uuid::new_v4);
388        let scheduler_id = Uuid::new_v4();
389        request.uuid = Some(scheduler_id);
390        // The dispatcher never waits on a request stream. A caller that leaves
391        // this fixed queue full is cancelled so it cannot block unrelated
392        // requests or turn its declared response length into admission policy.
393        let output_capacity = self.inner.request_output_capacity.map_or_else(
394            || output_length.max(1),
395            |capacity| output_length.max(1).min(capacity.get()),
396        );
397        let (tx, rx) = mpsc::channel(output_capacity);
398        let route = Arc::new(RequestRoute::new(client_id, scheduler_id, tx));
399        match self.inner.routes.by_client.entry(client_id) {
400            Entry::Occupied(_) => bail!("request {client_id} is already active"),
401            Entry::Vacant(entry) => {
402                entry.insert(Arc::clone(&route));
403            }
404        }
405        match self.inner.routes.by_scheduler.entry(scheduler_id) {
406            Entry::Occupied(_) => {
407                remove_route(&self.inner.routes, &route);
408                bail!("internal scheduler request ID collision");
409            }
410            Entry::Vacant(entry) => {
411                entry.insert(Arc::clone(&route));
412            }
413        }
414        // Own the registration before the first await. The scheduler admission
415        // task survives cancellation of this submit future, so stream-drop
416        // cleanup can wait for admission before using the independent
417        // cancellation lane.
418        let live = LiveRequest {
419            client_id,
420            rx,
421            route: Arc::downgrade(&route),
422            routes: Arc::clone(&self.inner.routes),
423            cancellation_tx: self.inner.cancellation_tx.clone(),
424            runtime: self.inner.runtime.clone(),
425        };
426
427        let routes = Arc::clone(&self.inner.routes);
428        let submission_route = Arc::clone(&route);
429        let command_tx = self.inner.command_tx.clone();
430        let submission = self.inner.runtime.spawn(async move {
431            let result = send_command(&command_tx, SchedulerCommand::Submit(request)).await;
432            let admission = match result {
433                Ok(SchedulerCommandResult::Submitted(submitted)) if submitted == scheduler_id => {
434                    Ok(())
435                }
436                Ok(result) => Err(anyhow!(
437                    "unexpected scheduler submit result for {client_id}: {result:?}"
438                )),
439                Err(error) => Err(error),
440            };
441            if admission.is_ok() {
442                submission_route.activate();
443            } else {
444                submission_route.shutdown();
445                remove_route(&routes, &submission_route);
446            }
447            admission
448        });
449        match submission.await {
450            Ok(result) => result?,
451            Err(error) => {
452                route.shutdown();
453                remove_route(&self.inner.routes, &route);
454                return Err(anyhow!("live Mocker submission task failed: {error}"));
455            }
456        }
457        if self.inner.cancel.is_cancelled() {
458            route.shutdown();
459            remove_route(&self.inner.routes, &route);
460            bail!("live Mocker engine stopped during submission");
461        }
462
463        Ok(live)
464    }
465
466    /// Cancel an active request and wait until the scheduler applies it.
467    pub async fn cancel(&self, request_id: Uuid) -> anyhow::Result<bool> {
468        let Some(route) = self
469            .inner
470            .routes
471            .by_client
472            .get(&request_id)
473            .map(|entry| Arc::clone(entry.value()))
474        else {
475            return Ok(false);
476        };
477        // ID-based cancellation is an Abort boundary: stop forwarding the
478        // response immediately so a backpressured dispatcher cannot delay the
479        // scheduler cancellation acknowledgement.
480        route.abandon_stream();
481        await_cancellation(spawn_cancellation(
482            &self.inner.runtime,
483            self.inner.cancellation_tx.clone(),
484            Arc::clone(&self.inner.routes),
485            route,
486            true,
487        ))
488        .await
489    }
490
491    /// Subscribe to live scheduler occupancy and KV metrics.
492    pub fn metrics_receiver(&self) -> tokio::sync::watch::Receiver<MockerMetrics> {
493        self.inner.metrics_rx.clone()
494    }
495
496    /// Number of response streams currently registered with the dispatcher.
497    pub fn active_request_count(&self) -> usize {
498        self.inner.routes.by_client.len()
499    }
500
501    pub(crate) async fn shutdown(&self) -> anyhow::Result<()> {
502        self.inner.cancel.cancel();
503        shutdown_routes(&self.inner.routes);
504        let (scheduler_actor, dispatcher_supervisor) = {
505            let mut tasks = self.inner.tasks.lock().unwrap();
506            (
507                tasks.scheduler_actor.take(),
508                tasks.dispatcher_supervisor.take(),
509            )
510        };
511
512        let mut first_error = None;
513        if let Some(scheduler_actor) = scheduler_actor {
514            match scheduler_actor.await {
515                Ok(Ok(())) => {}
516                Ok(Err(error)) => first_error = Some(error.context("live Mocker scheduler failed")),
517                Err(error) => {
518                    first_error = Some(anyhow!("live Mocker scheduler task failed: {error}"))
519                }
520            }
521        }
522        if let Some(dispatcher_supervisor) = dispatcher_supervisor {
523            match dispatcher_supervisor.await {
524                Ok(Ok(())) => {}
525                Ok(Err(error)) if first_error.is_none() => {
526                    first_error = Some(error.context("live Mocker event dispatcher failed"))
527                }
528                Err(error) if first_error.is_none() => {
529                    first_error = Some(anyhow!("live Mocker dispatcher supervisor failed: {error}"))
530                }
531                Ok(Err(_)) | Err(_) => {}
532            }
533        }
534        shutdown_routes(&self.inner.routes);
535        if let Some(error) = first_error {
536            return Err(error);
537        }
538        anyhow::ensure!(
539            self.inner.routes.by_client.is_empty() && self.inner.routes.by_scheduler.is_empty(),
540            "live Mocker shutdown left active request routes"
541        );
542        Ok(())
543    }
544}
545
546impl Drop for LiveEngineInner {
547    fn drop(&mut self) {
548        self.cancel.cancel();
549    }
550}
551
552/// Request-owned stream of Mocker output signals.
553pub struct LiveRequest {
554    client_id: Uuid,
555    rx: mpsc::Receiver<ObservedOutput>,
556    route: Weak<RequestRoute>,
557    routes: Routes,
558    cancellation_tx: mpsc::Sender<SchedulerCancellationEnvelope>,
559    runtime: Handle,
560}
561
562impl LiveRequest {
563    pub fn id(&self) -> Uuid {
564        self.client_id
565    }
566
567    pub async fn recv(&mut self) -> Option<OutputSignal> {
568        self.recv_observed().await.map(|output| output.event)
569    }
570
571    pub(crate) async fn recv_observed(&mut self) -> Option<ObservedOutput> {
572        self.rx.recv().await
573    }
574
575    /// Cancel this request and wait for scheduler-side cleanup.
576    pub async fn cancel(self) -> anyhow::Result<bool> {
577        let Some(route) = self.route.upgrade() else {
578            return Ok(false);
579        };
580        route.abandon_stream();
581        await_cancellation(spawn_cancellation(
582            &self.runtime,
583            self.cancellation_tx.clone(),
584            Arc::clone(&self.routes),
585            route,
586            true,
587        ))
588        .await
589    }
590}
591
592impl Drop for LiveRequest {
593    fn drop(&mut self) {
594        let Some(route) = self.route.upgrade() else {
595            return;
596        };
597        route.abandon_stream();
598        drop(spawn_cancellation(
599            &self.runtime,
600            self.cancellation_tx.clone(),
601            Arc::clone(&self.routes),
602            route,
603            true,
604        ));
605    }
606}
607
608fn remove_route(routes: &RequestRoutes, route: &Arc<RequestRoute>) -> bool {
609    let removed = routes
610        .by_client
611        .remove_if(&route.client_id, |_, current| Arc::ptr_eq(current, route))
612        .is_some();
613    routes
614        .by_scheduler
615        .remove_if(&route.scheduler_id, |_, current| {
616            Arc::ptr_eq(current, route)
617        });
618    removed
619}
620
621fn route_is_registered(routes: &RequestRoutes, route: &Arc<RequestRoute>) -> bool {
622    routes
623        .by_client
624        .get(&route.client_id)
625        .is_some_and(|current| Arc::ptr_eq(current.value(), route))
626        && routes
627            .by_scheduler
628            .get(&route.scheduler_id)
629            .is_some_and(|current| Arc::ptr_eq(current.value(), route))
630}
631
632#[allow(clippy::too_many_arguments)]
633async fn run_event_dispatcher(
634    mut event_rx: mpsc::Receiver<LiveEngineEvent>,
635    routes: Routes,
636    cancellation_tx: mpsc::Sender<SchedulerCancellationEnvelope>,
637    runtime: Handle,
638    cancel: CancellationToken,
639    mut output_gate: Option<watch::Receiver<bool>>,
640    admission_tx: Option<mpsc::UnboundedSender<ObservedAdmission>>,
641) -> anyhow::Result<()> {
642    let mut pending_event = None;
643    loop {
644        if cancel.is_cancelled() {
645            drop(pending_event.take());
646            while event_rx.recv().await.is_some() {}
647            return Ok(());
648        }
649
650        if matches!(pending_event.as_ref(), Some(LiveEngineEvent::Outputs(_)))
651            && output_gate.as_ref().is_some_and(|gate| !*gate.borrow())
652        {
653            let Some(gate) = output_gate.as_mut() else {
654                unreachable!("the output gate was checked above");
655            };
656            tokio::select! {
657                biased;
658                _ = cancel.cancelled() => continue,
659                changed = gate.changed() => {
660                    if changed.is_err() {
661                        bail!("live Mocker output gate closed");
662                    }
663                }
664            }
665            continue;
666        }
667
668        let event = if let Some(event) = pending_event.take() {
669            event
670        } else {
671            tokio::select! {
672                biased;
673                _ = cancel.cancelled() => continue,
674                event = event_rx.recv() => {
675                    let Some(event) = event else {
676                        if cancel.is_cancelled() {
677                            return Ok(());
678                        }
679                        bail!("live Mocker ordered event lane closed unexpectedly");
680                    };
681                    event
682                }
683            }
684        };
685
686        match event {
687            LiveEngineEvent::Admissions(batch) => {
688                dispatch_admission_batch(batch, &routes, admission_tx.as_ref())?;
689            }
690            LiveEngineEvent::Outputs(batch)
691                if output_gate.as_ref().is_some_and(|gate| !*gate.borrow()) =>
692            {
693                pending_event = Some(LiveEngineEvent::Outputs(batch));
694            }
695            LiveEngineEvent::Outputs(batch) => {
696                if !dispatch_output_batch(batch, &routes, &runtime, &cancellation_tx, &cancel) {
697                    return Ok(());
698                }
699            }
700        }
701    }
702}
703
704fn dispatch_admission_batch(
705    batch: Vec<crate::scheduler::AdmissionEvent>,
706    routes: &Routes,
707    admission_tx: Option<&mpsc::UnboundedSender<ObservedAdmission>>,
708) -> anyhow::Result<()> {
709    let Some(admission_tx) = admission_tx else {
710        return Ok(());
711    };
712    let observed_at = tokio::time::Instant::now();
713    for mut admission in batch {
714        let scheduler_id = admission.uuid;
715        let Some(route) = routes
716            .by_scheduler
717            .get(&scheduler_id)
718            .map(|entry| Arc::clone(entry.value()))
719        else {
720            continue;
721        };
722        admission.uuid = route.client_id;
723        admission_tx
724            .send(ObservedAdmission {
725                event: admission,
726                observed_at,
727            })
728            .map_err(|_| anyhow!("live Mocker admission receiver closed"))?;
729    }
730    Ok(())
731}
732
733async fn supervise_event_dispatcher(
734    dispatcher: tokio::task::JoinHandle<anyhow::Result<()>>,
735    routes: Routes,
736    cancel: CancellationToken,
737) -> anyhow::Result<()> {
738    let result = match dispatcher.await {
739        Ok(Ok(())) => Ok(()),
740        Ok(Err(error)) => Err(error),
741        Err(error) => Err(anyhow!("live Mocker event dispatcher task failed: {error}")),
742    };
743    if let Err(error) = &result {
744        tracing::error!(%error, "live Mocker event dispatcher failed");
745    } else if !cancel.is_cancelled() {
746        tracing::error!("live Mocker event dispatcher exited unexpectedly");
747    }
748    cancel.cancel();
749    shutdown_routes(&routes);
750    result
751}
752
753fn shutdown_routes(routes: &RequestRoutes) {
754    let active_routes = routes
755        .by_client
756        .iter()
757        .map(|entry| Arc::clone(entry.value()))
758        .collect::<Vec<_>>();
759    for route in active_routes {
760        route.shutdown();
761    }
762    routes.by_client.clear();
763    routes.by_scheduler.clear();
764}
765
766fn dispatch_output_batch(
767    batch: Vec<OutputSignal>,
768    routes: &Routes,
769    runtime: &Handle,
770    cancellation_tx: &mpsc::Sender<SchedulerCancellationEnvelope>,
771    cancel: &CancellationToken,
772) -> bool {
773    let observed_at = tokio::time::Instant::now();
774    for mut signal in batch {
775        if cancel.is_cancelled() {
776            return false;
777        }
778        let scheduler_id = signal.uuid;
779        let terminal = signal.completed;
780        let Some(route) = routes
781            .by_scheduler
782            .get(&scheduler_id)
783            .map(|entry| Arc::clone(entry.value()))
784        else {
785            continue;
786        };
787
788        signal.uuid = route.client_id;
789        let delivery = route.send_output(ObservedOutput {
790            event: signal,
791            observed_at,
792        });
793        if delivery != OutputDelivery::Delivered && route.abandon_stream() {
794            if delivery == OutputDelivery::Full {
795                tracing::debug!(
796                    client_id = %route.client_id,
797                    scheduler_id = %route.scheduler_id,
798                    "cancelling live Mocker request with a full output stream"
799                );
800            }
801            drop(spawn_cancellation(
802                runtime,
803                cancellation_tx.clone(),
804                Arc::clone(routes),
805                Arc::clone(&route),
806                true,
807            ));
808        }
809        if terminal && route.observe_terminal() {
810            remove_route(routes, &route);
811        }
812    }
813    true
814}
815
816fn spawn_cancellation(
817    runtime: &Handle,
818    cancellation_tx: mpsc::Sender<SchedulerCancellationEnvelope>,
819    routes: Routes,
820    route: Arc<RequestRoute>,
821    abandon_stream: bool,
822) -> tokio::task::JoinHandle<anyhow::Result<bool>> {
823    runtime.spawn(async move {
824        if !route.wait_for_admission().await {
825            return Ok(false);
826        }
827
828        let _cancel_guard = route.cancel_lock.lock().await;
829        if !route_is_registered(&routes, &route) {
830            return Ok(false);
831        }
832        if abandon_stream {
833            route.abandon_stream();
834        }
835        if !route.begin_cancellation() {
836            return Ok(false);
837        }
838
839        let result = cancel_request(
840            &cancellation_tx,
841            route.scheduler_id,
842            abandon_stream,
843        )
844        .await;
845        if route.finish_cancellation(&result) {
846            remove_route(&routes, &route);
847        }
848        if let Err(error) = &result {
849            tracing::debug!(client_id = %route.client_id, scheduler_id = %route.scheduler_id, %error, "live Mocker request cancellation failed");
850        }
851        result
852    })
853}
854
855async fn await_cancellation(
856    cancellation: tokio::task::JoinHandle<anyhow::Result<bool>>,
857) -> anyhow::Result<bool> {
858    match cancellation.await {
859        Ok(result) => result,
860        Err(error) => Err(anyhow!("live Mocker cancellation task failed: {error}")),
861    }
862}
863
864async fn cancel_request(
865    cancellation_tx: &mpsc::Sender<SchedulerCancellationEnvelope>,
866    request_id: Uuid,
867    discard_pending_output: bool,
868) -> anyhow::Result<bool> {
869    let (reply, response) = oneshot::channel();
870    cancellation_tx
871        .send(SchedulerCancellationEnvelope {
872            request_id,
873            discard_pending_output,
874            reply,
875        })
876        .await
877        .map_err(|_| anyhow!("Mocker scheduler is not accepting cancellations"))?;
878    let effects = response
879        .await
880        .map_err(|_| anyhow!("Mocker scheduler dropped a cancellation acknowledgement"))??;
881    match effects.result {
882        SchedulerCommandResult::Applied => Ok(true),
883        SchedulerCommandResult::Noop => Ok(false),
884        result => Err(anyhow!(
885            "unexpected scheduler cancellation result for {request_id}: {result:?}"
886        )),
887    }
888}
889
890async fn send_command(
891    command_tx: &mpsc::Sender<SchedulerCommandEnvelope>,
892    command: SchedulerCommand,
893) -> anyhow::Result<SchedulerCommandResult> {
894    let (reply, response) = oneshot::channel();
895    command_tx
896        .send(SchedulerCommandEnvelope { command, reply })
897        .await
898        .map_err(|_| anyhow!("Mocker scheduler is not accepting commands"))?;
899    let effects = response
900        .await
901        .map_err(|_| anyhow!("Mocker scheduler dropped a command acknowledgement"))??;
902    Ok(effects.result)
903}
904
905#[cfg(test)]
906mod tests;