Skip to main content

media_pp/elements/filter/
tee.rs

1use std::sync::{
2    Arc, Mutex, MutexGuard, Weak,
3    atomic::{AtomicBool, AtomicU64, Ordering},
4};
5use std::thread::{self, JoinHandle};
6
7use crate::pp_log::{PpLog, pp_error, pp_info};
8
9use crate::{
10    buffer::MediaBuffer,
11    bus::BusEvent,
12    contract::{InputContract, OutputContract},
13    control::ControlMsg,
14    element::{Context, Element, ElementType, Sink, element_pp_log},
15    error::Result,
16    graph::{BranchId, ElementId, GraphError, Incoming, PlannedEdge, PortRef, log_topology},
17    pad::SrcPad,
18    pipeline::{ChainBuilder, DetachedBranch},
19};
20
21/// Fans a single input out to multiple sinks. [`TeeBuilder`] owns the
22/// initial fan-out; later branches are added and removed through a
23/// [`TeeHandle`], which can be cloned and used from any thread, independent
24/// of whatever thread is driving `Tee::consume`
25/// (the pipeline's source/queue-worker thread). That's the whole reason
26/// `Tee` doesn't implement [`crate::element::Source`] like other
27/// multi-pad elements (e.g. [`crate::elements::FileDemuxer`]): its pads
28/// live in individually locked branch slots instead of being a plain
29/// `&mut [SrcPad]`. `consume` only holds the branch-list lock long enough
30/// to take a cheap `Arc` snapshot, so a slow downstream does not block
31/// unrelated attach/detach operations. Detach prevents any push that has
32/// not started yet; one already executing downstream call may finish.
33///
34/// Cheap to fan out: `MediaBuffer` wraps its payload in an `Arc`, so
35/// cloning a buffer for each output is a refcount bump, not a copy of the
36/// encoded/decoded data.
37pub struct Tee {
38    pp_log: PpLog,
39    id: ElementId,
40    name: Arc<str>,
41    shared: Arc<TeeShared>,
42    preroll: Option<Arc<crate::control::PrerollContext>>,
43}
44
45struct TeeShared {
46    branches: Mutex<Vec<Arc<TeeBranch>>>,
47    next_pad_id: AtomicU64,
48    context: Arc<Context>,
49    /// The identity the `Tee` element logs under, cloned once at
50    /// construction for the same reason [`TeeHandle`] clones it: a finisher
51    /// thread's outcome has to be reported under the `Tee`'s own identity,
52    /// including from [`Drop`], where the element itself is already gone.
53    pp_log: PpLog,
54    /// Threads started by [`TeeHandle::finish_branch`], each owning one
55    /// removed branch until its EOS has drained through it. Retained so the
56    /// `Tee` joins them rather than leaving a finalizing recording to race
57    /// process exit.
58    finishers: Mutex<Vec<JoinHandle<()>>>,
59}
60
61/// Joins the finisher threads in `finishers` that `select` picks, reporting
62/// any that panicked.
63///
64/// Every handle is joined rather than dropped, even one already finished:
65/// dropping it detaches the thread and discards its result, which is the one
66/// way a panicking branch teardown — a `Sink::drop` or a `Queue` worker that
67/// died mid-flush — would leave no trace at all. Joining a finished thread
68/// returns immediately, so the reaping caller pays nothing for it.
69fn join_finishers(
70    finishers: &mut Vec<JoinHandle<()>>,
71    pp_log: &PpLog,
72    select: impl Fn(&JoinHandle<()>) -> bool,
73) {
74    let mut index = 0;
75    while index < finishers.len() {
76        if !select(&finishers[index]) {
77            index += 1;
78            continue;
79        }
80        if finishers.remove(index).join().is_err() {
81            pp_error!(
82                pp_log: pp_log,
83                "a finished branch's teardown panicked; its output may be incomplete"
84            );
85        }
86    }
87}
88
89impl Drop for TeeShared {
90    fn drop(&mut self) {
91        // Not under the branch lock: a branch's own `Drop` may inspect the
92        // graph or call back into `Tee`, exactly as `detach` documents.
93        let mut finishers = std::mem::take(&mut *lock_unpoisoned(&self.finishers));
94        join_finishers(&mut finishers, &self.pp_log, |_| true);
95    }
96}
97
98struct TeeBranch {
99    id: Option<BranchId>,
100    root_id: ElementId,
101    active: AtomicBool,
102    pad: Mutex<SrcPad>,
103}
104
105/// Recovers the protected value after a panic instead of turning one
106/// poisoned Tee lock into a permanent source of follow-up panics. The
107/// original panic still unwinds normally; this only lets a caller that
108/// catches it keep using or detach the remaining branch state.
109fn lock_unpoisoned<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
110    match mutex.lock() {
111        Ok(guard) => guard,
112        Err(poisoned) => poisoned.into_inner(),
113    }
114}
115
116/// Build-time configuration for a [`Tee`]. Initial branches are merged
117/// with the Tee into one detached subgraph and committed by a single
118/// [`Context::attach`] call. Use [`TeeBuilder::build`] for a fixed fan-out,
119/// or [`TeeBuilder::build_dynamic`] when runtime changes need a
120/// [`TeeHandle`].
121pub struct TeeBuilder {
122    tee: Tee,
123    handle: TeeHandle,
124    initial_branches: Vec<DetachedBranch>,
125}
126
127/// A cheaply-cloneable handle for adding or removing a [`Tee`]'s sinks
128/// while the pipeline is running. It deliberately keeps only a [`Weak`]
129/// reference to the `Tee`'s shared state: retaining a handle after the
130/// pipeline finishes must not keep downstream sinks or the pipeline's
131/// [`crate::bus::Bus`] sender alive. Once the `Tee` is gone,
132/// [`TeeHandle::branch`] returns `None`.
133#[derive(Clone)]
134pub struct TeeHandle {
135    id: ElementId,
136    name: Arc<str>,
137    /// The same identity the `Tee` element logs under, cloned once at
138    /// construction — attach/detach must not rebuild it per call, and the
139    /// handle must not be able to disagree with the element about it.
140    pp_log: PpLog,
141    shared: Weak<TeeShared>,
142}
143
144impl Tee {
145    fn new(name: impl Into<String>, context: Arc<Context>) -> (Self, TeeHandle) {
146        let name: Arc<str> = name.into().into();
147        let pp_log = element_pp_log(ElementType::Tee, &name, Some(&context.pipeline_id));
148        pp_info!(pp_log: &pp_log, "created");
149        let id = context.graph.reserve_element_id();
150        let shared = Arc::new(TeeShared {
151            branches: Mutex::new(Vec::new()),
152            next_pad_id: AtomicU64::new(0),
153            context,
154            pp_log: pp_log.clone(),
155            finishers: Mutex::new(Vec::new()),
156        });
157        (
158            Self {
159                id,
160                name: name.clone(),
161                pp_log: pp_log.clone(),
162                shared: shared.clone(),
163                preroll: None,
164            },
165            TeeHandle {
166                id,
167                name,
168                pp_log,
169                shared: Arc::downgrade(&shared),
170            },
171        )
172    }
173
174    /// Posts a branch's `push` failure to the bus under *that branch's*
175    /// own identity (via [`SrcPad::peer_identity`]) — unlike `Queue`,
176    /// which only ever has one downstream and so can only attribute a
177    /// failure to itself, `Tee` fans out to several and does know which
178    /// one just failed. Reporting it that way (rather than folding every
179    /// branch's failures into one generic `Tee` event) is what lets a
180    /// caller watching the bus tell branches apart and identify the
181    /// corresponding runtime branch. The peer identity is captured just
182    /// before the downstream call, so the event stays attributable even
183    /// if that branch is detached while the call is running.
184    fn report_branch_error(
185        &self,
186        root_id: ElementId,
187        peer: Option<(ElementType, Arc<str>)>,
188        error: crate::error::Error,
189    ) {
190        let (element_type, name) = peer.unwrap_or((ElementType::Tee, self.name.clone()));
191        self.shared.context.bus.for_element(root_id).post(
192            &self.pp_log,
193            BusEvent::Error {
194                element_type,
195                name,
196                error,
197            },
198        );
199    }
200}
201
202impl TeeShared {
203    fn next_pad(&self, tee_name: &str) -> SrcPad {
204        let id = self.next_pad_id.fetch_add(1, Ordering::Relaxed);
205        // Every branch of a Tee sees the same buffers this Tee was given,
206        // so each pad carries the upstream contract through unchanged and
207        // a dynamically attached branch is checked against it too.
208        SrcPad::with_contract(format!("{tee_name}_src{id}"), OutputContract::Passthrough)
209    }
210}
211
212impl TeeBuilder {
213    /// Starts an initially empty Tee in the supplied pipeline context.
214    pub fn new(name: impl Into<String>, context: Arc<Context>) -> Self {
215        let (tee, handle) = Tee::new(name, context);
216        Self {
217            tee,
218            handle,
219            initial_branches: Vec::new(),
220        }
221    }
222
223    /// Adds one fixed initial branch to this fan-out subgraph.
224    pub fn branch(mut self, branch: DetachedBranch) -> Self {
225        self.initial_branches.push(branch);
226        self
227    }
228
229    /// Returns the complete fixed fan-out without exposing runtime control.
230    pub fn build(self) -> Result<DetachedBranch> {
231        self.finish().map(|(branch, _handle)| branch)
232    }
233
234    /// Returns the initial subgraph together with its runtime control handle.
235    /// Attach the branch through [`Context::attach`] before using the handle.
236    pub fn build_dynamic(self) -> Result<(DetachedBranch, TeeHandle)> {
237        self.finish()
238    }
239
240    fn finish(self) -> Result<(DetachedBranch, TeeHandle)> {
241        let Self {
242            tee,
243            handle,
244            initial_branches,
245        } = self;
246        let tee_id = tee.id;
247        let tee_name = tee.name.clone();
248        let shared = tee.shared.clone();
249        let context = shared.context.clone();
250        let mut tee_branch = context.branch().to(Box::new(tee))?;
251        // `ChainBuilder` ends a chain at a terminal `Sink`, which by
252        // definition emits nothing, so it recorded this `Tee` as producing
253        // `Unknown`. A `Tee` is the one terminal that does have outputs —
254        // its pads just live behind a lock instead of in `src_pads` — and
255        // every one of them forwards what it was given. Without this the
256        // flow stops at the `Tee` and no branch below it is ever checked.
257        if let Some(contracts) = tee_branch.plan.contracts.get_mut(&tee_id) {
258            contracts.output = OutputContract::Passthrough;
259        }
260        let mut runtime_branches = lock_unpoisoned(&shared.branches);
261
262        for branch in initial_branches {
263            let mut pad = shared.next_pad(&tee_name);
264            let from_port: Arc<str> = pad.name().into();
265            let DetachedBranch { root, plan } = branch;
266            let root_id = plan.root;
267
268            tee_branch.plan.edges.push(PlannedEdge {
269                from: PortRef {
270                    element: tee_id,
271                    port: from_port,
272                },
273                to: PortRef {
274                    element: root_id,
275                    port: "sink".into(),
276                },
277            });
278            tee_branch.plan.nodes.extend(plan.nodes);
279            tee_branch.plan.edges.extend(plan.edges);
280            // Merged as one plan, so the source attach that eventually
281            // commits this `Tee` validates every initial branch in the
282            // same walk — the fan-out edge above is what carries this
283            // `Tee`'s incoming contract into each of them.
284            tee_branch.plan.contracts.extend(plan.contracts);
285
286            pad.link(root);
287            runtime_branches.push(Arc::new(TeeBranch {
288                id: None,
289                root_id,
290                active: AtomicBool::new(true),
291                pad: Mutex::new(pad),
292            }));
293        }
294        drop(runtime_branches);
295
296        Ok((tee_branch, handle))
297    }
298}
299
300impl TeeHandle {
301    /// A [`crate::pipeline::ChainBuilder`] pre-wired with this `Tee`'s own
302    /// [`Context`] — lets a caller build a whole new branch (`.pipe(...)`
303    /// chains, ending in `.to(...)`) at any point after the pipeline
304    /// started running, then hand the result to [`TeeHandle::attach`],
305    /// without needing to retain the pipeline context separately
306    /// around separately. Returns `None` once the `Tee` has been dropped.
307    pub fn branch(&self) -> Option<ChainBuilder> {
308        let shared = self.shared.upgrade()?;
309        Some(shared.context.branch())
310    }
311
312    /// Attaches a runtime branch, returning the stable ID used to remove it.
313    /// Fixed initial branches belong in [`TeeBuilder`]. During a lifecycle or
314    /// seek operation this returns
315    /// [`GraphError::TimelineOperationInProgress`] immediately; build a fresh
316    /// detached branch and retry after the operation finishes.
317    pub fn attach(&self, branch: DetachedBranch) -> Result<BranchId> {
318        let shared = self
319            .shared
320            .upgrade()
321            .ok_or(GraphError::ParentNotAttached(self.id))?;
322        let _operation = match shared.context.operation.try_lock() {
323            Ok(guard) => guard,
324            Err(std::sync::TryLockError::Poisoned(poisoned)) => poisoned.into_inner(),
325            Err(std::sync::TryLockError::WouldBlock) => {
326                return Err(GraphError::TimelineOperationInProgress.into());
327            }
328        };
329        let mut branches = lock_unpoisoned(&shared.branches);
330        let mut pad = shared.next_pad(&self.name);
331        let from_port: Arc<str> = pad.name().into();
332        let DetachedBranch { root, plan } = branch;
333        let root_id = plan.root;
334        let branch_id = shared.context.graph.attach_with(
335            self.id,
336            from_port,
337            // What this `Tee` was itself resolved to receive: a
338            // branch added while the pipeline runs is checked
339            // against the same flow its siblings already carry.
340            Incoming::FromParent,
341            plan,
342            |branch_id| {
343                pad.link(root);
344                branches.push(Arc::new(TeeBranch {
345                    id: Some(branch_id),
346                    root_id,
347                    active: AtomicBool::new(true),
348                    pad: Mutex::new(pad),
349                }));
350                Ok(())
351            },
352        )?;
353        let snapshot =
354            crate::log::enabled(crate::log::Level::Info).then(|| shared.context.graph.snapshot());
355        drop(branches);
356        if let Some(snapshot) = snapshot {
357            log_topology(&self.pp_log, "attach", &snapshot);
358        }
359        Ok(branch_id)
360    }
361
362    /// Detaches exactly the branch returned by [`TeeHandle::attach`]. The
363    /// runtime peer and every graph node owned by it disappear in the same
364    /// transaction. Names are deliberately not used as graph keys.
365    ///
366    /// This abandons whatever the branch still held: no EOS is sent, so a
367    /// stateful codec does not flush and a muxer does not finalize. That is
368    /// what makes it the right call for a branch that is failing or wedged —
369    /// see [`TeeHandle::finish_branch`] for ending one that is working.
370    pub fn detach(&self, branch_id: BranchId) -> Result<()> {
371        let shared = self
372            .shared
373            .upgrade()
374            .ok_or(GraphError::BranchNotAttached(branch_id))?;
375        // The last Arc owns the downstream sink. Dropping it outside both
376        // the branch-list and graph locks allows arbitrary Sink::drop code
377        // to inspect the graph or call back into Tee without deadlocking.
378        let (removed, snapshot) = self.remove_branch(&shared, branch_id)?;
379        drop(removed);
380        if let Some(snapshot) = snapshot {
381            log_topology(&self.pp_log, "detach", &snapshot);
382        }
383        Ok(())
384    }
385
386    /// Ends one branch the way a recording ends: sends it an ordered `Eos`
387    /// behind everything already queued for it, so stateful codecs flush
388    /// their delayed output and a muxer writes its trailer, and detaches it.
389    ///
390    /// Returns as soon as the `Eos` is on its way. Draining it — which means
391    /// an encoder flush, a container's trailer, and joining the branch's own
392    /// `Queue` workers — happens on a thread this `Tee` owns and joins, so a
393    /// caller on a UI thread neither blocks nor has anything left to remember:
394    /// `branch_id` is already invalid when this returns, exactly as after
395    /// [`TeeHandle::detach`]. Watch the bus for the terminal's
396    /// [`BusEvent::Eos`] to learn when the branch's output is actually
397    /// complete — a file is only finished then, not when this call returns.
398    ///
399    /// Siblings are untouched: the `Eos` goes into this branch's pad alone,
400    /// unlike one arriving at the `Tee` itself, which every branch sees.
401    ///
402    /// The ordering guarantee is against what this `Tee` has already handed
403    /// the branch — everything queued for it arrives before the `Eos`. It is
404    /// not against buffers still upstream: anything the `Tee` has not consumed
405    /// yet when this is called belongs to the stream after the stop point and
406    /// never reaches this branch. There is no ordering between the two except
407    /// the one the caller creates by waiting for what it wants included.
408    pub fn finish_branch(&self, branch_id: BranchId) -> Result<()> {
409        let shared = self
410            .shared
411            .upgrade()
412            .ok_or(GraphError::BranchNotAttached(branch_id))?;
413
414        let branch = {
415            let branches = lock_unpoisoned(&shared.branches);
416            branches
417                .iter()
418                .find(|branch| branch.id == Some(branch_id))
419                .ok_or(GraphError::BranchNotAttached(branch_id))?
420                .clone()
421        };
422        // Deactivating under the pad lock is what puts the `Eos` last: a
423        // concurrent `consume` either already holds this lock and finishes
424        // its push first, or rechecks `active` after taking it and skips.
425        // The branch-list lock is released first, because an unqueued branch
426        // consumes the `Eos` synchronously right here.
427        let eos = {
428            let mut pad = lock_unpoisoned(&branch.pad);
429            branch.active.store(false, Ordering::Release);
430            pad.push_eos(&self.pp_log)
431        };
432        drop(branch);
433
434        // Detached even if the `Eos` failed: the branch is finished either
435        // way, and leaving a dead one attached is the outcome this call
436        // exists to make impossible. The error is still returned.
437        let (removed, snapshot) = self.remove_branch(&shared, branch_id)?;
438        if let Some(snapshot) = snapshot {
439            log_topology(&self.pp_log, "detach", &snapshot);
440        }
441
442        if let Some(removed) = removed {
443            let mut finishers = lock_unpoisoned(&shared.finishers);
444            join_finishers(&mut finishers, &self.pp_log, JoinHandle::is_finished);
445            match thread::Builder::new()
446                .name(format!("{}-finish", self.name))
447                .spawn(move || drop(removed))
448            {
449                Ok(finisher) => finishers.push(finisher),
450                // Nothing is leaked by falling back to this thread; the
451                // caller just waits for the drain it was meant to be spared.
452                Err(_) => drop(finishers),
453            }
454        }
455        eos
456    }
457
458    /// Takes the branch out of the graph and the branch list in one
459    /// transaction, handing its last `Arc` back rather than dropping it —
460    /// the caller decides which thread pays for that drop.
461    fn remove_branch(
462        &self,
463        shared: &Arc<TeeShared>,
464        branch_id: BranchId,
465    ) -> Result<(Option<Arc<TeeBranch>>, Option<crate::graph::GraphSnapshot>)> {
466        let mut branches = lock_unpoisoned(&shared.branches);
467        let index = branches
468            .iter()
469            .position(|branch| branch.id == Some(branch_id))
470            .ok_or(GraphError::BranchNotAttached(branch_id))?;
471        let mut removed = None;
472        shared.context.graph.detach_with(branch_id, || {
473            let branch = branches.remove(index);
474            branch.active.store(false, Ordering::Release);
475            removed = Some(branch);
476            Ok(())
477        })?;
478        let snapshot =
479            crate::log::enabled(crate::log::Level::Info).then(|| shared.context.graph.snapshot());
480        drop(branches);
481        Ok((removed, snapshot))
482    }
483
484    /// Resolves the owning branch from any element ID inside it and
485    /// detaches that branch. Useful when an error is attributed to a stage
486    /// behind a queue rather than to the branch root.
487    pub fn detach_branch_containing(&self, element: ElementId) -> Result<()> {
488        let shared = self
489            .shared
490            .upgrade()
491            .ok_or(GraphError::ParentNotAttached(self.id))?;
492        let branch_id = shared
493            .context
494            .graph
495            .branch_containing(element)
496            .ok_or(GraphError::ParentNotAttached(element))?;
497        self.detach(branch_id)
498    }
499
500    /// Returns the number of currently attached output branches.
501    ///
502    /// Returns zero after the tee element has been dropped.
503    pub fn sink_count(&self) -> usize {
504        self.shared
505            .upgrade()
506            .map(|shared| lock_unpoisoned(&shared.branches).len())
507            .unwrap_or(0)
508    }
509}
510
511impl Element for Tee {
512    fn name(&self) -> Arc<str> {
513        self.name.clone()
514    }
515
516    fn element_type(&self) -> ElementType {
517        ElementType::Tee
518    }
519
520    fn graph_id(&self) -> Option<ElementId> {
521        Some(self.id)
522    }
523
524    fn pp_log(&self) -> &PpLog {
525        &self.pp_log
526    }
527
528    fn pp_log_mut(&mut self) -> &mut PpLog {
529        &mut self.pp_log
530    }
531}
532
533impl Sink for Tee {
534    fn ready_consume(&mut self) -> bool {
535        let branches = lock_unpoisoned(&self.shared.branches).clone();
536        let graph = self
537            .preroll
538            .as_ref()
539            .map(|_| self.shared.context.graph.snapshot());
540        branches.into_iter().all(|branch| {
541            if !branch.active.load(Ordering::Acquire) {
542                return true;
543            }
544            if let (Some(context), Some(graph)) = (&self.preroll, &graph) {
545                let terminals = graph.terminal_ids_from(branch.root_id);
546                if context.are_ready(&terminals) {
547                    return true;
548                }
549            }
550            lock_unpoisoned(&branch.pad).ready_consume()
551        })
552    }
553
554    /// A Tee duplicates rather than transforms, so it accepts every kind
555    /// and hands each branch exactly what it received.
556    fn input_contract(&self) -> InputContract {
557        InputContract::Any
558    }
559
560    fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
561        let branches = lock_unpoisoned(&self.shared.branches).clone();
562        let graph = self
563            .preroll
564            .as_ref()
565            .map(|_| self.shared.context.graph.snapshot());
566        // One branch failing must not stop the buffer from reaching its
567        // siblings — same "errors never kill anything, just get reported"
568        // rule `Queue`'s worker loop follows. That buffer is dropped for
569        // the failing branch only; the branch itself stays wired and gets
570        // retried on the next one. Whoever's watching the bus decides
571        // whether to call `TeeHandle::detach` for it.
572        for branch in branches {
573            if !branch.active.load(Ordering::Acquire) {
574                continue;
575            }
576            if let (Some(context), Some(graph)) = (&self.preroll, &graph) {
577                let terminals = graph.terminal_ids_from(branch.root_id);
578                if context.are_ready(&terminals) {
579                    continue;
580                }
581            }
582            let mut pad = lock_unpoisoned(&branch.pad);
583            // Detach may have won the race while this thread waited for a
584            // previous push on the same branch to finish.
585            if !branch.active.load(Ordering::Acquire) {
586                continue;
587            }
588            let peer = pad.peer_identity();
589            let outcome = pad.push(buf.clone());
590            drop(pad);
591            if let Err(error) = outcome {
592                self.report_branch_error(branch.root_id, peer, error);
593            }
594        }
595        Ok(())
596    }
597
598    fn control(&mut self, msg: ControlMsg) -> Result<()> {
599        // Control failures follow the same isolation rule as data failures:
600        // report the failed branch, but still deliver the message to every
601        // sibling. This is especially important for Stop and Pause.
602        let branches = lock_unpoisoned(&self.shared.branches).clone();
603        for branch in branches {
604            if !branch.active.load(Ordering::Acquire) {
605                continue;
606            }
607            let mut pad = lock_unpoisoned(&branch.pad);
608            if !branch.active.load(Ordering::Acquire) {
609                continue;
610            }
611            let peer = pad.peer_identity();
612            let outcome = pad.control(msg.clone());
613            drop(pad);
614            if let Err(error) = outcome {
615                self.report_branch_error(branch.root_id, peer, error);
616            }
617        }
618        match &msg {
619            ControlMsg::Preroll(context) => self.preroll = Some(Arc::clone(context)),
620            ControlMsg::Pause | ControlMsg::Resume | ControlMsg::Stop => self.preroll = None,
621            ControlMsg::Flush | ControlMsg::CheckSeek(_) | ControlMsg::Seek(_) => {}
622        }
623        Ok(())
624    }
625}
626
627#[cfg(test)]
628mod tests {
629    use std::{
630        panic::{AssertUnwindSafe, catch_unwind},
631        sync::{
632            Barrier,
633            atomic::{AtomicBool, AtomicUsize, Ordering},
634            mpsc,
635        },
636        thread,
637        time::Duration,
638    };
639
640    use super::*;
641    use crate::{bus::Bus, graph::PipelineGraph};
642
643    fn packet() -> MediaBuffer {
644        MediaBuffer::Packet(Arc::new(ffmpeg_next::Packet::empty()))
645    }
646
647    struct CountingSink {
648        pp_log: PpLog,
649        name: &'static str,
650        count: Arc<AtomicUsize>,
651    }
652
653    impl Element for CountingSink {
654        fn name(&self) -> Arc<str> {
655            self.name.into()
656        }
657
658        fn element_type(&self) -> ElementType {
659            ElementType::Other
660        }
661
662        fn pp_log(&self) -> &PpLog {
663            &self.pp_log
664        }
665
666        fn pp_log_mut(&mut self) -> &mut PpLog {
667            &mut self.pp_log
668        }
669    }
670
671    impl Sink for CountingSink {
672        fn consume(&mut self, _buf: MediaBuffer) -> Result<()> {
673            self.count.fetch_add(1, Ordering::SeqCst);
674            Ok(())
675        }
676
677        fn control(&mut self, _msg: ControlMsg) -> Result<()> {
678            Ok(())
679        }
680    }
681
682    struct AlwaysFailSink {
683        pp_log: PpLog,
684    }
685
686    impl Element for AlwaysFailSink {
687        fn name(&self) -> Arc<str> {
688            "always-fail".into()
689        }
690
691        fn element_type(&self) -> ElementType {
692            ElementType::Other
693        }
694
695        fn pp_log(&self) -> &PpLog {
696            &self.pp_log
697        }
698
699        fn pp_log_mut(&mut self) -> &mut PpLog {
700            &mut self.pp_log
701        }
702    }
703
704    impl Sink for AlwaysFailSink {
705        fn consume(&mut self, _buf: MediaBuffer) -> Result<()> {
706            Err(crate::error::Error::Other(
707                "simulated branch failure".into(),
708            ))
709        }
710
711        fn control(&mut self, _msg: ControlMsg) -> Result<()> {
712            Ok(())
713        }
714    }
715
716    struct ControlObservingSink {
717        pp_log: PpLog,
718        name: &'static str,
719        count: Arc<AtomicUsize>,
720        fail: bool,
721    }
722
723    impl Element for ControlObservingSink {
724        fn name(&self) -> Arc<str> {
725            self.name.into()
726        }
727
728        fn element_type(&self) -> ElementType {
729            ElementType::Other
730        }
731
732        fn pp_log(&self) -> &PpLog {
733            &self.pp_log
734        }
735
736        fn pp_log_mut(&mut self) -> &mut PpLog {
737            &mut self.pp_log
738        }
739    }
740
741    impl Sink for ControlObservingSink {
742        fn consume(&mut self, _buf: MediaBuffer) -> Result<()> {
743            Ok(())
744        }
745
746        fn control(&mut self, _msg: ControlMsg) -> Result<()> {
747            self.count.fetch_add(1, Ordering::SeqCst);
748            if self.fail {
749                Err(crate::error::Error::Other(
750                    "simulated control failure".into(),
751                ))
752            } else {
753                Ok(())
754            }
755        }
756    }
757
758    struct PanicOnceSink {
759        pp_log: PpLog,
760        panicked: bool,
761        successful: Arc<AtomicUsize>,
762    }
763
764    impl Element for PanicOnceSink {
765        fn name(&self) -> Arc<str> {
766            "panic-once".into()
767        }
768
769        fn element_type(&self) -> ElementType {
770            ElementType::Other
771        }
772
773        fn pp_log(&self) -> &PpLog {
774            &self.pp_log
775        }
776
777        fn pp_log_mut(&mut self) -> &mut PpLog {
778            &mut self.pp_log
779        }
780    }
781
782    impl Sink for PanicOnceSink {
783        fn consume(&mut self, _buf: MediaBuffer) -> Result<()> {
784            if !self.panicked {
785                self.panicked = true;
786                panic!("simulated downstream panic");
787            }
788            self.successful.fetch_add(1, Ordering::SeqCst);
789            Ok(())
790        }
791
792        fn control(&mut self, _msg: ControlMsg) -> Result<()> {
793            Ok(())
794        }
795    }
796
797    struct BlockingSink {
798        pp_log: PpLog,
799        entered: Option<mpsc::Sender<()>>,
800        release: mpsc::Receiver<()>,
801    }
802
803    impl Element for BlockingSink {
804        fn name(&self) -> Arc<str> {
805            "blocking".into()
806        }
807
808        fn element_type(&self) -> ElementType {
809            ElementType::Other
810        }
811
812        fn pp_log(&self) -> &PpLog {
813            &self.pp_log
814        }
815
816        fn pp_log_mut(&mut self) -> &mut PpLog {
817            &mut self.pp_log
818        }
819    }
820
821    impl Sink for BlockingSink {
822        fn consume(&mut self, _buf: MediaBuffer) -> Result<()> {
823            if let Some(entered) = self.entered.take() {
824                let _ = entered.send(());
825            }
826            let _ = self.release.recv();
827            Ok(())
828        }
829
830        fn control(&mut self, _msg: ControlMsg) -> Result<()> {
831            Ok(())
832        }
833    }
834
835    struct GraphInspectingDropSink {
836        pp_log: PpLog,
837        graph: PipelineGraph,
838        dropped: Option<mpsc::Sender<()>>,
839    }
840
841    impl Element for GraphInspectingDropSink {
842        fn name(&self) -> Arc<str> {
843            "graph-inspecting-drop".into()
844        }
845
846        fn element_type(&self) -> ElementType {
847            ElementType::Other
848        }
849
850        fn pp_log(&self) -> &PpLog {
851            &self.pp_log
852        }
853
854        fn pp_log_mut(&mut self) -> &mut PpLog {
855            &mut self.pp_log
856        }
857    }
858
859    impl Sink for GraphInspectingDropSink {
860        fn consume(&mut self, _buf: MediaBuffer) -> Result<()> {
861            Ok(())
862        }
863
864        fn control(&mut self, _msg: ControlMsg) -> Result<()> {
865            Ok(())
866        }
867    }
868
869    impl Drop for GraphInspectingDropSink {
870        fn drop(&mut self) {
871            let _ = self.graph.snapshot();
872            if let Some(dropped) = self.dropped.take() {
873                let _ = dropped.send(());
874            }
875        }
876    }
877
878    /// A failing branch must not stop the same buffer from reaching its
879    /// siblings, nor stop `Tee::consume` itself from returning `Ok` — same
880    /// "errors get reported, nothing dies" rule `Queue`'s worker loop
881    /// follows (see `queue::tests::a_failing_consume_drops_that_buffer_but_keeps_the_worker_alive`).
882    /// Wires the failing branch in the *middle* (`before`/`after` on
883    /// either side of it) so the test also proves a mid-`rest` failure
884    /// doesn't short-circuit the fan-out to what comes after it,
885    /// including `last`.
886    #[test]
887    fn a_failing_branch_does_not_block_its_siblings() {
888        let (bus, bus_rx) = Bus::new();
889        let graph = PipelineGraph::new();
890        let source_id = graph.add_source(ElementType::Other, "source".into());
891        let context = Arc::new(Context::for_test(bus, "test", graph, source_id));
892        let before_count = Arc::new(AtomicUsize::new(0));
893        let after_count = Arc::new(AtomicUsize::new(0));
894        let before = context
895            .branch()
896            .to(Box::new(CountingSink {
897                name: "before",
898                count: before_count.clone(),
899                pp_log: element_pp_log(ElementType::Other, "before", None),
900            }))
901            .unwrap();
902        let failing = context
903            .branch()
904            .to(Box::new(AlwaysFailSink {
905                pp_log: element_pp_log(ElementType::Other, "always-fail", None),
906            }))
907            .unwrap();
908        let after = context
909            .branch()
910            .to(Box::new(CountingSink {
911                name: "after",
912                count: after_count.clone(),
913                pp_log: element_pp_log(ElementType::Other, "after", None),
914            }))
915            .unwrap();
916        let tee_branch = TeeBuilder::new("tee", context.clone())
917            .branch(before)
918            .branch(failing)
919            .branch(after)
920            .build()
921            .unwrap();
922        let mut upstream = SrcPad::new("source_src");
923        context.attach_pad(&mut upstream, tee_branch).unwrap();
924
925        for _ in 0..3 {
926            upstream
927                .push(packet())
928                .expect("a branch failing must not surface as an error from Tee::consume");
929        }
930
931        assert_eq!(before_count.load(Ordering::SeqCst), 3);
932        assert_eq!(after_count.load(Ordering::SeqCst), 3);
933
934        drop(upstream);
935        drop(context);
936        let errors: Vec<_> = bus_rx
937            .iter()
938            .filter(|e| matches!(e, BusEvent::Error { .. }))
939            .collect();
940        assert_eq!(
941            errors.len(),
942            3,
943            "expected one Error event per failed push, not a fatal short-circuit"
944        );
945        assert!(
946            errors.iter().all(|e| matches!(
947                e,
948                BusEvent::Error { name, .. } if &**name == "always-fail"
949            )),
950            "each Error event should be attributed to the branch that actually \
951             failed, not to Tee itself — that's what lets a caller call \
952             TeeHandle::detach(branch_id) straight off the bus; got {errors:?}"
953        );
954    }
955
956    #[test]
957    fn a_failing_control_branch_does_not_block_its_siblings() {
958        let (bus, bus_rx) = Bus::new();
959        let graph = PipelineGraph::new();
960        let source_id = graph.add_source(ElementType::Other, "source".into());
961        let context = Arc::new(Context::for_test(bus, "test", graph, source_id));
962        let failing_count = Arc::new(AtomicUsize::new(0));
963        let healthy_count = Arc::new(AtomicUsize::new(0));
964        let failing = context
965            .branch()
966            .to(Box::new(ControlObservingSink {
967                name: "control-fail",
968                count: failing_count.clone(),
969                fail: true,
970                pp_log: element_pp_log(ElementType::Other, "control-fail", None),
971            }))
972            .unwrap();
973        let healthy = context
974            .branch()
975            .to(Box::new(ControlObservingSink {
976                name: "control-ok",
977                count: healthy_count.clone(),
978                fail: false,
979                pp_log: element_pp_log(ElementType::Other, "control-ok", None),
980            }))
981            .unwrap();
982        let tee_branch = TeeBuilder::new("tee", context.clone())
983            .branch(failing)
984            .branch(healthy)
985            .build()
986            .unwrap();
987        let mut upstream = SrcPad::new("source_src");
988        context.attach_pad(&mut upstream, tee_branch).unwrap();
989
990        upstream
991            .control(ControlMsg::Pause)
992            .expect("a branch control failure should be reported, not short-circuit Tee");
993
994        assert_eq!(failing_count.load(Ordering::SeqCst), 1);
995        assert_eq!(healthy_count.load(Ordering::SeqCst), 1);
996        let message = bus_rx
997            .try_recv_message()
998            .expect("the failing control branch should post an Error event");
999        assert!(matches!(
1000            message.event,
1001            BusEvent::Error { name, .. } if &*name == "control-fail"
1002        ));
1003        assert!(bus_rx.try_recv_message().is_none());
1004    }
1005
1006    #[test]
1007    fn a_poisoned_branch_pad_can_be_used_after_the_panic_is_caught() {
1008        let (bus, _bus_rx) = Bus::new();
1009        let graph = PipelineGraph::new();
1010        let source_id = graph.add_source(ElementType::Other, "source".into());
1011        let context = Arc::new(Context::for_test(bus, "test", graph, source_id));
1012        let successful = Arc::new(AtomicUsize::new(0));
1013        let panic_once = context
1014            .branch()
1015            .to(Box::new(PanicOnceSink {
1016                panicked: false,
1017                successful: successful.clone(),
1018                pp_log: element_pp_log(ElementType::Other, "panic-once", None),
1019            }))
1020            .unwrap();
1021        let tee_branch = TeeBuilder::new("tee", context.clone())
1022            .branch(panic_once)
1023            .build()
1024            .unwrap();
1025        let mut upstream = SrcPad::new("source_src");
1026        context.attach_pad(&mut upstream, tee_branch).unwrap();
1027
1028        let first = catch_unwind(AssertUnwindSafe(|| upstream.push(packet())));
1029        assert!(
1030            first.is_err(),
1031            "the original downstream panic must propagate"
1032        );
1033        upstream
1034            .push(packet())
1035            .expect("the poisoned branch pad should be recovered on the next push");
1036        assert_eq!(successful.load(Ordering::SeqCst), 1);
1037    }
1038
1039    /// Records what a branch actually received, in order — enough to tell an
1040    /// EOS that arrived from one that never did, and to prove a sibling was
1041    /// not dragged along.
1042    struct RecordingSink {
1043        pp_log: PpLog,
1044        name: &'static str,
1045        seen: Arc<Mutex<Vec<&'static str>>>,
1046    }
1047
1048    impl Element for RecordingSink {
1049        fn name(&self) -> Arc<str> {
1050            self.name.into()
1051        }
1052
1053        fn element_type(&self) -> ElementType {
1054            ElementType::Other
1055        }
1056
1057        fn pp_log(&self) -> &PpLog {
1058            &self.pp_log
1059        }
1060
1061        fn pp_log_mut(&mut self) -> &mut PpLog {
1062            &mut self.pp_log
1063        }
1064    }
1065
1066    impl Sink for RecordingSink {
1067        fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
1068            lock_unpoisoned(&self.seen).push(if buf.is_eos() { "eos" } else { "data" });
1069            Ok(())
1070        }
1071
1072        fn control(&mut self, _msg: ControlMsg) -> Result<()> {
1073            Ok(())
1074        }
1075    }
1076
1077    /// The whole point of `finish_branch`: one branch is ended cleanly while
1078    /// its siblings keep running, and the caller is left with nothing to
1079    /// remember — the branch is gone when the call returns.
1080    #[test]
1081    fn finish_branch_sends_eos_to_that_branch_alone_and_detaches_it() {
1082        let (bus, _bus_rx) = Bus::new();
1083        let graph = PipelineGraph::new();
1084        let source_id = graph.add_source(ElementType::Other, "source".into());
1085        let context = Arc::new(Context::for_test(bus, "test", graph, source_id));
1086
1087        let kept = Arc::new(Mutex::new(Vec::new()));
1088        let keep_branch = context
1089            .branch()
1090            .to(Box::new(RecordingSink {
1091                name: "preview",
1092                seen: kept.clone(),
1093                pp_log: element_pp_log(ElementType::Other, "preview", None),
1094            }))
1095            .unwrap();
1096        let (tee_branch, handle) = TeeBuilder::new("tee", context.clone())
1097            .branch(keep_branch)
1098            .build_dynamic()
1099            .unwrap();
1100        let mut upstream = SrcPad::new("source_src");
1101        context.attach_pad(&mut upstream, tee_branch).unwrap();
1102
1103        let recorded = Arc::new(Mutex::new(Vec::new()));
1104        let recording = handle
1105            .branch()
1106            .unwrap()
1107            .to(Box::new(RecordingSink {
1108                name: "recording",
1109                seen: recorded.clone(),
1110                pp_log: element_pp_log(ElementType::Other, "recording", None),
1111            }))
1112            .unwrap();
1113        let branch_id = handle.attach(recording).unwrap();
1114
1115        upstream.push(packet()).unwrap();
1116        assert_eq!(*lock_unpoisoned(&recorded), ["data"]);
1117
1118        handle.finish_branch(branch_id).unwrap();
1119        assert_eq!(
1120            *lock_unpoisoned(&recorded),
1121            ["data", "eos"],
1122            "the finished branch must see EOS behind its data"
1123        );
1124        assert_eq!(handle.sink_count(), 1, "finish_branch also detaches");
1125        assert!(
1126            matches!(
1127                handle.detach(branch_id),
1128                Err(crate::Error::GraphError(GraphError::BranchNotAttached(_)))
1129            ),
1130            "the branch id is spent once finished"
1131        );
1132
1133        // The sibling saw the data and nothing else, before or after.
1134        upstream.push(packet()).unwrap();
1135        assert_eq!(*lock_unpoisoned(&kept), ["data", "data"]);
1136        assert_eq!(
1137            *lock_unpoisoned(&recorded),
1138            ["data", "eos"],
1139            "a detached branch must not receive anything more"
1140        );
1141    }
1142
1143    /// The synchronous test above proves the EOS goes to one branch only.
1144    /// This one proves the part that is actually hard: with a `Queue` between
1145    /// the `Tee` and the sink, `finish_branch` is called while the worker is
1146    /// still chewing on the first buffer, and the EOS still has to land
1147    /// *behind* everything already handed to that branch rather than racing
1148    /// past it or cutting it short.
1149    #[test]
1150    fn finish_branch_lands_behind_buffers_still_queued_for_that_branch() {
1151        let (bus, _bus_rx) = Bus::new();
1152        let graph = PipelineGraph::new();
1153        let source_id = graph.add_source(ElementType::Other, "source".into());
1154        let context = Arc::new(Context::for_test(bus, "test", graph, source_id));
1155        let (tee_branch, handle) = TeeBuilder::new("tee", context.clone())
1156            .build_dynamic()
1157            .unwrap();
1158        let mut upstream = SrcPad::new("source_src");
1159        context.attach_pad(&mut upstream, tee_branch).unwrap();
1160
1161        let seen = Arc::new(Mutex::new(Vec::new()));
1162        let recording = handle
1163            .branch()
1164            .unwrap()
1165            .queue("recording", 8)
1166            .to(Box::new(SlowRecordingSink {
1167                name: "recorder",
1168                seen: seen.clone(),
1169                pp_log: element_pp_log(ElementType::Other, "recorder", None),
1170            }))
1171            .unwrap();
1172        let branch_id = handle.attach(recording).unwrap();
1173
1174        // These return once the Queue has accepted them, long before its
1175        // worker has consumed them: the sink deliberately takes its time.
1176        for _ in 0..3 {
1177            upstream.push(packet()).unwrap();
1178        }
1179        handle.finish_branch(branch_id).unwrap();
1180
1181        let deadline = std::time::Instant::now() + Duration::from_secs(10);
1182        while std::time::Instant::now() < deadline && lock_unpoisoned(&seen).last() != Some(&"eos")
1183        {
1184            thread::sleep(Duration::from_millis(10));
1185        }
1186        assert_eq!(
1187            *lock_unpoisoned(&seen),
1188            ["data", "data", "data", "eos"],
1189            "every buffer already queued for the branch must be delivered, and the EOS last"
1190        );
1191    }
1192
1193    /// A finisher thread runs arbitrary `Sink::drop` code. One that panics
1194    /// must be joined and reported rather than detached and forgotten, and it
1195    /// must not take the `Tee` — or any other branch — with it.
1196    #[test]
1197    fn a_panicking_branch_teardown_is_joined_rather_than_lost() {
1198        let (bus, _bus_rx) = Bus::new();
1199        let graph = PipelineGraph::new();
1200        let source_id = graph.add_source(ElementType::Other, "source".into());
1201        let context = Arc::new(Context::for_test(bus, "test", graph, source_id));
1202
1203        let survivor_count = Arc::new(AtomicUsize::new(0));
1204        let survivor = context
1205            .branch()
1206            .to(Box::new(CountingSink {
1207                name: "survivor",
1208                count: survivor_count.clone(),
1209                pp_log: element_pp_log(ElementType::Other, "survivor", None),
1210            }))
1211            .unwrap();
1212        let (tee_branch, handle) = TeeBuilder::new("tee", context.clone())
1213            .branch(survivor)
1214            .build_dynamic()
1215            .unwrap();
1216        let mut upstream = SrcPad::new("source_src");
1217        context.attach_pad(&mut upstream, tee_branch).unwrap();
1218
1219        let branch = handle
1220            .branch()
1221            .unwrap()
1222            .to(Box::new(PanicOnDropSink {
1223                pp_log: element_pp_log(ElementType::Other, "panics-on-drop", None),
1224            }))
1225            .unwrap();
1226        let branch_id = handle.attach(branch).unwrap();
1227        handle.finish_branch(branch_id).unwrap();
1228
1229        // The next `finish_branch` reaps it; there is nothing else to finish,
1230        // so drop the Tee and let its own teardown do the joining.
1231        upstream.push(packet()).unwrap();
1232        assert_eq!(survivor_count.load(Ordering::SeqCst), 1);
1233        drop(upstream);
1234        drop(handle);
1235        drop(context);
1236        // Reaching here at all is the assertion: a detached panicking thread
1237        // would otherwise be free to outlive everything it borrowed.
1238    }
1239
1240    struct PanicOnDropSink {
1241        pp_log: PpLog,
1242    }
1243
1244    impl Element for PanicOnDropSink {
1245        fn name(&self) -> Arc<str> {
1246            "panics-on-drop".into()
1247        }
1248
1249        fn element_type(&self) -> ElementType {
1250            ElementType::Other
1251        }
1252
1253        fn pp_log(&self) -> &PpLog {
1254            &self.pp_log
1255        }
1256
1257        fn pp_log_mut(&mut self) -> &mut PpLog {
1258            &mut self.pp_log
1259        }
1260    }
1261
1262    impl Sink for PanicOnDropSink {
1263        fn consume(&mut self, _buf: MediaBuffer) -> Result<()> {
1264            Ok(())
1265        }
1266
1267        fn control(&mut self, _msg: ControlMsg) -> Result<()> {
1268            Ok(())
1269        }
1270    }
1271
1272    impl Drop for PanicOnDropSink {
1273        fn drop(&mut self) {
1274            panic!("teardown of a finished branch panics");
1275        }
1276    }
1277
1278    /// Consumes slowly enough that `finish_branch` is guaranteed to run while
1279    /// buffers are still sitting in the branch's `Queue`.
1280    struct SlowRecordingSink {
1281        pp_log: PpLog,
1282        name: &'static str,
1283        seen: Arc<Mutex<Vec<&'static str>>>,
1284    }
1285
1286    impl Element for SlowRecordingSink {
1287        fn name(&self) -> Arc<str> {
1288            self.name.into()
1289        }
1290
1291        fn element_type(&self) -> ElementType {
1292            ElementType::Other
1293        }
1294
1295        fn pp_log(&self) -> &PpLog {
1296            &self.pp_log
1297        }
1298
1299        fn pp_log_mut(&mut self) -> &mut PpLog {
1300            &mut self.pp_log
1301        }
1302    }
1303
1304    impl Sink for SlowRecordingSink {
1305        fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
1306            let is_eos = buf.is_eos();
1307            if !is_eos {
1308                thread::sleep(Duration::from_millis(30));
1309            }
1310            lock_unpoisoned(&self.seen).push(if is_eos { "eos" } else { "data" });
1311            Ok(())
1312        }
1313
1314        fn control(&mut self, _msg: ControlMsg) -> Result<()> {
1315            Ok(())
1316        }
1317    }
1318
1319    #[test]
1320    fn a_poisoned_branch_list_does_not_break_attach_or_detach() {
1321        let (bus, _bus_rx) = Bus::new();
1322        let graph = PipelineGraph::new();
1323        let source_id = graph.add_source(ElementType::Other, "source".into());
1324        let context = Arc::new(Context::for_test(bus, "test", graph, source_id));
1325        let (tee_branch, handle) = TeeBuilder::new("tee", context.clone())
1326            .build_dynamic()
1327            .unwrap();
1328        let mut upstream = SrcPad::new("source_src");
1329        context.attach_pad(&mut upstream, tee_branch).unwrap();
1330        let shared = handle.shared.upgrade().unwrap();
1331
1332        let poisoned = catch_unwind(AssertUnwindSafe(|| {
1333            let _branches = shared.branches.lock().unwrap();
1334            panic!("poison the branch-list lock");
1335        }));
1336        assert!(poisoned.is_err());
1337        assert_eq!(handle.sink_count(), 0);
1338
1339        let branch = handle
1340            .branch()
1341            .unwrap()
1342            .to(Box::new(CountingSink {
1343                name: "after-poison",
1344                count: Arc::new(AtomicUsize::new(0)),
1345                pp_log: element_pp_log(ElementType::Other, "after-poison", None),
1346            }))
1347            .unwrap();
1348        let branch_id = handle.attach(branch).unwrap();
1349        assert_eq!(handle.sink_count(), 1);
1350        handle.detach(branch_id).unwrap();
1351        assert_eq!(handle.sink_count(), 0);
1352    }
1353
1354    #[test]
1355    fn blocked_downstream_does_not_block_unrelated_attach_or_detach() {
1356        let (bus, _bus_rx) = Bus::new();
1357        let graph = PipelineGraph::new();
1358        let source_id = graph.add_source(ElementType::Other, "source".into());
1359        let context = Arc::new(Context::for_test(bus, "test", graph, source_id));
1360        let (tee_branch, handle) = TeeBuilder::new("tee", context.clone())
1361            .build_dynamic()
1362            .unwrap();
1363        let mut upstream = SrcPad::new("source_src");
1364        context.attach_pad(&mut upstream, tee_branch).unwrap();
1365
1366        let (entered_tx, entered_rx) = mpsc::channel();
1367        let (release_tx, release_rx) = mpsc::channel();
1368        let blocking = handle
1369            .branch()
1370            .unwrap()
1371            .to(Box::new(BlockingSink {
1372                entered: Some(entered_tx),
1373                release: release_rx,
1374                pp_log: element_pp_log(ElementType::Other, "blocking", None),
1375            }))
1376            .unwrap();
1377        let blocking_id = handle.attach(blocking).unwrap();
1378
1379        let push_thread = thread::spawn(move || upstream.push(packet()));
1380        entered_rx
1381            .recv_timeout(Duration::from_secs(1))
1382            .expect("blocking branch was never entered");
1383
1384        let new_branch = handle
1385            .branch()
1386            .unwrap()
1387            .to(Box::new(CountingSink {
1388                name: "new",
1389                count: Arc::new(AtomicUsize::new(0)),
1390                pp_log: element_pp_log(ElementType::Other, "new", None),
1391            }))
1392            .unwrap();
1393        let (attach_tx, attach_rx) = mpsc::channel();
1394        let attach_handle = handle.clone();
1395        let attach_thread = thread::spawn(move || {
1396            let _ = attach_tx.send(attach_handle.attach(new_branch));
1397        });
1398
1399        let (detach_tx, detach_rx) = mpsc::channel();
1400        let detach_handle = handle.clone();
1401        let detach_thread = thread::spawn(move || {
1402            let _ = detach_tx.send(detach_handle.detach(blocking_id));
1403        });
1404
1405        let attach_before_release = attach_rx.recv_timeout(Duration::from_millis(250)).ok();
1406        let detach_before_release = detach_rx.recv_timeout(Duration::from_millis(250)).ok();
1407        let attach_completed_while_blocked = attach_before_release.is_some();
1408        let detach_completed_while_blocked = detach_before_release.is_some();
1409        let _ = release_tx.send(());
1410
1411        push_thread.join().unwrap().unwrap();
1412        attach_thread.join().unwrap();
1413        detach_thread.join().unwrap();
1414        let attach_result = attach_before_release
1415            .unwrap_or_else(|| attach_rx.recv_timeout(Duration::from_secs(1)).unwrap());
1416        let detach_result = detach_before_release
1417            .unwrap_or_else(|| detach_rx.recv_timeout(Duration::from_secs(1)).unwrap());
1418        attach_result.unwrap();
1419        detach_result.unwrap();
1420
1421        assert!(
1422            attach_completed_while_blocked,
1423            "an unrelated attach waited for the blocked downstream"
1424        );
1425        assert!(
1426            detach_completed_while_blocked,
1427            "detach waited for an already-running downstream call"
1428        );
1429    }
1430
1431    #[test]
1432    fn concurrent_push_attach_and_detach_stays_consistent_under_stress() {
1433        const MIN_PUSHES: usize = 10_000;
1434        const MUTATIONS: usize = 500;
1435
1436        let (bus, _bus_rx) = Bus::new();
1437        let graph = PipelineGraph::new();
1438        let source_id = graph.add_source(ElementType::Other, "source".into());
1439        let context = Arc::new(Context::for_test(bus, "test", graph, source_id));
1440        let initial_count = Arc::new(AtomicUsize::new(0));
1441        let initial = context
1442            .branch()
1443            .to(Box::new(CountingSink {
1444                name: "initial",
1445                count: initial_count.clone(),
1446                pp_log: element_pp_log(ElementType::Other, "initial", None),
1447            }))
1448            .unwrap();
1449        let (tee_branch, handle) = TeeBuilder::new("tee", context.clone())
1450            .branch(initial)
1451            .build_dynamic()
1452            .unwrap();
1453        let mut upstream = SrcPad::new("source_src");
1454        context.attach_pad(&mut upstream, tee_branch).unwrap();
1455
1456        let start = Arc::new(Barrier::new(2));
1457        let mutating = Arc::new(AtomicBool::new(true));
1458        let (push_done_tx, push_done_rx) = mpsc::channel();
1459        let push_start = start.clone();
1460        let push_mutating = mutating.clone();
1461        let push_thread = thread::spawn(move || {
1462            push_start.wait();
1463            let packet = packet();
1464            let mut pushed = 0;
1465            let mut outcome = Ok(());
1466            while push_mutating.load(Ordering::Acquire) || pushed < MIN_PUSHES {
1467                if let Err(error) = upstream.push(packet.clone()) {
1468                    outcome = Err(error.to_string());
1469                    break;
1470                }
1471                pushed += 1;
1472                if pushed % 32 == 0 {
1473                    thread::yield_now();
1474                }
1475            }
1476            let _ = push_done_tx.send((upstream, outcome, pushed));
1477        });
1478
1479        let (mutation_done_tx, mutation_done_rx) = mpsc::channel();
1480        let mutation_start = start;
1481        let mutation_handle = handle.clone();
1482        let mutation_thread = thread::spawn(move || {
1483            mutation_start.wait();
1484            let outcome = (|| -> std::result::Result<(), String> {
1485                for _ in 0..MUTATIONS {
1486                    let branch = mutation_handle
1487                        .branch()
1488                        .ok_or_else(|| "Tee disappeared during stress test".to_owned())?
1489                        .to(Box::new(CountingSink {
1490                            name: "dynamic",
1491                            count: Arc::new(AtomicUsize::new(0)),
1492                            pp_log: element_pp_log(ElementType::Other, "dynamic", None),
1493                        }))
1494                        .map_err(|error| error.to_string())?;
1495                    let branch_id = mutation_handle
1496                        .attach(branch)
1497                        .map_err(|error| error.to_string())?;
1498                    thread::yield_now();
1499                    mutation_handle
1500                        .detach(branch_id)
1501                        .map_err(|error| error.to_string())?;
1502                }
1503                Ok(())
1504            })();
1505            mutating.store(false, Ordering::Release);
1506            let _ = mutation_done_tx.send(outcome);
1507        });
1508
1509        mutation_done_rx
1510            .recv_timeout(Duration::from_secs(10))
1511            .expect("attach/detach stress thread timed out")
1512            .expect("attach/detach stress thread failed");
1513        let (upstream, push_outcome, pushed) = push_done_rx
1514            .recv_timeout(Duration::from_secs(10))
1515            .expect("push stress thread timed out");
1516        push_outcome.expect("push stress thread failed");
1517        push_thread.join().unwrap();
1518        mutation_thread.join().unwrap();
1519
1520        assert!(pushed >= MIN_PUSHES);
1521        assert_eq!(initial_count.load(Ordering::SeqCst), pushed);
1522        assert_eq!(handle.sink_count(), 1);
1523        let graph = context.graph.snapshot();
1524        assert_eq!(graph.nodes.len(), 3);
1525        assert_eq!(graph.edges.len(), 2);
1526        assert_eq!(graph.revision, 2 + (MUTATIONS as u64 * 2));
1527        drop(upstream);
1528    }
1529
1530    #[test]
1531    fn detached_sink_is_dropped_outside_the_graph_lock() {
1532        let (bus, _bus_rx) = Bus::new();
1533        let graph = PipelineGraph::new();
1534        let source_id = graph.add_source(ElementType::Other, "source".into());
1535        let context = Arc::new(Context::for_test(bus, "test", graph.clone(), source_id));
1536        let (tee_branch, handle) = TeeBuilder::new("tee", context.clone())
1537            .build_dynamic()
1538            .unwrap();
1539        let mut upstream = SrcPad::new("source_src");
1540        context.attach_pad(&mut upstream, tee_branch).unwrap();
1541
1542        let (dropped_tx, dropped_rx) = mpsc::channel();
1543        let branch = handle
1544            .branch()
1545            .unwrap()
1546            .to(Box::new(GraphInspectingDropSink {
1547                graph,
1548                dropped: Some(dropped_tx),
1549                pp_log: element_pp_log(ElementType::Other, "graph-inspecting-drop", None),
1550            }))
1551            .unwrap();
1552        let branch_id = handle.attach(branch).unwrap();
1553        let (done_tx, done_rx) = mpsc::channel();
1554        let detach_thread = thread::spawn(move || {
1555            let _ = done_tx.send(handle.detach(branch_id));
1556        });
1557
1558        dropped_rx
1559            .recv_timeout(Duration::from_secs(1))
1560            .expect("sink Drop deadlocked while inspecting the graph");
1561        done_rx
1562            .recv_timeout(Duration::from_secs(1))
1563            .expect("detach did not finish")
1564            .unwrap();
1565        detach_thread.join().unwrap();
1566        drop(upstream);
1567    }
1568
1569    #[test]
1570    fn retained_handle_does_not_keep_tee_context_or_bus_alive() {
1571        let (bus, bus_rx) = Bus::new();
1572        let graph = PipelineGraph::new();
1573        let source_id = graph.add_source(ElementType::Other, "source".into());
1574        let context = Arc::new(Context::for_test(bus, "test", graph, source_id));
1575        let (tee_branch, handle) = TeeBuilder::new("tee", context.clone())
1576            .build_dynamic()
1577            .unwrap();
1578
1579        drop(context);
1580        drop(tee_branch);
1581
1582        assert!(handle.branch().is_none());
1583        assert_eq!(handle.sink_count(), 0);
1584        assert!(
1585            bus_rx.iter().next().is_none(),
1586            "a retained TeeHandle must not keep the Bus sender alive"
1587        );
1588    }
1589}