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::cutask_anytime::{
550 AnytimePolicy, AnytimeStatus, CuAnytimeRunner, CuAnytimeTask, Quality, quality_from_f32,
551 };
552 use crate::input_msg;
553 use crate::output_msg;
554 use cu29_clock::CuDuration;
555 use cu29_traits::CuResult;
556 use rayon::ThreadPoolBuilder;
557 use std::borrow::BorrowMut;
558 use std::sync::OnceLock;
559 use std::sync::mpsc;
560 use std::time::Duration;
561
562 static READY_RX: OnceLock<Arc<Mutex<mpsc::Receiver<CuTime>>>> = OnceLock::new();
563 static DONE_TX: OnceLock<mpsc::Sender<()>> = OnceLock::new();
564 #[derive(Reflect)]
565 struct TestTask {}
566
567 impl Freezable for TestTask {}
568
569 impl CuTask for TestTask {
570 type Resources<'r> = ();
571 type Input<'m> = input_msg!(u32);
572 type Output<'m> = output_msg!(u32);
573
574 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
575 where
576 Self: Sized,
577 {
578 Ok(Self {})
579 }
580
581 fn process(
582 &mut self,
583 _ctx: &CuContext,
584 input: &Self::Input<'_>,
585 output: &mut Self::Output<'_>,
586 ) -> CuResult<()> {
587 output.borrow_mut().set_payload(*input.payload().unwrap());
588 Ok(())
589 }
590 }
591
592 #[test]
593 fn test_lifecycle() {
594 let tp = Arc::new(
595 rayon::ThreadPoolBuilder::new()
596 .num_threads(1)
597 .build()
598 .unwrap(),
599 );
600
601 let config = ComponentConfig::default();
602 let context = CuContext::new_with_clock();
603 let mut async_task: CuAsyncTask<TestTask, u32> =
604 CuAsyncTask::new(Some(&config), (), tp).unwrap();
605 let input = CuMsg::new(Some(42u32));
606 let mut output = CuMsg::new(None);
607
608 loop {
609 {
610 let output_ref: &mut CuMsg<u32> = &mut output;
611 async_task.process(&context, &input, output_ref).unwrap();
612 }
613
614 if let Some(val) = output.payload() {
615 assert_eq!(*val, 42u32);
616 break;
617 }
618 }
619 }
620
621 #[derive(Reflect)]
622 struct ControlledTask;
623
624 impl Freezable for ControlledTask {}
625
626 impl CuTask for ControlledTask {
627 type Resources<'r> = ();
628 type Input<'m> = input_msg!(u32);
629 type Output<'m> = output_msg!(u32);
630
631 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
632 where
633 Self: Sized,
634 {
635 Ok(Self {})
636 }
637
638 fn process(
639 &mut self,
640 ctx: &CuContext,
641 _input: &Self::Input<'_>,
642 output: &mut Self::Output<'_>,
643 ) -> CuResult<()> {
644 let rx = READY_RX
645 .get()
646 .expect("ready channel not set")
647 .lock()
648 .unwrap();
649 let ready_time = rx
650 .recv_timeout(Duration::from_secs(1))
651 .expect("timed out waiting for ready signal");
652
653 output.set_payload(ready_time.as_nanos() as u32);
654 output.metadata.process_time.start = ctx.now().into();
655 output.metadata.process_time.end = ready_time.into();
656
657 if let Some(done_tx) = DONE_TX.get() {
658 let _ = done_tx.send(());
659 }
660 Ok(())
661 }
662 }
663
664 fn wait_until_async_idle<T, O>(async_task: &CuAsyncTask<T, O>)
665 where
666 T: for<'m> CuTask<Output<'m> = CuMsg<O>> + Send + 'static,
667 O: CuMsgPayload + Send + 'static,
668 {
669 for _ in 0..100 {
670 let state = async_task.state.lock().unwrap();
671 if !state.processing {
672 return;
673 }
674 drop(state);
675 std::thread::sleep(Duration::from_millis(1));
676 }
677 panic!("background task never became idle");
678 }
679
680 fn wait_until_async_src_idle<T, O>(async_task: &CuAsyncSrcTask<T, O>)
681 where
682 T: for<'m> CuSrcTask<Output<'m> = CuMsg<O>> + Send + 'static,
683 O: CuMsgPayload + Send + 'static,
684 {
685 for _ in 0..100 {
686 let state = async_task.state.lock().unwrap();
687 if !state.processing {
688 return;
689 }
690 drop(state);
691 std::thread::sleep(Duration::from_millis(1));
692 }
693 panic!("background source never became idle");
694 }
695
696 #[derive(Clone)]
697 struct ActionTaskResources {
698 actions: Arc<Mutex<mpsc::Receiver<Option<u32>>>>,
699 done: mpsc::Sender<()>,
700 }
701
702 #[derive(Reflect)]
703 #[reflect(no_field_bounds, from_reflect = false)]
704 struct ActionTask {
705 #[reflect(ignore)]
706 actions: Arc<Mutex<mpsc::Receiver<Option<u32>>>>,
707 #[reflect(ignore)]
708 done: mpsc::Sender<()>,
709 }
710
711 impl Freezable for ActionTask {}
712
713 impl CuTask for ActionTask {
714 type Resources<'r> = ActionTaskResources;
715 type Input<'m> = input_msg!(u32);
716 type Output<'m> = output_msg!(u32);
717
718 fn new(config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self>
719 where
720 Self: Sized,
721 {
722 let _ = config;
723 Ok(Self {
724 actions: resources.actions,
725 done: resources.done,
726 })
727 }
728
729 fn process(
730 &mut self,
731 _ctx: &CuContext,
732 _input: &Self::Input<'_>,
733 output: &mut Self::Output<'_>,
734 ) -> CuResult<()> {
735 let action = self
736 .actions
737 .lock()
738 .unwrap()
739 .recv_timeout(Duration::from_secs(1))
740 .expect("timed out waiting for action");
741 if let Some(value) = action {
742 output.set_payload(value);
743 }
744 let _ = self.done.send(());
745 Ok(())
746 }
747 }
748
749 #[derive(Reflect)]
750 #[reflect(no_field_bounds, from_reflect = false)]
751 struct ActionSrc {
752 #[reflect(ignore)]
753 actions: Arc<Mutex<mpsc::Receiver<Option<u32>>>>,
754 #[reflect(ignore)]
755 done: mpsc::Sender<()>,
756 }
757
758 impl Freezable for ActionSrc {}
759
760 impl CuSrcTask for ActionSrc {
761 type Resources<'r> = ActionTaskResources;
762 type Output<'m> = output_msg!(u32);
763
764 fn new(config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self>
765 where
766 Self: Sized,
767 {
768 let _ = config;
769 Ok(Self {
770 actions: resources.actions,
771 done: resources.done,
772 })
773 }
774
775 fn process(&mut self, _ctx: &CuContext, output: &mut Self::Output<'_>) -> CuResult<()> {
776 let action = self
777 .actions
778 .lock()
779 .unwrap()
780 .recv_timeout(Duration::from_secs(1))
781 .expect("timed out waiting for source action");
782 if let Some(value) = action {
783 output.set_payload(value);
784 }
785 let _ = self.done.send(());
786 Ok(())
787 }
788 }
789
790 #[derive(Clone)]
791 struct ControlledSrcResources {
792 ready_times: Arc<Mutex<mpsc::Receiver<CuTime>>>,
793 done: mpsc::Sender<()>,
794 }
795
796 #[derive(Reflect)]
797 #[reflect(no_field_bounds, from_reflect = false)]
798 struct ControlledSrc {
799 #[reflect(ignore)]
800 ready_times: Arc<Mutex<mpsc::Receiver<CuTime>>>,
801 #[reflect(ignore)]
802 done: mpsc::Sender<()>,
803 }
804
805 impl Freezable for ControlledSrc {}
806
807 impl CuSrcTask for ControlledSrc {
808 type Resources<'r> = ControlledSrcResources;
809 type Output<'m> = output_msg!(u32);
810
811 fn new(config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self>
812 where
813 Self: Sized,
814 {
815 let _ = config;
816 Ok(Self {
817 ready_times: resources.ready_times,
818 done: resources.done,
819 })
820 }
821
822 fn process(&mut self, ctx: &CuContext, output: &mut Self::Output<'_>) -> CuResult<()> {
823 let ready_time = self
824 .ready_times
825 .lock()
826 .unwrap()
827 .recv_timeout(Duration::from_secs(1))
828 .expect("timed out waiting for ready signal");
829 output.set_payload(ready_time.as_nanos() as u32);
830 output.metadata.process_time.start = ctx.now().into();
831 output.metadata.process_time.end = ready_time.into();
832 let _ = self.done.send(());
833 Ok(())
834 }
835 }
836
837 #[test]
838 fn background_clears_output_while_processing() {
839 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
840 let context = CuContext::new_with_clock();
841 let (action_tx, action_rx) = mpsc::channel::<Option<u32>>();
842 let (done_tx, done_rx) = mpsc::channel::<()>();
843 let resources = ActionTaskResources {
844 actions: Arc::new(Mutex::new(action_rx)),
845 done: done_tx,
846 };
847
848 let mut async_task: CuAsyncTask<ActionTask, u32> =
849 CuAsyncTask::new(Some(&ComponentConfig::default()), resources, tp).unwrap();
850 let input = CuMsg::new(Some(1u32));
851 let mut output = CuMsg::new(None);
852
853 async_task.process(&context, &input, &mut output).unwrap();
854 assert!(output.payload().is_none());
855
856 output.set_payload(999);
857 async_task.process(&context, &input, &mut output).unwrap();
858 assert!(
859 output.payload().is_none(),
860 "background poll should clear stale output while the worker is still running"
861 );
862
863 action_tx.send(Some(7)).unwrap();
864 done_rx
865 .recv_timeout(Duration::from_secs(1))
866 .expect("background worker never finished");
867 }
868
869 #[test]
870 fn background_empty_run_does_not_reemit_previous_payload() {
871 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
872 let context = CuContext::new_with_clock();
873 let (action_tx, action_rx) = mpsc::channel::<Option<u32>>();
874 let (done_tx, done_rx) = mpsc::channel::<()>();
875 let resources = ActionTaskResources {
876 actions: Arc::new(Mutex::new(action_rx)),
877 done: done_tx,
878 };
879
880 let mut async_task: CuAsyncTask<ActionTask, u32> =
881 CuAsyncTask::new(Some(&ComponentConfig::default()), resources, tp).unwrap();
882 let some_input = CuMsg::new(Some(1u32));
883 let no_input = CuMsg::new(None::<u32>);
884 let mut output = CuMsg::new(None);
885
886 action_tx.send(Some(42)).unwrap();
887 async_task
888 .process(&context, &some_input, &mut output)
889 .expect("failed to start first background run");
890 done_rx
891 .recv_timeout(Duration::from_secs(1))
892 .expect("first background run never finished");
893 wait_until_async_idle(&async_task);
894
895 action_tx.send(None).unwrap();
896 async_task
897 .process(&context, &no_input, &mut output)
898 .expect("failed to start empty background run");
899 assert_eq!(output.payload(), Some(&42));
900 done_rx
901 .recv_timeout(Duration::from_secs(1))
902 .expect("empty background run never finished");
903 wait_until_async_idle(&async_task);
904
905 action_tx.send(None).unwrap();
906 async_task
907 .process(&context, &no_input, &mut output)
908 .expect("failed to poll after empty background run");
909 assert!(
910 output.payload().is_none(),
911 "background task re-emitted the previous payload after an empty run"
912 );
913 done_rx
914 .recv_timeout(Duration::from_secs(1))
915 .expect("cleanup background run never finished");
916 }
917
918 #[test]
919 fn background_source_clears_output_while_processing() {
920 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
921 let context = CuContext::new_with_clock();
922 let (action_tx, action_rx) = mpsc::channel::<Option<u32>>();
923 let (done_tx, done_rx) = mpsc::channel::<()>();
924 let resources = ActionTaskResources {
925 actions: Arc::new(Mutex::new(action_rx)),
926 done: done_tx,
927 };
928
929 let mut async_src: CuAsyncSrcTask<ActionSrc, u32> =
930 CuAsyncSrcTask::new(Some(&ComponentConfig::default()), resources, tp).unwrap();
931 let mut output = CuMsg::new(None);
932
933 async_src.process(&context, &mut output).unwrap();
934 assert!(output.payload().is_none());
935
936 output.set_payload(999);
937 async_src.process(&context, &mut output).unwrap();
938 assert!(
939 output.payload().is_none(),
940 "background source poll should clear stale output while the worker is still running"
941 );
942
943 action_tx.send(Some(7)).unwrap();
944 done_rx
945 .recv_timeout(Duration::from_secs(1))
946 .expect("background source never finished");
947 }
948
949 #[test]
950 fn background_source_empty_run_does_not_reemit_previous_payload() {
951 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
952 let context = CuContext::new_with_clock();
953 let (action_tx, action_rx) = mpsc::channel::<Option<u32>>();
954 let (done_tx, done_rx) = mpsc::channel::<()>();
955 let resources = ActionTaskResources {
956 actions: Arc::new(Mutex::new(action_rx)),
957 done: done_tx,
958 };
959
960 let mut async_src: CuAsyncSrcTask<ActionSrc, u32> =
961 CuAsyncSrcTask::new(Some(&ComponentConfig::default()), resources, tp).unwrap();
962 let mut output = CuMsg::new(None);
963
964 action_tx.send(Some(42)).unwrap();
965 async_src
966 .process(&context, &mut output)
967 .expect("failed to start first background source run");
968 done_rx
969 .recv_timeout(Duration::from_secs(1))
970 .expect("first background source run never finished");
971 wait_until_async_src_idle(&async_src);
972
973 action_tx.send(None).unwrap();
974 async_src
975 .process(&context, &mut output)
976 .expect("failed to start empty background source run");
977 assert_eq!(output.payload(), Some(&42));
978 done_rx
979 .recv_timeout(Duration::from_secs(1))
980 .expect("empty background source run never finished");
981 wait_until_async_src_idle(&async_src);
982
983 action_tx.send(None).unwrap();
984 async_src
985 .process(&context, &mut output)
986 .expect("failed to poll background source after empty run");
987 assert!(
988 output.payload().is_none(),
989 "background source re-emitted the previous payload after an empty run"
990 );
991 done_rx
992 .recv_timeout(Duration::from_secs(1))
993 .expect("cleanup background source run never finished");
994 }
995
996 #[test]
997 fn background_respects_recorded_ready_time() {
998 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
999 let (context, clock_mock) = CuContext::new_mock_clock();
1000
1001 let (ready_tx, ready_rx) = mpsc::channel::<CuTime>();
1003 let (done_tx, done_rx) = mpsc::channel::<()>();
1004 READY_RX
1005 .set(Arc::new(Mutex::new(ready_rx)))
1006 .expect("ready channel already set");
1007 DONE_TX
1008 .set(done_tx)
1009 .expect("completion channel already set");
1010
1011 let mut async_task: CuAsyncTask<ControlledTask, u32> =
1012 CuAsyncTask::new(Some(&ComponentConfig::default()), (), tp.clone()).unwrap();
1013 let input = CuMsg::new(Some(1u32));
1014 let mut output = CuMsg::new(None);
1015
1016 clock_mock.set_value(0);
1018 async_task.process(&context, &input, &mut output).unwrap();
1019 assert!(output.payload().is_none());
1020
1021 clock_mock.set_value(10);
1023 async_task.process(&context, &input, &mut output).unwrap();
1024 assert!(output.payload().is_none());
1025
1026 clock_mock.set_value(30);
1028 ready_tx.send(CuTime::from(30u64)).unwrap();
1029 done_rx
1030 .recv_timeout(Duration::from_secs(1))
1031 .expect("background task never finished");
1032 let mut ready_at_recorded = None;
1034 for _ in 0..100 {
1035 let state = async_task.state.lock().unwrap();
1036 if !state.processing {
1037 ready_at_recorded = state.ready_at;
1038 if ready_at_recorded.is_some() {
1039 break;
1040 }
1041 }
1042 drop(state);
1043 std::thread::sleep(Duration::from_millis(1));
1044 }
1045 assert!(
1046 ready_at_recorded.is_some(),
1047 "background task finished without recording ready_at"
1048 );
1049
1050 clock_mock.set_value(20);
1052 async_task.process(&context, &input, &mut output).unwrap();
1053 assert!(
1054 output.payload().is_none(),
1055 "Output surfaced before recorded ready time"
1056 );
1057
1058 clock_mock.set_value(30);
1060 async_task.process(&context, &input, &mut output).unwrap();
1061 assert_eq!(output.payload(), Some(&30u32));
1062
1063 ready_tx.send(CuTime::from(40u64)).unwrap();
1065 let _ = done_rx.recv_timeout(Duration::from_secs(1));
1066 }
1067
1068 #[derive(Reflect)]
1071 struct IncrementalPlanner {
1072 target: u32,
1073 acc: u32,
1074 }
1075
1076 impl Freezable for IncrementalPlanner {}
1077
1078 impl CuAnytimeTask for IncrementalPlanner {
1079 type Input<'m> = input_msg!(u32);
1080 type Output<'m> = output_msg!(u32);
1081 type Resources<'r> = ();
1082 type Quality = Quality;
1083
1084 fn new(_config: Option<&ComponentConfig>, _resources: ()) -> CuResult<Self> {
1085 Ok(Self { target: 0, acc: 0 })
1086 }
1087
1088 fn base(
1089 &mut self,
1090 _ctx: &CuContext,
1091 input: &Self::Input<'_>,
1092 output: &mut Self::Output<'_>,
1093 ) -> CuResult<AnytimeStatus<Quality>> {
1094 self.target = input.payload().copied().ok_or("planner: no input")?;
1095 self.acc = 0;
1096 output.set_payload(self.acc);
1097 Ok(AnytimeStatus::Improved(quality_from_f32(0.0)))
1098 }
1099
1100 fn refine(
1101 &mut self,
1102 _ctx: &CuContext,
1103 output: &mut Self::Output<'_>,
1104 ) -> CuResult<AnytimeStatus<Quality>> {
1105 self.acc += 1;
1106 output.set_payload(self.acc);
1107 Ok(AnytimeStatus::Improved(quality_from_f32(
1108 self.acc as f32 / self.target as f32,
1109 )))
1110 }
1111 }
1112
1113 struct ThreeQuantaPolicy;
1115 impl<Q: Copy + PartialOrd> AnytimePolicy<Q> for ThreeQuantaPolicy {
1116 const TIME_BUDGET: Option<CuDuration> = None;
1117 const MAX_AGE: Option<CuDuration> = None;
1118 const MAX_STALL: Option<u32> = None;
1119 const MAX_REFINES: Option<u32> = Some(3);
1120 }
1121
1122 #[test]
1123 fn background_anytime_job_lands_with_its_status_stamp() {
1124 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1125 let context = CuContext::new_with_clock();
1126 let mut task: CuAsyncTask<CuAnytimeRunner<IncrementalPlanner, ThreeQuantaPolicy>, u32> =
1127 CuAsyncTask::new(Some(&ComponentConfig::default()), (), tp).unwrap();
1128
1129 let input = CuMsg::new(Some(5u32));
1130 let mut output = CuMsg::new(None);
1131
1132 for _ in 0..1000 {
1134 task.process(&context, &input, &mut output).unwrap();
1135 if output.payload().is_some() {
1136 break;
1137 }
1138 std::thread::sleep(Duration::from_millis(1));
1139 }
1140
1141 assert_eq!(output.payload(), Some(&3));
1144 assert_eq!(output.metadata.status_txt.0.as_str(), "any:3it q=0.60 max");
1145 }
1146
1147 #[test]
1148 fn background_source_respects_recorded_ready_time() {
1149 let tp = Arc::new(ThreadPoolBuilder::new().num_threads(1).build().unwrap());
1150 let (context, clock_mock) = CuContext::new_mock_clock();
1151 let (ready_tx, ready_rx) = mpsc::channel::<CuTime>();
1152 let (done_tx, done_rx) = mpsc::channel::<()>();
1153 let resources = ControlledSrcResources {
1154 ready_times: Arc::new(Mutex::new(ready_rx)),
1155 done: done_tx,
1156 };
1157
1158 let mut async_src: CuAsyncSrcTask<ControlledSrc, u32> =
1159 CuAsyncSrcTask::new(Some(&ComponentConfig::default()), resources, tp.clone()).unwrap();
1160 let mut output = CuMsg::new(None);
1161
1162 clock_mock.set_value(0);
1163 async_src.process(&context, &mut output).unwrap();
1164 assert!(output.payload().is_none());
1165
1166 clock_mock.set_value(10);
1167 async_src.process(&context, &mut output).unwrap();
1168 assert!(output.payload().is_none());
1169
1170 clock_mock.set_value(30);
1171 ready_tx.send(CuTime::from(30u64)).unwrap();
1172 done_rx
1173 .recv_timeout(Duration::from_secs(1))
1174 .expect("background source never finished");
1175
1176 let mut ready_at_recorded = None;
1177 for _ in 0..100 {
1178 let state = async_src.state.lock().unwrap();
1179 if !state.processing {
1180 ready_at_recorded = state.ready_at;
1181 if ready_at_recorded.is_some() {
1182 break;
1183 }
1184 }
1185 drop(state);
1186 std::thread::sleep(Duration::from_millis(1));
1187 }
1188 assert!(
1189 ready_at_recorded.is_some(),
1190 "background source finished without recording ready_at"
1191 );
1192
1193 clock_mock.set_value(20);
1194 async_src.process(&context, &mut output).unwrap();
1195 assert!(
1196 output.payload().is_none(),
1197 "background source surfaced output before recorded ready time"
1198 );
1199
1200 clock_mock.set_value(30);
1201 async_src.process(&context, &mut output).unwrap();
1202 assert_eq!(output.payload(), Some(&30u32));
1203
1204 ready_tx.send(CuTime::from(40u64)).unwrap();
1205 let _ = done_rx.recv_timeout(Duration::from_secs(1));
1206 }
1207}