1use std::collections::HashSet;
16use std::sync::Arc;
17
18use async_trait::async_trait;
19use tokio::task::JoinSet;
20
21use crate::processor::frame::Frame;
22use crate::processor::runtime::{channel, ProcessorRx, ProcessorTx, NORMAL_CHAN_CAP};
23use crate::processor::{into_topology, Envelope, FrameProcessor, ProcessorSetup, Topology};
24
25use super::{link_topology, Linked, Pipeline};
26
27pub struct ParallelPipeline {
30 branches: Vec<Pipeline>,
31}
32
33impl ParallelPipeline {
34 pub fn new(branches: Vec<Pipeline>) -> Self {
36 Self { branches }
37 }
38
39 pub fn len(&self) -> usize {
41 self.branches.len()
42 }
43
44 pub fn is_empty(&self) -> bool {
45 self.branches.is_empty()
46 }
47}
48
49#[async_trait]
50impl FrameProcessor for ParallelPipeline {
51 fn name(&self) -> &str {
52 "ParallelPipeline"
53 }
54
55 fn explode(&mut self) -> Option<Topology> {
56 let branches = std::mem::take(&mut self.branches);
57 Some(Topology::Parallel(
58 branches
59 .into_iter()
60 .map(|b| into_topology(Box::new(b)))
61 .collect(),
62 ))
63 }
64}
65
66pub(crate) fn link_parallel(
69 branches: Vec<Topology>,
70 prev: Option<ProcessorTx>,
71 next: Option<ProcessorTx>,
72 setup: &ProcessorSetup,
73 tasks: &mut JoinSet<()>,
74) -> Linked {
75 let branch_count = branches.len().max(1);
76
77 let (head_tx, head_rx) = channel(Arc::from("ParallelIn"), NORMAL_CHAN_CAP);
79 let (tail_tx, tail_rx) = channel(Arc::from("ParallelOut"), NORMAL_CHAN_CAP);
81
82 let (fanin_tx, fanin_rx) = channel(Arc::from("ParallelFanIn"), NORMAL_CHAN_CAP);
84 let (upstream_tx, upstream_rx) = channel(Arc::from("ParallelUpstream"), NORMAL_CHAN_CAP);
86
87 let mut branch_heads: Vec<ProcessorTx> = Vec::with_capacity(branch_count);
89 for branch in branches {
90 let linked = link_topology(
91 branch,
92 Some(upstream_tx.clone()),
93 Some(fanin_tx.clone()),
94 setup,
95 tasks,
96 );
97 branch_heads.push(linked.head);
98 }
99 drop(fanin_tx);
100 drop(upstream_tx);
101
102 tasks.spawn(fan_out(head_rx, branch_heads));
104
105 tasks.spawn(fan_in(fanin_rx, next, branch_count));
107
108 tasks.spawn(forward(upstream_rx, prev));
110
111 tasks.spawn(forward(tail_rx, None));
118
119 Linked {
120 head: head_tx,
121 tail: tail_tx,
122 }
123}
124
125async fn fan_out(mut rx: ProcessorRx, branch_heads: Vec<ProcessorTx>) {
127 loop {
128 let env = tokio::select! {
129 biased;
130 Some(e) = rx.system.recv() => e,
131 Some(e) = rx.normal.recv() => e,
132 else => break,
133 };
134 let terminal = env.frame.is_terminal();
135 for head in &branch_heads {
136 head.send(env.clone()).await;
137 }
138 if terminal {
139 break;
140 }
141 }
142}
143
144async fn fan_in(mut rx: ProcessorRx, next: Option<ProcessorTx>, branch_count: usize) {
148 let mut seen_ids: HashSet<u64> = HashSet::new();
149 let mut lifecycle_pending: Option<(Frame, usize)> = None;
151 let mut buffer: Vec<Envelope> = Vec::new();
152
153 loop {
154 let env = tokio::select! {
155 biased;
156 Some(e) = rx.system.recv() => e,
157 Some(e) = rx.normal.recv() => e,
158 else => break,
159 };
160
161 if is_synced_lifecycle(&env.frame) {
162 match &mut lifecycle_pending {
166 Some((pending, count)) if same_lifecycle(pending, &env.frame) => {
167 *count += 1;
168 if *count >= branch_count {
169 forward_one(&next, env).await;
172 for buffered in buffer.drain(..) {
173 forward_one(&next, buffered).await;
174 }
175 lifecycle_pending = None;
176 }
177 }
178 Some(_) => {
179 buffer.push(env);
181 }
182 None => {
183 lifecycle_pending = Some((env.frame.clone(), 1));
184 if branch_count <= 1 {
185 forward_one(&next, env).await;
187 lifecycle_pending = None;
188 }
189 }
190 }
191 continue;
192 }
193
194 if !seen_ids.insert(env.meta.id) {
196 continue; }
198 if lifecycle_pending.is_some() {
199 buffer.push(env);
201 } else {
202 forward_one(&next, env).await;
203 }
204 }
205}
206
207async fn forward(mut rx: ProcessorRx, dst: Option<ProcessorTx>) {
209 loop {
210 let env = tokio::select! {
211 biased;
212 Some(e) = rx.system.recv() => e,
213 Some(e) = rx.normal.recv() => e,
214 else => break,
215 };
216 forward_one(&dst, env).await;
217 }
218}
219
220async fn forward_one(dst: &Option<ProcessorTx>, env: Envelope) {
221 if let Some(tx) = dst {
222 tx.send(env).await;
223 }
224}
225
226fn is_synced_lifecycle(frame: &Frame) -> bool {
228 matches!(
229 frame,
230 Frame::Start(_) | Frame::End { .. } | Frame::Cancel { .. }
231 )
232}
233
234fn same_lifecycle(a: &Frame, b: &Frame) -> bool {
236 matches!(
237 (a, b),
238 (Frame::Start(_), Frame::Start(_))
239 | (Frame::End { .. }, Frame::End { .. })
240 | (Frame::Cancel { .. }, Frame::Cancel { .. })
241 )
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use crate::error::Result;
248 use crate::processor::frame::{Direction, Frame};
249 use crate::processor::{Clock, Link};
250 use std::sync::atomic::{AtomicUsize, Ordering};
251 use std::sync::Mutex;
252 use std::time::Duration;
253
254 fn setup() -> ProcessorSetup {
255 ProcessorSetup {
256 clock: Clock::new(),
257 observer: None,
258 cancel: tokio_util::sync::CancellationToken::new(),
259 enable_metrics: false,
260 enable_usage_metrics: false,
261 }
262 }
263
264 struct Counter {
266 name: &'static str,
267 count: Arc<AtomicUsize>,
268 }
269 #[async_trait]
270 impl FrameProcessor for Counter {
271 fn name(&self) -> &str {
272 self.name
273 }
274 async fn process_frame(&mut self, env: Envelope, link: &Link) -> Result<()> {
275 if matches!(env.frame, Frame::Text(_)) {
276 self.count.fetch_add(1, Ordering::Relaxed);
277 }
278 link.push(env.meta, env.frame, env.direction).await;
279 Ok(())
280 }
281 }
282
283 struct SlowEnd {
286 name: &'static str,
287 delay_ms: u64,
288 end_order: Arc<Mutex<Vec<&'static str>>>,
289 }
290 #[async_trait]
291 impl FrameProcessor for SlowEnd {
292 fn name(&self) -> &str {
293 self.name
294 }
295 async fn stop(&mut self, _r: crate::processor::StopReason) -> Result<()> {
296 if self.delay_ms > 0 {
297 tokio::time::sleep(Duration::from_millis(self.delay_ms)).await;
298 }
299 self.end_order.lock().unwrap().push(self.name);
300 Ok(())
301 }
302 }
303
304 #[tokio::test]
305 async fn frame_fans_to_n_branches_and_emits_once() {
306 let c1 = Arc::new(AtomicUsize::new(0));
307 let c2 = Arc::new(AtomicUsize::new(0));
308 let b1 = Pipeline::new(vec![Box::new(Counter {
309 name: "b1",
310 count: c1.clone(),
311 })]);
312 let b2 = Pipeline::new(vec![Box::new(Counter {
313 name: "b2",
314 count: c2.clone(),
315 })]);
316
317 let topo = into_topology(Box::new(ParallelPipeline::new(vec![b1, b2])));
318
319 let (out_tx, out_rx) = channel(Arc::from("out"), NORMAL_CHAN_CAP);
321 let st = setup();
322 let mut tasks = JoinSet::new();
323 let linked = link_topology(topo, None, Some(out_tx), &st, &mut tasks);
324
325 linked
327 .head
328 .send(Envelope::new(
329 Frame::Text("x".into()),
330 Direction::Downstream,
331 ))
332 .await;
333
334 let mut out_rx = out_rx;
336 let first = tokio::time::timeout(Duration::from_secs(2), out_rx.normal.recv())
337 .await
338 .expect("timed out")
339 .expect("closed");
340 assert!(matches!(first.frame, Frame::Text(t) if t == "x"));
341 let second = tokio::time::timeout(Duration::from_millis(200), out_rx.normal.recv()).await;
343 assert!(second.is_err(), "fanned frame must emit once, got a dup");
344
345 linked
347 .head
348 .send(Envelope::new(
349 Frame::End { reason: None },
350 Direction::Downstream,
351 ))
352 .await;
353 let _ = tokio::time::timeout(Duration::from_secs(2), async {
354 while tasks.join_next().await.is_some() {}
355 })
356 .await;
357 assert_eq!(c1.load(Ordering::Relaxed), 1);
358 assert_eq!(c2.load(Ordering::Relaxed), 1);
359 }
360
361 #[tokio::test]
362 async fn fast_branch_end_is_held_until_slow_branch_passes() {
363 let order = Arc::new(Mutex::new(Vec::new()));
364 let fast = Pipeline::new(vec![Box::new(SlowEnd {
365 name: "fast",
366 delay_ms: 0,
367 end_order: order.clone(),
368 })]);
369 let slow = Pipeline::new(vec![Box::new(SlowEnd {
370 name: "slow",
371 delay_ms: 150,
372 end_order: order.clone(),
373 })]);
374 let topo = into_topology(Box::new(ParallelPipeline::new(vec![fast, slow])));
375
376 let (out_tx, out_rx) = channel(Arc::from("out"), NORMAL_CHAN_CAP);
377 let st = setup();
378 let mut tasks = JoinSet::new();
379 let linked = link_topology(topo, None, Some(out_tx), &st, &mut tasks);
380
381 linked
382 .head
383 .send(Envelope::new(
384 Frame::End { reason: None },
385 Direction::Downstream,
386 ))
387 .await;
388
389 let mut out_rx = out_rx;
392 let got = tokio::time::timeout(Duration::from_secs(3), out_rx.normal.recv())
393 .await
394 .expect("End never synced")
395 .expect("closed");
396 assert!(matches!(got.frame, Frame::End { .. }));
397
398 let seq = order.lock().unwrap().clone();
399 assert!(
400 seq.contains(&"fast") && seq.contains(&"slow"),
401 "both branches must pass End, got {seq:?}"
402 );
403
404 let _ = tokio::time::timeout(Duration::from_secs(2), async {
405 while tasks.join_next().await.is_some() {}
406 })
407 .await;
408 }
409}