1pub mod cascaded;
18pub mod parallel;
19pub mod runner;
20pub mod s2s;
21pub mod source_pump;
22pub mod task;
23
24#[cfg(test)]
28pub(crate) mod s2s_test_mocks;
29
30use std::sync::Arc;
31
32use async_trait::async_trait;
33use tokio::task::JoinSet;
34
35use crate::error::Result;
36use crate::processor::frame::{Direction, Frame, StartParams};
37use crate::processor::runtime::{channel, ProcessorTx, NORMAL_CHAN_CAP};
38use crate::processor::{
39 into_topology, run_processor, Envelope, FrameProcessor, Link, ProcessorSetup, StopReason,
40 Topology,
41};
42
43pub use cascaded::{
44 build_cascaded_call_with_observers, build_cascaded_pipeline, build_cascaded_task,
45 build_cascaded_task_with_observers, CascadedConfig, CascadedTask, ContextSummarizer,
46 SummarizerConfig,
47};
48pub use parallel::ParallelPipeline;
49pub use runner::PipelineRunner;
50pub use source_pump::{SourceHandle, SourcePump};
51pub use task::{PipelineTask, PipelineTaskParams};
52
53pub struct Pipeline {
59 processors: Vec<Box<dyn FrameProcessor>>,
60}
61
62impl Pipeline {
63 pub fn new(processors: Vec<Box<dyn FrameProcessor>>) -> Self {
65 Self { processors }
66 }
67
68 pub fn len(&self) -> usize {
70 self.processors.len()
71 }
72
73 pub fn is_empty(&self) -> bool {
74 self.processors.is_empty()
75 }
76}
77
78#[async_trait]
79impl FrameProcessor for Pipeline {
80 fn name(&self) -> &str {
81 "Pipeline"
82 }
83
84 fn explode(&mut self) -> Option<Topology> {
85 let procs = std::mem::take(&mut self.processors);
86 Some(Topology::Chain(
87 procs.into_iter().map(into_topology).collect(),
88 ))
89 }
90}
91
92pub(crate) struct Linked {
98 pub head: ProcessorTx,
100 #[allow(dead_code)]
104 pub tail: ProcessorTx,
105}
106
107fn make_link(
109 name: Arc<str>,
110 next: Option<ProcessorTx>,
111 prev: Option<ProcessorTx>,
112 setup: &ProcessorSetup,
113) -> Link {
114 Link {
115 next,
116 prev,
117 name,
118 clock: setup.clock.clone(),
119 observer: setup.observer.clone(),
120 enable_metrics: setup.enable_metrics,
121 enable_usage_metrics: setup.enable_usage_metrics,
122 ttfb_start: Arc::new(std::sync::atomic::AtomicI64::new(0)),
123 processing_start: Arc::new(std::sync::atomic::AtomicI64::new(0)),
124 }
125}
126
127pub(crate) fn link_topology(
131 topo: Topology,
132 prev: Option<ProcessorTx>,
133 next: Option<ProcessorTx>,
134 setup: &ProcessorSetup,
135 tasks: &mut JoinSet<()>,
136) -> Linked {
137 match topo {
138 Topology::Leaf(p) => {
139 let name: Arc<str> = Arc::from(p.name());
140 let (tx, rx) = channel(name.clone(), NORMAL_CHAN_CAP);
141 let link = make_link(name, next, prev, setup);
142 let setup2 = setup.clone();
143 tasks.spawn(async move {
144 run_processor(p, rx, link, setup2).await;
145 });
146 Linked {
147 head: tx.clone(),
148 tail: tx,
149 }
150 }
151 Topology::Chain(elems) => link_chain(elems, prev, next, setup, tasks),
152 Topology::Parallel(branches) => parallel::link_parallel(branches, prev, next, setup, tasks),
153 }
154}
155
156fn link_chain(
166 elems: Vec<Topology>,
167 prev: Option<ProcessorTx>,
168 next: Option<ProcessorTx>,
169 setup: &ProcessorSetup,
170 tasks: &mut JoinSet<()>,
171) -> Linked {
172 if elems.is_empty() {
173 return link_topology(
174 Topology::Leaf(Box::new(PassThrough)),
175 prev,
176 next,
177 setup,
178 tasks,
179 );
180 }
181 let n = elems.len();
182
183 let mut inputs: Vec<ProcessorTx> = Vec::with_capacity(n);
188 let mut pending: Vec<PendingElem> = Vec::with_capacity(n);
189 for elem in elems {
190 match elem {
191 Topology::Leaf(p) => {
192 let name: Arc<str> = Arc::from(p.name());
193 let (tx, rx) = channel(name.clone(), NORMAL_CHAN_CAP);
194 inputs.push(tx);
195 pending.push(PendingElem::Leaf { name, p, rx });
196 }
197 other => {
198 let name: Arc<str> = Arc::from("nested");
200 let (tx, rx) = channel(name, NORMAL_CHAN_CAP);
201 inputs.push(tx);
202 pending.push(PendingElem::Composite {
203 topo: other,
204 in_rx: rx,
205 });
206 }
207 }
208 }
209
210 for (i, elem) in pending.into_iter().enumerate() {
212 let next_tx = if i + 1 < n {
213 Some(inputs[i + 1].clone())
214 } else {
215 next.clone()
216 };
217 let prev_tx = if i > 0 {
218 Some(inputs[i - 1].clone())
219 } else {
220 prev.clone()
221 };
222 match elem {
223 PendingElem::Leaf { name, p, rx } => {
224 let link = make_link(name, next_tx, prev_tx, setup);
225 let setup2 = setup.clone();
226 tasks.spawn(async move {
227 run_processor(p, rx, link, setup2).await;
228 });
229 }
230 PendingElem::Composite { topo, in_rx } => {
231 let linked = link_topology(topo, prev_tx, next_tx, setup, tasks);
234 let real_head = linked.head;
235 tasks.spawn(bridge(in_rx, real_head));
236 }
237 }
238 }
239
240 Linked {
241 head: inputs[0].clone(),
242 tail: inputs[n - 1].clone(),
243 }
244}
245
246enum PendingElem {
248 Leaf {
249 name: Arc<str>,
250 p: Box<dyn FrameProcessor>,
251 rx: crate::processor::runtime::ProcessorRx,
252 },
253 Composite {
254 topo: Topology,
255 in_rx: crate::processor::runtime::ProcessorRx,
256 },
257}
258
259pub(crate) async fn bridge(mut rx: crate::processor::runtime::ProcessorRx, dst: ProcessorTx) {
263 loop {
264 let env = tokio::select! {
265 biased;
266 Some(e) = rx.system.recv() => e,
267 Some(e) = rx.normal.recv() => e,
268 else => break,
269 };
270 let terminal = env.frame.is_terminal();
271 dst.send(env).await;
272 if terminal {
273 break;
274 }
275 }
276}
277
278struct PassThrough;
280
281#[async_trait]
282impl FrameProcessor for PassThrough {
283 fn name(&self) -> &str {
284 "PassThrough"
285 }
286}
287
288pub(crate) struct Source;
298
299#[async_trait]
300impl FrameProcessor for Source {
301 fn name(&self) -> &str {
302 "Source"
303 }
304
305 async fn process_frame(&mut self, env: Envelope, link: &Link) -> Result<()> {
306 if env.direction == Direction::Upstream {
309 match &env.frame {
310 Frame::End { .. } | Frame::Stop | Frame::Cancel { .. } => {
311 link.push_down(env.frame).await;
312 return Ok(());
313 }
314 _ => {}
315 }
316 }
317 link.push(env.meta, env.frame, env.direction).await;
318 Ok(())
319 }
320}
321
322pub(crate) struct Sink {
327 pub(crate) tap: tokio::sync::mpsc::UnboundedSender<Frame>,
329}
330
331#[async_trait]
332impl FrameProcessor for Sink {
333 fn name(&self) -> &str {
334 "Sink"
335 }
336
337 async fn start(&mut self, _setup: &ProcessorSetup, params: &StartParams) -> Result<()> {
346 let _ = self.tap.send(Frame::Start(params.clone()));
347 Ok(())
348 }
349
350 async fn stop(&mut self, reason: StopReason) -> Result<()> {
351 let frame = match reason {
352 StopReason::EndOfTask => Frame::End { reason: None },
353 StopReason::Stopped => Frame::Stop,
354 StopReason::Cancelled => Frame::Cancel { reason: None },
355 };
356 let _ = self.tap.send(frame);
357 Ok(())
358 }
359
360 async fn process_frame(&mut self, env: Envelope, link: &Link) -> Result<()> {
361 if env.direction == Direction::Downstream {
362 let _ = self.tap.send(env.frame.clone());
363 }
364 link.push(env.meta, env.frame, env.direction).await;
365 Ok(())
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372 use crate::processor::frame::{Frame, StartParams};
373 use crate::processor::Clock;
374 use std::sync::atomic::{AtomicUsize, Ordering};
375 use std::sync::Arc as StdArc;
376 use tokio::sync::mpsc;
377
378 struct Recorder {
380 name: &'static str,
381 log: StdArc<std::sync::Mutex<Vec<String>>>,
382 }
383 #[async_trait]
384 impl FrameProcessor for Recorder {
385 fn name(&self) -> &str {
386 self.name
387 }
388 async fn process_frame(&mut self, env: Envelope, link: &Link) -> Result<()> {
389 if let Frame::Text(t) = &env.frame {
390 self.log
391 .lock()
392 .unwrap()
393 .push(format!("{}:{}", self.name, t));
394 }
395 link.push(env.meta, env.frame, env.direction).await;
396 Ok(())
397 }
398 }
399
400 fn setup() -> ProcessorSetup {
401 ProcessorSetup {
402 clock: Clock::new(),
403 observer: None,
404 cancel: tokio_util::sync::CancellationToken::new(),
405 enable_metrics: false,
406 enable_usage_metrics: false,
407 }
408 }
409
410 #[tokio::test]
411 async fn pipeline_is_a_processor_and_nests_reaching_the_sink() {
412 let log = StdArc::new(std::sync::Mutex::new(Vec::new()));
413 let inner = Pipeline::new(vec![Box::new(Recorder {
415 name: "inner",
416 log: log.clone(),
417 })]);
418 let outer = Pipeline::new(vec![
419 Box::new(Recorder {
420 name: "a",
421 log: log.clone(),
422 }),
423 Box::new(inner),
424 Box::new(Recorder {
425 name: "b",
426 log: log.clone(),
427 }),
428 ]);
429
430 let (sink_tap_tx, mut sink_tap_rx) = mpsc::unbounded_channel::<Frame>();
431 let topo = Topology::Chain(vec![
433 Topology::Leaf(Box::new(Source)),
434 into_topology(Box::new(outer)),
435 Topology::Leaf(Box::new(Sink { tap: sink_tap_tx })),
436 ]);
437 let st = setup();
438 let mut tasks = JoinSet::new();
439 let linked = link_topology(topo, None, None, &st, &mut tasks);
440
441 linked
443 .head
444 .send(Envelope::new(
445 Frame::Text("hi".into()),
446 Direction::Downstream,
447 ))
448 .await;
449 let got = tokio::time::timeout(std::time::Duration::from_secs(2), sink_tap_rx.recv())
451 .await
452 .expect("sink frame timed out")
453 .expect("sink closed");
454 assert!(matches!(got, Frame::Text(t) if t == "hi"));
455
456 linked
458 .head
459 .send(Envelope::new(
460 Frame::End { reason: None },
461 Direction::Downstream,
462 ))
463 .await;
464 let _ = tokio::time::timeout(std::time::Duration::from_secs(2), async {
465 while tasks.join_next().await.is_some() {}
466 })
467 .await;
468
469 let order = log.lock().unwrap().clone();
470 assert_eq!(order, vec!["a:hi", "inner:hi", "b:hi"]);
471 }
472
473 struct StartCounter {
475 seen_data_before_start: StdArc<AtomicUsize>,
476 started: StdArc<AtomicUsize>,
477 }
478 #[async_trait]
479 impl FrameProcessor for StartCounter {
480 fn name(&self) -> &str {
481 "StartCounter"
482 }
483 async fn start(&mut self, _s: &ProcessorSetup, _p: &StartParams) -> Result<()> {
484 self.started.fetch_add(1, Ordering::Relaxed);
485 Ok(())
486 }
487 async fn process_frame(&mut self, env: Envelope, link: &Link) -> Result<()> {
488 if matches!(env.frame, Frame::Text(_)) && self.started.load(Ordering::Relaxed) == 0 {
489 self.seen_data_before_start.fetch_add(1, Ordering::Relaxed);
490 }
491 link.push(env.meta, env.frame, env.direction).await;
492 Ok(())
493 }
494 }
495
496 #[tokio::test]
497 async fn start_runs_before_data_in_a_linked_chain() {
498 let started = StdArc::new(AtomicUsize::new(0));
499 let bad = StdArc::new(AtomicUsize::new(0));
500 let (sink_tap_tx, _rx) = mpsc::unbounded_channel::<Frame>();
501 let topo = Topology::Chain(vec![
502 Topology::Leaf(Box::new(Source)),
503 Topology::Leaf(Box::new(StartCounter {
504 seen_data_before_start: bad.clone(),
505 started: started.clone(),
506 })),
507 Topology::Leaf(Box::new(Sink { tap: sink_tap_tx })),
508 ]);
509 let st = setup();
510 let mut tasks = JoinSet::new();
511 let linked = link_topology(topo, None, None, &st, &mut tasks);
512 linked
513 .head
514 .send(Envelope::new(
515 Frame::Start(StartParams::default()),
516 Direction::Downstream,
517 ))
518 .await;
519 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
521 linked
522 .head
523 .send(Envelope::new(
524 Frame::Text("data".into()),
525 Direction::Downstream,
526 ))
527 .await;
528 linked
529 .head
530 .send(Envelope::new(
531 Frame::End { reason: None },
532 Direction::Downstream,
533 ))
534 .await;
535 let _ = tokio::time::timeout(std::time::Duration::from_secs(2), async {
536 while tasks.join_next().await.is_some() {}
537 })
538 .await;
539 assert_eq!(started.load(Ordering::Relaxed), 1);
540 assert_eq!(
541 bad.load(Ordering::Relaxed),
542 0,
543 "data must not precede start"
544 );
545 }
546}