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
116unsafe 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 unsafe { core::mem::replace(&mut *self.input.get(), input) }
133 }
134
135 fn get(&self) -> &CuMsg<I> {
136 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#[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
297pub 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 #[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 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 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
644pub 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 #[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 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 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 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 clock_mock.set_value(0);
1391 async_task.process(&context, &input, &mut output).unwrap();
1392 assert!(output.payload().is_none());
1393
1394 clock_mock.set_value(10);
1396 async_task.process(&context, &input, &mut output).unwrap();
1397 assert!(output.payload().is_none());
1398
1399 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 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 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 clock_mock.set_value(30);
1431 async_task.process(&context, &input, &mut output).unwrap();
1432 assert_eq!(output.payload(), Some(&30u32));
1433
1434 ready_tx.send(CuTime::from(40u64)).unwrap();
1436 let _ = done_rx.recv_timeout(Duration::from_secs(1));
1437 }
1438
1439 #[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 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 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 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(¤t_context, ¤t_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(¤t_context, ¤t_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(¤t_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}