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
21pub 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 pp_log: PpLog,
54 finishers: Mutex<Vec<JoinHandle<()>>>,
59}
60
61fn 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 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
105fn 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
116pub struct TeeBuilder {
122 tee: Tee,
123 handle: TeeHandle,
124 initial_branches: Vec<DetachedBranch>,
125}
126
127#[derive(Clone)]
134pub struct TeeHandle {
135 id: ElementId,
136 name: Arc<str>,
137 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 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 SrcPad::with_contract(format!("{tee_name}_src{id}"), OutputContract::Passthrough)
209 }
210}
211
212impl TeeBuilder {
213 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 pub fn branch(mut self, branch: DetachedBranch) -> Self {
225 self.initial_branches.push(branch);
226 self
227 }
228
229 pub fn build(self) -> Result<DetachedBranch> {
231 self.finish().map(|(branch, _handle)| branch)
232 }
233
234 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 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 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 pub fn branch(&self) -> Option<ChainBuilder> {
308 let shared = self.shared.upgrade()?;
309 Some(shared.context.branch())
310 }
311
312 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 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 pub fn detach(&self, branch_id: BranchId) -> Result<()> {
371 let shared = self
372 .shared
373 .upgrade()
374 .ok_or(GraphError::BranchNotAttached(branch_id))?;
375 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 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 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 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 Err(_) => drop(finishers),
453 }
454 }
455 eos
456 }
457
458 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 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 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 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 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 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 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 #[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 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 #[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 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 #[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 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 #[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 upstream.push(packet()).unwrap();
1232 assert_eq!(survivor_count.load(Ordering::SeqCst), 1);
1233 drop(upstream);
1234 drop(handle);
1235 drop(context);
1236 }
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 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}