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, 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>(state: &Mutex<AsyncState<O>>, error: CuError) {
236 let mut guard = match state.lock() {
237 Ok(guard) => guard,
238 Err(poison) => poison.into_inner(),
239 };
240 guard.status = failure(error);
241}
242
243#[derive(Reflect)]
244#[reflect(no_field_bounds, from_reflect = false, type_path = false)]
245pub struct CuAsyncTask<T, O>
246where
247 T: for<'m> CuTask<Output<'m> = CuMsg<O>> + Send + 'static,
248 O: CuMsgPayload + Send + 'static,
249{
250 #[reflect(ignore)]
251 task: Arc<Mutex<T>>,
252 #[reflect(ignore)]
253 state: Arc<Mutex<AsyncState<O>>>,
254 #[reflect(ignore)]
255 dispatch: Option<ErasedDispatchSlot>,
256 #[reflect(ignore)]
257 tp: Arc<ThreadPool>,
258}
259
260impl<T, O> TypePath for CuAsyncTask<T, O>
261where
262 T: for<'m> CuTask<Output<'m> = CuMsg<O>> + Send + 'static,
263 O: CuMsgPayload + Send + 'static,
264{
265 fn type_path() -> &'static str {
266 "cu29_runtime::cuasynctask::CuAsyncTask"
267 }
268
269 fn short_type_path() -> &'static str {
270 "CuAsyncTask"
271 }
272
273 fn type_ident() -> Option<&'static str> {
274 Some("CuAsyncTask")
275 }
276
277 fn crate_name() -> Option<&'static str> {
278 Some("cu29_runtime")
279 }
280
281 fn module_path() -> Option<&'static str> {
282 Some("cuasynctask")
283 }
284}
285
286pub struct CuAsyncTaskResources<'r, T: CuTask> {
288 pub inner: T::Resources<'r>,
289 pub threadpool: Arc<ThreadPool>,
290}
291
292impl<T, O> CuAsyncTask<T, O>
293where
294 T: for<'m> CuTask<Output<'m> = CuMsg<O>> + Send + 'static,
295 O: CuMsgPayload + Send + 'static,
296{
297 #[allow(unused)]
298 pub fn new(
299 config: Option<&ComponentConfig>,
300 resources: T::Resources<'_>,
301 tp: Arc<ThreadPool>,
302 ) -> CuResult<Self> {
303 let task = Arc::new(Mutex::new(T::new(config, resources)?));
304 Ok(Self {
305 task,
306 state: Arc::new(Mutex::new(AsyncState::new())),
307 dispatch: None,
308 tp,
309 })
310 }
311
312 fn initialize_dispatch<I>(&mut self) -> CuResult<()>
313 where
314 I: CuMsgPayload + Send + Sync + 'static,
315 {
316 if let Some(dispatch) = self.dispatch.as_ref() {
317 let _ = dispatch_slot::<I>(dispatch)?;
318 } else {
319 self.dispatch = Some(Arc::new(DispatchSlot::<I>::new()));
320 }
321 Ok(())
322 }
323}
324
325impl<T, O> Freezable for CuAsyncTask<T, O>
326where
327 T: for<'m> CuTask<Output<'m> = CuMsg<O>> + Send + 'static,
328 O: CuMsgPayload + Send + 'static,
329{
330 fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
331 let state = self
332 .state
333 .lock()
334 .map_err(|_| EncodeError::OtherString("async task state mutex poisoned".to_string()))?;
335 let pending = encode_async_state(&state, encoder)?;
336 if pending {
337 self.dispatch
338 .as_ref()
339 .ok_or_else(|| {
340 EncodeError::OtherString(
341 "async task pending before dispatch initialization".to_string(),
342 )
343 })?
344 .encode_input(encoder.writer())?;
345 }
346 Ok(())
347 }
348
349 fn thaw<D: Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
350 let mut task = self
351 .task
352 .lock()
353 .map_err(|_| DecodeError::OtherString("async task mutex poisoned".to_string()))?;
354 let restored_state = decode_async_state(decoder)?;
355 if matches!(restored_state.status, AsyncStatus::ReplayPending(_)) {
356 self.dispatch
357 .as_ref()
358 .ok_or_else(|| {
359 DecodeError::OtherString(
360 "async task restored before dispatch initialization".to_string(),
361 )
362 })?
363 .decode_input(decoder.reader())?;
364 }
365 thaw_from(&mut *task, &restored_state.committed_task)?;
366
367 let mut state = self
368 .state
369 .lock()
370 .map_err(|_| DecodeError::OtherString("async task state mutex poisoned".to_string()))?;
371 *state = restored_state;
372 Ok(())
373 }
374}
375
376impl<T, I, O> CuTask for CuAsyncTask<T, O>
377where
378 T: for<'i, 'o> CuTask<Input<'i> = CuMsg<I>, Output<'o> = CuMsg<O>> + Send + 'static,
379 I: CuMsgPayload + Send + Sync + 'static,
380 O: CuMsgPayload + Send + 'static,
381{
382 type Resources<'r> = CuAsyncTaskResources<'r, T>;
383 type Input<'m> = T::Input<'m>;
384 type Output<'m> = T::Output<'m>;
385
386 fn new(config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self>
387 where
388 Self: Sized,
389 {
390 CuAsyncTask::new(config, resources.inner, resources.threadpool)
391 }
392
393 fn start(&mut self, ctx: &CuContext) -> CuResult<()> {
394 self.initialize_dispatch::<I>()?;
395 let mut task = self
396 .task
397 .lock()
398 .map_err(|_| CuError::from("Async task mutex poisoned during start"))?;
399 task.start(ctx)?;
400 let mut state = self
401 .state
402 .lock()
403 .map_err(|_| CuError::from("Async task state mutex poisoned during start"))?;
404 let mut snapshot = core::mem::take(&mut state.task_scratch);
405 freeze_into(&*task, &mut snapshot).map_err(|error| {
406 CuError::from("Failed to snapshot async task after start").with_cause(error)
407 })?;
408 commit_initial_snapshot(&mut state, snapshot);
409 Ok(())
410 }
411
412 fn process<'i, 'o>(
413 &mut self,
414 ctx: &CuContext,
415 input: &Self::Input<'i>,
416 real_output: &mut Self::Output<'o>,
417 ) -> CuResult<()> {
418 let (dispatch, dispatch_cl_id, mut task_snapshot, retired_input) = {
419 let mut state = self.state.lock().map_err(|_| {
420 CuError::from("Async task state mutex poisoned while scheduling background work")
421 })?;
422 if matches!(state.status, AsyncStatus::Failed(_)) {
423 let AsyncStatus::Failed(error) =
424 core::mem::replace(&mut state.status, AsyncStatus::Idle)
425 else {
426 unreachable!();
427 };
428 return Err(CuError::from(error));
429 }
430 if matches!(state.status, AsyncStatus::Running(_)) {
431 *real_output = CuMsg::default();
432 return Ok(());
433 }
434 if let AsyncStatus::Waiting(ready_at) = state.status
435 && ctx.now() < ready_at
436 {
437 *real_output = CuMsg::default();
438 return Ok(());
439 }
440
441 let dispatch = self
442 .dispatch
443 .as_ref()
444 .ok_or_else(|| CuError::from("Async task dispatch slot was not initialized"))?;
445 let typed_dispatch = dispatch_slot::<I>(dispatch)?;
446 let (dispatch_cl_id, replay_pending) =
447 if let AsyncStatus::ReplayPending(cl_id) = state.status {
448 (cl_id, true)
449 } else {
450 (ctx.cl_id(), false)
451 };
452 let retired_input = if replay_pending {
453 CuMsg::default()
454 } else {
455 typed_dispatch.replace((*input).clone())
456 };
457 *real_output = state.committed_output.clone();
458 state.status = AsyncStatus::Running(dispatch_cl_id);
459 (
460 dispatch.clone(),
461 dispatch_cl_id,
462 core::mem::take(&mut state.task_scratch),
463 retired_input,
464 )
465 };
466
467 self.tp.spawn_fifo({
468 let ctx = ctx.with_cl_id(dispatch_cl_id);
469 let task = self.task.clone();
470 let state = self.state.clone();
471 move || {
472 let mut worker_output = CuMsg::default();
473 let typed_dispatch =
474 if let Some(slot) = dispatch.as_any().downcast_ref::<DispatchSlot<I>>() {
475 slot
476 } else {
477 record_async_error(
478 &state,
479 CuError::from("Async task dispatch slot type did not match its input"),
480 );
481 return;
482 };
483 let input_ref = typed_dispatch.get();
484 let mut task_guard = match task.lock() {
485 Ok(guard) => guard,
486 Err(poison) => {
487 record_async_error(
488 &state,
489 CuError::from(format!("Async task mutex poisoned: {poison}")),
490 );
491 return;
492 }
493 };
494 if worker_output.metadata.process_time.start.is_none() {
498 worker_output.metadata.process_time.start = ctx.now().into();
499 }
500 let task_result = task_guard.process(&ctx, input_ref, &mut worker_output);
501 let fallback_end = ctx.now();
502 let end_from_metadata: Option<CuTime> =
503 worker_output.metadata.process_time.end.into();
504 let ready_at = end_from_metadata.unwrap_or_else(|| {
505 worker_output.metadata.process_time.end = fallback_end.into();
506 fallback_end
507 });
508 let snapshot_result = freeze_into(&*task_guard, &mut task_snapshot);
509 let (commit_snapshot, status) = match snapshot_result {
510 Ok(()) => (
511 true,
512 match task_result {
513 Ok(()) => AsyncStatus::Waiting(ready_at),
514 Err(error) => failure(error),
515 },
516 ),
517 Err(error) => (
518 false,
519 failure(
520 CuError::from("Failed to snapshot completed async task")
521 .with_cause(error),
522 ),
523 ),
524 };
525
526 let mut guard = state.lock().unwrap_or_else(|poison| poison.into_inner());
527 let retired_output = if commit_snapshot {
528 guard.task_scratch =
529 core::mem::replace(&mut guard.committed_task, task_snapshot);
530 Some(core::mem::replace(
531 &mut guard.committed_output,
532 worker_output,
533 ))
534 } else {
535 guard.task_scratch = task_snapshot;
536 None
537 };
538 guard.status = status;
539 drop(guard);
540 drop(task_guard);
541 drop(retired_output);
542 drop(retired_input);
543 }
544 });
545 Ok(())
546 }
547
548 fn stop(&mut self, ctx: &CuContext) -> CuResult<()> {
549 let mut task = self
550 .task
551 .lock()
552 .map_err(|_| CuError::from("Async task mutex poisoned during stop"))?;
553 task.stop(ctx)
554 }
555}
556
557#[derive(Reflect)]
558#[reflect(no_field_bounds, from_reflect = false, type_path = false)]
559pub struct CuAsyncSrcTask<T, O>
560where
561 T: for<'m> CuSrcTask<Output<'m> = CuMsg<O>> + Send + 'static,
562 O: CuMsgPayload + Send + 'static,
563{
564 #[reflect(ignore)]
565 task: Arc<Mutex<T>>,
566 #[reflect(ignore)]
567 state: Arc<Mutex<AsyncState<O>>>,
568 #[reflect(ignore)]
569 tp: Arc<ThreadPool>,
570}
571
572impl<T, O> TypePath for CuAsyncSrcTask<T, O>
573where
574 T: for<'m> CuSrcTask<Output<'m> = CuMsg<O>> + Send + 'static,
575 O: CuMsgPayload + Send + 'static,
576{
577 fn type_path() -> &'static str {
578 "cu29_runtime::cuasynctask::CuAsyncSrcTask"
579 }
580
581 fn short_type_path() -> &'static str {
582 "CuAsyncSrcTask"
583 }
584
585 fn type_ident() -> Option<&'static str> {
586 Some("CuAsyncSrcTask")
587 }
588
589 fn crate_name() -> Option<&'static str> {
590 Some("cu29_runtime")
591 }
592
593 fn module_path() -> Option<&'static str> {
594 Some("cuasynctask")
595 }
596}
597
598pub struct CuAsyncSrcTaskResources<'r, T: CuSrcTask> {
600 pub inner: T::Resources<'r>,
601 pub threadpool: Arc<ThreadPool>,
602}
603
604impl<T, O> CuAsyncSrcTask<T, O>
605where
606 T: for<'m> CuSrcTask<Output<'m> = CuMsg<O>> + Send + 'static,
607 O: CuMsgPayload + Send + 'static,
608{
609 #[allow(unused)]
610 pub fn new(
611 config: Option<&ComponentConfig>,
612 resources: T::Resources<'_>,
613 tp: Arc<ThreadPool>,
614 ) -> CuResult<Self> {
615 let task = Arc::new(Mutex::new(T::new(config, resources)?));
616 Ok(Self {
617 task,
618 state: Arc::new(Mutex::new(AsyncState::new())),
619 tp,
620 })
621 }
622}
623
624impl<T, O> Freezable for CuAsyncSrcTask<T, O>
625where
626 T: for<'m> CuSrcTask<Output<'m> = CuMsg<O>> + Send + 'static,
627 O: CuMsgPayload + Send + 'static,
628{
629 fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
630 let state = self.state.lock().map_err(|_| {
631 EncodeError::OtherString("async source state mutex poisoned".to_string())
632 })?;
633 let _ = encode_async_state(&state, encoder)?;
634 Ok(())
635 }
636
637 fn thaw<D: Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
638 let mut task = self
639 .task
640 .lock()
641 .map_err(|_| DecodeError::OtherString("async source mutex poisoned".to_string()))?;
642 let restored_state = decode_async_state(decoder)?;
643 thaw_from(&mut *task, &restored_state.committed_task)?;
644
645 let mut state = self.state.lock().map_err(|_| {
646 DecodeError::OtherString("async source state mutex poisoned".to_string())
647 })?;
648 *state = restored_state;
649 Ok(())
650 }
651}
652
653impl<T, O> CuSrcTask for CuAsyncSrcTask<T, O>
654where
655 T: for<'m> CuSrcTask<Output<'m> = CuMsg<O>> + Send + 'static,
656 O: CuMsgPayload + Send + 'static,
657{
658 type Resources<'r> = CuAsyncSrcTaskResources<'r, T>;
659 type Output<'m> = T::Output<'m>;
660
661 fn new(config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self>
662 where
663 Self: Sized,
664 {
665 CuAsyncSrcTask::new(config, resources.inner, resources.threadpool)
666 }
667
668 fn start(&mut self, ctx: &CuContext) -> CuResult<()> {
669 let mut task = self
670 .task
671 .lock()
672 .map_err(|_| CuError::from("Async source mutex poisoned during start"))?;
673 task.start(ctx)?;
674 let mut state = self
675 .state
676 .lock()
677 .map_err(|_| CuError::from("Async source state mutex poisoned during start"))?;
678 let mut snapshot = core::mem::take(&mut state.task_scratch);
679 freeze_into(&*task, &mut snapshot).map_err(|error| {
680 CuError::from("Failed to snapshot async source after start").with_cause(error)
681 })?;
682 commit_initial_snapshot(&mut state, snapshot);
683 Ok(())
684 }
685
686 fn process<'o>(&mut self, ctx: &CuContext, real_output: &mut Self::Output<'o>) -> CuResult<()> {
687 let (dispatch_cl_id, mut task_snapshot) = {
688 let mut state = self.state.lock().map_err(|_| {
689 CuError::from("Async source state mutex poisoned while scheduling background work")
690 })?;
691 if matches!(state.status, AsyncStatus::Failed(_)) {
692 let AsyncStatus::Failed(error) =
693 core::mem::replace(&mut state.status, AsyncStatus::Idle)
694 else {
695 unreachable!();
696 };
697 return Err(CuError::from(error));
698 }
699 if matches!(state.status, AsyncStatus::Running(_)) {
700 *real_output = CuMsg::default();
701 return Ok(());
702 }
703 if let AsyncStatus::Waiting(ready_at) = state.status
704 && ctx.now() < ready_at
705 {
706 *real_output = CuMsg::default();
707 return Ok(());
708 }
709
710 let dispatch_cl_id = if let AsyncStatus::ReplayPending(cl_id) = state.status {
711 cl_id
712 } else {
713 ctx.cl_id()
714 };
715 *real_output = state.committed_output.clone();
716 state.status = AsyncStatus::Running(dispatch_cl_id);
717 (dispatch_cl_id, core::mem::take(&mut state.task_scratch))
718 };
719
720 self.tp.spawn_fifo({
721 let ctx = ctx.with_cl_id(dispatch_cl_id);
722 let task = self.task.clone();
723 let state = self.state.clone();
724 move || {
725 let mut worker_output = CuMsg::default();
726 let mut task_guard = match task.lock() {
727 Ok(guard) => guard,
728 Err(poison) => {
729 record_async_error(
730 &state,
731 CuError::from(format!("Async source mutex poisoned: {poison}")),
732 );
733 return;
734 }
735 };
736 if worker_output.metadata.process_time.start.is_none() {
737 worker_output.metadata.process_time.start = ctx.now().into();
738 }
739 let task_result = task_guard.process(&ctx, &mut worker_output);
740 let fallback_end = ctx.now();
741 let end_from_metadata: Option<CuTime> =
742 worker_output.metadata.process_time.end.into();
743 let ready_at = end_from_metadata.unwrap_or_else(|| {
744 worker_output.metadata.process_time.end = fallback_end.into();
745 fallback_end
746 });
747 let snapshot_result = freeze_into(&*task_guard, &mut task_snapshot);
748 let (commit_snapshot, status) = match snapshot_result {
749 Ok(()) => (
750 true,
751 match task_result {
752 Ok(()) => AsyncStatus::Waiting(ready_at),
753 Err(error) => failure(error),
754 },
755 ),
756 Err(error) => (
757 false,
758 failure(
759 CuError::from("Failed to snapshot completed async source")
760 .with_cause(error),
761 ),
762 ),
763 };
764
765 let mut guard = state.lock().unwrap_or_else(|poison| poison.into_inner());
766 let retired_output = if commit_snapshot {
767 guard.task_scratch =
768 core::mem::replace(&mut guard.committed_task, task_snapshot);
769 Some(core::mem::replace(
770 &mut guard.committed_output,
771 worker_output,
772 ))
773 } else {
774 guard.task_scratch = task_snapshot;
775 None
776 };
777 guard.status = status;
778 drop(guard);
779 drop(task_guard);
780 drop(retired_output);
781 }
782 });
783 Ok(())
784 }
785
786 fn stop(&mut self, ctx: &CuContext) -> CuResult<()> {
787 let mut task = self
788 .task
789 .lock()
790 .map_err(|_| CuError::from("Async source mutex poisoned during stop"))?;
791 task.stop(ctx)
792 }
793}
794
795#[cfg(test)]
796mod tests {
797 use super::*;
798 use crate::config::ComponentConfig;
799 use crate::cutask::CuMsg;
800 use crate::cutask::Freezable;
801 use crate::cutask_anytime::{
802 AnytimePolicy, AnytimeStatus, CuAnytimeRunner, CuAnytimeTask, Quality, quality_from_f32,
803 };
804 use crate::input_msg;
805 use crate::output_msg;
806 use cu29_clock::CuDuration;
807 use cu29_traits::CuResult;
808 use rayon::ThreadPoolBuilder;
809 use std::borrow::BorrowMut;
810 use std::sync::OnceLock;
811 use std::sync::mpsc;
812 use std::time::Duration;
813
814 static READY_RX: OnceLock<Arc<Mutex<mpsc::Receiver<CuTime>>>> = OnceLock::new();
815 static DONE_TX: OnceLock<mpsc::Sender<()>> = OnceLock::new();
816 #[derive(Reflect)]
817 struct TestTask {}
818
819 impl Freezable for TestTask {}
820
821 impl CuTask for TestTask {
822 type Resources<'r> = ();
823 type Input<'m> = input_msg!(u32);
824 type Output<'m> = output_msg!(u32);
825
826 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
827 where
828 Self: Sized,
829 {
830 Ok(Self {})
831 }
832
833 fn process(
834 &mut self,
835 _ctx: &CuContext,
836 input: &Self::Input<'_>,
837 output: &mut Self::Output<'_>,
838 ) -> CuResult<()> {
839 output.borrow_mut().set_payload(*input.payload().unwrap());
840 Ok(())
841 }
842 }
843
844 #[test]
845 fn test_lifecycle() {
846 let tp = Arc::new(
847 rayon::ThreadPoolBuilder::new()
848 .num_threads(1)
849 .build()
850 .unwrap(),
851 );
852
853 let config = ComponentConfig::default();
854 let context = CuContext::new_with_clock();
855 let mut async_task: CuAsyncTask<TestTask, u32> =
856 CuAsyncTask::new(Some(&config), (), tp).unwrap();
857 async_task.start(&context).unwrap();
858 let input = CuMsg::new(Some(42u32));
859 let mut output = CuMsg::new(None);
860
861 loop {
862 {
863 let output_ref: &mut CuMsg<u32> = &mut output;
864 async_task.process(&context, &input, output_ref).unwrap();
865 }
866
867 if let Some(val) = output.payload() {
868 assert_eq!(*val, 42u32);
869 break;
870 }
871 }
872 }
873
874 #[derive(Reflect)]
875 struct ControlledTask;
876
877 impl Freezable for ControlledTask {}
878
879 impl CuTask for ControlledTask {
880 type Resources<'r> = ();
881 type Input<'m> = input_msg!(u32);
882 type Output<'m> = output_msg!(u32);
883
884 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
885 where
886 Self: Sized,
887 {
888 Ok(Self {})
889 }
890
891 fn process(
892 &mut self,
893 ctx: &CuContext,
894 _input: &Self::Input<'_>,
895 output: &mut Self::Output<'_>,
896 ) -> CuResult<()> {
897 let rx = READY_RX
898 .get()
899 .expect("ready channel not set")
900 .lock()
901 .unwrap();
902 let ready_time = rx
903 .recv_timeout(Duration::from_secs(1))
904 .expect("timed out waiting for ready signal");
905
906 output.set_payload(ready_time.as_nanos() as u32);
907 output.metadata.process_time.start = ctx.now().into();
908 output.metadata.process_time.end = ready_time.into();
909
910 if let Some(done_tx) = DONE_TX.get() {
911 let _ = done_tx.send(());
912 }
913 Ok(())
914 }
915 }
916
917 fn wait_until_async_idle<T, O>(async_task: &CuAsyncTask<T, O>)
918 where
919 T: for<'m> CuTask<Output<'m> = CuMsg<O>> + Send + 'static,
920 O: CuMsgPayload + Send + 'static,
921 {
922 for _ in 0..100 {
923 let state = async_task.state.lock().unwrap();
924 if !matches!(state.status, AsyncStatus::Running(_)) {
925 return;
926 }
927 drop(state);
928 std::thread::sleep(Duration::from_millis(1));
929 }
930 panic!("background task never became idle");
931 }
932
933 fn wait_until_async_src_idle<T, O>(async_task: &CuAsyncSrcTask<T, O>)
934 where
935 T: for<'m> CuSrcTask<Output<'m> = CuMsg<O>> + Send + 'static,
936 O: CuMsgPayload + Send + 'static,
937 {
938 for _ in 0..100 {
939 let state = async_task.state.lock().unwrap();
940 if !matches!(state.status, AsyncStatus::Running(_)) {
941 return;
942 }
943 drop(state);
944 std::thread::sleep(Duration::from_millis(1));
945 }
946 panic!("background source never became idle");
947 }
948
949 #[derive(Clone)]
950 struct ActionTaskResources {
951 actions: Arc<Mutex<mpsc::Receiver<Option<u32>>>>,
952 done: mpsc::Sender<()>,
953 }
954
955 #[derive(Reflect)]
956 #[reflect(no_field_bounds, from_reflect = false)]
957 struct ActionTask {
958 #[reflect(ignore)]
959 actions: Arc<Mutex<mpsc::Receiver<Option<u32>>>>,
960 #[reflect(ignore)]
961 done: mpsc::Sender<()>,
962 }
963
964 impl Freezable for ActionTask {}
965
966 impl CuTask for ActionTask {
967 type Resources<'r> = ActionTaskResources;
968 type Input<'m> = input_msg!(u32);
969 type Output<'m> = output_msg!(u32);
970
971 fn new(config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self>
972 where
973 Self: Sized,
974 {
975 let _ = config;
976 Ok(Self {
977 actions: resources.actions,
978 done: resources.done,
979 })
980 }
981
982 fn process(
983 &mut self,
984 _ctx: &CuContext,
985 _input: &Self::Input<'_>,
986 output: &mut Self::Output<'_>,
987 ) -> CuResult<()> {
988 let action = self
989 .actions
990 .lock()
991 .unwrap()
992 .recv_timeout(Duration::from_secs(1))
993 .expect("timed out waiting for action");
994 if let Some(value) = action {
995 output.set_payload(value);
996 }
997 let _ = self.done.send(());
998 Ok(())
999 }
1000 }
1001
1002 #[derive(Reflect)]
1003 #[reflect(no_field_bounds, from_reflect = false)]
1004 struct ActionSrc {
1005 #[reflect(ignore)]
1006 actions: Arc<Mutex<mpsc::Receiver<Option<u32>>>>,
1007 #[reflect(ignore)]
1008 done: mpsc::Sender<()>,
1009 }
1010
1011 impl Freezable for ActionSrc {}
1012
1013 impl CuSrcTask for ActionSrc {
1014 type Resources<'r> = ActionTaskResources;
1015 type Output<'m> = output_msg!(u32);
1016
1017 fn new(config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self>
1018 where
1019 Self: Sized,
1020 {
1021 let _ = config;
1022 Ok(Self {
1023 actions: resources.actions,
1024 done: resources.done,
1025 })
1026 }
1027
1028 fn process(&mut self, _ctx: &CuContext, output: &mut Self::Output<'_>) -> CuResult<()> {
1029 let action = self
1030 .actions
1031 .lock()
1032 .unwrap()
1033 .recv_timeout(Duration::from_secs(1))
1034 .expect("timed out waiting for source action");
1035 if let Some(value) = action {
1036 output.set_payload(value);
1037 }
1038 let _ = self.done.send(());
1039 Ok(())
1040 }
1041 }
1042
1043 #[derive(Clone)]
1044 struct ControlledSrcResources {
1045 ready_times: Arc<Mutex<mpsc::Receiver<CuTime>>>,
1046 done: mpsc::Sender<()>,
1047 }
1048
1049 #[derive(Reflect)]
1050 #[reflect(no_field_bounds, from_reflect = false)]
1051 struct ControlledSrc {
1052 #[reflect(ignore)]
1053 ready_times: Arc<Mutex<mpsc::Receiver<CuTime>>>,
1054 #[reflect(ignore)]
1055 done: mpsc::Sender<()>,
1056 }
1057
1058 impl Freezable for ControlledSrc {}
1059
1060 impl CuSrcTask for ControlledSrc {
1061 type Resources<'r> = ControlledSrcResources;
1062 type Output<'m> = output_msg!(u32);
1063
1064 fn new(config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self>
1065 where
1066 Self: Sized,
1067 {
1068 let _ = config;
1069 Ok(Self {
1070 ready_times: resources.ready_times,
1071 done: resources.done,
1072 })
1073 }
1074
1075 fn process(&mut self, ctx: &CuContext, output: &mut Self::Output<'_>) -> CuResult<()> {
1076 let ready_time = self
1077 .ready_times
1078 .lock()
1079 .unwrap()
1080 .recv_timeout(Duration::from_secs(1))
1081 .expect("timed out waiting for ready signal");
1082 output.set_payload(ready_time.as_nanos() as u32);
1083 output.metadata.process_time.start = ctx.now().into();
1084 output.metadata.process_time.end = ready_time.into();
1085 let _ = self.done.send(());
1086 Ok(())
1087 }
1088 }
1089
1090 #[test]
1091 fn background_clears_output_while_processing() {
1092 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1093 let context = CuContext::new_with_clock();
1094 let (action_tx, action_rx) = mpsc::channel::<Option<u32>>();
1095 let (done_tx, done_rx) = mpsc::channel::<()>();
1096 let resources = ActionTaskResources {
1097 actions: Arc::new(Mutex::new(action_rx)),
1098 done: done_tx,
1099 };
1100
1101 let mut async_task: CuAsyncTask<ActionTask, u32> =
1102 CuAsyncTask::new(Some(&ComponentConfig::default()), resources, tp).unwrap();
1103 async_task.start(&context).unwrap();
1104 let input = CuMsg::new(Some(1u32));
1105 let mut output = CuMsg::new(None);
1106
1107 async_task.process(&context, &input, &mut output).unwrap();
1108 assert!(output.payload().is_none());
1109
1110 output.set_payload(999);
1111 async_task.process(&context, &input, &mut output).unwrap();
1112 assert!(
1113 output.payload().is_none(),
1114 "background poll should clear stale output while the worker is still running"
1115 );
1116
1117 action_tx.send(Some(7)).unwrap();
1118 done_rx
1119 .recv_timeout(Duration::from_secs(1))
1120 .expect("background worker never finished");
1121 }
1122
1123 #[test]
1124 fn background_empty_run_does_not_reemit_previous_payload() {
1125 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1126 let context = CuContext::new_with_clock();
1127 let (action_tx, action_rx) = mpsc::channel::<Option<u32>>();
1128 let (done_tx, done_rx) = mpsc::channel::<()>();
1129 let resources = ActionTaskResources {
1130 actions: Arc::new(Mutex::new(action_rx)),
1131 done: done_tx,
1132 };
1133
1134 let mut async_task: CuAsyncTask<ActionTask, u32> =
1135 CuAsyncTask::new(Some(&ComponentConfig::default()), resources, tp).unwrap();
1136 async_task.start(&context).unwrap();
1137 let some_input = CuMsg::new(Some(1u32));
1138 let no_input = CuMsg::new(None::<u32>);
1139 let mut output = CuMsg::new(None);
1140
1141 action_tx.send(Some(42)).unwrap();
1142 async_task
1143 .process(&context, &some_input, &mut output)
1144 .expect("failed to start first background run");
1145 done_rx
1146 .recv_timeout(Duration::from_secs(1))
1147 .expect("first background run never finished");
1148 wait_until_async_idle(&async_task);
1149
1150 action_tx.send(None).unwrap();
1151 async_task
1152 .process(&context, &no_input, &mut output)
1153 .expect("failed to start empty background run");
1154 assert_eq!(output.payload(), Some(&42));
1155 done_rx
1156 .recv_timeout(Duration::from_secs(1))
1157 .expect("empty background run never finished");
1158 wait_until_async_idle(&async_task);
1159
1160 action_tx.send(None).unwrap();
1161 async_task
1162 .process(&context, &no_input, &mut output)
1163 .expect("failed to poll after empty background run");
1164 assert!(
1165 output.payload().is_none(),
1166 "background task re-emitted the previous payload after an empty run"
1167 );
1168 done_rx
1169 .recv_timeout(Duration::from_secs(1))
1170 .expect("cleanup background run never finished");
1171 }
1172
1173 #[test]
1174 fn background_source_clears_output_while_processing() {
1175 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1176 let context = CuContext::new_with_clock();
1177 let (action_tx, action_rx) = mpsc::channel::<Option<u32>>();
1178 let (done_tx, done_rx) = mpsc::channel::<()>();
1179 let resources = ActionTaskResources {
1180 actions: Arc::new(Mutex::new(action_rx)),
1181 done: done_tx,
1182 };
1183
1184 let mut async_src: CuAsyncSrcTask<ActionSrc, u32> =
1185 CuAsyncSrcTask::new(Some(&ComponentConfig::default()), resources, tp).unwrap();
1186 let mut output = CuMsg::new(None);
1187
1188 async_src.process(&context, &mut output).unwrap();
1189 assert!(output.payload().is_none());
1190
1191 output.set_payload(999);
1192 async_src.process(&context, &mut output).unwrap();
1193 assert!(
1194 output.payload().is_none(),
1195 "background source poll should clear stale output while the worker is still running"
1196 );
1197
1198 action_tx.send(Some(7)).unwrap();
1199 done_rx
1200 .recv_timeout(Duration::from_secs(1))
1201 .expect("background source never finished");
1202 }
1203
1204 #[test]
1205 fn background_source_empty_run_does_not_reemit_previous_payload() {
1206 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1207 let context = CuContext::new_with_clock();
1208 let (action_tx, action_rx) = mpsc::channel::<Option<u32>>();
1209 let (done_tx, done_rx) = mpsc::channel::<()>();
1210 let resources = ActionTaskResources {
1211 actions: Arc::new(Mutex::new(action_rx)),
1212 done: done_tx,
1213 };
1214
1215 let mut async_src: CuAsyncSrcTask<ActionSrc, u32> =
1216 CuAsyncSrcTask::new(Some(&ComponentConfig::default()), resources, tp).unwrap();
1217 let mut output = CuMsg::new(None);
1218
1219 action_tx.send(Some(42)).unwrap();
1220 async_src
1221 .process(&context, &mut output)
1222 .expect("failed to start first background source run");
1223 done_rx
1224 .recv_timeout(Duration::from_secs(1))
1225 .expect("first background source run never finished");
1226 wait_until_async_src_idle(&async_src);
1227
1228 action_tx.send(None).unwrap();
1229 async_src
1230 .process(&context, &mut output)
1231 .expect("failed to start empty background source run");
1232 assert_eq!(output.payload(), Some(&42));
1233 done_rx
1234 .recv_timeout(Duration::from_secs(1))
1235 .expect("empty background source run never finished");
1236 wait_until_async_src_idle(&async_src);
1237
1238 action_tx.send(None).unwrap();
1239 async_src
1240 .process(&context, &mut output)
1241 .expect("failed to poll background source after empty run");
1242 assert!(
1243 output.payload().is_none(),
1244 "background source re-emitted the previous payload after an empty run"
1245 );
1246 done_rx
1247 .recv_timeout(Duration::from_secs(1))
1248 .expect("cleanup background source run never finished");
1249 }
1250
1251 #[test]
1252 fn background_respects_recorded_ready_time() {
1253 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1254 let (context, clock_mock) = CuContext::new_mock_clock();
1255
1256 let (ready_tx, ready_rx) = mpsc::channel::<CuTime>();
1258 let (done_tx, done_rx) = mpsc::channel::<()>();
1259 READY_RX
1260 .set(Arc::new(Mutex::new(ready_rx)))
1261 .expect("ready channel already set");
1262 DONE_TX
1263 .set(done_tx)
1264 .expect("completion channel already set");
1265
1266 let mut async_task: CuAsyncTask<ControlledTask, u32> =
1267 CuAsyncTask::new(Some(&ComponentConfig::default()), (), tp.clone()).unwrap();
1268 async_task.start(&context).unwrap();
1269 let input = CuMsg::new(Some(1u32));
1270 let mut output = CuMsg::new(None);
1271
1272 clock_mock.set_value(0);
1274 async_task.process(&context, &input, &mut output).unwrap();
1275 assert!(output.payload().is_none());
1276
1277 clock_mock.set_value(10);
1279 async_task.process(&context, &input, &mut output).unwrap();
1280 assert!(output.payload().is_none());
1281
1282 clock_mock.set_value(30);
1284 ready_tx.send(CuTime::from(30u64)).unwrap();
1285 done_rx
1286 .recv_timeout(Duration::from_secs(1))
1287 .expect("background task never finished");
1288 let mut ready_at_recorded = false;
1290 for _ in 0..100 {
1291 let state = async_task.state.lock().unwrap();
1292 if matches!(state.status, AsyncStatus::Waiting(_)) {
1293 ready_at_recorded = true;
1294 break;
1295 }
1296 drop(state);
1297 std::thread::sleep(Duration::from_millis(1));
1298 }
1299 assert!(
1300 ready_at_recorded,
1301 "background task finished without recording ready_at"
1302 );
1303
1304 clock_mock.set_value(20);
1306 async_task.process(&context, &input, &mut output).unwrap();
1307 assert!(
1308 output.payload().is_none(),
1309 "Output surfaced before recorded ready time"
1310 );
1311
1312 clock_mock.set_value(30);
1314 async_task.process(&context, &input, &mut output).unwrap();
1315 assert_eq!(output.payload(), Some(&30u32));
1316
1317 ready_tx.send(CuTime::from(40u64)).unwrap();
1319 let _ = done_rx.recv_timeout(Duration::from_secs(1));
1320 }
1321
1322 #[derive(Reflect)]
1325 struct IncrementalPlanner {
1326 target: u32,
1327 acc: u32,
1328 }
1329
1330 impl Freezable for IncrementalPlanner {}
1331
1332 impl CuAnytimeTask for IncrementalPlanner {
1333 type Input<'m> = input_msg!(u32);
1334 type Output<'m> = output_msg!(u32);
1335 type Resources<'r> = ();
1336 type Quality = Quality;
1337
1338 fn new(_config: Option<&ComponentConfig>, _resources: ()) -> CuResult<Self> {
1339 Ok(Self { target: 0, acc: 0 })
1340 }
1341
1342 fn base(
1343 &mut self,
1344 _ctx: &CuContext,
1345 input: &Self::Input<'_>,
1346 output: &mut Self::Output<'_>,
1347 ) -> CuResult<AnytimeStatus<Quality>> {
1348 self.target = input.payload().copied().ok_or("planner: no input")?;
1349 self.acc = 0;
1350 output.set_payload(self.acc);
1351 Ok(AnytimeStatus::Improved(quality_from_f32(0.0)))
1352 }
1353
1354 fn refine(
1355 &mut self,
1356 _ctx: &CuContext,
1357 output: &mut Self::Output<'_>,
1358 ) -> CuResult<AnytimeStatus<Quality>> {
1359 self.acc += 1;
1360 output.set_payload(self.acc);
1361 Ok(AnytimeStatus::Improved(quality_from_f32(
1362 self.acc as f32 / self.target as f32,
1363 )))
1364 }
1365 }
1366
1367 struct ThreeQuantaPolicy;
1369 impl<Q: Copy + PartialOrd> AnytimePolicy<Q> for ThreeQuantaPolicy {
1370 const TIME_BUDGET: Option<CuDuration> = None;
1371 const MAX_AGE: Option<CuDuration> = None;
1372 const MAX_STALL: Option<u32> = None;
1373 const MAX_REFINES: Option<u32> = Some(3);
1374 }
1375
1376 #[test]
1377 fn background_anytime_job_lands_with_its_status_stamp() {
1378 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1379 let context = CuContext::new_with_clock();
1380 let mut task: CuAsyncTask<CuAnytimeRunner<IncrementalPlanner, ThreeQuantaPolicy>, u32> =
1381 CuAsyncTask::new(Some(&ComponentConfig::default()), (), tp).unwrap();
1382 task.start(&context).unwrap();
1383
1384 let input = CuMsg::new(Some(5u32));
1385 let mut output = CuMsg::new(None);
1386
1387 for _ in 0..1000 {
1389 task.process(&context, &input, &mut output).unwrap();
1390 if output.payload().is_some() {
1391 break;
1392 }
1393 std::thread::sleep(Duration::from_millis(1));
1394 }
1395
1396 assert_eq!(output.payload(), Some(&3));
1399 assert_eq!(output.metadata.status_txt.0.as_str(), "any:3it q=0.60 max");
1400 }
1401
1402 #[test]
1403 fn background_source_respects_recorded_ready_time() {
1404 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1405 let (context, clock_mock) = CuContext::new_mock_clock();
1406 let (ready_tx, ready_rx) = mpsc::channel::<CuTime>();
1407 let (done_tx, done_rx) = mpsc::channel::<()>();
1408 let resources = ControlledSrcResources {
1409 ready_times: Arc::new(Mutex::new(ready_rx)),
1410 done: done_tx,
1411 };
1412
1413 let mut async_src: CuAsyncSrcTask<ControlledSrc, u32> =
1414 CuAsyncSrcTask::new(Some(&ComponentConfig::default()), resources, tp.clone()).unwrap();
1415 let mut output = CuMsg::new(None);
1416
1417 clock_mock.set_value(0);
1418 async_src.process(&context, &mut output).unwrap();
1419 assert!(output.payload().is_none());
1420
1421 clock_mock.set_value(10);
1422 async_src.process(&context, &mut output).unwrap();
1423 assert!(output.payload().is_none());
1424
1425 clock_mock.set_value(30);
1426 ready_tx.send(CuTime::from(30u64)).unwrap();
1427 done_rx
1428 .recv_timeout(Duration::from_secs(1))
1429 .expect("background source never finished");
1430
1431 let mut ready_at_recorded = false;
1432 for _ in 0..100 {
1433 let state = async_src.state.lock().unwrap();
1434 if matches!(state.status, AsyncStatus::Waiting(_)) {
1435 ready_at_recorded = true;
1436 break;
1437 }
1438 drop(state);
1439 std::thread::sleep(Duration::from_millis(1));
1440 }
1441 assert!(
1442 ready_at_recorded,
1443 "background source finished without recording ready_at"
1444 );
1445
1446 clock_mock.set_value(20);
1447 async_src.process(&context, &mut output).unwrap();
1448 assert!(
1449 output.payload().is_none(),
1450 "background source surfaced output before recorded ready time"
1451 );
1452
1453 clock_mock.set_value(30);
1454 async_src.process(&context, &mut output).unwrap();
1455 assert_eq!(output.payload(), Some(&30u32));
1456
1457 ready_tx.send(CuTime::from(40u64)).unwrap();
1458 let _ = done_rx.recv_timeout(Duration::from_secs(1));
1459 }
1460
1461 type ReplayTaskObservation = (u32, u64, u32, u32);
1462
1463 #[derive(Clone)]
1464 struct ReplayTaskResources {
1465 release: Arc<Mutex<mpsc::Receiver<()>>>,
1466 observed: mpsc::Sender<ReplayTaskObservation>,
1467 }
1468
1469 #[derive(Reflect)]
1470 #[reflect(no_field_bounds, from_reflect = false)]
1471 struct ReplayTask {
1472 counter: u32,
1473 #[reflect(ignore)]
1474 release: Arc<Mutex<mpsc::Receiver<()>>>,
1475 #[reflect(ignore)]
1476 observed: mpsc::Sender<ReplayTaskObservation>,
1477 }
1478
1479 impl Freezable for ReplayTask {
1480 fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
1481 self.counter.encode(encoder)
1482 }
1483
1484 fn thaw<D: Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
1485 self.counter = u32::decode(decoder)?;
1486 Ok(())
1487 }
1488 }
1489
1490 impl CuTask for ReplayTask {
1491 type Resources<'r> = ReplayTaskResources;
1492 type Input<'m> = input_msg!(u32);
1493 type Output<'m> = output_msg!(u32);
1494
1495 fn new(
1496 _config: Option<&ComponentConfig>,
1497 resources: Self::Resources<'_>,
1498 ) -> CuResult<Self> {
1499 Ok(Self {
1500 counter: 0,
1501 release: resources.release,
1502 observed: resources.observed,
1503 })
1504 }
1505
1506 fn start(&mut self, _ctx: &CuContext) -> CuResult<()> {
1507 self.counter = 10;
1508 Ok(())
1509 }
1510
1511 fn process(
1512 &mut self,
1513 ctx: &CuContext,
1514 input: &Self::Input<'_>,
1515 output: &mut Self::Output<'_>,
1516 ) -> CuResult<()> {
1517 self.release
1518 .lock()
1519 .unwrap()
1520 .recv_timeout(Duration::from_secs(1))
1521 .expect("timed out waiting to release replay task");
1522 let input = input.payload().copied().expect("replay task input");
1523 self.observed
1524 .send((input, ctx.cl_id(), ctx.instance_id(), self.counter))
1525 .expect("failed to record replay task dispatch");
1526 self.counter += 1;
1527 output.set_payload(input + self.counter);
1528 Ok(())
1529 }
1530 }
1531
1532 fn replay_task_resources() -> (
1533 ReplayTaskResources,
1534 mpsc::Sender<()>,
1535 mpsc::Receiver<ReplayTaskObservation>,
1536 ) {
1537 let (release_tx, release_rx) = mpsc::channel();
1538 let (observed_tx, observed_rx) = mpsc::channel();
1539 (
1540 ReplayTaskResources {
1541 release: Arc::new(Mutex::new(release_rx)),
1542 observed: observed_tx,
1543 },
1544 release_tx,
1545 observed_rx,
1546 )
1547 }
1548
1549 #[test]
1550 fn background_freeze_mid_run_replays_original_dispatch_from_committed_state() {
1551 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1552 let (resources, release_tx, observed_rx) = replay_task_resources();
1553 let (base_context, _) = CuContext::new_mock_clock();
1554 let mut dispatch_context = CuContext::builder(base_context.clock.clone())
1555 .cl_id(7)
1556 .instance_id(3)
1557 .task_ids(&["replay"])
1558 .build();
1559 dispatch_context.set_current_task(0);
1560 let mut original: CuAsyncTask<ReplayTask, u32> =
1561 CuAsyncTask::new(Some(&ComponentConfig::default()), resources, tp).unwrap();
1562 original.start(&dispatch_context).unwrap();
1563 let original_input = CuMsg::new(Some(41u32));
1564 let mut output = CuMsg::default();
1565 original
1566 .process(&dispatch_context, &original_input, &mut output)
1567 .unwrap();
1568
1569 let frozen = bincode::encode_to_vec(BincodeAdapter(&original), standard())
1570 .expect("mid-run async freeze failed");
1571 let committed_task = bincode::encode_to_vec(10u32, standard()).unwrap();
1572 let expected = bincode::encode_to_vec(
1573 (
1574 committed_task,
1575 CuMsg::<u32>::default(),
1576 ASYNC_PENDING_TAG,
1577 7u64,
1578 original_input.clone(),
1579 ),
1580 standard(),
1581 )
1582 .unwrap();
1583 assert_eq!(
1584 frozen, expected,
1585 "pending task frames contain only committed state, CL id, and input"
1586 );
1587 assert!(
1588 matches!(
1589 original.state.lock().unwrap().status,
1590 AsyncStatus::Running(7)
1591 ),
1592 "freezing must not disturb the live worker"
1593 );
1594 release_tx.send(()).unwrap();
1595 assert_eq!(
1596 observed_rx.recv_timeout(Duration::from_secs(1)).unwrap(),
1597 (41, 7, 3, 10)
1598 );
1599
1600 let replay_tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1601 let (replay_resources, replay_release_tx, replay_observed_rx) = replay_task_resources();
1602 let mut restored: CuAsyncTask<ReplayTask, u32> = CuAsyncTask::new(
1603 Some(&ComponentConfig::default()),
1604 replay_resources,
1605 replay_tp,
1606 )
1607 .unwrap();
1608 restored.start(&dispatch_context).unwrap();
1609 let reader = bincode::de::read::SliceReader::new(&frozen);
1610 let mut decoder = bincode::de::DecoderImpl::new(reader, standard(), ());
1611 restored.thaw(&mut decoder).unwrap();
1612 assert!(matches!(
1613 restored.state.lock().unwrap().status,
1614 AsyncStatus::ReplayPending(7)
1615 ));
1616
1617 let mut current_context = CuContext::builder(base_context.clock.clone())
1618 .cl_id(99)
1619 .instance_id(8)
1620 .task_ids(&["replay"])
1621 .build();
1622 current_context.set_current_task(0);
1623 let current_input = CuMsg::new(Some(999u32));
1624 restored
1625 .process(¤t_context, ¤t_input, &mut output)
1626 .unwrap();
1627 replay_release_tx.send(()).unwrap();
1628 assert_eq!(
1629 replay_observed_rx
1630 .recv_timeout(Duration::from_secs(1))
1631 .unwrap(),
1632 (41, 7, 8, 10),
1633 "replay must retain only the original input and CopperList id"
1634 );
1635 wait_until_async_idle(&restored);
1636
1637 restored
1638 .process(¤t_context, ¤t_input, &mut output)
1639 .unwrap();
1640 assert_eq!(output.payload(), Some(&52));
1641 replay_release_tx.send(()).unwrap();
1642 let _ = replay_observed_rx.recv_timeout(Duration::from_secs(1));
1643 }
1644
1645 #[cfg(feature = "memory_monitoring")]
1646 #[test]
1647 fn background_freeze_mid_run_does_not_allocate() {
1648 const SNAPSHOT_CAPACITY: usize = 4 * 1024;
1649
1650 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1651 let (resources, release_tx, observed_rx) = replay_task_resources();
1652 let (context, _) = CuContext::new_mock_clock();
1653 let mut async_task: CuAsyncTask<ReplayTask, u32> =
1654 CuAsyncTask::new(Some(&ComponentConfig::default()), resources, tp).unwrap();
1655 async_task.start(&context).unwrap();
1656 let input = CuMsg::new(Some(41u32));
1657 let mut output = CuMsg::default();
1658 async_task.process(&context, &input, &mut output).unwrap();
1659
1660 let mut snapshot = [0u8; SNAPSHOT_CAPACITY];
1661 let allocations = crate::monitoring::ScopedAllocCounter::new();
1662 let writer = bincode::enc::write::SliceWriter::new(&mut snapshot);
1663 let mut encoder = EncoderImpl::new(writer, standard());
1664 BincodeAdapter(&async_task).encode(&mut encoder).unwrap();
1665 assert_eq!(allocations.allocated(), 0);
1666
1667 release_tx.send(()).unwrap();
1668 let _ = observed_rx.recv_timeout(Duration::from_secs(1));
1669 }
1670
1671 #[test]
1672 fn background_freeze_thaw_preserves_pending_error() {
1673 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1674 let context = CuContext::new_with_clock();
1675 let mut original: CuAsyncTask<TestTask, u32> =
1676 CuAsyncTask::new(Some(&ComponentConfig::default()), (), tp).unwrap();
1677 original.start(&context).unwrap();
1678 {
1679 let mut state = original.state.lock().unwrap();
1680 let error = CuError::from("expected async failure");
1681 state.status = failure(error);
1682 }
1683 let frozen = bincode::encode_to_vec(BincodeAdapter(&original), standard()).unwrap();
1684
1685 let replay_tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1686 let mut restored: CuAsyncTask<TestTask, u32> =
1687 CuAsyncTask::new(Some(&ComponentConfig::default()), (), replay_tp).unwrap();
1688 restored.start(&context).unwrap();
1689 let reader = bincode::de::read::SliceReader::new(&frozen);
1690 let mut decoder = bincode::de::DecoderImpl::new(reader, standard(), ());
1691 restored.thaw(&mut decoder).unwrap();
1692
1693 let input = CuMsg::new(Some(1u32));
1694 let mut output = CuMsg::default();
1695 let error = restored.process(&context, &input, &mut output).unwrap_err();
1696 assert!(error.to_string().contains("expected async failure"));
1697 }
1698
1699 #[test]
1700 fn worker_completion_racing_freeze_is_an_atomic_committed_snapshot() {
1701 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1702 let (resources, release_tx, observed_rx) = replay_task_resources();
1703 let (context, _) = CuContext::new_mock_clock();
1704 let mut async_task: CuAsyncTask<ReplayTask, u32> =
1705 CuAsyncTask::new(Some(&ComponentConfig::default()), resources, tp).unwrap();
1706 async_task.start(&context).unwrap();
1707 let input = CuMsg::new(Some(41u32));
1708 let mut output = CuMsg::default();
1709 async_task.process(&context, &input, &mut output).unwrap();
1710
1711 let mut snapshots = vec![
1712 bincode::encode_to_vec(BincodeAdapter(&async_task), standard())
1713 .expect("freeze running async task"),
1714 ];
1715 release_tx.send(()).unwrap();
1716 observed_rx
1717 .recv_timeout(Duration::from_secs(1))
1718 .expect("worker did not complete");
1719 for _ in 0..100 {
1720 snapshots.push(
1721 bincode::encode_to_vec(BincodeAdapter(&async_task), standard())
1722 .expect("freeze async task racing completion"),
1723 );
1724 if !matches!(
1725 async_task.state.lock().unwrap().status,
1726 AsyncStatus::Running(_)
1727 ) {
1728 break;
1729 }
1730 std::thread::yield_now();
1731 }
1732 wait_until_async_idle(&async_task);
1733 snapshots.push(
1734 bincode::encode_to_vec(BincodeAdapter(&async_task), standard())
1735 .expect("freeze completed async task"),
1736 );
1737
1738 let mut saw_running = false;
1739 let mut saw_idle = false;
1740 for snapshot in snapshots {
1741 let reader = bincode::de::read::SliceReader::new(&snapshot);
1742 let mut decoder = bincode::de::DecoderImpl::new(reader, standard(), ());
1743 let dispatch = DispatchSlot::<u32>::new();
1744 let state: AsyncState<u32> =
1745 decode_async_state(&mut decoder).expect("decode raced snapshot");
1746 if matches!(state.status, AsyncStatus::ReplayPending(_)) {
1747 dispatch.decode_input(decoder.reader()).unwrap();
1748 }
1749 let (task_counter, bytes_read): (u32, usize) =
1750 bincode::decode_from_slice(&state.committed_task, standard())
1751 .expect("decode committed task");
1752 assert_eq!(bytes_read, state.committed_task.len());
1753 match state.status {
1754 AsyncStatus::ReplayPending(0) => {
1755 saw_running = true;
1756 assert_eq!(task_counter, 10);
1757 assert!(state.committed_output.payload().is_none());
1758 assert_eq!(dispatch.get().payload(), Some(&41));
1759 }
1760 AsyncStatus::Waiting(_) => {
1761 saw_idle = true;
1762 assert_eq!(task_counter, 11);
1763 assert_eq!(state.committed_output.payload(), Some(&52));
1764 }
1765 _ => panic!("decoded keyframe was neither pending nor committed"),
1766 }
1767 }
1768 assert!(saw_running, "race did not capture the pre-completion state");
1769 assert!(
1770 saw_idle,
1771 "race did not capture the committed completion state"
1772 );
1773 }
1774
1775 #[derive(Clone)]
1776 struct ReplaySrcResources {
1777 release: Arc<Mutex<mpsc::Receiver<()>>>,
1778 observed: mpsc::Sender<(u64, u32)>,
1779 }
1780
1781 #[derive(Reflect)]
1782 #[reflect(no_field_bounds, from_reflect = false)]
1783 struct ReplaySrc {
1784 counter: u32,
1785 #[reflect(ignore)]
1786 release: Arc<Mutex<mpsc::Receiver<()>>>,
1787 #[reflect(ignore)]
1788 observed: mpsc::Sender<(u64, u32)>,
1789 }
1790
1791 impl Freezable for ReplaySrc {
1792 fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
1793 self.counter.encode(encoder)
1794 }
1795
1796 fn thaw<D: Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
1797 self.counter = u32::decode(decoder)?;
1798 Ok(())
1799 }
1800 }
1801
1802 impl CuSrcTask for ReplaySrc {
1803 type Resources<'r> = ReplaySrcResources;
1804 type Output<'m> = output_msg!(u32);
1805
1806 fn new(
1807 _config: Option<&ComponentConfig>,
1808 resources: Self::Resources<'_>,
1809 ) -> CuResult<Self> {
1810 Ok(Self {
1811 counter: 0,
1812 release: resources.release,
1813 observed: resources.observed,
1814 })
1815 }
1816
1817 fn start(&mut self, _ctx: &CuContext) -> CuResult<()> {
1818 self.counter = 20;
1819 Ok(())
1820 }
1821
1822 fn process(&mut self, ctx: &CuContext, output: &mut Self::Output<'_>) -> CuResult<()> {
1823 self.release
1824 .lock()
1825 .unwrap()
1826 .recv_timeout(Duration::from_secs(1))
1827 .expect("timed out waiting to release replay source");
1828 self.observed
1829 .send((ctx.cl_id(), self.counter))
1830 .expect("failed to record replay source dispatch");
1831 self.counter += 1;
1832 output.set_payload(self.counter);
1833 Ok(())
1834 }
1835 }
1836
1837 fn replay_src_resources() -> (
1838 ReplaySrcResources,
1839 mpsc::Sender<()>,
1840 mpsc::Receiver<(u64, u32)>,
1841 ) {
1842 let (release_tx, release_rx) = mpsc::channel();
1843 let (observed_tx, observed_rx) = mpsc::channel();
1844 (
1845 ReplaySrcResources {
1846 release: Arc::new(Mutex::new(release_rx)),
1847 observed: observed_tx,
1848 },
1849 release_tx,
1850 observed_rx,
1851 )
1852 }
1853
1854 #[test]
1855 fn background_source_freeze_mid_run_replays_original_context() {
1856 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1857 let (resources, release_tx, observed_rx) = replay_src_resources();
1858 let (base_context, _) = CuContext::new_mock_clock();
1859 let dispatch_context = CuContext::builder(base_context.clock.clone())
1860 .cl_id(7)
1861 .build();
1862 let mut original: CuAsyncSrcTask<ReplaySrc, u32> =
1863 CuAsyncSrcTask::new(Some(&ComponentConfig::default()), resources, tp).unwrap();
1864 original.start(&dispatch_context).unwrap();
1865 let mut output = CuMsg::default();
1866 original.process(&dispatch_context, &mut output).unwrap();
1867 let frozen = bincode::encode_to_vec(BincodeAdapter(&original), standard())
1868 .expect("mid-run async source freeze failed");
1869 let committed_task = bincode::encode_to_vec(20u32, standard()).unwrap();
1870 let expected = bincode::encode_to_vec(
1871 (
1872 committed_task,
1873 CuMsg::<u32>::default(),
1874 ASYNC_PENDING_TAG,
1875 7u64,
1876 ),
1877 standard(),
1878 )
1879 .unwrap();
1880 assert_eq!(
1881 frozen, expected,
1882 "pending source frames contain no task-only dispatch marker"
1883 );
1884 release_tx.send(()).unwrap();
1885 assert_eq!(
1886 observed_rx.recv_timeout(Duration::from_secs(1)).unwrap(),
1887 (7, 20)
1888 );
1889
1890 let replay_tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1891 let (replay_resources, replay_release_tx, replay_observed_rx) = replay_src_resources();
1892 let mut restored: CuAsyncSrcTask<ReplaySrc, u32> = CuAsyncSrcTask::new(
1893 Some(&ComponentConfig::default()),
1894 replay_resources,
1895 replay_tp,
1896 )
1897 .unwrap();
1898 restored.start(&dispatch_context).unwrap();
1899 let reader = bincode::de::read::SliceReader::new(&frozen);
1900 let mut decoder = bincode::de::DecoderImpl::new(reader, standard(), ());
1901 restored.thaw(&mut decoder).unwrap();
1902 let current_context = CuContext::builder(base_context.clock.clone())
1903 .cl_id(99)
1904 .build();
1905 restored.process(¤t_context, &mut output).unwrap();
1906 replay_release_tx.send(()).unwrap();
1907 assert_eq!(
1908 replay_observed_rx
1909 .recv_timeout(Duration::from_secs(1))
1910 .unwrap(),
1911 (7, 20)
1912 );
1913 }
1914}