Skip to main content

eredu_runtime/
realtime_session.rs

1//! Backend-neutral realtime session ownership over the singular fair scheduler.
2
3use std::{
4    num::NonZeroUsize,
5    sync::atomic::{AtomicU64, Ordering},
6    time::Instant,
7};
8
9use eredu_core::{
10    consensus::{validate_ranked_identity_bounded, BoundedConsensusTransport},
11    scheduler::{
12        RequestId, RequestStatus, Scheduler, SchedulerCapabilities, SchedulerError,
13        SchedulerLimits, SchedulerProgress, SchedulerReport, SemanticStateTransaction,
14        TransitionOutput, WorkId,
15    },
16    BoundedCompletionWait, Completion, ParallelTopology, RealtimeInputFrame, RealtimeSampling,
17    RealtimeSpeechConfig,
18};
19use sha2::{Digest, Sha256};
20
21use crate::{
22    CommunicationCompletionPolicy, LayerWeightResidency, RealtimeGenerationBranch,
23    RealtimeGenerationState, RealtimeGenerationTransactionError, RealtimeIdentity,
24    RealtimeIngressContract, RealtimePayloadContract, RealtimePayloadGeneration,
25    RealtimePayloadOwnerIdentity, SelectedRealtimeRealization,
26};
27
28static NEXT_REALTIME_INCARNATION: AtomicU64 = AtomicU64::new(1);
29
30/// Exact selected model identity bound to every session in one scheduler.
31#[derive(Debug, Clone, Eq, PartialEq)]
32pub struct RealtimeModelSessionIdentity {
33    selected: Option<Box<SelectedRealtimeRealization>>,
34    architecture: RealtimeIdentity,
35    source: RealtimeIdentity,
36    execution: RealtimeIdentity,
37    schedule_identity: RealtimeIdentity,
38    schedule: RealtimeSpeechConfig,
39    state_layout: RealtimeIdentity,
40    topology_policy: RealtimeIdentity,
41    topology: ParallelTopology,
42    rank: usize,
43    residency: LayerWeightResidency,
44    completion: CommunicationCompletionPolicy,
45}
46
47impl RealtimeModelSessionIdentity {
48    /// Derives exact session identity exclusively from one neutral selected realization.
49    pub fn from_selected(selected: &SelectedRealtimeRealization) -> Self {
50        Self {
51            selected: Some(Box::new(selected.clone())),
52            architecture: selected.requirements().architecture().clone(),
53            source: selected.source().clone(),
54            execution: selected.execution().clone(),
55            schedule_identity: selected.requirements().speech_schedule_identity().clone(),
56            schedule: selected.requirements().speech_schedule().clone(),
57            state_layout: selected.requirements().state_layout_identity().clone(),
58            topology_policy: selected.requirements().topology().identity().clone(),
59            topology: selected.topology(),
60            rank: selected.rank(),
61            residency: selected.residency(),
62            completion: selected.completion(),
63        }
64    }
65
66    #[cfg(test)]
67    #[allow(clippy::too_many_arguments)]
68    fn from_parts(
69        architecture: RealtimeIdentity,
70        source: RealtimeIdentity,
71        execution: RealtimeIdentity,
72        schedule_identity: RealtimeIdentity,
73        schedule: RealtimeSpeechConfig,
74        state_layout: RealtimeIdentity,
75        topology_policy: RealtimeIdentity,
76        topology: ParallelTopology,
77        rank: usize,
78        residency: LayerWeightResidency,
79        completion: CommunicationCompletionPolicy,
80    ) -> Self {
81        Self {
82            selected: None,
83            architecture,
84            source,
85            execution,
86            schedule_identity,
87            schedule,
88            state_layout,
89            topology_policy,
90            topology,
91            rank,
92            residency,
93            completion,
94        }
95    }
96
97    /// Exact portable schedule selected for this model.
98    pub const fn schedule(&self) -> &RealtimeSpeechConfig {
99        &self.schedule
100    }
101
102    /// Exact selected execution topology.
103    pub const fn topology(&self) -> ParallelTopology {
104        self.topology
105    }
106
107    /// Selected bounded completion policy for topology-wide coordination.
108    pub const fn completion(&self) -> CommunicationCompletionPolicy {
109        self.completion
110    }
111
112    /// Derives a typed model-owner identity without backend names or objects.
113    pub fn model_owner(&self) -> RealtimeModelOwnerIdentity {
114        RealtimeModelOwnerIdentity(self.clone())
115    }
116
117    fn distributed_consensus_identity(&self) -> [u32; 8] {
118        let mut digest = Sha256::new();
119        for identity in [
120            &self.architecture,
121            &self.source,
122            &self.execution,
123            &self.schedule_identity,
124            &self.topology_policy,
125        ] {
126            let bytes = identity.as_str().as_bytes();
127            digest.update(
128                u64::try_from(bytes.len())
129                    .expect("identity byte length fits u64")
130                    .to_le_bytes(),
131            );
132            digest.update(bytes);
133        }
134        for value in [
135            format!("{:?}", self.schedule),
136            format!("{:?}", self.topology),
137            format!("{:?}", self.residency),
138            format!("{:?}", self.completion),
139        ] {
140            digest.update(
141                u64::try_from(value.len())
142                    .expect("identity component length fits u64")
143                    .to_le_bytes(),
144            );
145            digest.update(value.as_bytes());
146        }
147        if let Some(selected) = &self.selected {
148            for value in [
149                format!("{:?}", selected.state()),
150                format!("{:?}", selected.observations()),
151            ] {
152                digest.update(
153                    u64::try_from(value.len())
154                        .expect("selected identity component length fits u64")
155                        .to_le_bytes(),
156                );
157                digest.update(value.as_bytes());
158            }
159        }
160        let digest = digest.finalize();
161        std::array::from_fn(|index| {
162            let start = index * 4;
163            u32::from_le_bytes(
164                digest[start..start + 4]
165                    .try_into()
166                    .expect("SHA-256 has eight complete u32 words"),
167            )
168        })
169    }
170}
171
172/// Typed model owner derived from an exact neutral selected contract.
173#[derive(Debug, Clone, Eq, PartialEq)]
174pub struct RealtimeModelOwnerIdentity(RealtimeModelSessionIdentity);
175
176impl RealtimeModelOwnerIdentity {
177    /// Exact neutral model/session identity represented by this owner.
178    pub const fn session_identity(&self) -> &RealtimeModelSessionIdentity {
179        &self.0
180    }
181}
182
183/// Monotonically allocated identity for one newly registered session.
184#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
185pub struct RealtimeSessionIncarnation(u64);
186
187impl RealtimeSessionIncarnation {
188    /// Monotonic process-local incarnation value.
189    pub const fn value(self) -> u64 {
190        self.0
191    }
192}
193
194/// History-generation identity preserved across release and resume.
195#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
196pub struct RealtimeHistoryGeneration(u64);
197
198impl RealtimeHistoryGeneration {
199    /// Monotonic process-local history-generation value.
200    pub const fn value(self) -> u64 {
201        self.0
202    }
203}
204
205/// Canonical state for one fair-scheduler realtime request.
206pub struct RealtimeSessionState<M, S, R, C> {
207    model: RealtimeModelSessionIdentity,
208    owner: RealtimeModelOwnerIdentity,
209    incarnation: RealtimeSessionIncarnation,
210    history_generation: RealtimeHistoryGeneration,
211    committed_batch: Option<NonZeroUsize>,
212    generation: RealtimeGenerationState<M, S, R, C>,
213}
214
215impl<M, S, R, C> RealtimeSessionState<M, S, R, C> {
216    /// Exact selected model identity.
217    pub const fn model_identity(&self) -> &RealtimeModelSessionIdentity {
218        &self.model
219    }
220
221    /// Typed model owner derived from the selected contract.
222    pub const fn model_owner(&self) -> &RealtimeModelOwnerIdentity {
223        &self.owner
224    }
225
226    /// Session incarnation allocated at first registration.
227    pub const fn incarnation(&self) -> RealtimeSessionIncarnation {
228        self.incarnation
229    }
230
231    /// Coordinate-history generation preserved by release/resume.
232    pub const fn history_generation(&self) -> RealtimeHistoryGeneration {
233        self.history_generation
234    }
235
236    /// Batch committed by the first successful frame, if any.
237    pub const fn committed_batch(&self) -> Option<NonZeroUsize> {
238        self.committed_batch
239    }
240
241    /// Derives the exact payload contract for the committed session batch.
242    pub fn payload_contract(
243        &self,
244        ingress: &RealtimeIngressContract,
245    ) -> Result<RealtimePayloadContract, RealtimeSessionExecutionError> {
246        if ingress.schedule() != self.model.schedule() {
247            return Err(RealtimeSessionExecutionError::IngressScheduleMismatch);
248        }
249        let batch = self
250            .committed_batch
251            .ok_or(RealtimeSessionExecutionError::BatchNotAdmitted)?;
252        Ok(RealtimePayloadContract::new(
253            ingress.schedule().clone(),
254            batch.get(),
255            ingress.text_domain(),
256            ingress.audio_domain(),
257            RealtimePayloadGeneration::new(self.history_generation.value())
258                .expect("scheduler history generations are nonzero"),
259            RealtimePayloadOwnerIdentity::new(self.incarnation.value())
260                .expect("scheduler session incarnations are nonzero"),
261        )
262        .expect("scheduler-admitted payload contract has a positive batch"))
263    }
264
265    /// Canonical atomic generation state.
266    pub const fn generation(&self) -> &RealtimeGenerationState<M, S, R, C> {
267        &self.generation
268    }
269
270    /// Mutably borrows generation state while the scheduler proves the request idle.
271    pub fn generation_mut(&mut self) -> &mut RealtimeGenerationState<M, S, R, C> {
272        &mut self.generation
273    }
274}
275
276/// Unpublished realtime session branch owned by the core scheduler.
277pub struct RealtimeSessionBranch<MB, S, R, C> {
278    model: RealtimeModelSessionIdentity,
279    owner: RealtimeModelOwnerIdentity,
280    incarnation: RealtimeSessionIncarnation,
281    history_generation: RealtimeHistoryGeneration,
282    committed_batch: Option<NonZeroUsize>,
283    generation: RealtimeGenerationBranch<MB, S, R, C>,
284}
285
286impl<MB, S, R, C> RealtimeSessionBranch<MB, S, R, C> {
287    /// Exact selected model identity.
288    pub const fn model_identity(&self) -> &RealtimeModelSessionIdentity {
289        &self.model
290    }
291
292    /// Typed model owner used by payload contracts.
293    pub const fn model_owner(&self) -> &RealtimeModelOwnerIdentity {
294        &self.owner
295    }
296
297    /// Exact session incarnation.
298    pub const fn incarnation(&self) -> RealtimeSessionIncarnation {
299        self.incarnation
300    }
301
302    /// Exact coordinate-history generation.
303    pub const fn history_generation(&self) -> RealtimeHistoryGeneration {
304        self.history_generation
305    }
306
307    /// Batch admitted by this unpublished branch, including its current frame.
308    pub const fn committed_batch(&self) -> Option<NonZeroUsize> {
309        self.committed_batch
310    }
311
312    /// Derives the exact payload contract after scheduler batch admission.
313    pub fn payload_contract(
314        &self,
315        ingress: &RealtimeIngressContract,
316    ) -> Result<RealtimePayloadContract, RealtimeSessionExecutionError> {
317        if ingress.schedule() != self.model.schedule() {
318            return Err(RealtimeSessionExecutionError::IngressScheduleMismatch);
319        }
320        let batch = self
321            .committed_batch
322            .ok_or(RealtimeSessionExecutionError::BatchNotAdmitted)?;
323        Ok(RealtimePayloadContract::new(
324            ingress.schedule().clone(),
325            batch.get(),
326            ingress.text_domain(),
327            ingress.audio_domain(),
328            RealtimePayloadGeneration::new(self.history_generation.value())
329                .expect("scheduler history generations are nonzero"),
330            RealtimePayloadOwnerIdentity::new(self.incarnation.value())
331                .expect("scheduler session incarnations are nonzero"),
332        )
333        .expect("scheduler-admitted payload contract has a positive batch"))
334    }
335
336    /// Unpublished generation branch passed to the injected executor.
337    pub fn generation_mut(&mut self) -> &mut RealtimeGenerationBranch<MB, S, R, C> {
338        &mut self.generation
339    }
340
341    fn admit_batch(&mut self, batch: usize) -> Result<(), RealtimeSessionExecutionError> {
342        let batch = NonZeroUsize::new(batch).ok_or(RealtimeSessionExecutionError::EmptyBatch)?;
343        match self.committed_batch {
344            Some(committed) if committed != batch => Err(RealtimeSessionExecutionError::Batch {
345                committed: committed.get(),
346                submitted: batch.get(),
347            }),
348            Some(_) => Ok(()),
349            None => {
350                self.committed_batch = Some(batch);
351                Ok(())
352            }
353        }
354    }
355}
356
357impl<M, S, R, C> SemanticStateTransaction for RealtimeSessionState<M, S, R, C>
358where
359    M: SemanticStateTransaction,
360    M::Error: 'static,
361    S: Clone,
362    R: Clone,
363    C: Completion,
364{
365    type Branch = RealtimeSessionBranch<M::Branch, S, R, C>;
366    type Error = RealtimeSessionTransactionError<M::Error, C::Error>;
367
368    fn branch(&self) -> Result<Self::Branch, Self::Error> {
369        Ok(RealtimeSessionBranch {
370            model: self.model.clone(),
371            owner: self.owner.clone(),
372            incarnation: self.incarnation,
373            history_generation: self.history_generation,
374            committed_batch: self.committed_batch,
375            generation: self
376                .generation
377                .branch()
378                .map_err(RealtimeSessionTransactionError::Generation)?,
379        })
380    }
381
382    fn commit_branch(&mut self, branch: Self::Branch) -> Result<(), Self::Error> {
383        if self.model != branch.model
384            || self.owner != branch.owner
385            || self.incarnation != branch.incarnation
386            || self.history_generation != branch.history_generation
387        {
388            Self::discard_branch(branch)?;
389            return Err(RealtimeSessionTransactionError::IdentityMismatch);
390        }
391        if let (Some(committed), Some(submitted)) = (self.committed_batch, branch.committed_batch) {
392            if committed != submitted {
393                Self::discard_branch(branch)?;
394                return Err(RealtimeSessionTransactionError::BatchMismatch);
395            }
396        }
397        self.generation
398            .commit_branch(branch.generation)
399            .map_err(RealtimeSessionTransactionError::Generation)?;
400        self.committed_batch = branch.committed_batch;
401        Ok(())
402    }
403
404    fn discard_branch(branch: Self::Branch) -> Result<(), Self::Error> {
405        RealtimeGenerationState::<M, S, R, C>::discard_branch(branch.generation)
406            .map_err(RealtimeSessionTransactionError::Generation)
407    }
408
409    fn permits_parallel_branches(&self) -> bool {
410        false
411    }
412}
413
414/// Released canonical state which can resume only under the exact model identity.
415pub struct ReleasedRealtimeSession<M, S, R, C> {
416    state: RealtimeSessionState<M, S, R, C>,
417}
418
419impl<M, S, R, C> ReleasedRealtimeSession<M, S, R, C> {
420    /// Exact selected model identity required for resumption.
421    pub const fn model_identity(&self) -> &RealtimeModelSessionIdentity {
422        self.state.model_identity()
423    }
424
425    /// Preserved session incarnation.
426    pub const fn incarnation(&self) -> RealtimeSessionIncarnation {
427        self.state.incarnation()
428    }
429
430    /// Batch committed by the first successful frame, if any.
431    pub const fn committed_batch(&self) -> Option<NonZeroUsize> {
432        self.state.committed_batch()
433    }
434}
435
436/// Singular fair scheduler for one exact selected realtime model.
437pub struct RealtimeSessionScheduler<M, S, R, C, O>
438where
439    M: SemanticStateTransaction,
440    M::Error: 'static,
441    S: Clone,
442    R: Clone,
443    C: Completion,
444    O: TransitionOutput,
445{
446    model: RealtimeModelSessionIdentity,
447    scheduler: Scheduler<RealtimeInputFrame, RealtimeSessionState<M, S, R, C>, O>,
448}
449
450impl<M, S, R, C, O> RealtimeSessionScheduler<M, S, R, C, O>
451where
452    M: SemanticStateTransaction,
453    M::Error: 'static,
454    S: Clone,
455    R: Clone,
456    C: Completion,
457    O: TransitionOutput,
458{
459    /// Exact selected model identity shared by every admitted request.
460    pub const fn model_identity(&self) -> &RealtimeModelSessionIdentity {
461        &self.model
462    }
463
464    /// Creates one scheduler for both single-request and concurrent production use.
465    pub fn new(
466        model: RealtimeModelSessionIdentity,
467        limits: SchedulerLimits,
468    ) -> Result<Self, SchedulerError> {
469        Ok(Self {
470            model,
471            scheduler: Scheduler::new(limits)?,
472        })
473    }
474
475    /// Registers a new canonical session with a fresh monotonic incarnation.
476    pub fn register(
477        &mut self,
478        request: RequestId,
479        generation: RealtimeGenerationState<M, S, R, C>,
480    ) -> Result<RealtimeSessionIncarnation, RealtimeSessionError> {
481        if generation.schedule_state().schedule() != self.model.schedule() {
482            return Err(RealtimeSessionError::ScheduleMismatch);
483        }
484        self.scheduler.validate_registration(request)?;
485        let incarnation = allocate_incarnation()?;
486        let state = RealtimeSessionState {
487            model: self.model.clone(),
488            owner: self.model.model_owner(),
489            incarnation,
490            history_generation: RealtimeHistoryGeneration(incarnation.0),
491            committed_batch: None,
492            generation,
493        };
494        self.scheduler.register(request, state)?;
495        Ok(incarnation)
496    }
497
498    /// Resumes released state only under the exact selected model identity.
499    pub fn resume(
500        &mut self,
501        request: RequestId,
502        released: ReleasedRealtimeSession<M, S, R, C>,
503    ) -> Result<(), RealtimeSessionResumeError<M, S, R, C>> {
504        if released.state.model != self.model {
505            return Err(RealtimeSessionResumeError {
506                reason: RealtimeSessionError::ModelIdentityMismatch,
507                released: Box::new(released),
508            });
509        }
510        if let Err(error) = self.scheduler.validate_registration(request) {
511            return Err(RealtimeSessionResumeError {
512                reason: RealtimeSessionError::Scheduler(error),
513                released: Box::new(released),
514            });
515        }
516        self.scheduler
517            .register(request, released.state)
518            .expect("prevalidated realtime resumption cannot fail registration");
519        Ok(())
520    }
521
522    /// Enqueues one portable frame on the singular fair path.
523    pub fn enqueue(
524        &mut self,
525        request: RequestId,
526        frame: RealtimeInputFrame,
527    ) -> Result<WorkId, SchedulerError> {
528        self.scheduler.enqueue(request, frame)
529    }
530
531    /// Enqueues one portable frame with an absolute deadline.
532    pub fn enqueue_with_deadline(
533        &mut self,
534        request: RequestId,
535        frame: RealtimeInputFrame,
536        deadline: Option<Instant>,
537    ) -> Result<WorkId, SchedulerError> {
538        self.scheduler
539            .enqueue_with_deadline(request, frame, deadline)
540    }
541
542    /// Atomically enqueues ordered frames on one request.
543    pub fn enqueue_batch(
544        &mut self,
545        request: RequestId,
546        frames: Vec<RealtimeInputFrame>,
547    ) -> Result<Vec<WorkId>, SchedulerError> {
548        self.scheduler.enqueue_batch(request, frames)
549    }
550
551    /// Runs one fair local turn using an injected family/backend-independent submission closure.
552    pub fn run_local_turn<E>(
553        &mut self,
554        now: Instant,
555        mut execute: impl FnMut(
556            WorkId,
557            &RealtimeInputFrame,
558            &mut RealtimeSessionBranch<M::Branch, S, R, C>,
559        ) -> Result<O, E>,
560    ) -> Result<SchedulerProgress<RealtimeInputFrame, O>, SchedulerError>
561    where
562        E: std::error::Error,
563    {
564        self.ensure_local_topology()?;
565        self.scheduler.run_local_turn(now, |id, frame, branch| {
566            branch
567                .admit_batch(frame.batch())
568                .map_err(RealtimeSessionSubmissionError::<E>::Session)?;
569            execute(id, frame, branch).map_err(RealtimeSessionSubmissionError::Execution)
570        })
571    }
572
573    /// Runs one local turn while admitting at most `maximum_frames` new transitions.
574    pub fn run_local_bounded<E>(
575        &mut self,
576        now: Instant,
577        maximum_frames: usize,
578        mut execute: impl FnMut(
579            WorkId,
580            &RealtimeInputFrame,
581            &mut RealtimeSessionBranch<M::Branch, S, R, C>,
582        ) -> Result<O, E>,
583    ) -> Result<SchedulerProgress<RealtimeInputFrame, O>, SchedulerError>
584    where
585        E: std::error::Error,
586    {
587        self.ensure_local_topology()?;
588        let mut progress = self.scheduler.poll_completions(now);
589        self.scheduler.prepare_bounded(maximum_frames, now)?;
590        progress.newly_submitted = self.scheduler.submit_prepared(now, |id, frame, branch| {
591            branch
592                .admit_batch(frame.batch())
593                .map_err(RealtimeSessionSubmissionError::<E>::Session)?;
594            execute(id, frame, branch).map_err(RealtimeSessionSubmissionError::Execution)
595        })?;
596        let completed = self.scheduler.poll_completions(now);
597        progress.committed.extend(completed.committed);
598        progress.failed.extend(completed.failed);
599        Ok(progress)
600    }
601
602    /// Runs one fair turn with mandatory topology-wide schedule and completion consensus.
603    pub fn run_distributed_turn<T, E>(
604        &mut self,
605        protocol: u64,
606        transport: &T,
607        now: Instant,
608        mut execute: impl FnMut(
609            WorkId,
610            &RealtimeInputFrame,
611            &mut RealtimeSessionBranch<M::Branch, S, R, C>,
612        ) -> Result<O, E>,
613    ) -> Result<SchedulerProgress<RealtimeInputFrame, O>, SchedulerError>
614    where
615        T: BoundedConsensusTransport,
616        <T::Completion as Completion>::Error: std::fmt::Display,
617        E: std::error::Error,
618        O: eredu_core::scheduler::DistributedTransitionOutput,
619    {
620        if self.model.topology().is_replicated() {
621            return Err(SchedulerError::Consensus(
622                "distributed realtime turns require a non-replicated selected topology".into(),
623            ));
624        }
625        let participants = self.model.topology().world_size();
626        if transport.participant_count() != participants {
627            return Err(SchedulerError::Consensus(format!(
628                "distributed realtime transport has {} participants; selected topology requires {participants}",
629                transport.participant_count(),
630            )));
631        }
632        let wait: BoundedCompletionWait = self.model.completion().bounded_wait();
633        validate_ranked_identity_bounded(
634            transport,
635            protocol,
636            &self.model.distributed_consensus_identity(),
637            self.model.rank,
638            wait,
639        )
640        .map_err(|error| SchedulerError::Consensus(error.to_string()))?;
641        self.scheduler
642            .run_distributed_turn(protocol, transport, wait, now, |id, frame, branch| {
643                branch
644                    .admit_batch(frame.batch())
645                    .map_err(RealtimeSessionSubmissionError::<E>::Session)?;
646                execute(id, frame, branch).map_err(RealtimeSessionSubmissionError::Execution)
647            })
648    }
649
650    fn ensure_local_topology(&self) -> Result<(), SchedulerError> {
651        if self.model.topology().is_replicated() {
652            Ok(())
653        } else {
654            Err(SchedulerError::Consensus(
655                "rank-local realtime turns are forbidden for a non-replicated selected topology"
656                    .into(),
657            ))
658        }
659    }
660
661    /// Atomically replaces sampling only when the request owns no queued or branched work.
662    pub fn replace_sampling<E>(
663        &mut self,
664        request: RequestId,
665        sampling: RealtimeSampling,
666        realize: impl FnOnce(RealtimeSampling) -> Result<(Vec<S>, Option<R>), E>,
667    ) -> Result<(), RealtimeSamplingUpdateError<E, M::Error, C::Error>> {
668        if self.scheduler.queued_for_request(request) != 0 {
669            return Err(RealtimeSamplingReplacementError::QueuedWork);
670        }
671        let state = self
672            .scheduler
673            .request_state_mut(request)
674            .map_err(RealtimeSamplingReplacementError::Scheduler)?;
675        let (samplers, random) =
676            realize(sampling).map_err(RealtimeSamplingReplacementError::Realization)?;
677        state
678            .generation_mut()
679            .replace_sampling(sampling, samplers, random)
680            .map_err(RealtimeSamplingReplacementError::Generation)
681    }
682
683    /// Cancels queued/prepared/submitted work using core scheduler semantics.
684    pub fn cancel(&mut self, request: RequestId) -> Result<(), SchedulerError> {
685        self.scheduler.cancel(request)
686    }
687
688    /// Marks a request finished using core scheduler semantics.
689    pub fn finish(&mut self, request: RequestId) -> Result<(), SchedulerError> {
690        self.scheduler.finish(request)
691    }
692
693    /// Releases an idle canonical session for exact later resumption.
694    pub fn release(
695        &mut self,
696        request: RequestId,
697    ) -> Result<ReleasedRealtimeSession<M, S, R, C>, SchedulerError> {
698        self.scheduler
699            .release(request)
700            .map(|state| ReleasedRealtimeSession { state })
701    }
702
703    /// Active or terminal request status.
704    pub fn request_status(&self, request: RequestId) -> Option<RequestStatus> {
705        self.scheduler.request_status(request)
706    }
707
708    /// Number of portable frames still queued for one active request.
709    pub fn queued_for_request(&self, request: RequestId) -> usize {
710        self.scheduler.queued_for_request(request)
711    }
712
713    /// Removes a terminal identity so the caller may explicitly reuse it.
714    pub fn forget_terminal(&mut self, request: RequestId) -> Result<RequestStatus, SchedulerError> {
715        self.scheduler.forget_terminal(request)
716    }
717
718    /// Immutable canonical session state, when active.
719    pub fn request_state(&self, request: RequestId) -> Option<&RealtimeSessionState<M, S, R, C>> {
720        self.scheduler.request_state(request)
721    }
722
723    /// Current scheduler telemetry.
724    pub fn report(&self) -> SchedulerReport {
725        self.scheduler.report()
726    }
727
728    /// Configured and observed scheduler capabilities.
729    pub fn capabilities(&self) -> SchedulerCapabilities {
730        self.scheduler.capabilities()
731    }
732}
733
734fn allocate_incarnation() -> Result<RealtimeSessionIncarnation, RealtimeSessionError> {
735    NEXT_REALTIME_INCARNATION
736        .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
737            value.checked_add(1)
738        })
739        .map(RealtimeSessionIncarnation)
740        .map_err(|_| RealtimeSessionError::IncarnationExhausted)
741}
742
743/// Stable session ownership failure before scheduler submission.
744#[derive(Debug, thiserror::Error)]
745#[non_exhaustive]
746pub enum RealtimeSessionError {
747    /// Core scheduler lifecycle failure.
748    #[error(transparent)]
749    Scheduler(#[from] SchedulerError),
750    /// Generation schedule differs from the exact selected model schedule.
751    #[error("realtime generation schedule differs from selected model identity")]
752    ScheduleMismatch,
753    /// Released state belongs to another exact selected model.
754    #[error("released realtime session model identity does not match")]
755    ModelIdentityMismatch,
756    /// Process-local monotonic incarnation space is exhausted.
757    #[error("realtime session incarnation identity space is exhausted")]
758    IncarnationExhausted,
759}
760
761/// Batch admission failure before an injected frame executor is called.
762#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
763#[non_exhaustive]
764pub enum RealtimeSessionExecutionError {
765    /// Payload-contract derivation was attempted before scheduler batch admission.
766    #[error("realtime session frame batch has not been admitted")]
767    BatchNotAdmitted,
768    /// Portable frames must have a positive batch.
769    #[error("realtime session frame batch must be positive")]
770    EmptyBatch,
771    /// Every frame in an incarnation must use the first committed batch.
772    #[error("realtime session batch is {submitted}, committed batch is {committed}")]
773    Batch {
774        /// Batch already committed by this incarnation.
775        committed: usize,
776        /// Batch carried by the rejected frame.
777        submitted: usize,
778    },
779    /// Frame ingress schedule differs from the exact selected session schedule.
780    #[error("realtime ingress schedule differs from selected session schedule")]
781    IngressScheduleMismatch,
782}
783
784/// Failed resumption retaining full released state for retry or disposal.
785pub struct RealtimeSessionResumeError<M, S, R, C> {
786    reason: RealtimeSessionError,
787    released: Box<ReleasedRealtimeSession<M, S, R, C>>,
788}
789
790impl<M, S, R, C> RealtimeSessionResumeError<M, S, R, C> {
791    /// Stable reason resumption was rejected.
792    pub const fn reason(&self) -> &RealtimeSessionError {
793        &self.reason
794    }
795
796    /// Recovers the unchanged released state.
797    pub fn into_released(self) -> ReleasedRealtimeSession<M, S, R, C> {
798        *self.released
799    }
800}
801
802impl<M, S, R, C> std::fmt::Debug for RealtimeSessionResumeError<M, S, R, C> {
803    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
804        formatter
805            .debug_struct("RealtimeSessionResumeError")
806            .field("reason", &self.reason)
807            .finish_non_exhaustive()
808    }
809}
810
811impl<M, S, R, C> std::fmt::Display for RealtimeSessionResumeError<M, S, R, C> {
812    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
813        self.reason.fmt(formatter)
814    }
815}
816
817impl<M, S, R, C> std::error::Error for RealtimeSessionResumeError<M, S, R, C> {}
818
819#[derive(Debug, thiserror::Error)]
820enum RealtimeSessionSubmissionError<E: std::error::Error> {
821    #[error(transparent)]
822    Session(RealtimeSessionExecutionError),
823    #[error(transparent)]
824    Execution(E),
825}
826
827/// Failure while publishing or discarding complete session state.
828#[derive(Debug, thiserror::Error)]
829pub enum RealtimeSessionTransactionError<M, C>
830where
831    M: std::error::Error,
832    C: std::error::Error,
833{
834    /// Atomic generation-state transaction failed.
835    #[error(transparent)]
836    Generation(RealtimeGenerationTransactionError<M, C>),
837    /// An unpublished branch did not retain its exact session identity.
838    #[error("realtime session branch identity does not match canonical state")]
839    IdentityMismatch,
840    /// An unpublished branch attempted to change committed batch.
841    #[error("realtime session branch batch does not match canonical state")]
842    BatchMismatch,
843}
844
845/// Sampling replacement failure while preserving queued-work ordering.
846#[derive(Debug, thiserror::Error)]
847pub enum RealtimeSamplingReplacementError<E, G> {
848    /// Queued frames retain the sampling policy under which they were accepted.
849    #[error("cannot replace realtime sampling while frames are queued")]
850    QueuedWork,
851    /// Core scheduler did not admit mutable idle-state access.
852    #[error(transparent)]
853    Scheduler(SchedulerError),
854    /// Backend-neutral sampler/RNG realization failed.
855    #[error("realtime sampling realization failed")]
856    Realization(E),
857    /// New sampler/RNG state did not match generation geometry.
858    #[error("realtime generation rejected replacement sampling")]
859    Generation(G),
860}
861
862/// Sampling replacement failure specialized to one generation transaction.
863pub type RealtimeSamplingUpdateError<E, M, C> =
864    RealtimeSamplingReplacementError<E, RealtimeGenerationTransactionError<M, C>>;
865
866#[cfg(test)]
867mod tests {
868    use std::{cell::Cell, convert::Infallible, rc::Rc, time::Duration};
869
870    use eredu_core::{
871        consensus::{BoundedConsensusTransport, ConsensusTransport},
872        scheduler::{RequestStatus, TransitionOutput},
873        BoundedCompletion, BoundedCompletionOutcome, BoundedCompletionWait,
874        CompletionCancellationMode, RealtimeFrameConvention, Submission,
875    };
876
877    use super::*;
878    use crate::TokenDomain;
879
880    #[derive(Debug, Clone, Eq, PartialEq)]
881    struct ModelState(usize);
882
883    #[derive(Debug, Clone, Copy, thiserror::Error)]
884    #[error("model transaction failed")]
885    struct ModelError;
886
887    impl SemanticStateTransaction for ModelState {
888        type Branch = Self;
889        type Error = ModelError;
890
891        fn branch(&self) -> Result<Self::Branch, Self::Error> {
892            Ok(self.clone())
893        }
894
895        fn commit_branch(&mut self, branch: Self::Branch) -> Result<(), Self::Error> {
896            *self = branch;
897            Ok(())
898        }
899    }
900
901    #[derive(Debug, Clone)]
902    struct MockCompletion(Rc<Cell<bool>>);
903
904    #[derive(Debug, Clone, Copy, thiserror::Error)]
905    #[error("completion failed")]
906    struct CompletionError;
907
908    impl Completion for MockCompletion {
909        type Error = CompletionError;
910
911        fn is_complete(&self) -> Result<bool, Self::Error> {
912            Ok(self.0.get())
913        }
914
915        fn wait(&self) -> Result<(), Self::Error> {
916            self.0.get().then_some(()).ok_or(CompletionError)
917        }
918    }
919
920    #[derive(Debug)]
921    struct MockOutput {
922        completion: MockCompletion,
923    }
924
925    impl TransitionOutput for MockOutput {
926        type Error = CompletionError;
927
928        fn is_complete(&self) -> Result<bool, Self::Error> {
929            self.completion.is_complete()
930        }
931
932        fn retained_resources(&self) -> usize {
933            1
934        }
935    }
936
937    impl eredu_core::scheduler::DistributedTransitionOutput for MockOutput {
938        fn encode_distributed_output(&self, output: &mut Vec<u32>) -> Result<(), String> {
939            output.push(0);
940            Ok(())
941        }
942    }
943
944    type Sessions = RealtimeSessionScheduler<ModelState, (), (), MockCompletion, MockOutput>;
945
946    fn schedule() -> RealtimeSpeechConfig {
947        RealtimeSpeechConfig::new(
948            2,
949            1,
950            1,
951            1,
952            9,
953            8,
954            RealtimeFrameConvention::FeedbackAlignedHistory,
955            vec![0, 0, 1],
956        )
957        .unwrap()
958    }
959
960    fn identity(suffix: &str) -> RealtimeModelSessionIdentity {
961        identity_with_topology(suffix, ParallelTopology::new(1, 1, 1, 1).unwrap())
962    }
963
964    fn identity_with_topology(
965        suffix: &str,
966        topology: ParallelTopology,
967    ) -> RealtimeModelSessionIdentity {
968        let id = |prefix: &str| RealtimeIdentity::new(format!("{prefix}-{suffix}")).unwrap();
969        RealtimeModelSessionIdentity::from_parts(
970            id("architecture"),
971            id("source"),
972            id("execution"),
973            id("schedule"),
974            schedule(),
975            id("state"),
976            id("topology"),
977            topology,
978            0,
979            LayerWeightResidency::FullyResident,
980            CommunicationCompletionPolicy::new(
981                Duration::from_secs(1),
982                CompletionCancellationMode::QuarantineUntilComplete,
983            )
984            .unwrap(),
985        )
986    }
987
988    #[derive(Debug, Clone, Copy)]
989    struct ReadyConsensusCompletion;
990
991    impl Completion for ReadyConsensusCompletion {
992        type Error = Infallible;
993
994        fn is_complete(&self) -> Result<bool, Self::Error> {
995            Ok(true)
996        }
997
998        fn wait(&self) -> Result<(), Self::Error> {
999            Ok(())
1000        }
1001    }
1002
1003    impl BoundedCompletion for ReadyConsensusCompletion {
1004        fn wait_bounded(
1005            self,
1006            _: BoundedCompletionWait,
1007        ) -> Result<BoundedCompletionOutcome, Self::Error> {
1008            Ok(BoundedCompletionOutcome::Completed)
1009        }
1010    }
1011
1012    struct DisagreeingTransport {
1013        calls: Cell<usize>,
1014        disagree_on: usize,
1015    }
1016
1017    impl DisagreeingTransport {
1018        fn model_identity() -> Self {
1019            Self {
1020                calls: Cell::new(0),
1021                disagree_on: 1,
1022            }
1023        }
1024
1025        fn schedule() -> Self {
1026            Self {
1027                calls: Cell::new(0),
1028                disagree_on: 4,
1029            }
1030        }
1031    }
1032
1033    impl ConsensusTransport for DisagreeingTransport {
1034        type Error = Infallible;
1035
1036        fn participant_count(&self) -> usize {
1037            2
1038        }
1039
1040        fn all_gather_words(&self, local: &[u32]) -> Result<Vec<u32>, Self::Error> {
1041            let call = self.calls.get() + 1;
1042            self.calls.set(call);
1043            let mut gathered = local.repeat(2);
1044            if call == 1 {
1045                *gathered.last_mut().expect("identity frame has a rank word") = 1;
1046            }
1047            if call == self.disagree_on {
1048                gathered[local.len()] ^= 1;
1049            }
1050            Ok(gathered)
1051        }
1052    }
1053
1054    impl BoundedConsensusTransport for DisagreeingTransport {
1055        type Completion = ReadyConsensusCompletion;
1056        type GatherOutput = Vec<u32>;
1057
1058        fn submit_all_gather_words(
1059            &self,
1060            local: &[u32],
1061        ) -> Result<Submission<Self::GatherOutput, Self::Completion>, Self::Error> {
1062            Ok(Submission {
1063                output: self.all_gather_words(local)?,
1064                completion: ReadyConsensusCompletion,
1065            })
1066        }
1067
1068        fn resolve_all_gather_words(
1069            &self,
1070            output: Self::GatherOutput,
1071        ) -> Result<Vec<u32>, Self::Error> {
1072            Ok(output)
1073        }
1074    }
1075
1076    fn generation() -> RealtimeGenerationState<ModelState, (), (), MockCompletion> {
1077        RealtimeGenerationState::new(
1078            ModelState(0),
1079            schedule(),
1080            RealtimeSampling::greedy(),
1081            vec![(), ()],
1082            None,
1083        )
1084        .unwrap()
1085    }
1086
1087    fn ingress() -> RealtimeIngressContract {
1088        RealtimeIngressContract::new(schedule(), TokenDomain::new(32), TokenDomain::new(16))
1089            .unwrap()
1090    }
1091
1092    fn frame(batch: usize) -> RealtimeInputFrame {
1093        RealtimeInputFrame::new(batch, vec![1; batch])
1094    }
1095
1096    fn limits(submissions: usize) -> SchedulerLimits {
1097        SchedulerLimits::with_execution_bounds(8, 32, submissions, 8, 1, usize::MAX).unwrap()
1098    }
1099
1100    fn execute_immediately(
1101        _id: WorkId,
1102        _frame: &RealtimeInputFrame,
1103        branch: &mut RealtimeSessionBranch<ModelState, (), (), MockCompletion>,
1104    ) -> Result<MockOutput, Infallible> {
1105        branch.generation_mut().model_state_mut().0 += 1;
1106        let completion = MockCompletion(Rc::new(Cell::new(true)));
1107        branch
1108            .generation_mut()
1109            .attach_submission_completion(completion.clone())
1110            .unwrap();
1111        Ok(MockOutput { completion })
1112    }
1113
1114    #[test]
1115    fn one_scheduler_round_robins_single_lane_sessions() {
1116        let mut sessions = Sessions::new(identity("a"), limits(2)).unwrap();
1117        let first = RequestId::new(1);
1118        let second = RequestId::new(2);
1119        let first_incarnation = sessions.register(first, generation()).unwrap();
1120        let second_incarnation = sessions.register(second, generation()).unwrap();
1121        assert_ne!(first_incarnation, second_incarnation);
1122        sessions
1123            .enqueue_batch(first, vec![frame(1), frame(1)])
1124            .unwrap();
1125        sessions
1126            .enqueue_batch(second, vec![frame(1), frame(1)])
1127            .unwrap();
1128
1129        let progress = sessions
1130            .run_local_turn(Instant::now(), execute_immediately)
1131            .unwrap();
1132        assert_eq!(progress.committed.len(), 2);
1133        assert_eq!(progress.committed[0].0.request(), first);
1134        assert_eq!(progress.committed[1].0.request(), second);
1135        assert_eq!(
1136            sessions
1137                .request_state(first)
1138                .unwrap()
1139                .generation()
1140                .model_state()
1141                .0,
1142            1
1143        );
1144        assert_eq!(
1145            sessions
1146                .request_state(second)
1147                .unwrap()
1148                .generation()
1149                .model_state()
1150                .0,
1151            1
1152        );
1153    }
1154
1155    #[test]
1156    fn non_replicated_identity_rejects_local_turn_before_submission() {
1157        let topology = ParallelTopology::new(2, 1, 1, 1).unwrap();
1158        let mut sessions =
1159            Sessions::new(identity_with_topology("tp", topology), limits(1)).unwrap();
1160        let request = RequestId::new(90);
1161        sessions.register(request, generation()).unwrap();
1162        sessions.enqueue(request, frame(1)).unwrap();
1163        let calls = Rc::new(Cell::new(0));
1164        let observed = calls.clone();
1165        let error = sessions
1166            .run_local_turn(Instant::now(), move |id, frame, branch| {
1167                observed.set(observed.get() + 1);
1168                execute_immediately(id, frame, branch)
1169            })
1170            .unwrap_err();
1171        assert!(matches!(error, SchedulerError::Consensus(_)));
1172        assert_eq!(calls.get(), 0);
1173        assert_eq!(sessions.queued_for_request(request), 1);
1174
1175        let error = sessions
1176            .run_local_bounded(Instant::now(), 1, |id, frame, branch| {
1177                calls.set(calls.get() + 1);
1178                execute_immediately(id, frame, branch)
1179            })
1180            .unwrap_err();
1181        assert!(matches!(error, SchedulerError::Consensus(_)));
1182        assert_eq!(calls.get(), 0);
1183        assert_eq!(sessions.queued_for_request(request), 1);
1184    }
1185
1186    #[test]
1187    fn distributed_model_identity_disagreement_submits_and_publishes_nothing() {
1188        let topology = ParallelTopology::new(2, 1, 1, 1).unwrap();
1189        let mut sessions = Sessions::new(
1190            identity_with_topology("tp-disagreement", topology),
1191            limits(1),
1192        )
1193        .unwrap();
1194        let request = RequestId::new(91);
1195        sessions.register(request, generation()).unwrap();
1196        sessions.enqueue(request, frame(1)).unwrap();
1197        let calls = Rc::new(Cell::new(0));
1198        let observed = calls.clone();
1199        let error = sessions
1200            .run_distributed_turn(
1201                17,
1202                &DisagreeingTransport::model_identity(),
1203                Instant::now(),
1204                move |id, frame, branch| {
1205                    observed.set(observed.get() + 1);
1206                    execute_immediately(id, frame, branch)
1207                },
1208            )
1209            .unwrap_err();
1210        assert!(matches!(error, SchedulerError::Consensus(_)));
1211        assert_eq!(calls.get(), 0);
1212        assert_eq!(
1213            sessions
1214                .request_state(request)
1215                .unwrap()
1216                .generation()
1217                .model_state()
1218                .0,
1219            0
1220        );
1221        assert_eq!(sessions.report().completed_work, 0);
1222    }
1223
1224    #[test]
1225    fn distributed_schedule_disagreement_submits_and_publishes_nothing() {
1226        let topology = ParallelTopology::new(2, 1, 1, 1).unwrap();
1227        let mut sessions =
1228            Sessions::new(identity_with_topology("tp-schedule", topology), limits(1)).unwrap();
1229        let request = RequestId::new(92);
1230        sessions.register(request, generation()).unwrap();
1231        sessions.enqueue(request, frame(1)).unwrap();
1232        let calls = Rc::new(Cell::new(0));
1233        let observed = calls.clone();
1234        let error = sessions
1235            .run_distributed_turn(
1236                18,
1237                &DisagreeingTransport::schedule(),
1238                Instant::now(),
1239                move |id, frame, branch| {
1240                    observed.set(observed.get() + 1);
1241                    execute_immediately(id, frame, branch)
1242                },
1243            )
1244            .unwrap_err();
1245        assert!(matches!(error, SchedulerError::Consensus(_)));
1246        assert_eq!(calls.get(), 0);
1247        assert_eq!(sessions.report().completed_work, 0);
1248    }
1249
1250    #[test]
1251    fn scheduler_branch_derives_payload_identity_before_native_execution() {
1252        let mut sessions = Sessions::new(identity("payload"), limits(1)).unwrap();
1253        let request = RequestId::new(20);
1254        let incarnation = sessions.register(request, generation()).unwrap();
1255        sessions.enqueue(request, frame(2)).unwrap();
1256
1257        sessions
1258            .run_local_turn(Instant::now(), |id, submitted, branch| {
1259                let contract = branch.payload_contract(&ingress()).unwrap();
1260                assert_eq!(contract.batch().get(), 2);
1261                assert_eq!(contract.owner().value(), incarnation.value());
1262                assert_eq!(
1263                    contract.generation().value(),
1264                    branch.history_generation().value()
1265                );
1266                execute_immediately(id, submitted, branch)
1267            })
1268            .unwrap();
1269    }
1270
1271    #[test]
1272    fn committed_batch_rejects_a_different_batch_before_execution() {
1273        let mut sessions = Sessions::new(identity("a"), limits(1)).unwrap();
1274        let request = RequestId::new(3);
1275        sessions.register(request, generation()).unwrap();
1276        sessions.enqueue(request, frame(1)).unwrap();
1277        sessions
1278            .run_local_turn(Instant::now(), execute_immediately)
1279            .unwrap();
1280        assert_eq!(
1281            sessions
1282                .request_state(request)
1283                .unwrap()
1284                .committed_batch()
1285                .unwrap()
1286                .get(),
1287            1
1288        );
1289
1290        sessions.enqueue(request, frame(2)).unwrap();
1291        let calls = Rc::new(Cell::new(0));
1292        let observed = calls.clone();
1293        assert!(sessions
1294            .run_local_turn(Instant::now(), move |id, frame, branch| {
1295                observed.set(observed.get() + 1);
1296                execute_immediately(id, frame, branch)
1297            })
1298            .is_err());
1299        assert_eq!(calls.get(), 0);
1300        assert_eq!(
1301            sessions.request_status(request),
1302            Some(RequestStatus::Failed)
1303        );
1304    }
1305
1306    #[test]
1307    fn release_resume_preserves_incarnation_and_wrong_model_keeps_state_recoverable() {
1308        let request = RequestId::new(4);
1309        let mut first = Sessions::new(identity("a"), limits(1)).unwrap();
1310        let incarnation = first.register(request, generation()).unwrap();
1311        let released = first.release(request).unwrap();
1312        assert_eq!(released.incarnation(), incarnation);
1313
1314        let mut wrong = Sessions::new(identity("b"), limits(1)).unwrap();
1315        let error = wrong.resume(request, released).unwrap_err();
1316        assert!(matches!(
1317            error.reason(),
1318            RealtimeSessionError::ModelIdentityMismatch
1319        ));
1320        let released = error.into_released();
1321        first.resume(request, released).unwrap();
1322        assert_eq!(
1323            first.request_state(request).unwrap().incarnation(),
1324            incarnation
1325        );
1326    }
1327
1328    #[test]
1329    fn queued_work_blocks_sampling_replacement_before_realization() {
1330        let request = RequestId::new(5);
1331        let mut sessions = Sessions::new(identity("a"), limits(1)).unwrap();
1332        sessions.register(request, generation()).unwrap();
1333        sessions.enqueue(request, frame(1)).unwrap();
1334        let realized = Rc::new(Cell::new(false));
1335        let observed = realized.clone();
1336        let result = sessions.replace_sampling(
1337            request,
1338            RealtimeSampling::greedy(),
1339            move |_| -> Result<_, Infallible> {
1340                observed.set(true);
1341                Ok((vec![(), ()], None))
1342            },
1343        );
1344        assert!(matches!(
1345            result,
1346            Err(RealtimeSamplingReplacementError::QueuedWork)
1347        ));
1348        assert!(!realized.get());
1349    }
1350
1351    #[test]
1352    fn cancellation_and_deadline_use_core_terminal_semantics() {
1353        let mut sessions = Sessions::new(identity("a"), limits(1)).unwrap();
1354        let cancelled = RequestId::new(6);
1355        sessions.register(cancelled, generation()).unwrap();
1356        sessions.enqueue(cancelled, frame(1)).unwrap();
1357        sessions.cancel(cancelled).unwrap();
1358        assert_eq!(
1359            sessions.request_status(cancelled),
1360            Some(RequestStatus::Cancelled)
1361        );
1362
1363        let expired = RequestId::new(7);
1364        sessions.register(expired, generation()).unwrap();
1365        sessions
1366            .enqueue_with_deadline(
1367                expired,
1368                frame(1),
1369                Some(Instant::now() - Duration::from_secs(1)),
1370            )
1371            .unwrap();
1372        let calls = Rc::new(Cell::new(0));
1373        let observed = calls.clone();
1374        sessions
1375            .run_local_turn(Instant::now(), move |id, frame, branch| {
1376                observed.set(observed.get() + 1);
1377                execute_immediately(id, frame, branch)
1378            })
1379            .unwrap();
1380        assert_eq!(calls.get(), 0);
1381        assert_eq!(
1382            sessions.request_status(expired),
1383            Some(RequestStatus::DeadlineExceeded)
1384        );
1385    }
1386
1387    #[test]
1388    fn cancelling_submitted_work_retains_it_until_exact_completion() {
1389        let request = RequestId::new(8);
1390        let mut sessions = Sessions::new(identity("a"), limits(1)).unwrap();
1391        sessions.register(request, generation()).unwrap();
1392        sessions.enqueue(request, frame(1)).unwrap();
1393        let completion = Rc::new(Cell::new(false));
1394        let submitted = completion.clone();
1395        sessions
1396            .run_local_turn(Instant::now(), move |_, _, branch| {
1397                let completion = MockCompletion(submitted.clone());
1398                branch
1399                    .generation_mut()
1400                    .attach_submission_completion(completion.clone())
1401                    .unwrap();
1402                Ok::<_, Infallible>(MockOutput { completion })
1403            })
1404            .unwrap();
1405        sessions.cancel(request).unwrap();
1406        assert_eq!(sessions.report().abandoned_in_flight_work, 1);
1407        completion.set(true);
1408        sessions
1409            .run_local_turn(Instant::now(), execute_immediately)
1410            .unwrap();
1411        assert_eq!(sessions.report().abandoned_in_flight_work, 0);
1412    }
1413}