Skip to main content

cu29_runtime/
cuasynctask.rs

1use crate::config::ComponentConfig;
2use crate::context::CuContext;
3use crate::cutask::{BincodeAdapter, CuMsg, CuMsgPayload, CuSrcTask, CuTask, Freezable};
4use crate::reflect::{Reflect, TypePath};
5use bincode::config::standard;
6use bincode::de::read::Reader;
7use bincode::de::{Decode, Decoder};
8use bincode::enc::write::Writer;
9use bincode::enc::{Encode, Encoder, EncoderImpl};
10use bincode::error::{DecodeError, EncodeError};
11use cu29_clock::CuTime;
12use cu29_traits::{CuError, CuResult};
13use rayon::ThreadPool;
14use std::any::Any;
15use std::cell::UnsafeCell;
16use std::sync::{Arc, Condvar, Mutex};
17
18const ASYNC_IDLE_TAG: u8 = 0xA0;
19const ASYNC_WAITING_TAG: u8 = 0xA1;
20const ASYNC_FAILED_TAG: u8 = 0xA2;
21const ASYNC_PENDING_TAG: u8 = 0xA3;
22
23enum AsyncStatus {
24    Idle,
25    Waiting(CuTime),
26    Failed(String),
27    Running(u64),
28    ReplayPending(u64),
29}
30
31struct AsyncState<O: CuMsgPayload> {
32    status: AsyncStatus,
33    committed_task: Vec<u8>,
34    task_scratch: Vec<u8>,
35    committed_output: CuMsg<O>,
36}
37
38impl<O: CuMsgPayload> AsyncState<O> {
39    fn new() -> Self {
40        Self {
41            status: AsyncStatus::Idle,
42            committed_task: Vec::new(),
43            task_scratch: Vec::new(),
44            committed_output: CuMsg::default(),
45        }
46    }
47}
48
49fn commit_initial_snapshot<O: CuMsgPayload>(state: &mut AsyncState<O>, snapshot: Vec<u8>) {
50    let mut scratch = core::mem::replace(&mut state.committed_task, snapshot);
51    if scratch.capacity() < state.committed_task.len() {
52        scratch.reserve_exact(state.committed_task.len() - scratch.len());
53    }
54    state.task_scratch = scratch;
55}
56
57struct BufferWriter<'a>(&'a mut Vec<u8>);
58
59impl Writer for BufferWriter<'_> {
60    fn write(&mut self, bytes: &[u8]) -> Result<(), EncodeError> {
61        self.0.extend_from_slice(bytes);
62        Ok(())
63    }
64}
65
66fn encode_value_into(value: &impl Encode, buffer: &mut Vec<u8>) -> Result<(), EncodeError> {
67    buffer.clear();
68    let mut encoder = EncoderImpl::new(BufferWriter(buffer), standard());
69    value.encode(&mut encoder)
70}
71
72fn freeze_into(task: &impl Freezable, buffer: &mut Vec<u8>) -> Result<(), EncodeError> {
73    encode_value_into(&BincodeAdapter(task), buffer)
74}
75
76fn thaw_from(task: &mut impl Freezable, buffer: &[u8]) -> Result<(), DecodeError> {
77    let reader = bincode::de::read::SliceReader::new(buffer);
78    let mut decoder = bincode::de::DecoderImpl::new(reader, standard(), ());
79    task.thaw(&mut decoder)
80}
81
82struct DynWriter<'a>(&'a mut dyn Writer);
83
84impl Writer for DynWriter<'_> {
85    fn write(&mut self, bytes: &[u8]) -> Result<(), EncodeError> {
86        self.0.write(bytes)
87    }
88}
89
90struct DynReader<'a>(&'a mut dyn Reader);
91
92impl Reader for DynReader<'_> {
93    fn read(&mut self, bytes: &mut [u8]) -> Result<(), DecodeError> {
94        self.0.read(bytes)
95    }
96
97    fn peek_read(&mut self, length: usize) -> Option<&[u8]> {
98        self.0.peek_read(length)
99    }
100
101    fn consume(&mut self, length: usize) {
102        self.0.consume(length);
103    }
104}
105
106trait ErasedDispatch: Any + Send + Sync {
107    fn as_any(&self) -> &dyn Any;
108    fn encode_input(&self, writer: &mut dyn Writer) -> Result<(), EncodeError>;
109    fn decode_input(&self, reader: &mut dyn Reader) -> Result<(), DecodeError>;
110}
111
112struct DispatchSlot<I: CuMsgPayload> {
113    input: UnsafeCell<CuMsg<I>>,
114}
115
116// SAFETY: the slot is written only before a run is published or while thaw holds the
117// inner-task mutex. While a run is published, workers and keyframes only read it.
118unsafe impl<I: CuMsgPayload + Send + Sync> Sync for DispatchSlot<I> {}
119
120impl<I> DispatchSlot<I>
121where
122    I: CuMsgPayload + Send + Sync + 'static,
123{
124    fn new() -> Self {
125        Self {
126            input: UnsafeCell::new(CuMsg::default()),
127        }
128    }
129
130    fn replace(&self, input: CuMsg<I>) -> CuMsg<I> {
131        // SAFETY: callers write only while no worker can hold a reference to the slot.
132        unsafe { core::mem::replace(&mut *self.input.get(), input) }
133    }
134
135    fn get(&self) -> &CuMsg<I> {
136        // SAFETY: published dispatch input remains immutable until the worker finishes.
137        unsafe { &*self.input.get() }
138    }
139}
140
141impl<I> ErasedDispatch for DispatchSlot<I>
142where
143    I: CuMsgPayload + Send + Sync + 'static,
144{
145    fn as_any(&self) -> &dyn Any {
146        self
147    }
148
149    fn encode_input(&self, writer: &mut dyn Writer) -> Result<(), EncodeError> {
150        let mut encoder = EncoderImpl::new(DynWriter(writer), standard());
151        self.get().encode(&mut encoder)
152    }
153
154    fn decode_input(&self, reader: &mut dyn Reader) -> Result<(), DecodeError> {
155        let mut decoder = bincode::de::DecoderImpl::new(DynReader(reader), standard(), ());
156        drop(self.replace(CuMsg::<I>::decode(&mut decoder)?));
157        Ok(())
158    }
159}
160
161fn failure(error: CuError) -> AsyncStatus {
162    AsyncStatus::Failed(error.to_string())
163}
164
165fn encode_async_state<O, E>(state: &AsyncState<O>, encoder: &mut E) -> Result<bool, EncodeError>
166where
167    O: CuMsgPayload + Send + 'static,
168    E: Encoder,
169{
170    Encode::encode(&state.committed_task, encoder)?;
171    Encode::encode(&state.committed_output, encoder)?;
172    match &state.status {
173        AsyncStatus::Idle => {
174            ASYNC_IDLE_TAG.encode(encoder)?;
175            Ok(false)
176        }
177        AsyncStatus::Waiting(ready_at) => {
178            ASYNC_WAITING_TAG.encode(encoder)?;
179            ready_at.encode(encoder)?;
180            Ok(false)
181        }
182        AsyncStatus::Failed(snapshot) => {
183            ASYNC_FAILED_TAG.encode(encoder)?;
184            snapshot.encode(encoder)?;
185            Ok(false)
186        }
187        AsyncStatus::Running(cl_id) | AsyncStatus::ReplayPending(cl_id) => {
188            ASYNC_PENDING_TAG.encode(encoder)?;
189            cl_id.encode(encoder)?;
190            Ok(true)
191        }
192    }
193}
194
195fn decode_async_state<O, D>(decoder: &mut D) -> Result<AsyncState<O>, DecodeError>
196where
197    O: CuMsgPayload + Send + 'static,
198    D: Decoder,
199{
200    let committed_task = Decode::decode(decoder)?;
201    let committed_output = CuMsg::<O>::decode(&mut decoder.with_context(()))?;
202    let status = match u8::decode(decoder)? {
203        ASYNC_IDLE_TAG => AsyncStatus::Idle,
204        ASYNC_WAITING_TAG => AsyncStatus::Waiting(Decode::decode(decoder)?),
205        ASYNC_FAILED_TAG => {
206            let snapshot: String = Decode::decode(decoder)?;
207            AsyncStatus::Failed(snapshot)
208        }
209        ASYNC_PENDING_TAG => AsyncStatus::ReplayPending(u64::decode(decoder)?),
210        tag => {
211            return Err(DecodeError::OtherString(format!(
212                "unsupported async keyframe payload tag {tag:#04x}; expected version 1"
213            )));
214        }
215    };
216    Ok(AsyncState {
217        status,
218        committed_task,
219        task_scratch: Vec::new(),
220        committed_output,
221    })
222}
223
224type ErasedDispatchSlot = Arc<dyn ErasedDispatch>;
225
226fn dispatch_slot<I>(slot: &ErasedDispatchSlot) -> CuResult<&DispatchSlot<I>>
227where
228    I: CuMsgPayload + Send + Sync + 'static,
229{
230    slot.as_any()
231        .downcast_ref::<DispatchSlot<I>>()
232        .ok_or_else(|| CuError::from("Async task dispatch slot type did not match its input"))
233}
234
235fn record_async_error<O: CuMsgPayload>(
236    state: &Mutex<AsyncState<O>>,
237    completion: &Condvar,
238    error: CuError,
239) {
240    let mut guard = match state.lock() {
241        Ok(guard) => guard,
242        Err(poison) => poison.into_inner(),
243    };
244    guard.status = failure(error);
245    drop(guard);
246    completion.notify_all();
247}
248
249/// Background task wrapper. By default, skips jobs without an input payload
250/// while collecting each completed result once, including on empty ticks.
251/// Set `PROCESS_EMPTY` to true to dispatch empty inputs as jobs.
252#[derive(Reflect)]
253#[reflect(no_field_bounds, from_reflect = false, type_path = false)]
254pub struct CuAsyncTask<T, O, const PROCESS_EMPTY: bool = false>
255where
256    T: for<'m> CuTask<Output<'m> = CuMsg<O>> + Send + 'static,
257    O: CuMsgPayload + Send + 'static,
258{
259    #[reflect(ignore)]
260    task: Arc<Mutex<T>>,
261    #[reflect(ignore)]
262    state: Arc<Mutex<AsyncState<O>>>,
263    #[reflect(ignore)]
264    completion: Arc<Condvar>,
265    #[reflect(ignore)]
266    dispatch: Option<ErasedDispatchSlot>,
267    #[reflect(ignore)]
268    tp: Arc<ThreadPool>,
269}
270
271impl<T, O, const PROCESS_EMPTY: bool> TypePath for CuAsyncTask<T, O, PROCESS_EMPTY>
272where
273    T: for<'m> CuTask<Output<'m> = CuMsg<O>> + Send + 'static,
274    O: CuMsgPayload + Send + 'static,
275{
276    fn type_path() -> &'static str {
277        "cu29_runtime::cuasynctask::CuAsyncTask"
278    }
279
280    fn short_type_path() -> &'static str {
281        "CuAsyncTask"
282    }
283
284    fn type_ident() -> Option<&'static str> {
285        Some("CuAsyncTask")
286    }
287
288    fn crate_name() -> Option<&'static str> {
289        Some("cu29_runtime")
290    }
291
292    fn module_path() -> Option<&'static str> {
293        Some("cuasynctask")
294    }
295}
296
297/// Resource bundle required by a backgrounded task.
298pub struct CuAsyncTaskResources<'r, T: CuTask> {
299    pub inner: T::Resources<'r>,
300    pub threadpool: Arc<ThreadPool>,
301}
302
303impl<T, O, const PROCESS_EMPTY: bool> CuAsyncTask<T, O, PROCESS_EMPTY>
304where
305    T: for<'m> CuTask<Output<'m> = CuMsg<O>> + Send + 'static,
306    O: CuMsgPayload + Send + 'static,
307{
308    #[allow(unused)]
309    pub fn new(
310        config: Option<&ComponentConfig>,
311        resources: T::Resources<'_>,
312        tp: Arc<ThreadPool>,
313    ) -> CuResult<Self> {
314        let task = Arc::new(Mutex::new(T::new(config, resources)?));
315        Ok(Self {
316            task,
317            state: Arc::new(Mutex::new(AsyncState::new())),
318            completion: Arc::new(Condvar::new()),
319            dispatch: None,
320            tp,
321        })
322    }
323
324    /// Waits until the currently dispatched job has completed.
325    #[doc(hidden)]
326    pub fn wait_for_job(&self) -> CuResult<()> {
327        let poisoned = || CuError::from("Async task state mutex poisoned while waiting for a job");
328        let mut guard = self.state.lock().map_err(|_| poisoned())?;
329        while matches!(guard.status, AsyncStatus::Running(_)) {
330            guard = self.completion.wait(guard).map_err(|_| poisoned())?;
331        }
332        Ok(())
333    }
334
335    fn initialize_dispatch<I>(&mut self) -> CuResult<()>
336    where
337        I: CuMsgPayload + Send + Sync + 'static,
338    {
339        if let Some(dispatch) = self.dispatch.as_ref() {
340            let _ = dispatch_slot::<I>(dispatch)?;
341        } else {
342            self.dispatch = Some(Arc::new(DispatchSlot::<I>::new()));
343        }
344        Ok(())
345    }
346}
347
348impl<T, O, const PROCESS_EMPTY: bool> Freezable for CuAsyncTask<T, O, PROCESS_EMPTY>
349where
350    T: for<'m> CuTask<Output<'m> = CuMsg<O>> + Send + 'static,
351    O: CuMsgPayload + Send + 'static,
352{
353    fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
354        let state = self
355            .state
356            .lock()
357            .map_err(|_| EncodeError::OtherString("async task state mutex poisoned".to_string()))?;
358        let pending = encode_async_state(&state, encoder)?;
359        if pending {
360            self.dispatch
361                .as_ref()
362                .ok_or_else(|| {
363                    EncodeError::OtherString(
364                        "async task pending before dispatch initialization".to_string(),
365                    )
366                })?
367                .encode_input(encoder.writer())?;
368        }
369        Ok(())
370    }
371
372    fn thaw<D: Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
373        let mut task = self
374            .task
375            .lock()
376            .map_err(|_| DecodeError::OtherString("async task mutex poisoned".to_string()))?;
377        let restored_state = decode_async_state(decoder)?;
378        if matches!(restored_state.status, AsyncStatus::ReplayPending(_)) {
379            self.dispatch
380                .as_ref()
381                .ok_or_else(|| {
382                    DecodeError::OtherString(
383                        "async task restored before dispatch initialization".to_string(),
384                    )
385                })?
386                .decode_input(decoder.reader())?;
387        }
388        thaw_from(&mut *task, &restored_state.committed_task)?;
389
390        let mut state = self
391            .state
392            .lock()
393            .map_err(|_| DecodeError::OtherString("async task state mutex poisoned".to_string()))?;
394        *state = restored_state;
395        Ok(())
396    }
397}
398
399impl<T, I, O, const PROCESS_EMPTY: bool> CuTask for CuAsyncTask<T, O, PROCESS_EMPTY>
400where
401    T: for<'i, 'o> CuTask<Input<'i> = CuMsg<I>, Output<'o> = CuMsg<O>> + Send + 'static,
402    I: CuMsgPayload + Send + Sync + 'static,
403    O: CuMsgPayload + Send + 'static,
404{
405    type Resources<'r> = CuAsyncTaskResources<'r, T>;
406    type Input<'m> = T::Input<'m>;
407    type Output<'m> = T::Output<'m>;
408
409    fn new(config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self>
410    where
411        Self: Sized,
412    {
413        CuAsyncTask::new(config, resources.inner, resources.threadpool)
414    }
415
416    fn start(&mut self, ctx: &CuContext) -> CuResult<()> {
417        self.initialize_dispatch::<I>()?;
418        let mut task = self
419            .task
420            .lock()
421            .map_err(|_| CuError::from("Async task mutex poisoned during start"))?;
422        task.start(ctx)?;
423        let mut state = self
424            .state
425            .lock()
426            .map_err(|_| CuError::from("Async task state mutex poisoned during start"))?;
427        let mut snapshot = core::mem::take(&mut state.task_scratch);
428        freeze_into(&*task, &mut snapshot).map_err(|error| {
429            CuError::from("Failed to snapshot async task after start").with_cause(error)
430        })?;
431        commit_initial_snapshot(&mut state, snapshot);
432        Ok(())
433    }
434
435    fn process<'i, 'o>(
436        &mut self,
437        ctx: &CuContext,
438        input: &Self::Input<'i>,
439        real_output: &mut Self::Output<'o>,
440    ) -> CuResult<()> {
441        let (dispatch, dispatch_cl_id, mut task_snapshot, retired_input) = {
442            let mut state = self.state.lock().map_err(|_| {
443                CuError::from("Async task state mutex poisoned while scheduling background work")
444            })?;
445            if matches!(state.status, AsyncStatus::Failed(_)) {
446                let AsyncStatus::Failed(error) =
447                    core::mem::replace(&mut state.status, AsyncStatus::Idle)
448                else {
449                    unreachable!();
450                };
451                return Err(CuError::from(error));
452            }
453            if matches!(state.status, AsyncStatus::Running(_)) {
454                *real_output = CuMsg::default();
455                return Ok(());
456            }
457            if let AsyncStatus::Waiting(ready_at) = state.status
458                && ctx.now() < ready_at
459            {
460                *real_output = CuMsg::default();
461                return Ok(());
462            }
463
464            if !PROCESS_EMPTY {
465                *real_output = if matches!(state.status, AsyncStatus::Waiting(_)) {
466                    state.status = AsyncStatus::Idle;
467                    state.committed_output.clone()
468                } else {
469                    CuMsg::default()
470                };
471                // A restored pending job owns its saved input and must still run.
472                if input.payload().is_none()
473                    && !matches!(state.status, AsyncStatus::ReplayPending(_))
474                {
475                    return Ok(());
476                }
477            }
478
479            let dispatch = self
480                .dispatch
481                .as_ref()
482                .ok_or_else(|| CuError::from("Async task dispatch slot was not initialized"))?;
483            let typed_dispatch = dispatch_slot::<I>(dispatch)?;
484            let (dispatch_cl_id, replay_pending) =
485                if let AsyncStatus::ReplayPending(cl_id) = state.status {
486                    (cl_id, true)
487                } else {
488                    (ctx.cl_id(), false)
489                };
490            let retired_input = if replay_pending {
491                CuMsg::default()
492            } else {
493                typed_dispatch.replace((*input).clone())
494            };
495            if PROCESS_EMPTY {
496                *real_output = state.committed_output.clone();
497            }
498            state.status = AsyncStatus::Running(dispatch_cl_id);
499            (
500                dispatch.clone(),
501                dispatch_cl_id,
502                core::mem::take(&mut state.task_scratch),
503                retired_input,
504            )
505        };
506
507        self.tp.spawn_fifo({
508            let ctx = ctx.with_cl_id(dispatch_cl_id);
509            let task = self.task.clone();
510            let state = self.state.clone();
511            let completion = self.completion.clone();
512            move || {
513                let mut worker_output = CuMsg::default();
514                let typed_dispatch =
515                    if let Some(slot) = dispatch.as_any().downcast_ref::<DispatchSlot<I>>() {
516                        slot
517                    } else {
518                        record_async_error(
519                            &state,
520                            &completion,
521                            CuError::from("Async task dispatch slot type did not match its input"),
522                        );
523                        return;
524                    };
525                let input_ref = typed_dispatch.get();
526                let mut task_guard = match task.lock() {
527                    Ok(guard) => guard,
528                    Err(poison) => {
529                        record_async_error(
530                            &state,
531                            &completion,
532                            CuError::from(format!("Async task mutex poisoned: {poison}")),
533                        );
534                        return;
535                    }
536                };
537                // Each async run starts from an empty output so a task that
538                // chooses not to publish does not leak the previous payload.
539                // Track the actual processing interval so replay can honor it.
540                if worker_output.metadata.process_time.start.is_none() {
541                    worker_output.metadata.process_time.start = ctx.now().into();
542                }
543                let task_result = task_guard.process(&ctx, input_ref, &mut worker_output);
544                let fallback_end = ctx.now();
545                let end_from_metadata: Option<CuTime> =
546                    worker_output.metadata.process_time.end.into();
547                let ready_at = end_from_metadata.unwrap_or_else(|| {
548                    worker_output.metadata.process_time.end = fallback_end.into();
549                    fallback_end
550                });
551                let snapshot_result = freeze_into(&*task_guard, &mut task_snapshot);
552                let (commit_snapshot, status) = match snapshot_result {
553                    Ok(()) => (
554                        true,
555                        match task_result {
556                            Ok(()) => AsyncStatus::Waiting(ready_at),
557                            Err(error) => failure(error),
558                        },
559                    ),
560                    Err(error) => (
561                        false,
562                        failure(
563                            CuError::from("Failed to snapshot completed async task")
564                                .with_cause(error),
565                        ),
566                    ),
567                };
568
569                let mut guard = state.lock().unwrap_or_else(|poison| poison.into_inner());
570                let retired_output = if commit_snapshot {
571                    guard.task_scratch =
572                        core::mem::replace(&mut guard.committed_task, task_snapshot);
573                    Some(core::mem::replace(
574                        &mut guard.committed_output,
575                        worker_output,
576                    ))
577                } else {
578                    guard.task_scratch = task_snapshot;
579                    None
580                };
581                guard.status = status;
582                drop(guard);
583                completion.notify_all();
584                drop(task_guard);
585                drop(retired_output);
586                drop(retired_input);
587            }
588        });
589        Ok(())
590    }
591
592    fn stop(&mut self, ctx: &CuContext) -> CuResult<()> {
593        let mut task = self
594            .task
595            .lock()
596            .map_err(|_| CuError::from("Async task mutex poisoned during stop"))?;
597        task.stop(ctx)
598    }
599}
600
601#[derive(Reflect)]
602#[reflect(no_field_bounds, from_reflect = false, type_path = false)]
603pub struct CuAsyncSrcTask<T, O>
604where
605    T: for<'m> CuSrcTask<Output<'m> = CuMsg<O>> + Send + 'static,
606    O: CuMsgPayload + Send + 'static,
607{
608    #[reflect(ignore)]
609    task: Arc<Mutex<T>>,
610    #[reflect(ignore)]
611    state: Arc<Mutex<AsyncState<O>>>,
612    #[reflect(ignore)]
613    completion: Arc<Condvar>,
614    #[reflect(ignore)]
615    tp: Arc<ThreadPool>,
616}
617
618impl<T, O> TypePath for CuAsyncSrcTask<T, O>
619where
620    T: for<'m> CuSrcTask<Output<'m> = CuMsg<O>> + Send + 'static,
621    O: CuMsgPayload + Send + 'static,
622{
623    fn type_path() -> &'static str {
624        "cu29_runtime::cuasynctask::CuAsyncSrcTask"
625    }
626
627    fn short_type_path() -> &'static str {
628        "CuAsyncSrcTask"
629    }
630
631    fn type_ident() -> Option<&'static str> {
632        Some("CuAsyncSrcTask")
633    }
634
635    fn crate_name() -> Option<&'static str> {
636        Some("cu29_runtime")
637    }
638
639    fn module_path() -> Option<&'static str> {
640        Some("cuasynctask")
641    }
642}
643
644/// Resource bundle required by a backgrounded source.
645pub struct CuAsyncSrcTaskResources<'r, T: CuSrcTask> {
646    pub inner: T::Resources<'r>,
647    pub threadpool: Arc<ThreadPool>,
648}
649
650impl<T, O> CuAsyncSrcTask<T, O>
651where
652    T: for<'m> CuSrcTask<Output<'m> = CuMsg<O>> + Send + 'static,
653    O: CuMsgPayload + Send + 'static,
654{
655    #[allow(unused)]
656    pub fn new(
657        config: Option<&ComponentConfig>,
658        resources: T::Resources<'_>,
659        tp: Arc<ThreadPool>,
660    ) -> CuResult<Self> {
661        let task = Arc::new(Mutex::new(T::new(config, resources)?));
662        Ok(Self {
663            task,
664            state: Arc::new(Mutex::new(AsyncState::new())),
665            completion: Arc::new(Condvar::new()),
666            tp,
667        })
668    }
669
670    /// Waits until the currently dispatched job has completed.
671    #[doc(hidden)]
672    pub fn wait_for_job(&self) -> CuResult<()> {
673        let poisoned = || CuError::from("Async task state mutex poisoned while waiting for a job");
674        let mut guard = self.state.lock().map_err(|_| poisoned())?;
675        while matches!(guard.status, AsyncStatus::Running(_)) {
676            guard = self.completion.wait(guard).map_err(|_| poisoned())?;
677        }
678        Ok(())
679    }
680}
681
682impl<T, O> Freezable for CuAsyncSrcTask<T, O>
683where
684    T: for<'m> CuSrcTask<Output<'m> = CuMsg<O>> + Send + 'static,
685    O: CuMsgPayload + Send + 'static,
686{
687    fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
688        let state = self.state.lock().map_err(|_| {
689            EncodeError::OtherString("async source state mutex poisoned".to_string())
690        })?;
691        let _ = encode_async_state(&state, encoder)?;
692        Ok(())
693    }
694
695    fn thaw<D: Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
696        let mut task = self
697            .task
698            .lock()
699            .map_err(|_| DecodeError::OtherString("async source mutex poisoned".to_string()))?;
700        let restored_state = decode_async_state(decoder)?;
701        thaw_from(&mut *task, &restored_state.committed_task)?;
702
703        let mut state = self.state.lock().map_err(|_| {
704            DecodeError::OtherString("async source state mutex poisoned".to_string())
705        })?;
706        *state = restored_state;
707        Ok(())
708    }
709}
710
711impl<T, O> CuSrcTask for CuAsyncSrcTask<T, O>
712where
713    T: for<'m> CuSrcTask<Output<'m> = CuMsg<O>> + Send + 'static,
714    O: CuMsgPayload + Send + 'static,
715{
716    type Resources<'r> = CuAsyncSrcTaskResources<'r, T>;
717    type Output<'m> = T::Output<'m>;
718
719    fn new(config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self>
720    where
721        Self: Sized,
722    {
723        CuAsyncSrcTask::new(config, resources.inner, resources.threadpool)
724    }
725
726    fn start(&mut self, ctx: &CuContext) -> CuResult<()> {
727        let mut task = self
728            .task
729            .lock()
730            .map_err(|_| CuError::from("Async source mutex poisoned during start"))?;
731        task.start(ctx)?;
732        let mut state = self
733            .state
734            .lock()
735            .map_err(|_| CuError::from("Async source state mutex poisoned during start"))?;
736        let mut snapshot = core::mem::take(&mut state.task_scratch);
737        freeze_into(&*task, &mut snapshot).map_err(|error| {
738            CuError::from("Failed to snapshot async source after start").with_cause(error)
739        })?;
740        commit_initial_snapshot(&mut state, snapshot);
741        Ok(())
742    }
743
744    fn process<'o>(&mut self, ctx: &CuContext, real_output: &mut Self::Output<'o>) -> CuResult<()> {
745        let (dispatch_cl_id, mut task_snapshot) = {
746            let mut state = self.state.lock().map_err(|_| {
747                CuError::from("Async source state mutex poisoned while scheduling background work")
748            })?;
749            if matches!(state.status, AsyncStatus::Failed(_)) {
750                let AsyncStatus::Failed(error) =
751                    core::mem::replace(&mut state.status, AsyncStatus::Idle)
752                else {
753                    unreachable!();
754                };
755                return Err(CuError::from(error));
756            }
757            if matches!(state.status, AsyncStatus::Running(_)) {
758                *real_output = CuMsg::default();
759                return Ok(());
760            }
761            if let AsyncStatus::Waiting(ready_at) = state.status
762                && ctx.now() < ready_at
763            {
764                *real_output = CuMsg::default();
765                return Ok(());
766            }
767
768            let dispatch_cl_id = if let AsyncStatus::ReplayPending(cl_id) = state.status {
769                cl_id
770            } else {
771                ctx.cl_id()
772            };
773            *real_output = state.committed_output.clone();
774            state.status = AsyncStatus::Running(dispatch_cl_id);
775            (dispatch_cl_id, core::mem::take(&mut state.task_scratch))
776        };
777
778        self.tp.spawn_fifo({
779            let ctx = ctx.with_cl_id(dispatch_cl_id);
780            let task = self.task.clone();
781            let state = self.state.clone();
782            let completion = self.completion.clone();
783            move || {
784                let mut worker_output = CuMsg::default();
785                let mut task_guard = match task.lock() {
786                    Ok(guard) => guard,
787                    Err(poison) => {
788                        record_async_error(
789                            &state,
790                            &completion,
791                            CuError::from(format!("Async source mutex poisoned: {poison}")),
792                        );
793                        return;
794                    }
795                };
796                if worker_output.metadata.process_time.start.is_none() {
797                    worker_output.metadata.process_time.start = ctx.now().into();
798                }
799                let task_result = task_guard.process(&ctx, &mut worker_output);
800                let fallback_end = ctx.now();
801                let end_from_metadata: Option<CuTime> =
802                    worker_output.metadata.process_time.end.into();
803                let ready_at = end_from_metadata.unwrap_or_else(|| {
804                    worker_output.metadata.process_time.end = fallback_end.into();
805                    fallback_end
806                });
807                let snapshot_result = freeze_into(&*task_guard, &mut task_snapshot);
808                let (commit_snapshot, status) = match snapshot_result {
809                    Ok(()) => (
810                        true,
811                        match task_result {
812                            Ok(()) => AsyncStatus::Waiting(ready_at),
813                            Err(error) => failure(error),
814                        },
815                    ),
816                    Err(error) => (
817                        false,
818                        failure(
819                            CuError::from("Failed to snapshot completed async source")
820                                .with_cause(error),
821                        ),
822                    ),
823                };
824
825                let mut guard = state.lock().unwrap_or_else(|poison| poison.into_inner());
826                let retired_output = if commit_snapshot {
827                    guard.task_scratch =
828                        core::mem::replace(&mut guard.committed_task, task_snapshot);
829                    Some(core::mem::replace(
830                        &mut guard.committed_output,
831                        worker_output,
832                    ))
833                } else {
834                    guard.task_scratch = task_snapshot;
835                    None
836                };
837                guard.status = status;
838                drop(guard);
839                completion.notify_all();
840                drop(task_guard);
841                drop(retired_output);
842            }
843        });
844        Ok(())
845    }
846
847    fn stop(&mut self, ctx: &CuContext) -> CuResult<()> {
848        let mut task = self
849            .task
850            .lock()
851            .map_err(|_| CuError::from("Async source mutex poisoned during stop"))?;
852        task.stop(ctx)
853    }
854}
855
856#[cfg(test)]
857mod tests {
858    use super::*;
859    use crate::config::ComponentConfig;
860    use crate::cutask::CuMsg;
861    use crate::cutask::Freezable;
862    use crate::cutask_anytime::{
863        AnytimePolicy, AnytimeStatus, CuAnytimeRunner, CuAnytimeTask, Quality, quality_from_f32,
864    };
865    use crate::input_msg;
866    use crate::output_msg;
867    use cu29_clock::CuDuration;
868    use cu29_traits::CuResult;
869    use rayon::ThreadPoolBuilder;
870    use std::borrow::BorrowMut;
871    use std::sync::OnceLock;
872    use std::sync::mpsc;
873    use std::time::Duration;
874
875    static READY_RX: OnceLock<Arc<Mutex<mpsc::Receiver<CuTime>>>> = OnceLock::new();
876    static DONE_TX: OnceLock<mpsc::Sender<()>> = OnceLock::new();
877    #[derive(Reflect)]
878    struct TestTask {}
879
880    impl Freezable for TestTask {}
881
882    impl CuTask for TestTask {
883        type Resources<'r> = ();
884        type Input<'m> = input_msg!(u32);
885        type Output<'m> = output_msg!(u32);
886
887        fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
888        where
889            Self: Sized,
890        {
891            Ok(Self {})
892        }
893
894        fn process(
895            &mut self,
896            _ctx: &CuContext,
897            input: &Self::Input<'_>,
898            output: &mut Self::Output<'_>,
899        ) -> CuResult<()> {
900            output.borrow_mut().set_payload(*input.payload().unwrap());
901            Ok(())
902        }
903    }
904
905    #[test]
906    fn test_lifecycle() {
907        let tp = Arc::new(
908            rayon::ThreadPoolBuilder::new()
909                .num_threads(1)
910                .build()
911                .unwrap(),
912        );
913
914        let config = ComponentConfig::default();
915        let context = CuContext::new_with_clock();
916        let mut async_task: CuAsyncTask<TestTask, u32> =
917            CuAsyncTask::new(Some(&config), (), tp).unwrap();
918        async_task.start(&context).unwrap();
919        let input = CuMsg::new(Some(42u32));
920        let mut output = CuMsg::new(None);
921
922        loop {
923            {
924                let output_ref: &mut CuMsg<u32> = &mut output;
925                async_task.process(&context, &input, output_ref).unwrap();
926            }
927
928            if let Some(val) = output.payload() {
929                assert_eq!(*val, 42u32);
930                break;
931            }
932        }
933    }
934
935    #[derive(Reflect)]
936    struct ControlledTask;
937
938    impl Freezable for ControlledTask {}
939
940    impl CuTask for ControlledTask {
941        type Resources<'r> = ();
942        type Input<'m> = input_msg!(u32);
943        type Output<'m> = output_msg!(u32);
944
945        fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
946        where
947            Self: Sized,
948        {
949            Ok(Self {})
950        }
951
952        fn process(
953            &mut self,
954            ctx: &CuContext,
955            _input: &Self::Input<'_>,
956            output: &mut Self::Output<'_>,
957        ) -> CuResult<()> {
958            let rx = READY_RX
959                .get()
960                .expect("ready channel not set")
961                .lock()
962                .unwrap();
963            let ready_time = rx
964                .recv_timeout(Duration::from_secs(1))
965                .expect("timed out waiting for ready signal");
966
967            output.set_payload(ready_time.as_nanos() as u32);
968            output.metadata.process_time.start = ctx.now().into();
969            output.metadata.process_time.end = ready_time.into();
970
971            if let Some(done_tx) = DONE_TX.get() {
972                let _ = done_tx.send(());
973            }
974            Ok(())
975        }
976    }
977
978    fn wait_until_async_idle<T, O, const PROCESS_EMPTY: bool>(
979        async_task: &CuAsyncTask<T, O, PROCESS_EMPTY>,
980    ) where
981        T: for<'m> CuTask<Output<'m> = CuMsg<O>> + Send + 'static,
982        O: CuMsgPayload + Send + 'static,
983    {
984        for _ in 0..100 {
985            let state = async_task.state.lock().unwrap();
986            if !matches!(state.status, AsyncStatus::Running(_)) {
987                return;
988            }
989            drop(state);
990            std::thread::sleep(Duration::from_millis(1));
991        }
992        panic!("background task never became idle");
993    }
994
995    fn wait_until_async_src_idle<T, O>(async_task: &CuAsyncSrcTask<T, O>)
996    where
997        T: for<'m> CuSrcTask<Output<'m> = CuMsg<O>> + Send + 'static,
998        O: CuMsgPayload + Send + 'static,
999    {
1000        for _ in 0..100 {
1001            let state = async_task.state.lock().unwrap();
1002            if !matches!(state.status, AsyncStatus::Running(_)) {
1003                return;
1004            }
1005            drop(state);
1006            std::thread::sleep(Duration::from_millis(1));
1007        }
1008        panic!("background source never became idle");
1009    }
1010
1011    #[derive(Clone)]
1012    struct ActionTaskResources {
1013        actions: Arc<Mutex<mpsc::Receiver<Option<u32>>>>,
1014        done: mpsc::Sender<()>,
1015    }
1016
1017    #[derive(Reflect)]
1018    #[reflect(no_field_bounds, from_reflect = false)]
1019    struct ActionTask {
1020        #[reflect(ignore)]
1021        actions: Arc<Mutex<mpsc::Receiver<Option<u32>>>>,
1022        #[reflect(ignore)]
1023        done: mpsc::Sender<()>,
1024    }
1025
1026    impl Freezable for ActionTask {}
1027
1028    impl CuTask for ActionTask {
1029        type Resources<'r> = ActionTaskResources;
1030        type Input<'m> = input_msg!(u32);
1031        type Output<'m> = output_msg!(u32);
1032
1033        fn new(config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self>
1034        where
1035            Self: Sized,
1036        {
1037            let _ = config;
1038            Ok(Self {
1039                actions: resources.actions,
1040                done: resources.done,
1041            })
1042        }
1043
1044        fn process(
1045            &mut self,
1046            _ctx: &CuContext,
1047            _input: &Self::Input<'_>,
1048            output: &mut Self::Output<'_>,
1049        ) -> CuResult<()> {
1050            let action = self
1051                .actions
1052                .lock()
1053                .unwrap()
1054                .recv_timeout(Duration::from_secs(1))
1055                .expect("timed out waiting for action");
1056            if let Some(value) = action {
1057                output.set_payload(value);
1058            }
1059            let _ = self.done.send(());
1060            Ok(())
1061        }
1062    }
1063
1064    #[derive(Reflect)]
1065    #[reflect(no_field_bounds, from_reflect = false)]
1066    struct ActionSrc {
1067        #[reflect(ignore)]
1068        actions: Arc<Mutex<mpsc::Receiver<Option<u32>>>>,
1069        #[reflect(ignore)]
1070        done: mpsc::Sender<()>,
1071    }
1072
1073    impl Freezable for ActionSrc {}
1074
1075    impl CuSrcTask for ActionSrc {
1076        type Resources<'r> = ActionTaskResources;
1077        type Output<'m> = output_msg!(u32);
1078
1079        fn new(config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self>
1080        where
1081            Self: Sized,
1082        {
1083            let _ = config;
1084            Ok(Self {
1085                actions: resources.actions,
1086                done: resources.done,
1087            })
1088        }
1089
1090        fn process(&mut self, _ctx: &CuContext, output: &mut Self::Output<'_>) -> CuResult<()> {
1091            let action = self
1092                .actions
1093                .lock()
1094                .unwrap()
1095                .recv_timeout(Duration::from_secs(1))
1096                .expect("timed out waiting for source action");
1097            if let Some(value) = action {
1098                output.set_payload(value);
1099            }
1100            let _ = self.done.send(());
1101            Ok(())
1102        }
1103    }
1104
1105    #[derive(Clone)]
1106    struct ControlledSrcResources {
1107        ready_times: Arc<Mutex<mpsc::Receiver<CuTime>>>,
1108        done: mpsc::Sender<()>,
1109    }
1110
1111    #[derive(Reflect)]
1112    #[reflect(no_field_bounds, from_reflect = false)]
1113    struct ControlledSrc {
1114        #[reflect(ignore)]
1115        ready_times: Arc<Mutex<mpsc::Receiver<CuTime>>>,
1116        #[reflect(ignore)]
1117        done: mpsc::Sender<()>,
1118    }
1119
1120    impl Freezable for ControlledSrc {}
1121
1122    impl CuSrcTask for ControlledSrc {
1123        type Resources<'r> = ControlledSrcResources;
1124        type Output<'m> = output_msg!(u32);
1125
1126        fn new(config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self>
1127        where
1128            Self: Sized,
1129        {
1130            let _ = config;
1131            Ok(Self {
1132                ready_times: resources.ready_times,
1133                done: resources.done,
1134            })
1135        }
1136
1137        fn process(&mut self, ctx: &CuContext, output: &mut Self::Output<'_>) -> CuResult<()> {
1138            let ready_time = self
1139                .ready_times
1140                .lock()
1141                .unwrap()
1142                .recv_timeout(Duration::from_secs(1))
1143                .expect("timed out waiting for ready signal");
1144            output.set_payload(ready_time.as_nanos() as u32);
1145            output.metadata.process_time.start = ctx.now().into();
1146            output.metadata.process_time.end = ready_time.into();
1147            let _ = self.done.send(());
1148            Ok(())
1149        }
1150    }
1151
1152    #[test]
1153    fn background_skip_empty_collects_once_and_restores_consumed_state() {
1154        let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1155        let (context, clock) = CuContext::new_mock_clock();
1156        let (resources, release, observed) = replay_task_resources();
1157        let mut task: CuAsyncTask<ReplayTask, u32> = CuAsyncTask::new(None, resources, tp).unwrap();
1158        task.start(&context).unwrap();
1159        let empty = CuMsg::default();
1160        let mut output = CuMsg::new(Some(999));
1161        task.process(&context, &empty, &mut output).unwrap();
1162        assert!(output.payload().is_none());
1163        assert!(matches!(
1164            task.state.lock().unwrap().status,
1165            AsyncStatus::Idle
1166        ));
1167
1168        task.process(&context, &CuMsg::new(Some(41)), &mut output)
1169            .unwrap();
1170        task.process(&context, &empty, &mut output).unwrap();
1171        assert!(output.payload().is_none());
1172        clock.set_value(30);
1173        release.send(()).unwrap();
1174        observed.recv_timeout(Duration::from_secs(1)).unwrap();
1175        wait_until_async_idle(&task);
1176
1177        // Completion remains pending until its recorded ready time.
1178        clock.set_value(20);
1179        task.process(&context, &empty, &mut output).unwrap();
1180        assert!(output.payload().is_none());
1181        let waiting = bincode::encode_to_vec(BincodeAdapter(&task), standard()).unwrap();
1182        thaw_from(&mut task, &waiting).unwrap();
1183        clock.set_value(30);
1184        task.process(&context, &empty, &mut output).unwrap();
1185        assert_eq!(output.payload(), Some(&52));
1186        assert!(matches!(
1187            task.state.lock().unwrap().status,
1188            AsyncStatus::Idle
1189        ));
1190
1191        let consumed = bincode::encode_to_vec(BincodeAdapter(&task), standard()).unwrap();
1192        thaw_from(&mut task, &consumed).unwrap();
1193        task.process(&context, &empty, &mut output).unwrap();
1194        assert!(output.payload().is_none());
1195
1196        // New input starts another job without re-emitting the consumed result.
1197        task.process(&context, &CuMsg::new(Some(50)), &mut output)
1198            .unwrap();
1199        assert!(output.payload().is_none());
1200        release.send(()).unwrap();
1201        observed.recv_timeout(Duration::from_secs(1)).unwrap();
1202        wait_until_async_idle(&task);
1203        task.process(&context, &empty, &mut output).unwrap();
1204        assert_eq!(output.payload(), Some(&62));
1205    }
1206
1207    #[test]
1208    fn background_clears_output_while_processing() {
1209        let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1210        let context = CuContext::new_with_clock();
1211        let (action_tx, action_rx) = mpsc::channel::<Option<u32>>();
1212        let (done_tx, done_rx) = mpsc::channel::<()>();
1213        let resources = ActionTaskResources {
1214            actions: Arc::new(Mutex::new(action_rx)),
1215            done: done_tx,
1216        };
1217
1218        let mut async_task: CuAsyncTask<ActionTask, u32> =
1219            CuAsyncTask::new(Some(&ComponentConfig::default()), resources, tp).unwrap();
1220        async_task.start(&context).unwrap();
1221        let input = CuMsg::new(Some(1u32));
1222        let mut output = CuMsg::new(None);
1223
1224        async_task.process(&context, &input, &mut output).unwrap();
1225        assert!(output.payload().is_none());
1226
1227        output.set_payload(999);
1228        async_task.process(&context, &input, &mut output).unwrap();
1229        assert!(
1230            output.payload().is_none(),
1231            "background poll should clear stale output while the worker is still running"
1232        );
1233
1234        action_tx.send(Some(7)).unwrap();
1235        done_rx
1236            .recv_timeout(Duration::from_secs(1))
1237            .expect("background worker never finished");
1238    }
1239
1240    #[test]
1241    fn background_empty_run_does_not_reemit_previous_payload() {
1242        let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1243        let context = CuContext::new_with_clock();
1244        let (action_tx, action_rx) = mpsc::channel::<Option<u32>>();
1245        let (done_tx, done_rx) = mpsc::channel::<()>();
1246        let resources = ActionTaskResources {
1247            actions: Arc::new(Mutex::new(action_rx)),
1248            done: done_tx,
1249        };
1250
1251        let mut async_task: CuAsyncTask<ActionTask, u32, true> =
1252            CuAsyncTask::new(Some(&ComponentConfig::default()), resources, tp).unwrap();
1253        async_task.start(&context).unwrap();
1254        let some_input = CuMsg::new(Some(1u32));
1255        let no_input = CuMsg::new(None::<u32>);
1256        let mut output = CuMsg::new(None);
1257
1258        action_tx.send(Some(42)).unwrap();
1259        async_task
1260            .process(&context, &some_input, &mut output)
1261            .expect("failed to start first background run");
1262        done_rx
1263            .recv_timeout(Duration::from_secs(1))
1264            .expect("first background run never finished");
1265        wait_until_async_idle(&async_task);
1266
1267        action_tx.send(None).unwrap();
1268        async_task
1269            .process(&context, &no_input, &mut output)
1270            .expect("failed to start empty background run");
1271        assert_eq!(output.payload(), Some(&42));
1272        done_rx
1273            .recv_timeout(Duration::from_secs(1))
1274            .expect("empty background run never finished");
1275        wait_until_async_idle(&async_task);
1276
1277        action_tx.send(None).unwrap();
1278        async_task
1279            .process(&context, &no_input, &mut output)
1280            .expect("failed to poll after empty background run");
1281        assert!(
1282            output.payload().is_none(),
1283            "background task re-emitted the previous payload after an empty run"
1284        );
1285        done_rx
1286            .recv_timeout(Duration::from_secs(1))
1287            .expect("cleanup background run never finished");
1288    }
1289
1290    #[test]
1291    fn background_source_clears_output_while_processing() {
1292        let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1293        let context = CuContext::new_with_clock();
1294        let (action_tx, action_rx) = mpsc::channel::<Option<u32>>();
1295        let (done_tx, done_rx) = mpsc::channel::<()>();
1296        let resources = ActionTaskResources {
1297            actions: Arc::new(Mutex::new(action_rx)),
1298            done: done_tx,
1299        };
1300
1301        let mut async_src: CuAsyncSrcTask<ActionSrc, u32> =
1302            CuAsyncSrcTask::new(Some(&ComponentConfig::default()), resources, tp).unwrap();
1303        let mut output = CuMsg::new(None);
1304
1305        async_src.process(&context, &mut output).unwrap();
1306        assert!(output.payload().is_none());
1307
1308        output.set_payload(999);
1309        async_src.process(&context, &mut output).unwrap();
1310        assert!(
1311            output.payload().is_none(),
1312            "background source poll should clear stale output while the worker is still running"
1313        );
1314
1315        action_tx.send(Some(7)).unwrap();
1316        done_rx
1317            .recv_timeout(Duration::from_secs(1))
1318            .expect("background source never finished");
1319    }
1320
1321    #[test]
1322    fn background_source_empty_run_does_not_reemit_previous_payload() {
1323        let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1324        let context = CuContext::new_with_clock();
1325        let (action_tx, action_rx) = mpsc::channel::<Option<u32>>();
1326        let (done_tx, done_rx) = mpsc::channel::<()>();
1327        let resources = ActionTaskResources {
1328            actions: Arc::new(Mutex::new(action_rx)),
1329            done: done_tx,
1330        };
1331
1332        let mut async_src: CuAsyncSrcTask<ActionSrc, u32> =
1333            CuAsyncSrcTask::new(Some(&ComponentConfig::default()), resources, tp).unwrap();
1334        let mut output = CuMsg::new(None);
1335
1336        action_tx.send(Some(42)).unwrap();
1337        async_src
1338            .process(&context, &mut output)
1339            .expect("failed to start first background source run");
1340        done_rx
1341            .recv_timeout(Duration::from_secs(1))
1342            .expect("first background source run never finished");
1343        wait_until_async_src_idle(&async_src);
1344
1345        action_tx.send(None).unwrap();
1346        async_src
1347            .process(&context, &mut output)
1348            .expect("failed to start empty background source run");
1349        assert_eq!(output.payload(), Some(&42));
1350        done_rx
1351            .recv_timeout(Duration::from_secs(1))
1352            .expect("empty background source run never finished");
1353        wait_until_async_src_idle(&async_src);
1354
1355        action_tx.send(None).unwrap();
1356        async_src
1357            .process(&context, &mut output)
1358            .expect("failed to poll background source after empty run");
1359        assert!(
1360            output.payload().is_none(),
1361            "background source re-emitted the previous payload after an empty run"
1362        );
1363        done_rx
1364            .recv_timeout(Duration::from_secs(1))
1365            .expect("cleanup background source run never finished");
1366    }
1367
1368    #[test]
1369    fn background_respects_recorded_ready_time() {
1370        let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1371        let (context, clock_mock) = CuContext::new_mock_clock();
1372
1373        // Install the control channels for the task.
1374        let (ready_tx, ready_rx) = mpsc::channel::<CuTime>();
1375        let (done_tx, done_rx) = mpsc::channel::<()>();
1376        READY_RX
1377            .set(Arc::new(Mutex::new(ready_rx)))
1378            .expect("ready channel already set");
1379        DONE_TX
1380            .set(done_tx)
1381            .expect("completion channel already set");
1382
1383        let mut async_task: CuAsyncTask<ControlledTask, u32> =
1384            CuAsyncTask::new(Some(&ComponentConfig::default()), (), tp.clone()).unwrap();
1385        async_task.start(&context).unwrap();
1386        let input = CuMsg::new(Some(1u32));
1387        let mut output = CuMsg::new(None);
1388
1389        // Copperlist 0: kick off processing, nothing ready yet.
1390        clock_mock.set_value(0);
1391        async_task.process(&context, &input, &mut output).unwrap();
1392        assert!(output.payload().is_none());
1393
1394        // Copperlist 1 at time 10: still running in the background.
1395        clock_mock.set_value(10);
1396        async_task.process(&context, &input, &mut output).unwrap();
1397        assert!(output.payload().is_none());
1398
1399        // The background thread finishes at time 30 (recorded in metadata).
1400        clock_mock.set_value(30);
1401        ready_tx.send(CuTime::from(30u64)).unwrap();
1402        done_rx
1403            .recv_timeout(Duration::from_secs(1))
1404            .expect("background task never finished");
1405        // Wait until the async wrapper has cleared its processing flag and captured ready_at.
1406        let mut ready_at_recorded = false;
1407        for _ in 0..100 {
1408            let state = async_task.state.lock().unwrap();
1409            if matches!(state.status, AsyncStatus::Waiting(_)) {
1410                ready_at_recorded = true;
1411                break;
1412            }
1413            drop(state);
1414            std::thread::sleep(Duration::from_millis(1));
1415        }
1416        assert!(
1417            ready_at_recorded,
1418            "background task finished without recording ready_at"
1419        );
1420
1421        // Replay earlier than the recorded end time: the output should be held back.
1422        clock_mock.set_value(20);
1423        async_task.process(&context, &input, &mut output).unwrap();
1424        assert!(
1425            output.payload().is_none(),
1426            "Output surfaced before recorded ready time"
1427        );
1428
1429        // Once the mock clock reaches the recorded end time, the result is released.
1430        clock_mock.set_value(30);
1431        async_task.process(&context, &input, &mut output).unwrap();
1432        assert_eq!(output.payload(), Some(&30u32));
1433
1434        // Allow the background worker spawned by the last poll to complete so the thread pool shuts down cleanly.
1435        ready_tx.send(CuTime::from(40u64)).unwrap();
1436        let _ = done_rx.recv_timeout(Duration::from_secs(1));
1437    }
1438
1439    /// Anytime task under the wrapper: one increment per quantum, quality
1440    /// climbing toward the input target.
1441    #[derive(Reflect)]
1442    struct IncrementalPlanner {
1443        target: u32,
1444        acc: u32,
1445    }
1446
1447    impl Freezable for IncrementalPlanner {}
1448
1449    impl CuAnytimeTask for IncrementalPlanner {
1450        type Input<'m> = input_msg!(u32);
1451        type Output<'m> = output_msg!(u32);
1452        type Resources<'r> = ();
1453        type Quality = Quality;
1454
1455        fn new(_config: Option<&ComponentConfig>, _resources: ()) -> CuResult<Self> {
1456            Ok(Self { target: 0, acc: 0 })
1457        }
1458
1459        fn base(
1460            &mut self,
1461            _ctx: &CuContext,
1462            input: &Self::Input<'_>,
1463            output: &mut Self::Output<'_>,
1464        ) -> CuResult<AnytimeStatus<Quality>> {
1465            self.target = input.payload().copied().ok_or("planner: no input")?;
1466            self.acc = 0;
1467            output.set_payload(self.acc);
1468            Ok(AnytimeStatus::Improved(quality_from_f32(0.0)))
1469        }
1470
1471        fn refine(
1472            &mut self,
1473            _ctx: &CuContext,
1474            output: &mut Self::Output<'_>,
1475        ) -> CuResult<AnytimeStatus<Quality>> {
1476            self.acc += 1;
1477            output.set_payload(self.acc);
1478            Ok(AnytimeStatus::Improved(quality_from_f32(
1479                self.acc as f32 / self.target as f32,
1480            )))
1481        }
1482    }
1483
1484    /// Mirrors codegen for `anytime: (max_refines: 3)` on a background node.
1485    struct ThreeQuantaPolicy;
1486    impl<Q: Copy + PartialOrd> AnytimePolicy<Q> for ThreeQuantaPolicy {
1487        const TIME_BUDGET: Option<CuDuration> = None;
1488        const MAX_AGE: Option<CuDuration> = None;
1489        const MAX_STALL: Option<u32> = None;
1490        const MAX_REFINES: Option<u32> = Some(3);
1491    }
1492
1493    #[test]
1494    fn background_anytime_job_lands_with_its_status_stamp() {
1495        let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1496        let context = CuContext::new_with_clock();
1497        let mut task: CuAsyncTask<CuAnytimeRunner<IncrementalPlanner, ThreeQuantaPolicy>, u32> =
1498            CuAsyncTask::new(Some(&ComponentConfig::default()), (), tp).unwrap();
1499        task.start(&context).unwrap();
1500
1501        let input = CuMsg::new(Some(5u32));
1502        let mut output = CuMsg::new(None);
1503
1504        // Poll until the worker's job comes back through the buffered output.
1505        for _ in 0..1000 {
1506            task.process(&context, &input, &mut output).unwrap();
1507            if output.payload().is_some() {
1508                break;
1509            }
1510            std::thread::sleep(Duration::from_millis(1));
1511        }
1512
1513        // Three quanta of a job needing five: stopped by the quanta bound, and
1514        // the stamp the runner wrote survived the buffered-output copy.
1515        assert_eq!(output.payload(), Some(&3));
1516        assert_eq!(output.metadata.status_txt.0.as_str(), "any:3it q=0.60 max");
1517    }
1518
1519    #[test]
1520    fn background_source_respects_recorded_ready_time() {
1521        let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1522        let (context, clock_mock) = CuContext::new_mock_clock();
1523        let (ready_tx, ready_rx) = mpsc::channel::<CuTime>();
1524        let (done_tx, done_rx) = mpsc::channel::<()>();
1525        let resources = ControlledSrcResources {
1526            ready_times: Arc::new(Mutex::new(ready_rx)),
1527            done: done_tx,
1528        };
1529
1530        let mut async_src: CuAsyncSrcTask<ControlledSrc, u32> =
1531            CuAsyncSrcTask::new(Some(&ComponentConfig::default()), resources, tp.clone()).unwrap();
1532        let mut output = CuMsg::new(None);
1533
1534        clock_mock.set_value(0);
1535        async_src.process(&context, &mut output).unwrap();
1536        assert!(output.payload().is_none());
1537
1538        clock_mock.set_value(10);
1539        async_src.process(&context, &mut output).unwrap();
1540        assert!(output.payload().is_none());
1541
1542        clock_mock.set_value(30);
1543        ready_tx.send(CuTime::from(30u64)).unwrap();
1544        done_rx
1545            .recv_timeout(Duration::from_secs(1))
1546            .expect("background source never finished");
1547
1548        let mut ready_at_recorded = false;
1549        for _ in 0..100 {
1550            let state = async_src.state.lock().unwrap();
1551            if matches!(state.status, AsyncStatus::Waiting(_)) {
1552                ready_at_recorded = true;
1553                break;
1554            }
1555            drop(state);
1556            std::thread::sleep(Duration::from_millis(1));
1557        }
1558        assert!(
1559            ready_at_recorded,
1560            "background source finished without recording ready_at"
1561        );
1562
1563        clock_mock.set_value(20);
1564        async_src.process(&context, &mut output).unwrap();
1565        assert!(
1566            output.payload().is_none(),
1567            "background source surfaced output before recorded ready time"
1568        );
1569
1570        clock_mock.set_value(30);
1571        async_src.process(&context, &mut output).unwrap();
1572        assert_eq!(output.payload(), Some(&30u32));
1573
1574        ready_tx.send(CuTime::from(40u64)).unwrap();
1575        let _ = done_rx.recv_timeout(Duration::from_secs(1));
1576    }
1577
1578    type ReplayTaskObservation = (u32, u64, u32, u32);
1579
1580    #[derive(Clone)]
1581    struct ReplayTaskResources {
1582        release: Arc<Mutex<mpsc::Receiver<()>>>,
1583        observed: mpsc::Sender<ReplayTaskObservation>,
1584    }
1585
1586    #[derive(Reflect)]
1587    #[reflect(no_field_bounds, from_reflect = false)]
1588    struct ReplayTask {
1589        counter: u32,
1590        #[reflect(ignore)]
1591        release: Arc<Mutex<mpsc::Receiver<()>>>,
1592        #[reflect(ignore)]
1593        observed: mpsc::Sender<ReplayTaskObservation>,
1594    }
1595
1596    impl Freezable for ReplayTask {
1597        fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
1598            self.counter.encode(encoder)
1599        }
1600
1601        fn thaw<D: Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
1602            self.counter = u32::decode(decoder)?;
1603            Ok(())
1604        }
1605    }
1606
1607    impl CuTask for ReplayTask {
1608        type Resources<'r> = ReplayTaskResources;
1609        type Input<'m> = input_msg!(u32);
1610        type Output<'m> = output_msg!(u32);
1611
1612        fn new(
1613            _config: Option<&ComponentConfig>,
1614            resources: Self::Resources<'_>,
1615        ) -> CuResult<Self> {
1616            Ok(Self {
1617                counter: 0,
1618                release: resources.release,
1619                observed: resources.observed,
1620            })
1621        }
1622
1623        fn start(&mut self, _ctx: &CuContext) -> CuResult<()> {
1624            self.counter = 10;
1625            Ok(())
1626        }
1627
1628        fn process(
1629            &mut self,
1630            ctx: &CuContext,
1631            input: &Self::Input<'_>,
1632            output: &mut Self::Output<'_>,
1633        ) -> CuResult<()> {
1634            self.release
1635                .lock()
1636                .unwrap()
1637                .recv_timeout(Duration::from_secs(1))
1638                .expect("timed out waiting to release replay task");
1639            let input = input.payload().copied().expect("replay task input");
1640            self.observed
1641                .send((input, ctx.cl_id(), ctx.instance_id(), self.counter))
1642                .expect("failed to record replay task dispatch");
1643            self.counter += 1;
1644            output.set_payload(input + self.counter);
1645            Ok(())
1646        }
1647    }
1648
1649    fn replay_task_resources() -> (
1650        ReplayTaskResources,
1651        mpsc::Sender<()>,
1652        mpsc::Receiver<ReplayTaskObservation>,
1653    ) {
1654        let (release_tx, release_rx) = mpsc::channel();
1655        let (observed_tx, observed_rx) = mpsc::channel();
1656        (
1657            ReplayTaskResources {
1658                release: Arc::new(Mutex::new(release_rx)),
1659                observed: observed_tx,
1660            },
1661            release_tx,
1662            observed_rx,
1663        )
1664    }
1665
1666    #[test]
1667    fn background_freeze_mid_run_replays_original_dispatch_from_committed_state() {
1668        background_replay_pending::<false>();
1669        background_replay_pending::<true>();
1670    }
1671
1672    fn background_replay_pending<const PROCESS_EMPTY: bool>() {
1673        let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1674        let (resources, release_tx, observed_rx) = replay_task_resources();
1675        let (base_context, _) = CuContext::new_mock_clock();
1676        let mut dispatch_context = CuContext::builder(base_context.clock.clone())
1677            .cl_id(7)
1678            .instance_id(3)
1679            .task_ids(&["replay"])
1680            .build();
1681        dispatch_context.set_current_task(0);
1682        let mut original: CuAsyncTask<ReplayTask, u32, PROCESS_EMPTY> =
1683            CuAsyncTask::new(Some(&ComponentConfig::default()), resources, tp).unwrap();
1684        original.start(&dispatch_context).unwrap();
1685        let original_input = CuMsg::new(Some(41u32));
1686        let mut output = CuMsg::default();
1687        original
1688            .process(&dispatch_context, &original_input, &mut output)
1689            .unwrap();
1690
1691        let frozen = bincode::encode_to_vec(BincodeAdapter(&original), standard())
1692            .expect("mid-run async freeze failed");
1693        let committed_task = bincode::encode_to_vec(10u32, standard()).unwrap();
1694        let expected = bincode::encode_to_vec(
1695            (
1696                committed_task,
1697                CuMsg::<u32>::default(),
1698                ASYNC_PENDING_TAG,
1699                7u64,
1700                original_input.clone(),
1701            ),
1702            standard(),
1703        )
1704        .unwrap();
1705        assert_eq!(
1706            frozen, expected,
1707            "pending task frames contain only committed state, CL id, and input"
1708        );
1709        assert!(
1710            matches!(
1711                original.state.lock().unwrap().status,
1712                AsyncStatus::Running(7)
1713            ),
1714            "freezing must not disturb the live worker"
1715        );
1716        release_tx.send(()).unwrap();
1717        assert_eq!(
1718            observed_rx.recv_timeout(Duration::from_secs(1)).unwrap(),
1719            (41, 7, 3, 10)
1720        );
1721
1722        let replay_tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1723        let (replay_resources, replay_release_tx, replay_observed_rx) = replay_task_resources();
1724        let mut restored: CuAsyncTask<ReplayTask, u32, PROCESS_EMPTY> = CuAsyncTask::new(
1725            Some(&ComponentConfig::default()),
1726            replay_resources,
1727            replay_tp,
1728        )
1729        .unwrap();
1730        restored.start(&dispatch_context).unwrap();
1731        let reader = bincode::de::read::SliceReader::new(&frozen);
1732        let mut decoder = bincode::de::DecoderImpl::new(reader, standard(), ());
1733        restored.thaw(&mut decoder).unwrap();
1734        assert!(matches!(
1735            restored.state.lock().unwrap().status,
1736            AsyncStatus::ReplayPending(7)
1737        ));
1738
1739        let mut current_context = CuContext::builder(base_context.clock.clone())
1740            .cl_id(99)
1741            .instance_id(8)
1742            .task_ids(&["replay"])
1743            .build();
1744        current_context.set_current_task(0);
1745        let current_input = CuMsg::new(if PROCESS_EMPTY { Some(999u32) } else { None });
1746        restored
1747            .process(&current_context, &current_input, &mut output)
1748            .unwrap();
1749        replay_release_tx.send(()).unwrap();
1750        assert_eq!(
1751            replay_observed_rx
1752                .recv_timeout(Duration::from_secs(1))
1753                .unwrap(),
1754            (41, 7, 8, 10),
1755            "replay must retain only the original input and CopperList id"
1756        );
1757        wait_until_async_idle(&restored);
1758
1759        restored
1760            .process(&current_context, &current_input, &mut output)
1761            .unwrap();
1762        assert_eq!(output.payload(), Some(&52));
1763        if PROCESS_EMPTY {
1764            replay_release_tx.send(()).unwrap();
1765            replay_observed_rx
1766                .recv_timeout(Duration::from_secs(1))
1767                .unwrap();
1768        }
1769    }
1770
1771    #[cfg(feature = "memory_monitoring")]
1772    #[test]
1773    fn background_freeze_mid_run_does_not_allocate() {
1774        const SNAPSHOT_CAPACITY: usize = 4 * 1024;
1775
1776        let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1777        let (resources, release_tx, observed_rx) = replay_task_resources();
1778        let (context, _) = CuContext::new_mock_clock();
1779        let mut async_task: CuAsyncTask<ReplayTask, u32> =
1780            CuAsyncTask::new(Some(&ComponentConfig::default()), resources, tp).unwrap();
1781        async_task.start(&context).unwrap();
1782        let input = CuMsg::new(Some(41u32));
1783        let mut output = CuMsg::default();
1784        async_task.process(&context, &input, &mut output).unwrap();
1785
1786        let mut snapshot = [0u8; SNAPSHOT_CAPACITY];
1787        let allocations = crate::monitoring::ScopedAllocCounter::new();
1788        let writer = bincode::enc::write::SliceWriter::new(&mut snapshot);
1789        let mut encoder = EncoderImpl::new(writer, standard());
1790        BincodeAdapter(&async_task).encode(&mut encoder).unwrap();
1791        assert_eq!(allocations.allocated(), 0);
1792
1793        release_tx.send(()).unwrap();
1794        let _ = observed_rx.recv_timeout(Duration::from_secs(1));
1795    }
1796
1797    #[test]
1798    fn background_freeze_thaw_preserves_pending_error() {
1799        let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1800        let context = CuContext::new_with_clock();
1801        let mut original: CuAsyncTask<TestTask, u32> =
1802            CuAsyncTask::new(Some(&ComponentConfig::default()), (), tp).unwrap();
1803        original.start(&context).unwrap();
1804        {
1805            let mut state = original.state.lock().unwrap();
1806            let error = CuError::from("expected async failure");
1807            state.status = failure(error);
1808        }
1809        let frozen = bincode::encode_to_vec(BincodeAdapter(&original), standard()).unwrap();
1810
1811        let replay_tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1812        let mut restored: CuAsyncTask<TestTask, u32> =
1813            CuAsyncTask::new(Some(&ComponentConfig::default()), (), replay_tp).unwrap();
1814        restored.start(&context).unwrap();
1815        let reader = bincode::de::read::SliceReader::new(&frozen);
1816        let mut decoder = bincode::de::DecoderImpl::new(reader, standard(), ());
1817        restored.thaw(&mut decoder).unwrap();
1818
1819        let input = CuMsg::new(Some(1u32));
1820        let mut output = CuMsg::default();
1821        let error = restored.process(&context, &input, &mut output).unwrap_err();
1822        assert!(error.to_string().contains("expected async failure"));
1823    }
1824
1825    #[test]
1826    fn worker_completion_racing_freeze_is_an_atomic_committed_snapshot() {
1827        let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1828        let (resources, release_tx, observed_rx) = replay_task_resources();
1829        let (context, _) = CuContext::new_mock_clock();
1830        let mut async_task: CuAsyncTask<ReplayTask, u32> =
1831            CuAsyncTask::new(Some(&ComponentConfig::default()), resources, tp).unwrap();
1832        async_task.start(&context).unwrap();
1833        let input = CuMsg::new(Some(41u32));
1834        let mut output = CuMsg::default();
1835        async_task.process(&context, &input, &mut output).unwrap();
1836
1837        let mut snapshots = vec![
1838            bincode::encode_to_vec(BincodeAdapter(&async_task), standard())
1839                .expect("freeze running async task"),
1840        ];
1841        release_tx.send(()).unwrap();
1842        observed_rx
1843            .recv_timeout(Duration::from_secs(1))
1844            .expect("worker did not complete");
1845        for _ in 0..100 {
1846            snapshots.push(
1847                bincode::encode_to_vec(BincodeAdapter(&async_task), standard())
1848                    .expect("freeze async task racing completion"),
1849            );
1850            if !matches!(
1851                async_task.state.lock().unwrap().status,
1852                AsyncStatus::Running(_)
1853            ) {
1854                break;
1855            }
1856            std::thread::yield_now();
1857        }
1858        wait_until_async_idle(&async_task);
1859        snapshots.push(
1860            bincode::encode_to_vec(BincodeAdapter(&async_task), standard())
1861                .expect("freeze completed async task"),
1862        );
1863
1864        let mut saw_running = false;
1865        let mut saw_idle = false;
1866        for snapshot in snapshots {
1867            let reader = bincode::de::read::SliceReader::new(&snapshot);
1868            let mut decoder = bincode::de::DecoderImpl::new(reader, standard(), ());
1869            let dispatch = DispatchSlot::<u32>::new();
1870            let state: AsyncState<u32> =
1871                decode_async_state(&mut decoder).expect("decode raced snapshot");
1872            if matches!(state.status, AsyncStatus::ReplayPending(_)) {
1873                dispatch.decode_input(decoder.reader()).unwrap();
1874            }
1875            let (task_counter, bytes_read): (u32, usize) =
1876                bincode::decode_from_slice(&state.committed_task, standard())
1877                    .expect("decode committed task");
1878            assert_eq!(bytes_read, state.committed_task.len());
1879            match state.status {
1880                AsyncStatus::ReplayPending(0) => {
1881                    saw_running = true;
1882                    assert_eq!(task_counter, 10);
1883                    assert!(state.committed_output.payload().is_none());
1884                    assert_eq!(dispatch.get().payload(), Some(&41));
1885                }
1886                AsyncStatus::Waiting(_) => {
1887                    saw_idle = true;
1888                    assert_eq!(task_counter, 11);
1889                    assert_eq!(state.committed_output.payload(), Some(&52));
1890                }
1891                _ => panic!("decoded keyframe was neither pending nor committed"),
1892            }
1893        }
1894        assert!(saw_running, "race did not capture the pre-completion state");
1895        assert!(
1896            saw_idle,
1897            "race did not capture the committed completion state"
1898        );
1899    }
1900
1901    #[derive(Clone)]
1902    struct ReplaySrcResources {
1903        release: Arc<Mutex<mpsc::Receiver<()>>>,
1904        observed: mpsc::Sender<(u64, u32)>,
1905    }
1906
1907    #[derive(Reflect)]
1908    #[reflect(no_field_bounds, from_reflect = false)]
1909    struct ReplaySrc {
1910        counter: u32,
1911        #[reflect(ignore)]
1912        release: Arc<Mutex<mpsc::Receiver<()>>>,
1913        #[reflect(ignore)]
1914        observed: mpsc::Sender<(u64, u32)>,
1915    }
1916
1917    impl Freezable for ReplaySrc {
1918        fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
1919            self.counter.encode(encoder)
1920        }
1921
1922        fn thaw<D: Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
1923            self.counter = u32::decode(decoder)?;
1924            Ok(())
1925        }
1926    }
1927
1928    impl CuSrcTask for ReplaySrc {
1929        type Resources<'r> = ReplaySrcResources;
1930        type Output<'m> = output_msg!(u32);
1931
1932        fn new(
1933            _config: Option<&ComponentConfig>,
1934            resources: Self::Resources<'_>,
1935        ) -> CuResult<Self> {
1936            Ok(Self {
1937                counter: 0,
1938                release: resources.release,
1939                observed: resources.observed,
1940            })
1941        }
1942
1943        fn start(&mut self, _ctx: &CuContext) -> CuResult<()> {
1944            self.counter = 20;
1945            Ok(())
1946        }
1947
1948        fn process(&mut self, ctx: &CuContext, output: &mut Self::Output<'_>) -> CuResult<()> {
1949            self.release
1950                .lock()
1951                .unwrap()
1952                .recv_timeout(Duration::from_secs(1))
1953                .expect("timed out waiting to release replay source");
1954            self.observed
1955                .send((ctx.cl_id(), self.counter))
1956                .expect("failed to record replay source dispatch");
1957            self.counter += 1;
1958            output.set_payload(self.counter);
1959            Ok(())
1960        }
1961    }
1962
1963    fn replay_src_resources() -> (
1964        ReplaySrcResources,
1965        mpsc::Sender<()>,
1966        mpsc::Receiver<(u64, u32)>,
1967    ) {
1968        let (release_tx, release_rx) = mpsc::channel();
1969        let (observed_tx, observed_rx) = mpsc::channel();
1970        (
1971            ReplaySrcResources {
1972                release: Arc::new(Mutex::new(release_rx)),
1973                observed: observed_tx,
1974            },
1975            release_tx,
1976            observed_rx,
1977        )
1978    }
1979
1980    #[test]
1981    fn background_source_freeze_mid_run_replays_original_context() {
1982        let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1983        let (resources, release_tx, observed_rx) = replay_src_resources();
1984        let (base_context, _) = CuContext::new_mock_clock();
1985        let dispatch_context = CuContext::builder(base_context.clock.clone())
1986            .cl_id(7)
1987            .build();
1988        let mut original: CuAsyncSrcTask<ReplaySrc, u32> =
1989            CuAsyncSrcTask::new(Some(&ComponentConfig::default()), resources, tp).unwrap();
1990        original.start(&dispatch_context).unwrap();
1991        let mut output = CuMsg::default();
1992        original.process(&dispatch_context, &mut output).unwrap();
1993        let frozen = bincode::encode_to_vec(BincodeAdapter(&original), standard())
1994            .expect("mid-run async source freeze failed");
1995        let committed_task = bincode::encode_to_vec(20u32, standard()).unwrap();
1996        let expected = bincode::encode_to_vec(
1997            (
1998                committed_task,
1999                CuMsg::<u32>::default(),
2000                ASYNC_PENDING_TAG,
2001                7u64,
2002            ),
2003            standard(),
2004        )
2005        .unwrap();
2006        assert_eq!(
2007            frozen, expected,
2008            "pending source frames contain no task-only dispatch marker"
2009        );
2010        release_tx.send(()).unwrap();
2011        assert_eq!(
2012            observed_rx.recv_timeout(Duration::from_secs(1)).unwrap(),
2013            (7, 20)
2014        );
2015
2016        let replay_tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
2017        let (replay_resources, replay_release_tx, replay_observed_rx) = replay_src_resources();
2018        let mut restored: CuAsyncSrcTask<ReplaySrc, u32> = CuAsyncSrcTask::new(
2019            Some(&ComponentConfig::default()),
2020            replay_resources,
2021            replay_tp,
2022        )
2023        .unwrap();
2024        restored.start(&dispatch_context).unwrap();
2025        let reader = bincode::de::read::SliceReader::new(&frozen);
2026        let mut decoder = bincode::de::DecoderImpl::new(reader, standard(), ());
2027        restored.thaw(&mut decoder).unwrap();
2028        let current_context = CuContext::builder(base_context.clock.clone())
2029            .cl_id(99)
2030            .build();
2031        restored.process(&current_context, &mut output).unwrap();
2032        replay_release_tx.send(()).unwrap();
2033        assert_eq!(
2034            replay_observed_rx
2035                .recv_timeout(Duration::from_secs(1))
2036                .unwrap(),
2037            (7, 20)
2038        );
2039    }
2040}