1use crate::config::ComponentConfig;
2use crate::context::CuContext;
3use crate::cutask::{CuMsg, CuMsgPayload, CuSrcTask, CuTask, Freezable};
4use crate::reflect::{Reflect, TypePath};
5use bincode::de::{Decode, Decoder};
6use bincode::enc::{Encode, Encoder};
7use bincode::error::{DecodeError, EncodeError};
8use cu29_clock::CuTime;
9use cu29_traits::{CuError, CuResult};
10use rayon::ThreadPool;
11use std::sync::{Arc, Mutex};
12
13struct AsyncState {
14 processing: bool,
15 ready_at: Option<CuTime>,
16 last_error: Option<CuError>,
17}
18
19fn encode_async_state<E: Encoder>(state: &AsyncState, encoder: &mut E) -> Result<(), EncodeError> {
20 if state.processing {
21 return Err(EncodeError::OtherString(
22 "cannot freeze async task while background work is in progress".to_string(),
23 ));
24 }
25
26 Encode::encode(&state.ready_at, encoder)?;
27 let last_error = state.last_error.as_ref().map(ToString::to_string);
28 Encode::encode(&last_error, encoder)?;
29 Ok(())
30}
31
32fn decode_async_state<D: Decoder>(
33 state: &mut AsyncState,
34 decoder: &mut D,
35) -> Result<(), DecodeError> {
36 state.processing = false;
37 state.ready_at = Decode::decode(decoder)?;
38 let last_error: Option<String> = Decode::decode(decoder)?;
39 state.last_error = last_error.map(CuError::from);
40 Ok(())
41}
42
43fn encode_buffered_output<O, E>(output: &CuMsg<O>, encoder: &mut E) -> Result<(), EncodeError>
44where
45 O: CuMsgPayload + Send + 'static,
46 E: Encoder,
47{
48 let bytes = bincode::encode_to_vec(output, bincode::config::standard())?;
49 Encode::encode(&bytes, encoder)
50}
51
52fn decode_buffered_output<O, D>(decoder: &mut D) -> Result<CuMsg<O>, DecodeError>
53where
54 O: CuMsgPayload + Send + 'static,
55 D: Decoder,
56{
57 let bytes: Vec<u8> = Decode::decode(decoder)?;
58 let (output, bytes_read): (CuMsg<O>, usize) =
59 bincode::decode_from_slice(&bytes, bincode::config::standard())?;
60 if bytes_read != bytes.len() {
61 return Err(DecodeError::OtherString(
62 "async task buffered output snapshot had trailing bytes".to_string(),
63 ));
64 }
65 Ok(output)
66}
67
68fn record_async_error(state: &Mutex<AsyncState>, error: CuError) {
69 let mut guard = match state.lock() {
70 Ok(guard) => guard,
71 Err(poison) => poison.into_inner(),
72 };
73 guard.processing = false;
74 guard.ready_at = None;
75 guard.last_error = Some(error);
76}
77
78fn begin_background_poll<O>(
79 ctx: &CuContext,
80 state: &Mutex<AsyncState>,
81 buffered_output: &Mutex<CuMsg<O>>,
82 real_output: &mut CuMsg<O>,
83) -> CuResult<bool>
84where
85 O: CuMsgPayload + Send + 'static,
86{
87 {
88 let mut state = state.lock().map_err(|_| {
89 CuError::from("Async task state mutex poisoned while scheduling background work")
90 })?;
91 if let Some(error) = state.last_error.take() {
92 return Err(error);
93 }
94 if state.processing {
95 *real_output = CuMsg::default();
96 return Ok(false);
97 }
98
99 if let Some(ready_at) = state.ready_at
100 && ctx.now() < ready_at
101 {
102 *real_output = CuMsg::default();
103 return Ok(false);
104 }
105
106 state.processing = true;
107 state.ready_at = None;
108 }
109
110 let buffered_output = buffered_output.lock().map_err(|_| {
111 let error = CuError::from("Async task output mutex poisoned");
112 record_async_error(state, error.clone());
113 error
114 })?;
115 *real_output = buffered_output.clone();
116 Ok(true)
117}
118
119fn finalize_background_run<O>(
120 state: &Mutex<AsyncState>,
121 output_ref: &mut CuMsg<O>,
122 fallback_end: CuTime,
123 task_result: CuResult<()>,
124) where
125 O: CuMsgPayload + Send + 'static,
126{
127 let mut guard = state.lock().unwrap_or_else(|poison| poison.into_inner());
128 guard.processing = false;
129
130 match task_result {
131 Ok(()) => {
132 let end_from_metadata: Option<CuTime> = output_ref.metadata.process_time.end.into();
133 let end_time = end_from_metadata.unwrap_or_else(|| {
134 output_ref.metadata.process_time.end = fallback_end.into();
135 fallback_end
136 });
137 guard.ready_at = Some(end_time);
138 }
139 Err(error) => {
140 guard.ready_at = None;
141 guard.last_error = Some(error);
142 }
143 }
144}
145
146#[derive(Reflect)]
147#[reflect(no_field_bounds, from_reflect = false, type_path = false)]
148pub struct CuAsyncTask<T, O>
149where
150 T: for<'m> CuTask<Output<'m> = CuMsg<O>> + Send + 'static,
151 O: CuMsgPayload + Send + 'static,
152{
153 #[reflect(ignore)]
154 task: Arc<Mutex<T>>,
155 #[reflect(ignore)]
156 output: Arc<Mutex<CuMsg<O>>>,
157 #[reflect(ignore)]
158 state: Arc<Mutex<AsyncState>>,
159 #[reflect(ignore)]
160 tp: Arc<ThreadPool>,
161}
162
163impl<T, O> TypePath for CuAsyncTask<T, O>
164where
165 T: for<'m> CuTask<Output<'m> = CuMsg<O>> + Send + 'static,
166 O: CuMsgPayload + Send + 'static,
167{
168 fn type_path() -> &'static str {
169 "cu29_runtime::cuasynctask::CuAsyncTask"
170 }
171
172 fn short_type_path() -> &'static str {
173 "CuAsyncTask"
174 }
175
176 fn type_ident() -> Option<&'static str> {
177 Some("CuAsyncTask")
178 }
179
180 fn crate_name() -> Option<&'static str> {
181 Some("cu29_runtime")
182 }
183
184 fn module_path() -> Option<&'static str> {
185 Some("cuasynctask")
186 }
187}
188
189pub struct CuAsyncTaskResources<'r, T: CuTask> {
191 pub inner: T::Resources<'r>,
192 pub threadpool: Arc<ThreadPool>,
193}
194
195impl<T, O> CuAsyncTask<T, O>
196where
197 T: for<'m> CuTask<Output<'m> = CuMsg<O>> + Send + 'static,
198 O: CuMsgPayload + Send + 'static,
199{
200 #[allow(unused)]
201 pub fn new(
202 config: Option<&ComponentConfig>,
203 resources: T::Resources<'_>,
204 tp: Arc<ThreadPool>,
205 ) -> CuResult<Self> {
206 let task = Arc::new(Mutex::new(T::new(config, resources)?));
207 let output = Arc::new(Mutex::new(CuMsg::default()));
208 Ok(Self {
209 task,
210 output,
211 state: Arc::new(Mutex::new(AsyncState {
212 processing: false,
213 ready_at: None,
214 last_error: None,
215 })),
216 tp,
217 })
218 }
219}
220
221impl<T, O> Freezable for CuAsyncTask<T, O>
222where
223 T: for<'m> CuTask<Output<'m> = CuMsg<O>> + Send + 'static,
224 O: CuMsgPayload + Send + 'static,
225{
226 fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
227 let state = self
228 .state
229 .lock()
230 .map_err(|_| EncodeError::OtherString("async task state mutex poisoned".to_string()))?;
231 encode_async_state(&state, encoder)?;
232
233 let task = self
234 .task
235 .lock()
236 .map_err(|_| EncodeError::OtherString("async task mutex poisoned".to_string()))?;
237 task.freeze(encoder)?;
238
239 let output = self.output.lock().map_err(|_| {
240 EncodeError::OtherString("async task output mutex poisoned".to_string())
241 })?;
242 encode_buffered_output(&output, encoder)?;
243 Ok(())
244 }
245
246 fn thaw<D: Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
247 let mut state = self
248 .state
249 .lock()
250 .map_err(|_| DecodeError::OtherString("async task state mutex poisoned".to_string()))?;
251 decode_async_state(&mut state, decoder)?;
252
253 let mut task = self
254 .task
255 .lock()
256 .map_err(|_| DecodeError::OtherString("async task mutex poisoned".to_string()))?;
257 task.thaw(decoder)?;
258
259 let mut output = self.output.lock().map_err(|_| {
260 DecodeError::OtherString("async task output mutex poisoned".to_string())
261 })?;
262 *output = decode_buffered_output(decoder)?;
263 Ok(())
264 }
265}
266
267impl<T, I, O> CuTask for CuAsyncTask<T, O>
268where
269 T: for<'i, 'o> CuTask<Input<'i> = CuMsg<I>, Output<'o> = CuMsg<O>> + Send + 'static,
270 I: CuMsgPayload + Send + Sync + 'static,
271 O: CuMsgPayload + Send + 'static,
272{
273 type Resources<'r> = CuAsyncTaskResources<'r, T>;
274 type Input<'m> = T::Input<'m>;
275 type Output<'m> = T::Output<'m>;
276
277 fn new(config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self>
278 where
279 Self: Sized,
280 {
281 CuAsyncTask::new(config, resources.inner, resources.threadpool)
282 }
283
284 fn start(&mut self, ctx: &CuContext) -> CuResult<()> {
285 let mut task = self
286 .task
287 .lock()
288 .map_err(|_| CuError::from("Async task mutex poisoned during start"))?;
289 task.start(ctx)
290 }
291
292 fn process<'i, 'o>(
293 &mut self,
294 ctx: &CuContext,
295 input: &Self::Input<'i>,
296 real_output: &mut Self::Output<'o>,
297 ) -> CuResult<()> {
298 if !begin_background_poll(ctx, &self.state, &self.output, real_output)? {
299 return Ok(());
300 }
301
302 self.tp.spawn_fifo({
304 let ctx = ctx.clone();
305 let input = (*input).clone();
306 let output = self.output.clone();
307 let task = self.task.clone();
308 let state = self.state.clone();
309 move || {
310 let input_ref: &CuMsg<I> = &input;
311 let mut output_guard = match output.lock() {
312 Ok(guard) => guard,
313 Err(_) => {
314 record_async_error(
315 &state,
316 CuError::from("Async task output mutex poisoned"),
317 );
318 return;
319 }
320 };
321 let output_ref: &mut CuMsg<O> = &mut output_guard;
322
323 *output_ref = CuMsg::default();
326
327 if output_ref.metadata.process_time.start.is_none() {
329 output_ref.metadata.process_time.start = ctx.now().into();
330 }
331 let task_result = match task.lock() {
332 Ok(mut task_guard) => task_guard.process(&ctx, input_ref, output_ref),
333 Err(poison) => Err(CuError::from(format!(
334 "Async task mutex poisoned: {poison}"
335 ))),
336 };
337 finalize_background_run(&state, output_ref, ctx.now(), task_result);
338 }
339 });
340 Ok(())
341 }
342
343 fn stop(&mut self, ctx: &CuContext) -> CuResult<()> {
344 let mut task = self
345 .task
346 .lock()
347 .map_err(|_| CuError::from("Async task mutex poisoned during stop"))?;
348 task.stop(ctx)
349 }
350}
351
352#[derive(Reflect)]
353#[reflect(no_field_bounds, from_reflect = false, type_path = false)]
354pub struct CuAsyncSrcTask<T, O>
355where
356 T: for<'m> CuSrcTask<Output<'m> = CuMsg<O>> + Send + 'static,
357 O: CuMsgPayload + Send + 'static,
358{
359 #[reflect(ignore)]
360 task: Arc<Mutex<T>>,
361 #[reflect(ignore)]
362 output: Arc<Mutex<CuMsg<O>>>,
363 #[reflect(ignore)]
364 state: Arc<Mutex<AsyncState>>,
365 #[reflect(ignore)]
366 tp: Arc<ThreadPool>,
367}
368
369impl<T, O> TypePath for CuAsyncSrcTask<T, O>
370where
371 T: for<'m> CuSrcTask<Output<'m> = CuMsg<O>> + Send + 'static,
372 O: CuMsgPayload + Send + 'static,
373{
374 fn type_path() -> &'static str {
375 "cu29_runtime::cuasynctask::CuAsyncSrcTask"
376 }
377
378 fn short_type_path() -> &'static str {
379 "CuAsyncSrcTask"
380 }
381
382 fn type_ident() -> Option<&'static str> {
383 Some("CuAsyncSrcTask")
384 }
385
386 fn crate_name() -> Option<&'static str> {
387 Some("cu29_runtime")
388 }
389
390 fn module_path() -> Option<&'static str> {
391 Some("cuasynctask")
392 }
393}
394
395pub struct CuAsyncSrcTaskResources<'r, T: CuSrcTask> {
397 pub inner: T::Resources<'r>,
398 pub threadpool: Arc<ThreadPool>,
399}
400
401impl<T, O> CuAsyncSrcTask<T, O>
402where
403 T: for<'m> CuSrcTask<Output<'m> = CuMsg<O>> + Send + 'static,
404 O: CuMsgPayload + Send + 'static,
405{
406 #[allow(unused)]
407 pub fn new(
408 config: Option<&ComponentConfig>,
409 resources: T::Resources<'_>,
410 tp: Arc<ThreadPool>,
411 ) -> CuResult<Self> {
412 let task = Arc::new(Mutex::new(T::new(config, resources)?));
413 let output = Arc::new(Mutex::new(CuMsg::default()));
414 Ok(Self {
415 task,
416 output,
417 state: Arc::new(Mutex::new(AsyncState {
418 processing: false,
419 ready_at: None,
420 last_error: None,
421 })),
422 tp,
423 })
424 }
425}
426
427impl<T, O> Freezable for CuAsyncSrcTask<T, O>
428where
429 T: for<'m> CuSrcTask<Output<'m> = CuMsg<O>> + Send + 'static,
430 O: CuMsgPayload + Send + 'static,
431{
432 fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
433 let state = self.state.lock().map_err(|_| {
434 EncodeError::OtherString("async source state mutex poisoned".to_string())
435 })?;
436 encode_async_state(&state, encoder)?;
437
438 let task = self
439 .task
440 .lock()
441 .map_err(|_| EncodeError::OtherString("async source mutex poisoned".to_string()))?;
442 task.freeze(encoder)?;
443
444 let output = self.output.lock().map_err(|_| {
445 EncodeError::OtherString("async source output mutex poisoned".to_string())
446 })?;
447 encode_buffered_output(&output, encoder)?;
448 Ok(())
449 }
450
451 fn thaw<D: Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
452 let mut state = self.state.lock().map_err(|_| {
453 DecodeError::OtherString("async source state mutex poisoned".to_string())
454 })?;
455 decode_async_state(&mut state, decoder)?;
456
457 let mut task = self
458 .task
459 .lock()
460 .map_err(|_| DecodeError::OtherString("async source mutex poisoned".to_string()))?;
461 task.thaw(decoder)?;
462
463 let mut output = self.output.lock().map_err(|_| {
464 DecodeError::OtherString("async source output mutex poisoned".to_string())
465 })?;
466 *output = decode_buffered_output(decoder)?;
467 Ok(())
468 }
469}
470
471impl<T, O> CuSrcTask for CuAsyncSrcTask<T, O>
472where
473 T: for<'m> CuSrcTask<Output<'m> = CuMsg<O>> + Send + 'static,
474 O: CuMsgPayload + Send + 'static,
475{
476 type Resources<'r> = CuAsyncSrcTaskResources<'r, T>;
477 type Output<'m> = T::Output<'m>;
478
479 fn new(config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self>
480 where
481 Self: Sized,
482 {
483 CuAsyncSrcTask::new(config, resources.inner, resources.threadpool)
484 }
485
486 fn start(&mut self, ctx: &CuContext) -> CuResult<()> {
487 let mut task = self
488 .task
489 .lock()
490 .map_err(|_| CuError::from("Async source mutex poisoned during start"))?;
491 task.start(ctx)
492 }
493
494 fn process<'o>(&mut self, ctx: &CuContext, real_output: &mut Self::Output<'o>) -> CuResult<()> {
495 if !begin_background_poll(ctx, &self.state, &self.output, real_output)? {
496 return Ok(());
497 }
498
499 self.tp.spawn_fifo({
500 let ctx = ctx.clone();
501 let output = self.output.clone();
502 let task = self.task.clone();
503 let state = self.state.clone();
504 move || {
505 let mut output_guard = match output.lock() {
506 Ok(guard) => guard,
507 Err(_) => {
508 record_async_error(
509 &state,
510 CuError::from("Async task output mutex poisoned"),
511 );
512 return;
513 }
514 };
515 let output_ref: &mut CuMsg<O> = &mut output_guard;
516
517 *output_ref = CuMsg::default();
518
519 if output_ref.metadata.process_time.start.is_none() {
520 output_ref.metadata.process_time.start = ctx.now().into();
521 }
522 let task_result = match task.lock() {
523 Ok(mut task_guard) => task_guard.process(&ctx, output_ref),
524 Err(poison) => Err(CuError::from(format!(
525 "Async source mutex poisoned: {poison}"
526 ))),
527 };
528 finalize_background_run(&state, output_ref, ctx.now(), task_result);
529 }
530 });
531 Ok(())
532 }
533
534 fn stop(&mut self, ctx: &CuContext) -> CuResult<()> {
535 let mut task = self
536 .task
537 .lock()
538 .map_err(|_| CuError::from("Async source mutex poisoned during stop"))?;
539 task.stop(ctx)
540 }
541}
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546 use crate::config::ComponentConfig;
547 use crate::cutask::CuMsg;
548 use crate::cutask::Freezable;
549 use crate::input_msg;
550 use crate::output_msg;
551 use cu29_traits::CuResult;
552 use rayon::ThreadPoolBuilder;
553 use std::borrow::BorrowMut;
554 use std::sync::OnceLock;
555 use std::sync::mpsc;
556 use std::time::Duration;
557
558 static READY_RX: OnceLock<Arc<Mutex<mpsc::Receiver<CuTime>>>> = OnceLock::new();
559 static DONE_TX: OnceLock<mpsc::Sender<()>> = OnceLock::new();
560 #[derive(Reflect)]
561 struct TestTask {}
562
563 impl Freezable for TestTask {}
564
565 impl CuTask for TestTask {
566 type Resources<'r> = ();
567 type Input<'m> = input_msg!(u32);
568 type Output<'m> = output_msg!(u32);
569
570 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
571 where
572 Self: Sized,
573 {
574 Ok(Self {})
575 }
576
577 fn process(
578 &mut self,
579 _ctx: &CuContext,
580 input: &Self::Input<'_>,
581 output: &mut Self::Output<'_>,
582 ) -> CuResult<()> {
583 output.borrow_mut().set_payload(*input.payload().unwrap());
584 Ok(())
585 }
586 }
587
588 #[test]
589 fn test_lifecycle() {
590 let tp = Arc::new(
591 rayon::ThreadPoolBuilder::new()
592 .num_threads(1)
593 .build()
594 .unwrap(),
595 );
596
597 let config = ComponentConfig::default();
598 let context = CuContext::new_with_clock();
599 let mut async_task: CuAsyncTask<TestTask, u32> =
600 CuAsyncTask::new(Some(&config), (), tp).unwrap();
601 let input = CuMsg::new(Some(42u32));
602 let mut output = CuMsg::new(None);
603
604 loop {
605 {
606 let output_ref: &mut CuMsg<u32> = &mut output;
607 async_task.process(&context, &input, output_ref).unwrap();
608 }
609
610 if let Some(val) = output.payload() {
611 assert_eq!(*val, 42u32);
612 break;
613 }
614 }
615 }
616
617 #[derive(Reflect)]
618 struct ControlledTask;
619
620 impl Freezable for ControlledTask {}
621
622 impl CuTask for ControlledTask {
623 type Resources<'r> = ();
624 type Input<'m> = input_msg!(u32);
625 type Output<'m> = output_msg!(u32);
626
627 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
628 where
629 Self: Sized,
630 {
631 Ok(Self {})
632 }
633
634 fn process(
635 &mut self,
636 ctx: &CuContext,
637 _input: &Self::Input<'_>,
638 output: &mut Self::Output<'_>,
639 ) -> CuResult<()> {
640 let rx = READY_RX
641 .get()
642 .expect("ready channel not set")
643 .lock()
644 .unwrap();
645 let ready_time = rx
646 .recv_timeout(Duration::from_secs(1))
647 .expect("timed out waiting for ready signal");
648
649 output.set_payload(ready_time.as_nanos() as u32);
650 output.metadata.process_time.start = ctx.now().into();
651 output.metadata.process_time.end = ready_time.into();
652
653 if let Some(done_tx) = DONE_TX.get() {
654 let _ = done_tx.send(());
655 }
656 Ok(())
657 }
658 }
659
660 fn wait_until_async_idle<T, O>(async_task: &CuAsyncTask<T, O>)
661 where
662 T: for<'m> CuTask<Output<'m> = CuMsg<O>> + Send + 'static,
663 O: CuMsgPayload + Send + 'static,
664 {
665 for _ in 0..100 {
666 let state = async_task.state.lock().unwrap();
667 if !state.processing {
668 return;
669 }
670 drop(state);
671 std::thread::sleep(Duration::from_millis(1));
672 }
673 panic!("background task never became idle");
674 }
675
676 fn wait_until_async_src_idle<T, O>(async_task: &CuAsyncSrcTask<T, O>)
677 where
678 T: for<'m> CuSrcTask<Output<'m> = CuMsg<O>> + Send + 'static,
679 O: CuMsgPayload + Send + 'static,
680 {
681 for _ in 0..100 {
682 let state = async_task.state.lock().unwrap();
683 if !state.processing {
684 return;
685 }
686 drop(state);
687 std::thread::sleep(Duration::from_millis(1));
688 }
689 panic!("background source never became idle");
690 }
691
692 #[derive(Clone)]
693 struct ActionTaskResources {
694 actions: Arc<Mutex<mpsc::Receiver<Option<u32>>>>,
695 done: mpsc::Sender<()>,
696 }
697
698 #[derive(Reflect)]
699 #[reflect(no_field_bounds, from_reflect = false)]
700 struct ActionTask {
701 #[reflect(ignore)]
702 actions: Arc<Mutex<mpsc::Receiver<Option<u32>>>>,
703 #[reflect(ignore)]
704 done: mpsc::Sender<()>,
705 }
706
707 impl Freezable for ActionTask {}
708
709 impl CuTask for ActionTask {
710 type Resources<'r> = ActionTaskResources;
711 type Input<'m> = input_msg!(u32);
712 type Output<'m> = output_msg!(u32);
713
714 fn new(config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self>
715 where
716 Self: Sized,
717 {
718 let _ = config;
719 Ok(Self {
720 actions: resources.actions,
721 done: resources.done,
722 })
723 }
724
725 fn process(
726 &mut self,
727 _ctx: &CuContext,
728 _input: &Self::Input<'_>,
729 output: &mut Self::Output<'_>,
730 ) -> CuResult<()> {
731 let action = self
732 .actions
733 .lock()
734 .unwrap()
735 .recv_timeout(Duration::from_secs(1))
736 .expect("timed out waiting for action");
737 if let Some(value) = action {
738 output.set_payload(value);
739 }
740 let _ = self.done.send(());
741 Ok(())
742 }
743 }
744
745 #[derive(Reflect)]
746 #[reflect(no_field_bounds, from_reflect = false)]
747 struct ActionSrc {
748 #[reflect(ignore)]
749 actions: Arc<Mutex<mpsc::Receiver<Option<u32>>>>,
750 #[reflect(ignore)]
751 done: mpsc::Sender<()>,
752 }
753
754 impl Freezable for ActionSrc {}
755
756 impl CuSrcTask for ActionSrc {
757 type Resources<'r> = ActionTaskResources;
758 type Output<'m> = output_msg!(u32);
759
760 fn new(config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self>
761 where
762 Self: Sized,
763 {
764 let _ = config;
765 Ok(Self {
766 actions: resources.actions,
767 done: resources.done,
768 })
769 }
770
771 fn process(&mut self, _ctx: &CuContext, output: &mut Self::Output<'_>) -> CuResult<()> {
772 let action = self
773 .actions
774 .lock()
775 .unwrap()
776 .recv_timeout(Duration::from_secs(1))
777 .expect("timed out waiting for source action");
778 if let Some(value) = action {
779 output.set_payload(value);
780 }
781 let _ = self.done.send(());
782 Ok(())
783 }
784 }
785
786 #[derive(Clone)]
787 struct ControlledSrcResources {
788 ready_times: Arc<Mutex<mpsc::Receiver<CuTime>>>,
789 done: mpsc::Sender<()>,
790 }
791
792 #[derive(Reflect)]
793 #[reflect(no_field_bounds, from_reflect = false)]
794 struct ControlledSrc {
795 #[reflect(ignore)]
796 ready_times: Arc<Mutex<mpsc::Receiver<CuTime>>>,
797 #[reflect(ignore)]
798 done: mpsc::Sender<()>,
799 }
800
801 impl Freezable for ControlledSrc {}
802
803 impl CuSrcTask for ControlledSrc {
804 type Resources<'r> = ControlledSrcResources;
805 type Output<'m> = output_msg!(u32);
806
807 fn new(config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self>
808 where
809 Self: Sized,
810 {
811 let _ = config;
812 Ok(Self {
813 ready_times: resources.ready_times,
814 done: resources.done,
815 })
816 }
817
818 fn process(&mut self, ctx: &CuContext, output: &mut Self::Output<'_>) -> CuResult<()> {
819 let ready_time = self
820 .ready_times
821 .lock()
822 .unwrap()
823 .recv_timeout(Duration::from_secs(1))
824 .expect("timed out waiting for ready signal");
825 output.set_payload(ready_time.as_nanos() as u32);
826 output.metadata.process_time.start = ctx.now().into();
827 output.metadata.process_time.end = ready_time.into();
828 let _ = self.done.send(());
829 Ok(())
830 }
831 }
832
833 #[test]
834 fn background_clears_output_while_processing() {
835 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
836 let context = CuContext::new_with_clock();
837 let (action_tx, action_rx) = mpsc::channel::<Option<u32>>();
838 let (done_tx, done_rx) = mpsc::channel::<()>();
839 let resources = ActionTaskResources {
840 actions: Arc::new(Mutex::new(action_rx)),
841 done: done_tx,
842 };
843
844 let mut async_task: CuAsyncTask<ActionTask, u32> =
845 CuAsyncTask::new(Some(&ComponentConfig::default()), resources, tp).unwrap();
846 let input = CuMsg::new(Some(1u32));
847 let mut output = CuMsg::new(None);
848
849 async_task.process(&context, &input, &mut output).unwrap();
850 assert!(output.payload().is_none());
851
852 output.set_payload(999);
853 async_task.process(&context, &input, &mut output).unwrap();
854 assert!(
855 output.payload().is_none(),
856 "background poll should clear stale output while the worker is still running"
857 );
858
859 action_tx.send(Some(7)).unwrap();
860 done_rx
861 .recv_timeout(Duration::from_secs(1))
862 .expect("background worker never finished");
863 }
864
865 #[test]
866 fn background_empty_run_does_not_reemit_previous_payload() {
867 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
868 let context = CuContext::new_with_clock();
869 let (action_tx, action_rx) = mpsc::channel::<Option<u32>>();
870 let (done_tx, done_rx) = mpsc::channel::<()>();
871 let resources = ActionTaskResources {
872 actions: Arc::new(Mutex::new(action_rx)),
873 done: done_tx,
874 };
875
876 let mut async_task: CuAsyncTask<ActionTask, u32> =
877 CuAsyncTask::new(Some(&ComponentConfig::default()), resources, tp).unwrap();
878 let some_input = CuMsg::new(Some(1u32));
879 let no_input = CuMsg::new(None::<u32>);
880 let mut output = CuMsg::new(None);
881
882 action_tx.send(Some(42)).unwrap();
883 async_task
884 .process(&context, &some_input, &mut output)
885 .expect("failed to start first background run");
886 done_rx
887 .recv_timeout(Duration::from_secs(1))
888 .expect("first background run never finished");
889 wait_until_async_idle(&async_task);
890
891 action_tx.send(None).unwrap();
892 async_task
893 .process(&context, &no_input, &mut output)
894 .expect("failed to start empty background run");
895 assert_eq!(output.payload(), Some(&42));
896 done_rx
897 .recv_timeout(Duration::from_secs(1))
898 .expect("empty background run never finished");
899 wait_until_async_idle(&async_task);
900
901 action_tx.send(None).unwrap();
902 async_task
903 .process(&context, &no_input, &mut output)
904 .expect("failed to poll after empty background run");
905 assert!(
906 output.payload().is_none(),
907 "background task re-emitted the previous payload after an empty run"
908 );
909 done_rx
910 .recv_timeout(Duration::from_secs(1))
911 .expect("cleanup background run never finished");
912 }
913
914 #[test]
915 fn background_source_clears_output_while_processing() {
916 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
917 let context = CuContext::new_with_clock();
918 let (action_tx, action_rx) = mpsc::channel::<Option<u32>>();
919 let (done_tx, done_rx) = mpsc::channel::<()>();
920 let resources = ActionTaskResources {
921 actions: Arc::new(Mutex::new(action_rx)),
922 done: done_tx,
923 };
924
925 let mut async_src: CuAsyncSrcTask<ActionSrc, u32> =
926 CuAsyncSrcTask::new(Some(&ComponentConfig::default()), resources, tp).unwrap();
927 let mut output = CuMsg::new(None);
928
929 async_src.process(&context, &mut output).unwrap();
930 assert!(output.payload().is_none());
931
932 output.set_payload(999);
933 async_src.process(&context, &mut output).unwrap();
934 assert!(
935 output.payload().is_none(),
936 "background source poll should clear stale output while the worker is still running"
937 );
938
939 action_tx.send(Some(7)).unwrap();
940 done_rx
941 .recv_timeout(Duration::from_secs(1))
942 .expect("background source never finished");
943 }
944
945 #[test]
946 fn background_source_empty_run_does_not_reemit_previous_payload() {
947 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
948 let context = CuContext::new_with_clock();
949 let (action_tx, action_rx) = mpsc::channel::<Option<u32>>();
950 let (done_tx, done_rx) = mpsc::channel::<()>();
951 let resources = ActionTaskResources {
952 actions: Arc::new(Mutex::new(action_rx)),
953 done: done_tx,
954 };
955
956 let mut async_src: CuAsyncSrcTask<ActionSrc, u32> =
957 CuAsyncSrcTask::new(Some(&ComponentConfig::default()), resources, tp).unwrap();
958 let mut output = CuMsg::new(None);
959
960 action_tx.send(Some(42)).unwrap();
961 async_src
962 .process(&context, &mut output)
963 .expect("failed to start first background source run");
964 done_rx
965 .recv_timeout(Duration::from_secs(1))
966 .expect("first background source run never finished");
967 wait_until_async_src_idle(&async_src);
968
969 action_tx.send(None).unwrap();
970 async_src
971 .process(&context, &mut output)
972 .expect("failed to start empty background source run");
973 assert_eq!(output.payload(), Some(&42));
974 done_rx
975 .recv_timeout(Duration::from_secs(1))
976 .expect("empty background source run never finished");
977 wait_until_async_src_idle(&async_src);
978
979 action_tx.send(None).unwrap();
980 async_src
981 .process(&context, &mut output)
982 .expect("failed to poll background source after empty run");
983 assert!(
984 output.payload().is_none(),
985 "background source re-emitted the previous payload after an empty run"
986 );
987 done_rx
988 .recv_timeout(Duration::from_secs(1))
989 .expect("cleanup background source run never finished");
990 }
991
992 #[test]
993 fn background_respects_recorded_ready_time() {
994 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
995 let (context, clock_mock) = CuContext::new_mock_clock();
996
997 let (ready_tx, ready_rx) = mpsc::channel::<CuTime>();
999 let (done_tx, done_rx) = mpsc::channel::<()>();
1000 READY_RX
1001 .set(Arc::new(Mutex::new(ready_rx)))
1002 .expect("ready channel already set");
1003 DONE_TX
1004 .set(done_tx)
1005 .expect("completion channel already set");
1006
1007 let mut async_task: CuAsyncTask<ControlledTask, u32> =
1008 CuAsyncTask::new(Some(&ComponentConfig::default()), (), tp.clone()).unwrap();
1009 let input = CuMsg::new(Some(1u32));
1010 let mut output = CuMsg::new(None);
1011
1012 clock_mock.set_value(0);
1014 async_task.process(&context, &input, &mut output).unwrap();
1015 assert!(output.payload().is_none());
1016
1017 clock_mock.set_value(10);
1019 async_task.process(&context, &input, &mut output).unwrap();
1020 assert!(output.payload().is_none());
1021
1022 clock_mock.set_value(30);
1024 ready_tx.send(CuTime::from(30u64)).unwrap();
1025 done_rx
1026 .recv_timeout(Duration::from_secs(1))
1027 .expect("background task never finished");
1028 let mut ready_at_recorded = None;
1030 for _ in 0..100 {
1031 let state = async_task.state.lock().unwrap();
1032 if !state.processing {
1033 ready_at_recorded = state.ready_at;
1034 if ready_at_recorded.is_some() {
1035 break;
1036 }
1037 }
1038 drop(state);
1039 std::thread::sleep(Duration::from_millis(1));
1040 }
1041 assert!(
1042 ready_at_recorded.is_some(),
1043 "background task finished without recording ready_at"
1044 );
1045
1046 clock_mock.set_value(20);
1048 async_task.process(&context, &input, &mut output).unwrap();
1049 assert!(
1050 output.payload().is_none(),
1051 "Output surfaced before recorded ready time"
1052 );
1053
1054 clock_mock.set_value(30);
1056 async_task.process(&context, &input, &mut output).unwrap();
1057 assert_eq!(output.payload(), Some(&30u32));
1058
1059 ready_tx.send(CuTime::from(40u64)).unwrap();
1061 let _ = done_rx.recv_timeout(Duration::from_secs(1));
1062 }
1063
1064 #[test]
1065 fn background_source_respects_recorded_ready_time() {
1066 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1067 let (context, clock_mock) = CuContext::new_mock_clock();
1068 let (ready_tx, ready_rx) = mpsc::channel::<CuTime>();
1069 let (done_tx, done_rx) = mpsc::channel::<()>();
1070 let resources = ControlledSrcResources {
1071 ready_times: Arc::new(Mutex::new(ready_rx)),
1072 done: done_tx,
1073 };
1074
1075 let mut async_src: CuAsyncSrcTask<ControlledSrc, u32> =
1076 CuAsyncSrcTask::new(Some(&ComponentConfig::default()), resources, tp.clone()).unwrap();
1077 let mut output = CuMsg::new(None);
1078
1079 clock_mock.set_value(0);
1080 async_src.process(&context, &mut output).unwrap();
1081 assert!(output.payload().is_none());
1082
1083 clock_mock.set_value(10);
1084 async_src.process(&context, &mut output).unwrap();
1085 assert!(output.payload().is_none());
1086
1087 clock_mock.set_value(30);
1088 ready_tx.send(CuTime::from(30u64)).unwrap();
1089 done_rx
1090 .recv_timeout(Duration::from_secs(1))
1091 .expect("background source never finished");
1092
1093 let mut ready_at_recorded = None;
1094 for _ in 0..100 {
1095 let state = async_src.state.lock().unwrap();
1096 if !state.processing {
1097 ready_at_recorded = state.ready_at;
1098 if ready_at_recorded.is_some() {
1099 break;
1100 }
1101 }
1102 drop(state);
1103 std::thread::sleep(Duration::from_millis(1));
1104 }
1105 assert!(
1106 ready_at_recorded.is_some(),
1107 "background source finished without recording ready_at"
1108 );
1109
1110 clock_mock.set_value(20);
1111 async_src.process(&context, &mut output).unwrap();
1112 assert!(
1113 output.payload().is_none(),
1114 "background source surfaced output before recorded ready time"
1115 );
1116
1117 clock_mock.set_value(30);
1118 async_src.process(&context, &mut output).unwrap();
1119 assert_eq!(output.payload(), Some(&30u32));
1120
1121 ready_tx.send(CuTime::from(40u64)).unwrap();
1122 let _ = done_rx.recv_timeout(Duration::from_secs(1));
1123 }
1124}