Skip to main content

media_pp/elements/filter/
tee.rs

1use std::sync::{
2    Arc, Mutex, MutexGuard, Weak,
3    atomic::{AtomicBool, AtomicU64, Ordering},
4};
5
6use crate::pp_log::{PpLog, pp_info};
7
8use crate::{
9    buffer::MediaBuffer,
10    bus::BusEvent,
11    control::ControlMsg,
12    element::{Context, Element, ElementType, Sink, element_pp_log},
13    error::Result,
14    graph::{BranchId, ElementId, GraphError, PlannedEdge, PortRef, log_topology},
15    pad::SrcPad,
16    pipeline::{ChainBuilder, DetachedBranch},
17};
18
19/// Fans a single input out to multiple sinks. [`TeeBuilder`] owns the
20/// initial fan-out; later branches are added and removed through a
21/// [`TeeHandle`], which can be cloned and used from any thread, independent
22/// of whatever thread is driving `Tee::consume`
23/// (the pipeline's source/queue-worker thread). That's the whole reason
24/// `Tee` doesn't implement [`crate::element::Source`] like other
25/// multi-pad elements (e.g. [`crate::elements::FileDemuxer`]): its pads
26/// live in individually locked branch slots instead of being a plain
27/// `&mut [SrcPad]`. `consume` only holds the branch-list lock long enough
28/// to take a cheap `Arc` snapshot, so a slow downstream does not block
29/// unrelated attach/detach operations. Detach prevents any push that has
30/// not started yet; one already executing downstream call may finish.
31///
32/// Cheap to fan out: `MediaBuffer` wraps its payload in an `Arc`, so
33/// cloning a buffer for each output is a refcount bump, not a copy of the
34/// encoded/decoded data.
35pub struct Tee {
36    pp_log: PpLog,
37    id: ElementId,
38    name: Arc<str>,
39    shared: Arc<TeeShared>,
40}
41
42struct TeeShared {
43    branches: Mutex<Vec<Arc<TeeBranch>>>,
44    next_pad_id: AtomicU64,
45    context: Arc<Context>,
46}
47
48struct TeeBranch {
49    id: Option<BranchId>,
50    root_id: ElementId,
51    active: AtomicBool,
52    pad: Mutex<SrcPad>,
53}
54
55/// Recovers the protected value after a panic instead of turning one
56/// poisoned Tee lock into a permanent source of follow-up panics. The
57/// original panic still unwinds normally; this only lets a caller that
58/// catches it keep using or detach the remaining branch state.
59fn lock_unpoisoned<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
60    match mutex.lock() {
61        Ok(guard) => guard,
62        Err(poisoned) => poisoned.into_inner(),
63    }
64}
65
66/// Build-time configuration for a [`Tee`]. Initial branches are merged
67/// with the Tee into one detached subgraph and committed by a single
68/// [`Context::attach`] call. Use [`TeeBuilder::build`] for a fixed fan-out,
69/// or [`TeeBuilder::build_dynamic`] when runtime changes need a
70/// [`TeeHandle`].
71pub struct TeeBuilder {
72    tee: Tee,
73    handle: TeeHandle,
74    initial_branches: Vec<DetachedBranch>,
75}
76
77/// A cheaply-cloneable handle for adding or removing a [`Tee`]'s sinks
78/// while the pipeline is running. It deliberately keeps only a [`Weak`]
79/// reference to the `Tee`'s shared state: retaining a handle after the
80/// pipeline finishes must not keep downstream sinks or the pipeline's
81/// [`crate::bus::Bus`] sender alive. Once the `Tee` is gone,
82/// [`TeeHandle::branch`] returns `None`.
83#[derive(Clone)]
84pub struct TeeHandle {
85    id: ElementId,
86    name: Arc<str>,
87    /// The same identity the `Tee` element logs under, cloned once at
88    /// construction — attach/detach must not rebuild it per call, and the
89    /// handle must not be able to disagree with the element about it.
90    pp_log: PpLog,
91    shared: Weak<TeeShared>,
92}
93
94impl Tee {
95    fn new(name: impl Into<String>, context: Arc<Context>) -> (Self, TeeHandle) {
96        let name: Arc<str> = name.into().into();
97        let pp_log = element_pp_log(ElementType::Tee, &name, Some(&context.pipeline_id));
98        pp_info!(pp_log: &pp_log, "created");
99        let id = context.graph.reserve_element_id();
100        let shared = Arc::new(TeeShared {
101            branches: Mutex::new(Vec::new()),
102            next_pad_id: AtomicU64::new(0),
103            context,
104        });
105        (
106            Self {
107                id,
108                name: name.clone(),
109                pp_log: pp_log.clone(),
110                shared: shared.clone(),
111            },
112            TeeHandle {
113                id,
114                name,
115                pp_log,
116                shared: Arc::downgrade(&shared),
117            },
118        )
119    }
120
121    /// Posts a branch's `push` failure to the bus under *that branch's*
122    /// own identity (via [`SrcPad::peer_identity`]) — unlike `Queue`,
123    /// which only ever has one downstream and so can only attribute a
124    /// failure to itself, `Tee` fans out to several and does know which
125    /// one just failed. Reporting it that way (rather than folding every
126    /// branch's failures into one generic `Tee` event) is what lets a
127    /// caller watching the bus tell branches apart and identify the
128    /// corresponding runtime branch. The peer identity is captured just
129    /// before the downstream call, so the event stays attributable even
130    /// if that branch is detached while the call is running.
131    fn report_branch_error(
132        &self,
133        root_id: ElementId,
134        peer: Option<(ElementType, Arc<str>)>,
135        error: crate::error::Error,
136    ) {
137        let (element_type, name) = peer.unwrap_or((ElementType::Tee, self.name.clone()));
138        self.shared.context.bus.for_element(root_id).post(
139            &self.pp_log,
140            BusEvent::Error {
141                element_type,
142                name,
143                error,
144            },
145        );
146    }
147}
148
149impl TeeShared {
150    fn next_pad(&self, tee_name: &str) -> SrcPad {
151        let id = self.next_pad_id.fetch_add(1, Ordering::Relaxed);
152        SrcPad::new(format!("{tee_name}_src{id}"))
153    }
154}
155
156impl TeeBuilder {
157    /// Starts an initially empty Tee in the supplied pipeline context.
158    pub fn new(name: impl Into<String>, context: Arc<Context>) -> Self {
159        let (tee, handle) = Tee::new(name, context);
160        Self {
161            tee,
162            handle,
163            initial_branches: Vec::new(),
164        }
165    }
166
167    /// Adds one fixed initial branch to this fan-out subgraph.
168    pub fn branch(mut self, branch: DetachedBranch) -> Self {
169        self.initial_branches.push(branch);
170        self
171    }
172
173    /// Returns the complete fixed fan-out without exposing runtime control.
174    pub fn build(self) -> Result<DetachedBranch> {
175        self.finish().map(|(branch, _handle)| branch)
176    }
177
178    /// Returns the initial subgraph together with its runtime control handle.
179    /// Attach the branch through [`Context::attach`] before using the handle.
180    pub fn build_dynamic(self) -> Result<(DetachedBranch, TeeHandle)> {
181        self.finish()
182    }
183
184    fn finish(self) -> Result<(DetachedBranch, TeeHandle)> {
185        let Self {
186            tee,
187            handle,
188            initial_branches,
189        } = self;
190        let tee_id = tee.id;
191        let tee_name = tee.name.clone();
192        let shared = tee.shared.clone();
193        let context = shared.context.clone();
194        let mut tee_branch = context.branch().to(Box::new(tee))?;
195        let mut runtime_branches = lock_unpoisoned(&shared.branches);
196
197        for branch in initial_branches {
198            let mut pad = shared.next_pad(&tee_name);
199            let from_port: Arc<str> = pad.name().into();
200            let DetachedBranch { root, plan } = branch;
201            let root_id = plan.root;
202
203            tee_branch.plan.edges.push(PlannedEdge {
204                from: PortRef {
205                    element: tee_id,
206                    port: from_port,
207                },
208                to: PortRef {
209                    element: root_id,
210                    port: "sink".into(),
211                },
212            });
213            tee_branch.plan.nodes.extend(plan.nodes);
214            tee_branch.plan.edges.extend(plan.edges);
215
216            pad.link(root);
217            runtime_branches.push(Arc::new(TeeBranch {
218                id: None,
219                root_id,
220                active: AtomicBool::new(true),
221                pad: Mutex::new(pad),
222            }));
223        }
224        drop(runtime_branches);
225
226        Ok((tee_branch, handle))
227    }
228}
229
230impl TeeHandle {
231    /// A [`crate::pipeline::ChainBuilder`] pre-wired with this `Tee`'s own
232    /// [`Context`] — lets a caller build a whole new branch (`.pipe(...)`
233    /// chains, ending in `.to(...)`) at any point after the pipeline
234    /// started running, then hand the result to [`TeeHandle::attach`],
235    /// without needing to retain the pipeline context separately
236    /// around separately. Returns `None` once the `Tee` has been dropped.
237    pub fn branch(&self) -> Option<ChainBuilder> {
238        let shared = self.shared.upgrade()?;
239        Some(shared.context.branch())
240    }
241
242    /// Attaches a runtime branch, returning the stable ID used to remove it.
243    /// Fixed initial branches belong in [`TeeBuilder`].
244    pub fn attach(&self, branch: DetachedBranch) -> Result<BranchId> {
245        let shared = self
246            .shared
247            .upgrade()
248            .ok_or(GraphError::ParentNotAttached(self.id))?;
249        let mut branches = lock_unpoisoned(&shared.branches);
250        let mut pad = shared.next_pad(&self.name);
251        let from_port: Arc<str> = pad.name().into();
252        let DetachedBranch { root, plan } = branch;
253        let root_id = plan.root;
254        let branch_id =
255            shared
256                .context
257                .graph
258                .attach_with(self.id, from_port, plan, |branch_id| {
259                    pad.link(root);
260                    branches.push(Arc::new(TeeBranch {
261                        id: Some(branch_id),
262                        root_id,
263                        active: AtomicBool::new(true),
264                        pad: Mutex::new(pad),
265                    }));
266                    Ok(())
267                })?;
268        let snapshot =
269            crate::log::enabled(crate::log::Level::Info).then(|| shared.context.graph.snapshot());
270        drop(branches);
271        if let Some(snapshot) = snapshot {
272            log_topology(&self.pp_log, "attach", &snapshot);
273        }
274        Ok(branch_id)
275    }
276
277    /// Detaches exactly the branch returned by [`TeeHandle::attach`]. The
278    /// runtime peer and every graph node owned by it disappear in the same
279    /// transaction. Names are deliberately not used as graph keys.
280    pub fn detach(&self, branch_id: BranchId) -> Result<()> {
281        let shared = self
282            .shared
283            .upgrade()
284            .ok_or(GraphError::BranchNotAttached(branch_id))?;
285        let mut branches = lock_unpoisoned(&shared.branches);
286        let index = branches
287            .iter()
288            .position(|branch| branch.id == Some(branch_id))
289            .ok_or(GraphError::BranchNotAttached(branch_id))?;
290        let mut removed = None;
291        shared.context.graph.detach_with(branch_id, || {
292            let branch = branches.remove(index);
293            branch.active.store(false, Ordering::Release);
294            removed = Some(branch);
295            Ok(())
296        })?;
297        let snapshot =
298            crate::log::enabled(crate::log::Level::Info).then(|| shared.context.graph.snapshot());
299        drop(branches);
300        // The last Arc owns the downstream sink. Dropping it outside both
301        // the branch-list and graph locks allows arbitrary Sink::drop code
302        // to inspect the graph or call back into Tee without deadlocking.
303        drop(removed);
304        if let Some(snapshot) = snapshot {
305            log_topology(&self.pp_log, "detach", &snapshot);
306        }
307        Ok(())
308    }
309
310    /// Resolves the owning branch from any element ID inside it and
311    /// detaches that branch. Useful when an error is attributed to a stage
312    /// behind a queue rather than to the branch root.
313    pub fn detach_branch_containing(&self, element: ElementId) -> Result<()> {
314        let shared = self
315            .shared
316            .upgrade()
317            .ok_or(GraphError::ParentNotAttached(self.id))?;
318        let branch_id = shared
319            .context
320            .graph
321            .branch_containing(element)
322            .ok_or(GraphError::ParentNotAttached(element))?;
323        self.detach(branch_id)
324    }
325
326    /// Returns the number of currently attached output branches.
327    ///
328    /// Returns zero after the tee element has been dropped.
329    pub fn sink_count(&self) -> usize {
330        self.shared
331            .upgrade()
332            .map(|shared| lock_unpoisoned(&shared.branches).len())
333            .unwrap_or(0)
334    }
335}
336
337impl Element for Tee {
338    fn name(&self) -> Arc<str> {
339        self.name.clone()
340    }
341
342    fn element_type(&self) -> ElementType {
343        ElementType::Tee
344    }
345
346    fn graph_id(&self) -> Option<ElementId> {
347        Some(self.id)
348    }
349
350    fn pp_log(&self) -> &PpLog {
351        &self.pp_log
352    }
353
354    fn pp_log_mut(&mut self) -> &mut PpLog {
355        &mut self.pp_log
356    }
357}
358
359impl Sink for Tee {
360    fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
361        let branches = lock_unpoisoned(&self.shared.branches).clone();
362        // One branch failing must not stop the buffer from reaching its
363        // siblings — same "errors never kill anything, just get reported"
364        // rule `Queue`'s worker loop follows. That buffer is dropped for
365        // the failing branch only; the branch itself stays wired and gets
366        // retried on the next one. Whoever's watching the bus decides
367        // whether to call `TeeHandle::detach` for it.
368        for branch in branches {
369            if !branch.active.load(Ordering::Acquire) {
370                continue;
371            }
372            let mut pad = lock_unpoisoned(&branch.pad);
373            // Detach may have won the race while this thread waited for a
374            // previous push on the same branch to finish.
375            if !branch.active.load(Ordering::Acquire) {
376                continue;
377            }
378            let peer = pad.peer_identity();
379            let outcome = pad.push(buf.clone());
380            drop(pad);
381            if let Err(error) = outcome {
382                self.report_branch_error(branch.root_id, peer, error);
383            }
384        }
385        Ok(())
386    }
387
388    fn control(&mut self, msg: ControlMsg) -> Result<()> {
389        // Control failures follow the same isolation rule as data failures:
390        // report the failed branch, but still deliver the message to every
391        // sibling. This is especially important for Stop and Pause.
392        let branches = lock_unpoisoned(&self.shared.branches).clone();
393        for branch in branches {
394            if !branch.active.load(Ordering::Acquire) {
395                continue;
396            }
397            let mut pad = lock_unpoisoned(&branch.pad);
398            if !branch.active.load(Ordering::Acquire) {
399                continue;
400            }
401            let peer = pad.peer_identity();
402            let outcome = pad.control(msg);
403            drop(pad);
404            if let Err(error) = outcome {
405                self.report_branch_error(branch.root_id, peer, error);
406            }
407        }
408        Ok(())
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    use std::{
415        panic::{AssertUnwindSafe, catch_unwind},
416        sync::{
417            Barrier,
418            atomic::{AtomicBool, AtomicUsize, Ordering},
419            mpsc,
420        },
421        thread,
422        time::Duration,
423    };
424
425    use super::*;
426    use crate::{bus::Bus, graph::PipelineGraph};
427
428    fn packet() -> MediaBuffer {
429        MediaBuffer::Packet(Arc::new(ffmpeg_next::Packet::empty()))
430    }
431
432    struct CountingSink {
433        pp_log: PpLog,
434        name: &'static str,
435        count: Arc<AtomicUsize>,
436    }
437
438    impl Element for CountingSink {
439        fn name(&self) -> Arc<str> {
440            self.name.into()
441        }
442
443        fn element_type(&self) -> ElementType {
444            ElementType::Other
445        }
446
447        fn pp_log(&self) -> &PpLog {
448            &self.pp_log
449        }
450
451        fn pp_log_mut(&mut self) -> &mut PpLog {
452            &mut self.pp_log
453        }
454    }
455
456    impl Sink for CountingSink {
457        fn consume(&mut self, _buf: MediaBuffer) -> Result<()> {
458            self.count.fetch_add(1, Ordering::SeqCst);
459            Ok(())
460        }
461
462        fn control(&mut self, _msg: ControlMsg) -> Result<()> {
463            Ok(())
464        }
465    }
466
467    struct AlwaysFailSink {
468        pp_log: PpLog,
469    }
470
471    impl Element for AlwaysFailSink {
472        fn name(&self) -> Arc<str> {
473            "always-fail".into()
474        }
475
476        fn element_type(&self) -> ElementType {
477            ElementType::Other
478        }
479
480        fn pp_log(&self) -> &PpLog {
481            &self.pp_log
482        }
483
484        fn pp_log_mut(&mut self) -> &mut PpLog {
485            &mut self.pp_log
486        }
487    }
488
489    impl Sink for AlwaysFailSink {
490        fn consume(&mut self, _buf: MediaBuffer) -> Result<()> {
491            Err(crate::error::Error::Other(
492                "simulated branch failure".into(),
493            ))
494        }
495
496        fn control(&mut self, _msg: ControlMsg) -> Result<()> {
497            Ok(())
498        }
499    }
500
501    struct ControlObservingSink {
502        pp_log: PpLog,
503        name: &'static str,
504        count: Arc<AtomicUsize>,
505        fail: bool,
506    }
507
508    impl Element for ControlObservingSink {
509        fn name(&self) -> Arc<str> {
510            self.name.into()
511        }
512
513        fn element_type(&self) -> ElementType {
514            ElementType::Other
515        }
516
517        fn pp_log(&self) -> &PpLog {
518            &self.pp_log
519        }
520
521        fn pp_log_mut(&mut self) -> &mut PpLog {
522            &mut self.pp_log
523        }
524    }
525
526    impl Sink for ControlObservingSink {
527        fn consume(&mut self, _buf: MediaBuffer) -> Result<()> {
528            Ok(())
529        }
530
531        fn control(&mut self, _msg: ControlMsg) -> Result<()> {
532            self.count.fetch_add(1, Ordering::SeqCst);
533            if self.fail {
534                Err(crate::error::Error::Other(
535                    "simulated control failure".into(),
536                ))
537            } else {
538                Ok(())
539            }
540        }
541    }
542
543    struct PanicOnceSink {
544        pp_log: PpLog,
545        panicked: bool,
546        successful: Arc<AtomicUsize>,
547    }
548
549    impl Element for PanicOnceSink {
550        fn name(&self) -> Arc<str> {
551            "panic-once".into()
552        }
553
554        fn element_type(&self) -> ElementType {
555            ElementType::Other
556        }
557
558        fn pp_log(&self) -> &PpLog {
559            &self.pp_log
560        }
561
562        fn pp_log_mut(&mut self) -> &mut PpLog {
563            &mut self.pp_log
564        }
565    }
566
567    impl Sink for PanicOnceSink {
568        fn consume(&mut self, _buf: MediaBuffer) -> Result<()> {
569            if !self.panicked {
570                self.panicked = true;
571                panic!("simulated downstream panic");
572            }
573            self.successful.fetch_add(1, Ordering::SeqCst);
574            Ok(())
575        }
576
577        fn control(&mut self, _msg: ControlMsg) -> Result<()> {
578            Ok(())
579        }
580    }
581
582    struct BlockingSink {
583        pp_log: PpLog,
584        entered: Option<mpsc::Sender<()>>,
585        release: mpsc::Receiver<()>,
586    }
587
588    impl Element for BlockingSink {
589        fn name(&self) -> Arc<str> {
590            "blocking".into()
591        }
592
593        fn element_type(&self) -> ElementType {
594            ElementType::Other
595        }
596
597        fn pp_log(&self) -> &PpLog {
598            &self.pp_log
599        }
600
601        fn pp_log_mut(&mut self) -> &mut PpLog {
602            &mut self.pp_log
603        }
604    }
605
606    impl Sink for BlockingSink {
607        fn consume(&mut self, _buf: MediaBuffer) -> Result<()> {
608            if let Some(entered) = self.entered.take() {
609                let _ = entered.send(());
610            }
611            let _ = self.release.recv();
612            Ok(())
613        }
614
615        fn control(&mut self, _msg: ControlMsg) -> Result<()> {
616            Ok(())
617        }
618    }
619
620    struct GraphInspectingDropSink {
621        pp_log: PpLog,
622        graph: PipelineGraph,
623        dropped: Option<mpsc::Sender<()>>,
624    }
625
626    impl Element for GraphInspectingDropSink {
627        fn name(&self) -> Arc<str> {
628            "graph-inspecting-drop".into()
629        }
630
631        fn element_type(&self) -> ElementType {
632            ElementType::Other
633        }
634
635        fn pp_log(&self) -> &PpLog {
636            &self.pp_log
637        }
638
639        fn pp_log_mut(&mut self) -> &mut PpLog {
640            &mut self.pp_log
641        }
642    }
643
644    impl Sink for GraphInspectingDropSink {
645        fn consume(&mut self, _buf: MediaBuffer) -> Result<()> {
646            Ok(())
647        }
648
649        fn control(&mut self, _msg: ControlMsg) -> Result<()> {
650            Ok(())
651        }
652    }
653
654    impl Drop for GraphInspectingDropSink {
655        fn drop(&mut self) {
656            let _ = self.graph.snapshot();
657            if let Some(dropped) = self.dropped.take() {
658                let _ = dropped.send(());
659            }
660        }
661    }
662
663    /// A failing branch must not stop the same buffer from reaching its
664    /// siblings, nor stop `Tee::consume` itself from returning `Ok` — same
665    /// "errors get reported, nothing dies" rule `Queue`'s worker loop
666    /// follows (see `queue::tests::a_failing_consume_drops_that_buffer_but_keeps_the_worker_alive`).
667    /// Wires the failing branch in the *middle* (`before`/`after` on
668    /// either side of it) so the test also proves a mid-`rest` failure
669    /// doesn't short-circuit the fan-out to what comes after it,
670    /// including `last`.
671    #[test]
672    fn a_failing_branch_does_not_block_its_siblings() {
673        let (bus, bus_rx) = Bus::new();
674        let graph = PipelineGraph::new();
675        let source_id = graph.add_source(ElementType::Other, "source".into());
676        let context = Arc::new(Context::for_test(bus, "test", graph, source_id));
677        let before_count = Arc::new(AtomicUsize::new(0));
678        let after_count = Arc::new(AtomicUsize::new(0));
679        let before = context
680            .branch()
681            .to(Box::new(CountingSink {
682                name: "before",
683                count: before_count.clone(),
684                pp_log: element_pp_log(ElementType::Other, "before", None),
685            }))
686            .unwrap();
687        let failing = context
688            .branch()
689            .to(Box::new(AlwaysFailSink {
690                pp_log: element_pp_log(ElementType::Other, "always-fail", None),
691            }))
692            .unwrap();
693        let after = context
694            .branch()
695            .to(Box::new(CountingSink {
696                name: "after",
697                count: after_count.clone(),
698                pp_log: element_pp_log(ElementType::Other, "after", None),
699            }))
700            .unwrap();
701        let tee_branch = TeeBuilder::new("tee", context.clone())
702            .branch(before)
703            .branch(failing)
704            .branch(after)
705            .build()
706            .unwrap();
707        let mut upstream = SrcPad::new("source_src");
708        context.attach_pad(&mut upstream, tee_branch).unwrap();
709
710        for _ in 0..3 {
711            upstream
712                .push(packet())
713                .expect("a branch failing must not surface as an error from Tee::consume");
714        }
715
716        assert_eq!(before_count.load(Ordering::SeqCst), 3);
717        assert_eq!(after_count.load(Ordering::SeqCst), 3);
718
719        drop(upstream);
720        drop(context);
721        let errors: Vec<_> = bus_rx
722            .iter()
723            .filter(|e| matches!(e, BusEvent::Error { .. }))
724            .collect();
725        assert_eq!(
726            errors.len(),
727            3,
728            "expected one Error event per failed push, not a fatal short-circuit"
729        );
730        assert!(
731            errors.iter().all(|e| matches!(
732                e,
733                BusEvent::Error { name, .. } if &**name == "always-fail"
734            )),
735            "each Error event should be attributed to the branch that actually \
736             failed, not to Tee itself — that's what lets a caller call \
737             TeeHandle::detach(branch_id) straight off the bus; got {errors:?}"
738        );
739    }
740
741    #[test]
742    fn a_failing_control_branch_does_not_block_its_siblings() {
743        let (bus, bus_rx) = Bus::new();
744        let graph = PipelineGraph::new();
745        let source_id = graph.add_source(ElementType::Other, "source".into());
746        let context = Arc::new(Context::for_test(bus, "test", graph, source_id));
747        let failing_count = Arc::new(AtomicUsize::new(0));
748        let healthy_count = Arc::new(AtomicUsize::new(0));
749        let failing = context
750            .branch()
751            .to(Box::new(ControlObservingSink {
752                name: "control-fail",
753                count: failing_count.clone(),
754                fail: true,
755                pp_log: element_pp_log(ElementType::Other, "control-fail", None),
756            }))
757            .unwrap();
758        let healthy = context
759            .branch()
760            .to(Box::new(ControlObservingSink {
761                name: "control-ok",
762                count: healthy_count.clone(),
763                fail: false,
764                pp_log: element_pp_log(ElementType::Other, "control-ok", None),
765            }))
766            .unwrap();
767        let tee_branch = TeeBuilder::new("tee", context.clone())
768            .branch(failing)
769            .branch(healthy)
770            .build()
771            .unwrap();
772        let mut upstream = SrcPad::new("source_src");
773        context.attach_pad(&mut upstream, tee_branch).unwrap();
774
775        upstream
776            .control(ControlMsg::Pause)
777            .expect("a branch control failure should be reported, not short-circuit Tee");
778
779        assert_eq!(failing_count.load(Ordering::SeqCst), 1);
780        assert_eq!(healthy_count.load(Ordering::SeqCst), 1);
781        let message = bus_rx
782            .try_recv_message()
783            .expect("the failing control branch should post an Error event");
784        assert!(matches!(
785            message.event,
786            BusEvent::Error { name, .. } if &*name == "control-fail"
787        ));
788        assert!(bus_rx.try_recv_message().is_none());
789    }
790
791    #[test]
792    fn a_poisoned_branch_pad_can_be_used_after_the_panic_is_caught() {
793        let (bus, _bus_rx) = Bus::new();
794        let graph = PipelineGraph::new();
795        let source_id = graph.add_source(ElementType::Other, "source".into());
796        let context = Arc::new(Context::for_test(bus, "test", graph, source_id));
797        let successful = Arc::new(AtomicUsize::new(0));
798        let panic_once = context
799            .branch()
800            .to(Box::new(PanicOnceSink {
801                panicked: false,
802                successful: successful.clone(),
803                pp_log: element_pp_log(ElementType::Other, "panic-once", None),
804            }))
805            .unwrap();
806        let tee_branch = TeeBuilder::new("tee", context.clone())
807            .branch(panic_once)
808            .build()
809            .unwrap();
810        let mut upstream = SrcPad::new("source_src");
811        context.attach_pad(&mut upstream, tee_branch).unwrap();
812
813        let first = catch_unwind(AssertUnwindSafe(|| upstream.push(packet())));
814        assert!(
815            first.is_err(),
816            "the original downstream panic must propagate"
817        );
818        upstream
819            .push(packet())
820            .expect("the poisoned branch pad should be recovered on the next push");
821        assert_eq!(successful.load(Ordering::SeqCst), 1);
822    }
823
824    #[test]
825    fn a_poisoned_branch_list_does_not_break_attach_or_detach() {
826        let (bus, _bus_rx) = Bus::new();
827        let graph = PipelineGraph::new();
828        let source_id = graph.add_source(ElementType::Other, "source".into());
829        let context = Arc::new(Context::for_test(bus, "test", graph, source_id));
830        let (tee_branch, handle) = TeeBuilder::new("tee", context.clone())
831            .build_dynamic()
832            .unwrap();
833        let mut upstream = SrcPad::new("source_src");
834        context.attach_pad(&mut upstream, tee_branch).unwrap();
835        let shared = handle.shared.upgrade().unwrap();
836
837        let poisoned = catch_unwind(AssertUnwindSafe(|| {
838            let _branches = shared.branches.lock().unwrap();
839            panic!("poison the branch-list lock");
840        }));
841        assert!(poisoned.is_err());
842        assert_eq!(handle.sink_count(), 0);
843
844        let branch = handle
845            .branch()
846            .unwrap()
847            .to(Box::new(CountingSink {
848                name: "after-poison",
849                count: Arc::new(AtomicUsize::new(0)),
850                pp_log: element_pp_log(ElementType::Other, "after-poison", None),
851            }))
852            .unwrap();
853        let branch_id = handle.attach(branch).unwrap();
854        assert_eq!(handle.sink_count(), 1);
855        handle.detach(branch_id).unwrap();
856        assert_eq!(handle.sink_count(), 0);
857    }
858
859    #[test]
860    fn blocked_downstream_does_not_block_unrelated_attach_or_detach() {
861        let (bus, _bus_rx) = Bus::new();
862        let graph = PipelineGraph::new();
863        let source_id = graph.add_source(ElementType::Other, "source".into());
864        let context = Arc::new(Context::for_test(bus, "test", graph, source_id));
865        let (tee_branch, handle) = TeeBuilder::new("tee", context.clone())
866            .build_dynamic()
867            .unwrap();
868        let mut upstream = SrcPad::new("source_src");
869        context.attach_pad(&mut upstream, tee_branch).unwrap();
870
871        let (entered_tx, entered_rx) = mpsc::channel();
872        let (release_tx, release_rx) = mpsc::channel();
873        let blocking = handle
874            .branch()
875            .unwrap()
876            .to(Box::new(BlockingSink {
877                entered: Some(entered_tx),
878                release: release_rx,
879                pp_log: element_pp_log(ElementType::Other, "blocking", None),
880            }))
881            .unwrap();
882        let blocking_id = handle.attach(blocking).unwrap();
883
884        let push_thread = thread::spawn(move || upstream.push(packet()));
885        entered_rx
886            .recv_timeout(Duration::from_secs(1))
887            .expect("blocking branch was never entered");
888
889        let new_branch = handle
890            .branch()
891            .unwrap()
892            .to(Box::new(CountingSink {
893                name: "new",
894                count: Arc::new(AtomicUsize::new(0)),
895                pp_log: element_pp_log(ElementType::Other, "new", None),
896            }))
897            .unwrap();
898        let (attach_tx, attach_rx) = mpsc::channel();
899        let attach_handle = handle.clone();
900        let attach_thread = thread::spawn(move || {
901            let _ = attach_tx.send(attach_handle.attach(new_branch));
902        });
903
904        let (detach_tx, detach_rx) = mpsc::channel();
905        let detach_handle = handle.clone();
906        let detach_thread = thread::spawn(move || {
907            let _ = detach_tx.send(detach_handle.detach(blocking_id));
908        });
909
910        let attach_before_release = attach_rx.recv_timeout(Duration::from_millis(250)).ok();
911        let detach_before_release = detach_rx.recv_timeout(Duration::from_millis(250)).ok();
912        let attach_completed_while_blocked = attach_before_release.is_some();
913        let detach_completed_while_blocked = detach_before_release.is_some();
914        let _ = release_tx.send(());
915
916        push_thread.join().unwrap().unwrap();
917        attach_thread.join().unwrap();
918        detach_thread.join().unwrap();
919        let attach_result = attach_before_release
920            .unwrap_or_else(|| attach_rx.recv_timeout(Duration::from_secs(1)).unwrap());
921        let detach_result = detach_before_release
922            .unwrap_or_else(|| detach_rx.recv_timeout(Duration::from_secs(1)).unwrap());
923        attach_result.unwrap();
924        detach_result.unwrap();
925
926        assert!(
927            attach_completed_while_blocked,
928            "an unrelated attach waited for the blocked downstream"
929        );
930        assert!(
931            detach_completed_while_blocked,
932            "detach waited for an already-running downstream call"
933        );
934    }
935
936    #[test]
937    fn concurrent_push_attach_and_detach_stays_consistent_under_stress() {
938        const MIN_PUSHES: usize = 10_000;
939        const MUTATIONS: usize = 500;
940
941        let (bus, _bus_rx) = Bus::new();
942        let graph = PipelineGraph::new();
943        let source_id = graph.add_source(ElementType::Other, "source".into());
944        let context = Arc::new(Context::for_test(bus, "test", graph, source_id));
945        let initial_count = Arc::new(AtomicUsize::new(0));
946        let initial = context
947            .branch()
948            .to(Box::new(CountingSink {
949                name: "initial",
950                count: initial_count.clone(),
951                pp_log: element_pp_log(ElementType::Other, "initial", None),
952            }))
953            .unwrap();
954        let (tee_branch, handle) = TeeBuilder::new("tee", context.clone())
955            .branch(initial)
956            .build_dynamic()
957            .unwrap();
958        let mut upstream = SrcPad::new("source_src");
959        context.attach_pad(&mut upstream, tee_branch).unwrap();
960
961        let start = Arc::new(Barrier::new(2));
962        let mutating = Arc::new(AtomicBool::new(true));
963        let (push_done_tx, push_done_rx) = mpsc::channel();
964        let push_start = start.clone();
965        let push_mutating = mutating.clone();
966        let push_thread = thread::spawn(move || {
967            push_start.wait();
968            let packet = packet();
969            let mut pushed = 0;
970            let mut outcome = Ok(());
971            while push_mutating.load(Ordering::Acquire) || pushed < MIN_PUSHES {
972                if let Err(error) = upstream.push(packet.clone()) {
973                    outcome = Err(error.to_string());
974                    break;
975                }
976                pushed += 1;
977                if pushed % 32 == 0 {
978                    thread::yield_now();
979                }
980            }
981            let _ = push_done_tx.send((upstream, outcome, pushed));
982        });
983
984        let (mutation_done_tx, mutation_done_rx) = mpsc::channel();
985        let mutation_start = start;
986        let mutation_handle = handle.clone();
987        let mutation_thread = thread::spawn(move || {
988            mutation_start.wait();
989            let outcome = (|| -> std::result::Result<(), String> {
990                for _ in 0..MUTATIONS {
991                    let branch = mutation_handle
992                        .branch()
993                        .ok_or_else(|| "Tee disappeared during stress test".to_owned())?
994                        .to(Box::new(CountingSink {
995                            name: "dynamic",
996                            count: Arc::new(AtomicUsize::new(0)),
997                            pp_log: element_pp_log(ElementType::Other, "dynamic", None),
998                        }))
999                        .map_err(|error| error.to_string())?;
1000                    let branch_id = mutation_handle
1001                        .attach(branch)
1002                        .map_err(|error| error.to_string())?;
1003                    thread::yield_now();
1004                    mutation_handle
1005                        .detach(branch_id)
1006                        .map_err(|error| error.to_string())?;
1007                }
1008                Ok(())
1009            })();
1010            mutating.store(false, Ordering::Release);
1011            let _ = mutation_done_tx.send(outcome);
1012        });
1013
1014        mutation_done_rx
1015            .recv_timeout(Duration::from_secs(10))
1016            .expect("attach/detach stress thread timed out")
1017            .expect("attach/detach stress thread failed");
1018        let (upstream, push_outcome, pushed) = push_done_rx
1019            .recv_timeout(Duration::from_secs(10))
1020            .expect("push stress thread timed out");
1021        push_outcome.expect("push stress thread failed");
1022        push_thread.join().unwrap();
1023        mutation_thread.join().unwrap();
1024
1025        assert!(pushed >= MIN_PUSHES);
1026        assert_eq!(initial_count.load(Ordering::SeqCst), pushed);
1027        assert_eq!(handle.sink_count(), 1);
1028        let graph = context.graph.snapshot();
1029        assert_eq!(graph.nodes.len(), 3);
1030        assert_eq!(graph.edges.len(), 2);
1031        assert_eq!(graph.revision, 2 + (MUTATIONS as u64 * 2));
1032        drop(upstream);
1033    }
1034
1035    #[test]
1036    fn detached_sink_is_dropped_outside_the_graph_lock() {
1037        let (bus, _bus_rx) = Bus::new();
1038        let graph = PipelineGraph::new();
1039        let source_id = graph.add_source(ElementType::Other, "source".into());
1040        let context = Arc::new(Context::for_test(bus, "test", graph.clone(), source_id));
1041        let (tee_branch, handle) = TeeBuilder::new("tee", context.clone())
1042            .build_dynamic()
1043            .unwrap();
1044        let mut upstream = SrcPad::new("source_src");
1045        context.attach_pad(&mut upstream, tee_branch).unwrap();
1046
1047        let (dropped_tx, dropped_rx) = mpsc::channel();
1048        let branch = handle
1049            .branch()
1050            .unwrap()
1051            .to(Box::new(GraphInspectingDropSink {
1052                graph,
1053                dropped: Some(dropped_tx),
1054                pp_log: element_pp_log(ElementType::Other, "graph-inspecting-drop", None),
1055            }))
1056            .unwrap();
1057        let branch_id = handle.attach(branch).unwrap();
1058        let (done_tx, done_rx) = mpsc::channel();
1059        let detach_thread = thread::spawn(move || {
1060            let _ = done_tx.send(handle.detach(branch_id));
1061        });
1062
1063        dropped_rx
1064            .recv_timeout(Duration::from_secs(1))
1065            .expect("sink Drop deadlocked while inspecting the graph");
1066        done_rx
1067            .recv_timeout(Duration::from_secs(1))
1068            .expect("detach did not finish")
1069            .unwrap();
1070        detach_thread.join().unwrap();
1071        drop(upstream);
1072    }
1073
1074    #[test]
1075    fn retained_handle_does_not_keep_tee_context_or_bus_alive() {
1076        let (bus, bus_rx) = Bus::new();
1077        let graph = PipelineGraph::new();
1078        let source_id = graph.add_source(ElementType::Other, "source".into());
1079        let context = Arc::new(Context::for_test(bus, "test", graph, source_id));
1080        let (tee_branch, handle) = TeeBuilder::new("tee", context.clone())
1081            .build_dynamic()
1082            .unwrap();
1083
1084        drop(context);
1085        drop(tee_branch);
1086
1087        assert!(handle.branch().is_none());
1088        assert_eq!(handle.sink_count(), 0);
1089        assert!(
1090            bus_rx.iter().next().is_none(),
1091            "a retained TeeHandle must not keep the Bus sender alive"
1092        );
1093    }
1094}