1use 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 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#[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 #[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 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 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 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 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 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 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 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 pub fn metrics_receiver(&self) -> tokio::sync::watch::Receiver<MockerMetrics> {
493 self.inner.metrics_rx.clone()
494 }
495
496 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
552pub 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 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;