1use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
15use std::sync::Arc;
16use std::time::Duration;
17
18use tokio::sync::mpsc;
19use tokio::task::JoinSet;
20use tokio_util::sync::CancellationToken;
21
22use crate::error::Result;
23use crate::observer::{FrameObserver, IdleFrameObserver, Observer};
24use crate::processor::frame::{Direction, Frame, FrameKind, StartParams};
25use crate::processor::runtime::ProcessorTx;
26use crate::processor::{into_topology, Clock, Envelope, ProcessorSetup, StopReason, Topology};
27
28use super::{link_topology, Sink, Source};
29
30#[derive(Debug, Clone)]
36pub struct PipelineTaskParams {
37 pub audio_in_sample_rate: u32,
38 pub audio_out_sample_rate: u32,
39 pub enable_metrics: bool,
40 pub enable_usage_metrics: bool,
41 pub enable_tracing: bool,
42 pub enable_heartbeats: bool,
43 pub heartbeat_period: Duration,
44 pub heartbeat_monitor: Duration,
45 pub idle_timeout: Option<Duration>,
46 pub cancel_on_idle: bool,
47 pub idle_timeout_frames: Vec<FrameKind>,
48}
49
50impl Default for PipelineTaskParams {
51 fn default() -> Self {
52 Self {
53 audio_in_sample_rate: 16_000,
54 audio_out_sample_rate: 24_000,
55 enable_metrics: false,
56 enable_usage_metrics: false,
57 enable_tracing: false,
58 enable_heartbeats: false,
60 heartbeat_period: Duration::from_secs(1),
61 heartbeat_monitor: Duration::from_secs(10),
62 idle_timeout: Some(Duration::from_secs(300)),
63 cancel_on_idle: true,
64 idle_timeout_frames: vec![FrameKind::BotSpeaking, FrameKind::UserSpeaking],
65 }
66 }
67}
68
69impl PipelineTaskParams {
70 fn start_params(&self) -> StartParams {
71 StartParams {
72 audio_in_sample_rate: self.audio_in_sample_rate,
73 audio_out_sample_rate: self.audio_out_sample_rate,
74 enable_metrics: self.enable_metrics,
75 enable_usage_metrics: self.enable_usage_metrics,
76 enable_tracing: self.enable_tracing,
77 report_only_initial_ttfb: true,
78 }
79 }
80}
81
82type CallbackUnit = Box<dyn Fn() + Send + Sync>;
83type CallbackReason = Box<dyn Fn(StopReason) + Send + Sync>;
84type CallbackError = Box<dyn Fn(&str, bool) + Send + Sync>;
85
86pub struct PipelineTask {
88 topo: Option<Topology>,
89 params: PipelineTaskParams,
90 observers: Vec<Arc<dyn FrameObserver>>,
91 cancel: CancellationToken,
92 finished: Arc<AtomicBool>,
93 queue_tx: mpsc::UnboundedSender<Frame>,
95 queue_rx: Option<mpsc::UnboundedReceiver<Frame>>,
96 on_started: Vec<CallbackUnit>,
98 on_finished: Vec<CallbackReason>,
99 on_error: Vec<CallbackError>,
100 on_idle_timeout: Vec<CallbackUnit>,
101 idle_observer: Arc<IdleFrameObserver>,
103}
104
105impl PipelineTask {
106 pub fn new(
108 pipeline: super::Pipeline,
109 params: PipelineTaskParams,
110 observers: Vec<Arc<dyn FrameObserver>>,
111 ) -> Self {
112 let (queue_tx, queue_rx) = mpsc::unbounded_channel();
113 let idle_observer = Arc::new(IdleFrameObserver::new(params.idle_timeout_frames.clone()));
114 Self {
115 topo: Some(into_topology(Box::new(pipeline))),
116 params,
117 observers,
118 cancel: CancellationToken::new(),
119 finished: Arc::new(AtomicBool::new(false)),
120 queue_tx,
121 queue_rx: Some(queue_rx),
122 on_started: Vec::new(),
123 on_finished: Vec::new(),
124 on_error: Vec::new(),
125 on_idle_timeout: Vec::new(),
126 idle_observer,
127 }
128 }
129
130 pub async fn queue_frame(&self, frame: Frame) {
132 let _ = self.queue_tx.send(frame);
133 }
134
135 pub async fn queue_frames(&self, frames: impl IntoIterator<Item = Frame>) {
137 for f in frames {
138 let _ = self.queue_tx.send(f);
139 }
140 }
141
142 pub fn queue_sender(&self) -> mpsc::UnboundedSender<Frame> {
147 self.queue_tx.clone()
148 }
149
150 pub async fn stop_when_done(&self) {
152 let _ = self.queue_tx.send(Frame::End { reason: None });
153 }
154
155 pub async fn cancel(&self, reason: Option<String>) {
157 let _ = self.queue_tx.send(Frame::Cancel { reason });
158 }
159
160 pub fn has_finished(&self) -> bool {
162 self.finished.load(Ordering::Relaxed)
163 }
164
165 pub fn cancel_token(&self) -> CancellationToken {
168 self.cancel.clone()
169 }
170
171 pub fn on_started(&mut self, f: impl Fn() + Send + Sync + 'static) {
174 self.on_started.push(Box::new(f));
175 }
176 pub fn on_finished(&mut self, f: impl Fn(StopReason) + Send + Sync + 'static) {
177 self.on_finished.push(Box::new(f));
178 }
179 pub fn on_error(&mut self, f: impl Fn(&str, bool) + Send + Sync + 'static) {
180 self.on_error.push(Box::new(f));
181 }
182 pub fn on_idle_timeout(&mut self, f: impl Fn() + Send + Sync + 'static) {
183 self.on_idle_timeout.push(Box::new(f));
184 }
185
186 pub async fn run(mut self) -> Result<()> {
190 let topo = self.topo.take().expect("run called once");
193 let mut queue_rx = self.queue_rx.take().expect("run called once");
194
195 let mut all_obs: Vec<Arc<dyn FrameObserver>> =
197 vec![self.idle_observer.clone() as Arc<dyn FrameObserver>];
198 all_obs.extend(self.observers.iter().cloned());
199 let observer = Observer::new(all_obs);
200
201 let clock = Clock::new();
202 let setup = ProcessorSetup {
203 clock: clock.clone(),
204 observer: Some(observer.clone()),
205 cancel: self.cancel.clone(),
206 enable_metrics: self.params.enable_metrics,
207 enable_usage_metrics: self.params.enable_usage_metrics,
208 };
209
210 let (sink_tap_tx, mut sink_tap_rx) = mpsc::unbounded_channel::<Frame>();
212
213 let wrapped = Topology::Chain(vec![
215 Topology::Leaf(Box::new(Source)),
216 topo,
217 Topology::Leaf(Box::new(Sink { tap: sink_tap_tx })),
218 ]);
219
220 let mut tasks = JoinSet::new();
221 let linked = link_topology(wrapped, None, None, &setup, &mut tasks);
222 let head: ProcessorTx = linked.head;
223
224 observer.on_pipeline_started().await;
225
226 head.send(Envelope::new(
228 Frame::Start(self.params.start_params()),
229 Direction::Downstream,
230 ))
231 .await;
232
233 let ready = wait_for(
234 &mut sink_tap_rx,
235 |f| matches!(f, Frame::Start(_)),
236 &self.cancel,
237 )
238 .await;
239 if ready {
240 for cb in &self.on_started {
241 cb();
242 }
243 }
244
245 let aux_cancel = self.cancel.child_token();
253
254 let last_heartbeat = Arc::new(AtomicU64::new(0));
256 if self.params.enable_heartbeats {
257 let hb_head = head.clone();
258 let period = self.params.heartbeat_period;
259 let cancel = aux_cancel.clone();
260 let clk = clock.clone();
261 tasks.spawn(async move {
262 loop {
263 tokio::select! {
264 _ = cancel.cancelled() => break,
265 _ = tokio::time::sleep(period) => {
266 hb_head
267 .send(Envelope::new(
268 Frame::Heartbeat { timestamp_ns: clk.now_ns() },
269 Direction::Downstream,
270 ))
271 .await;
272 }
273 }
274 }
275 });
276 }
277
278 let idle_fired = Arc::new(AtomicBool::new(false));
280 if let Some(timeout) = self.params.idle_timeout {
281 let idle_obs = self.idle_observer.clone();
282 let cancel = aux_cancel.clone();
283 let cancel_on_idle = self.params.cancel_on_idle;
284 let fired = idle_fired.clone();
285 let qtx = self.queue_tx.clone();
286 tasks.spawn(async move {
287 let mut last = idle_obs.activity_count();
288 loop {
289 tokio::select! {
290 _ = cancel.cancelled() => break,
291 _ = tokio::time::sleep(timeout) => {
292 let now = idle_obs.activity_count();
293 if now == last {
294 fired.store(true, Ordering::Relaxed);
295 if cancel_on_idle {
296 let _ = qtx.send(Frame::Cancel {
297 reason: Some("idle timeout".into()),
298 });
299 }
300 break;
301 }
302 last = now;
303 }
304 }
305 }
306 });
307 }
308
309 let mut stop_reason: Option<StopReason> = None;
311 loop {
312 tokio::select! {
313 biased;
314 _ = self.cancel.cancelled() => {
315 stop_reason = Some(StopReason::Cancelled);
316 head.send(Envelope::new(Frame::Cancel { reason: None }, Direction::Downstream)).await;
318 break;
319 }
320 maybe = queue_rx.recv() => {
321 match maybe {
322 Some(frame) => {
323 head.send(Envelope::new(frame, Direction::Downstream)).await;
324 }
325 None => {
326 }
328 }
329 }
330 maybe = sink_tap_rx.recv() => {
331 match maybe {
332 Some(Frame::Heartbeat { timestamp_ns }) => {
333 last_heartbeat.store(timestamp_ns as u64, Ordering::Relaxed);
334 }
335 Some(Frame::Error { fatal, message, .. }) => {
336 for cb in &self.on_error {
337 cb(&message, fatal);
338 }
339 if fatal {
340 head.send(Envelope::new(Frame::Cancel { reason: None }, Direction::Downstream)).await;
341 }
342 }
343 Some(f) if f.is_terminal() => {
344 stop_reason = Some(terminal_reason(&f));
345 break;
346 }
347 Some(_) => {}
348 None => break,
349 }
350 }
351 }
352 }
353
354 aux_cancel.cancel();
357 let _ = tokio::time::timeout(Duration::from_secs(5), async {
359 while tasks.join_next().await.is_some() {}
360 })
361 .await;
362 self.cancel.cancel();
364 tasks.shutdown().await;
365
366 if idle_fired.load(Ordering::Relaxed) {
367 for cb in &self.on_idle_timeout {
368 cb();
369 }
370 }
371
372 let reason = stop_reason.unwrap_or(StopReason::EndOfTask);
373 for cb in &self.on_finished {
374 cb(reason);
375 }
376 self.finished.store(true, Ordering::Relaxed);
377 Ok(())
378 }
379}
380
381fn terminal_reason(f: &Frame) -> StopReason {
382 match f {
383 Frame::End { .. } => StopReason::EndOfTask,
384 Frame::Stop => StopReason::Stopped,
385 Frame::Cancel { .. } => StopReason::Cancelled,
386 _ => StopReason::EndOfTask,
387 }
388}
389
390async fn wait_for(
393 rx: &mut mpsc::UnboundedReceiver<Frame>,
394 pred: impl Fn(&Frame) -> bool,
395 cancel: &CancellationToken,
396) -> bool {
397 loop {
398 tokio::select! {
399 _ = cancel.cancelled() => return false,
400 maybe = rx.recv() => match maybe {
401 Some(f) if pred(&f) => return true,
402 Some(_) => continue,
403 None => return false,
404 }
405 }
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412 use crate::error::Result;
413 use crate::processor::{FrameProcessor, Link};
414 use async_trait::async_trait;
415 use std::sync::atomic::{AtomicUsize, Ordering};
416 use std::sync::Mutex;
417
418 fn fast_idle(params: PipelineTaskParams) -> PipelineTaskParams {
419 params
420 }
421
422 struct Probe {
424 start_seen: Arc<AtomicBool>,
425 data_before_start: Arc<AtomicUsize>,
426 }
427 #[async_trait]
428 impl FrameProcessor for Probe {
429 fn name(&self) -> &str {
430 "Probe"
431 }
432 async fn start(&mut self, _s: &ProcessorSetup, _p: &StartParams) -> Result<()> {
433 self.start_seen.store(true, Ordering::Relaxed);
434 Ok(())
435 }
436 async fn process_frame(&mut self, env: Envelope, link: &Link) -> Result<()> {
437 if matches!(env.frame, Frame::Text(_)) && !self.start_seen.load(Ordering::Relaxed) {
438 self.data_before_start.fetch_add(1, Ordering::Relaxed);
439 }
440 link.push(env.meta, env.frame, env.direction).await;
441 Ok(())
442 }
443 }
444
445 struct EndRequester;
447 #[async_trait]
448 impl FrameProcessor for EndRequester {
449 fn name(&self) -> &str {
450 "EndRequester"
451 }
452 async fn process_frame(&mut self, env: Envelope, link: &Link) -> Result<()> {
453 if matches!(&env.frame, Frame::Text(t) if t == "bye") {
454 link.push_up(Frame::End {
455 reason: Some("requested".into()),
456 })
457 .await;
458 return Ok(());
459 }
460 link.push(env.meta, env.frame, env.direction).await;
461 Ok(())
462 }
463 }
464
465 #[tokio::test]
466 async fn start_reaches_sink_before_any_data_frame() {
467 let start_seen = Arc::new(AtomicBool::new(false));
468 let bad = Arc::new(AtomicUsize::new(0));
469 let pipeline = super::super::Pipeline::new(vec![Box::new(Probe {
470 start_seen: start_seen.clone(),
471 data_before_start: bad.clone(),
472 })]);
473 let started = Arc::new(AtomicBool::new(false));
474 let started2 = started.clone();
475 let mut task = PipelineTask::new(pipeline, PipelineTaskParams::default(), vec![]);
476 task.on_started(move || started2.store(true, Ordering::Relaxed));
477 task.queue_frame(Frame::Text("data".into())).await;
478 task.stop_when_done().await;
479 task.run().await.unwrap();
480 assert!(started.load(Ordering::Relaxed), "on_started must fire");
481 assert_eq!(
482 bad.load(Ordering::Relaxed),
483 0,
484 "data must not precede start"
485 );
486 }
487
488 #[tokio::test]
489 async fn stop_when_done_drains_then_ends() {
490 let count = Arc::new(AtomicUsize::new(0));
491 let count2 = count.clone();
492 let pipeline = super::super::Pipeline::new(vec![Box::new(CountText {
493 count: count.clone(),
494 })]);
495 let mut task = PipelineTask::new(pipeline, PipelineTaskParams::default(), vec![]);
496 let finished_reason = Arc::new(Mutex::new(None));
497 let fr = finished_reason.clone();
498 task.on_finished(move |r| *fr.lock().unwrap() = Some(r));
499 task.queue_frames(vec![
500 Frame::Text("a".into()),
501 Frame::Text("b".into()),
502 Frame::Text("c".into()),
503 ])
504 .await;
505 task.stop_when_done().await;
506 task.run().await.unwrap();
507 assert_eq!(
508 count2.load(Ordering::Relaxed),
509 3,
510 "all data drained before End"
511 );
512 assert_eq!(
513 *finished_reason.lock().unwrap(),
514 Some(StopReason::EndOfTask)
515 );
516 let _ = count;
517 }
518
519 #[tokio::test]
520 async fn cancel_ends_without_draining_everything() {
521 let pipeline = super::super::Pipeline::new(vec![Box::new(CountText {
522 count: Arc::new(AtomicUsize::new(0)),
523 })]);
524 let mut task = PipelineTask::new(pipeline, PipelineTaskParams::default(), vec![]);
525 let reason = Arc::new(Mutex::new(None));
526 let r2 = reason.clone();
527 task.on_finished(move |r| *r2.lock().unwrap() = Some(r));
528 task.cancel(None).await;
529 task.run().await.unwrap();
530 assert_eq!(*reason.lock().unwrap(), Some(StopReason::Cancelled));
531 }
532
533 #[tokio::test]
534 async fn upstream_end_request_converts_to_downstream_end() {
535 let pipeline = super::super::Pipeline::new(vec![Box::new(EndRequester)]);
536 let mut task = PipelineTask::new(pipeline, PipelineTaskParams::default(), vec![]);
537 let reason = Arc::new(Mutex::new(None));
538 let r2 = reason.clone();
539 task.on_finished(move |r| *r2.lock().unwrap() = Some(r));
540 task.queue_frame(Frame::Text("bye".into())).await;
541 task.run().await.unwrap();
543 assert_eq!(*reason.lock().unwrap(), Some(StopReason::EndOfTask));
544 }
545
546 #[tokio::test]
547 async fn idle_timeout_fires_and_cancels() {
548 let pipeline = super::super::Pipeline::new(vec![Box::new(CountText {
549 count: Arc::new(AtomicUsize::new(0)),
550 })]);
551 let params = fast_idle(PipelineTaskParams {
552 idle_timeout: Some(Duration::from_millis(80)),
553 cancel_on_idle: true,
554 ..Default::default()
555 });
556 let mut task = PipelineTask::new(pipeline, params, vec![]);
557 let idled = Arc::new(AtomicBool::new(false));
558 let i2 = idled.clone();
559 task.on_idle_timeout(move || i2.store(true, Ordering::Relaxed));
560 let reason = Arc::new(Mutex::new(None));
561 let r2 = reason.clone();
562 task.on_finished(move |r| *r2.lock().unwrap() = Some(r));
563 task.run().await.unwrap();
565 assert!(idled.load(Ordering::Relaxed), "idle hook must fire");
566 assert_eq!(*reason.lock().unwrap(), Some(StopReason::Cancelled));
567 }
568
569 struct CountText {
570 count: Arc<AtomicUsize>,
571 }
572 #[async_trait]
573 impl FrameProcessor for CountText {
574 fn name(&self) -> &str {
575 "CountText"
576 }
577 async fn process_frame(&mut self, env: Envelope, link: &Link) -> Result<()> {
578 if matches!(env.frame, Frame::Text(_)) {
579 self.count.fetch_add(1, Ordering::Relaxed);
580 }
581 link.push(env.meta, env.frame, env.direction).await;
582 Ok(())
583 }
584 }
585}