Skip to main content

media_pp/core/
queue.rs

1use std::{
2    sync::{
3        Arc,
4        atomic::{AtomicBool, Ordering},
5    },
6    thread::{self, JoinHandle},
7    time::Duration,
8};
9
10use crate::pp_log::{PpLog, pp_info, pp_trace};
11use crossbeam_channel::{
12    Receiver, RecvTimeoutError, SendTimeoutError, Sender, TrySendError, bounded, select,
13};
14use thiserror::Error as ThisError;
15
16use crate::{
17    buffer::MediaBuffer,
18    bus::{Bus, BusEvent},
19    control::{self, ControlMsg, ControlReceiver, ControlSender},
20    element::{Element, ElementType, Sink, element_pp_log},
21    error::Result,
22};
23
24/// Errors specific to `Queue`. Converts into the crate-wide `Error` via
25/// `?` (see [`crate::error::Error`]).
26#[derive(Debug, ThisError)]
27pub enum QueueError {
28    #[error("downstream channel closed")]
29    ChannelClosed,
30
31    /// [`OverflowPolicy::Block`] only — the channel stayed full for the
32    /// whole `after`, meaning whatever's downstream of this `Queue`
33    /// didn't just fall behind (ordinary, self-resolving backpressure),
34    /// it's genuinely stuck. Unlike [`OverflowPolicy::DropNewest`]'s
35    /// silent, expected-under-load `BusEvent::Dropped`, this is
36    /// surfaced as a real error precisely because it isn't expected —
37    /// see [`OverflowPolicy::Block`]'s own docs.
38    #[error("downstream didn't accept a buffer within {after:?} — send timed out")]
39    SendTimedOut { after: Duration },
40}
41
42/// How often the worker's blocking wait wakes up on its own (nothing
43/// ready on either channel) to check [`Queue`]'s `stop` flag — see
44/// [`worker_loop`] and [`apply_control`]'s pause loop. Only ever adds
45/// latency to the already-abnormal "torn down without ever being told to
46/// stop" path (see [`Queue::drop`]); real data/control traffic is always
47/// picked up immediately; this pause is only ever *waited out*, not
48/// polled on a timer.
49const STOP_POLL_INTERVAL: Duration = Duration::from_millis(20);
50
51/// What a `Queue` does when its channel is full.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum OverflowPolicy {
54    /// Block the pushing thread until there's room, up to `Duration` —
55    /// the right choice for offline/file processing, where correctness
56    /// matters more than staying caught up. Use [`Duration::MAX`] (what
57    /// [`OverflowPolicy::default`] does) for what's practically an
58    /// unbounded wait — [`Sender::send_timeout`] with that duration
59    /// isn't ever going to time out in a real program.
60    ///
61    /// A *finite* `Duration` is the escape hatch against the one thing
62    /// an actually-unbounded wait can't recover from: whatever's
63    /// downstream not just falling behind (ordinary backpressure, which
64    /// resolves on its own as the worker keeps draining) but genuinely
65    /// stuck — a `Sink::consume` call somewhere in the chain that never
66    /// returns. An unbounded wait here would then also wedge whoever's
67    /// pushing into this `Queue`, and transitively every `Queue`
68    /// upstream of *that*, since each one's worker can't get back to its
69    /// own `control_rx` until its current `downstream.consume()` call
70    /// returns (see [`Queue::control`]'s own docs on why control is only
71    /// ever checked *between* buffers, not able to preempt one already
72    /// in flight). Timing out bounds that: it's what lets a `Stop` sent
73    /// to an upstream `Queue` eventually reach it instead of waiting
74    /// forever. Doesn't help if the stall is inside a raw (non-`Queue`)
75    /// `Sink`'s own `consume()` call directly — nothing here retries or
76    /// times out *that* call itself, only the channel send. On timeout,
77    /// returns [`QueueError::SendTimedOut`] rather than losing the
78    /// buffer silently — unlike [`OverflowPolicy::DropNewest`], this
79    /// isn't an expected, routine condition.
80    ///
81    /// This timeout applies to ordinary data buffers only. `Queue` sends
82    /// `MediaBuffer::Eos` with an unbounded `send` under every policy so a
83    /// natural end-of-stream marker is never discarded; if downstream has
84    /// stopped consuming entirely, an EOS push can therefore still block.
85    Block(Duration),
86    /// Drop the incoming buffer instead of blocking, and post
87    /// [`BusEvent::Dropped`]. Never stalls the upstream thread — the
88    /// right choice for live sources, where falling behind is worse than
89    /// losing a frame.
90    DropNewest,
91}
92
93impl Default for OverflowPolicy {
94    fn default() -> Self {
95        OverflowPolicy::Block(Duration::MAX)
96    }
97}
98
99/// An explicit thread boundary.
100///
101/// Pushing into a `Queue` hands the buffer off through a bounded channel
102/// and returns immediately — it never blocks the caller on whatever is
103/// downstream (unless the channel is full and `policy` is `Block`). A
104/// dedicated worker thread owns everything downstream of the queue and
105/// drives it via direct `Sink::consume` calls, until it hits another
106/// `Queue`.
107///
108/// [`ControlMsg`] crosses this same thread boundary through a separate
109/// channel from data. The worker checks that channel before entering its
110/// combined wait on every iteration, so a control message already pending
111/// at that point jumps ahead of the data backlog. A control message that
112/// arrives in the narrow window after that check can race one ready data
113/// buffer in `select!`, but is checked again before another buffer is
114/// pulled. Every worker acks a control message *before* acting on
115/// it any further (e.g. before blocking on `Pause`), so the channel stays
116/// responsive to the next one — `Resume`/`Stop` always reaches a paused
117/// worker immediately, it's never stuck behind the pause itself. See the
118/// worker loop below.
119///
120/// Cheap elements (e.g. a muxer sitting right after an encoder) should
121/// simply *not* have a `Queue` between them and their upstream — they run
122/// as a direct call on the upstream element's thread instead of paying for
123/// a dedicated thread they don't need.
124///
125/// A failing `downstream.consume()` doesn't end the worker thread either —
126/// that buffer is dropped, `BusEvent::Error` is posted, and the loop moves
127/// on to the next one. This crate never decides an error is fatal on your
128/// behalf; watch [`crate::pipeline::Pipeline::bus`] and call
129/// [`crate::pipeline::Pipeline::stop`] yourself if a particular error
130/// means the whole pipeline should end.
131pub struct Queue {
132    pp_log: PpLog,
133    name: Arc<str>,
134    tx: Sender<MediaBuffer>,
135    policy: OverflowPolicy,
136    bus: Bus,
137    handle: Option<JoinHandle<()>>,
138    control: ControlSender,
139    /// Set by [`Queue::drop`], read by the worker's own wait loops
140    /// ([`worker_loop`], [`apply_control`]'s pause loop) — the one signal
141    /// that reaches the worker no matter which of those it's currently
142    /// blocked in, without competing with (and possibly cutting off)
143    /// whatever real data/control traffic is already legitimately queued.
144    /// See [`Queue::drop`] for why neither channel alone can play this
145    /// role safely.
146    stop: Arc<AtomicBool>,
147}
148
149impl Queue {
150    /// Spawns with [`OverflowPolicy::default`]. Use
151    /// [`Queue::spawn_with_policy`] to drop instead of blocking when full.
152    pub fn spawn(
153        name: impl Into<String>,
154        capacity: usize,
155        downstream: Box<dyn Sink>,
156        bus: Bus,
157        pipeline_id: Option<&str>,
158    ) -> Queue {
159        Self::spawn_with_policy(
160            name,
161            capacity,
162            downstream,
163            bus,
164            OverflowPolicy::default(),
165            pipeline_id,
166        )
167    }
168
169    /// Spawns the worker thread that owns `downstream` and starts pulling
170    /// from the channel immediately. `pipeline_id` (typically the owning
171    /// [`crate::pipeline::Pipeline`]'s own id — see
172    /// [`crate::pipeline::ChainBuilder`], which is what actually passes
173    /// one when this `Queue` came from a `.queue()`/`.queue_with_policy()`
174    /// call) becomes this `Queue`'s `pp_log` `pipeline_id`; `None` if it
175    /// wasn't built through a `Pipeline` at all (e.g. the tests below).
176    pub fn spawn_with_policy(
177        name: impl Into<String>,
178        capacity: usize,
179        downstream: Box<dyn Sink>,
180        bus: Bus,
181        policy: OverflowPolicy,
182        pipeline_id: Option<&str>,
183    ) -> Queue {
184        // Stored as `Arc<str>` (not `String`) so the `worker_name.clone()`
185        // below, and every subsequent `BusEvent` this posts, are a
186        // refcount bump instead of a fresh allocation — `Dropped` in
187        // particular can fire once per buffer under sustained overflow.
188        let name: Arc<str> = name.into().into();
189        let pp_log = element_pp_log(ElementType::Queue, &name, pipeline_id);
190        pp_info!(pp_log: &pp_log, "spawned: capacity={capacity}, policy={policy:?}");
191        let (tx, rx) = bounded::<MediaBuffer>(capacity);
192        let (control_tx, control_rx) = control::channel();
193        let worker_name = name.clone();
194        let worker_bus = bus.clone();
195        let worker_pp_log = pp_log.clone();
196        let stop = Arc::new(AtomicBool::new(false));
197        let worker_stop = stop.clone();
198
199        let handle = thread::Builder::new()
200            .name(format!("queue:{worker_name}"))
201            .spawn(move || {
202                worker_loop(
203                    rx,
204                    control_rx,
205                    downstream,
206                    worker_bus,
207                    worker_name,
208                    worker_pp_log,
209                    worker_stop,
210                )
211            })
212            .expect("failed to spawn queue worker thread");
213
214        Queue {
215            name,
216            pp_log,
217            tx,
218            policy,
219            bus,
220            handle: Some(handle),
221            control: control_tx,
222            stop,
223        }
224    }
225}
226
227impl Element for Queue {
228    fn name(&self) -> Arc<str> {
229        self.name.clone()
230    }
231
232    fn element_type(&self) -> ElementType {
233        ElementType::Queue
234    }
235
236    fn pp_log(&self) -> &PpLog {
237        &self.pp_log
238    }
239
240    fn pp_log_mut(&mut self) -> &mut PpLog {
241        &mut self.pp_log
242    }
243}
244
245impl Sink for Queue {
246    fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
247        // EOS must never be dropped, regardless of policy: unlike an
248        // explicit Stop or Queue::drop's private stop flag, this is the
249        // natural-completion signal that tells the worker to finish only
250        // after everything queued before it has reached downstream. The
251        // policy timeout intentionally does not apply to this send.
252        if buf.is_eos() {
253            pp_trace!(pp_log: &self.pp_log, "event=eos phase=received");
254            let result = self
255                .tx
256                .send(buf)
257                .map_err(|_| QueueError::ChannelClosed.into());
258            match &result {
259                Ok(()) => pp_trace!(
260                    pp_log: &self.pp_log,
261                    "event=eos phase=queued outcome=ok"
262                ),
263                Err(error) => pp_trace!(
264                    pp_log: &self.pp_log,
265                    "event=eos phase=queued outcome=error error={error}"
266                ),
267            }
268            return result;
269        }
270
271        match self.policy {
272            OverflowPolicy::Block(timeout) => match self.tx.send_timeout(buf, timeout) {
273                Ok(()) => Ok(()),
274                Err(SendTimeoutError::Timeout(_)) => {
275                    Err(QueueError::SendTimedOut { after: timeout }.into())
276                }
277                Err(SendTimeoutError::Disconnected(_)) => Err(QueueError::ChannelClosed.into()),
278            },
279            OverflowPolicy::DropNewest => match self.tx.try_send(buf) {
280                Ok(()) => Ok(()),
281                Err(TrySendError::Full(_)) => {
282                    self.bus.post(
283                        &self.pp_log,
284                        BusEvent::Dropped {
285                            element_type: ElementType::Queue,
286                            name: self.name.clone(),
287                        },
288                    );
289                    Ok(())
290                }
291                Err(TrySendError::Disconnected(_)) => Err(QueueError::ChannelClosed.into()),
292            },
293        }
294    }
295
296    fn control(&mut self, msg: ControlMsg) -> Result<()> {
297        // Blocks until the worker — and everything downstream of it — has
298        // finished handling this. Never stuck behind a data backlog: the
299        // worker checks this channel before every data buffer it pulls
300        // (see `worker_loop`), and while paused it's blocked *only* on
301        // this channel, so a `consume()` blocked sending data upstream of
302        // a paused queue just sits in ordinary backpressure — nothing
303        // feeds this queue while it's paused, since `Pause` blocks
304        // whatever's upstream the same way, all the way back to the
305        // source (see [`crate::control::drain_control`]).
306        pp_trace!(
307            pp_log: &self.pp_log,
308            "event=control control={msg:?} phase=received"
309        );
310        self.control.send(msg);
311        pp_trace!(
312            pp_log: &self.pp_log,
313            "event=control control={msg:?} phase=completed outcome=ok"
314        );
315        Ok(())
316    }
317}
318
319impl Drop for Queue {
320    fn drop(&mut self) {
321        if let Some(handle) = self.handle.take() {
322            // Wakes the worker if nothing else already would — it checks
323            // this on every idle wait-timeout, in both `worker_loop` and
324            // `apply_control`'s pause loop, so it's the one signal that
325            // reaches a genuinely-idle worker no matter which of those two
326            // places it's currently blocked in (e.g. a `.queue()`-having
327            // `Pipeline` dropped without ever being `run()`, or a bare
328            // `Queue` paused and then dropped without `Resume`/`Stop` —
329            // `handle.join()` below would otherwise hang on either).
330            // Doesn't race real pending data/control the way closing a
331            // channel to force this would: it's only ever consulted once
332            // `select!`/`recv_timeout` has already waited out a full
333            // `STOP_POLL_INTERVAL` with *nothing* ready on either channel,
334            // so any already-queued `Stop`/`Eos`/data is always drained
335            // first, same as `block_never_drops` and friends rely on.
336            self.stop.store(true, Ordering::Relaxed);
337            pp_info!(pp_log: &self.pp_log, "dropped: joining worker");
338            let _ = handle.join();
339        }
340    }
341}
342
343/// Owns `downstream` on its own thread: pulls from `data_rx` and calls
344/// `downstream.consume()`, same as before. Every iteration first checks
345/// `control_rx` non-blockingly, so a control request already pending there
346/// is handled before the next data buffer, however deep the backlog. A
347/// request arriving immediately afterward can race one ready data item in
348/// the combined `select!`; the next iteration checks control first again.
349/// `Pause` blocks this
350/// whole function (and therefore `downstream`) right here, without
351/// touching `data_rx` at all, until `Resume`/`Stop`.
352fn worker_loop(
353    data_rx: Receiver<MediaBuffer>,
354    control_rx: ControlReceiver,
355    mut downstream: Box<dyn Sink>,
356    bus: Bus,
357    name: Arc<str>,
358    // Cloned from `Queue`'s own field before this thread was spawned —
359    // same value, not rebuilt here, so a `pipeline_id` passed to
360    // `spawn_with_policy` actually reaches this thread's own log lines
361    // too.
362    pp_log: PpLog,
363    stop: Arc<AtomicBool>,
364) {
365    pp_info!(pp_log: &pp_log, "worker: starting");
366    let error_reporter = QueueErrorReporter {
367        bus: &bus,
368        name: &name,
369        pp_log: &pp_log,
370    };
371    loop {
372        if let Some((msg, ack)) = control_rx.try_recv() {
373            if apply_control(
374                &data_rx,
375                &mut downstream,
376                msg,
377                &ack,
378                &control_rx,
379                &error_reporter,
380                &stop,
381            ) {
382                pp_info!(pp_log: &pp_log, "worker: stopped");
383                return;
384            }
385            continue;
386        }
387
388        select! {
389            recv(control_rx.rx) -> req => {
390                match req {
391                    Ok(req) => {
392                        if apply_control(
393                            &data_rx,
394                            &mut downstream,
395                            req.msg,
396                            &req.ack,
397                            &control_rx,
398                            &error_reporter,
399                            &stop,
400                        ) {
401                            pp_info!(pp_log: &pp_log, "worker: stopped");
402                            return;
403                        }
404                    }
405                    Err(_) => {
406                        pp_info!(pp_log: &pp_log, "worker: control channel gone, ending");
407                        return; // sender (this Queue) dropped
408                    }
409                }
410            }
411            recv(data_rx) -> buf => {
412                match buf {
413                    Ok(buf) => {
414                        let is_eos = buf.is_eos();
415                        match downstream.consume(buf) {
416                            Ok(()) => {
417                                if is_eos {
418                                    pp_trace!(
419                                        pp_log: &pp_log,
420                                        "event=eos phase=completed outcome=ok"
421                                    );
422                                    bus.post(
423                                        &pp_log,
424                                        BusEvent::Eos {
425                                            element_type: ElementType::Queue,
426                                            name: name.clone(),
427                                        },
428                                    );
429                                    return;
430                                }
431                            }
432                            Err(error) => {
433                                if is_eos {
434                                    pp_trace!(
435                                        pp_log: &pp_log,
436                                        "event=eos phase=completed outcome=error error={error}"
437                                    );
438                                }
439                                // Report and move on to the next buffer —
440                                // this one's dropped, but nothing else
441                                // dies over it. Whoever's watching the bus
442                                // decides whether the error is fatal
443                                // enough to call `Pipeline::stop`.
444                                error_reporter.post(error);
445                            }
446                        }
447                    }
448                    Err(_) => {
449                        pp_info!(pp_log: &pp_log, "worker: producer (this Queue) gone, ending");
450                        return;
451                    }
452                }
453            }
454            // Only reached once neither branch above had anything ready
455            // for a whole `STOP_POLL_INTERVAL` — real traffic on either
456            // channel always wins first. See `Queue::drop`.
457            default(STOP_POLL_INTERVAL) => {
458                if stop.load(Ordering::Relaxed) {
459                    pp_info!(pp_log: &pp_log, "worker: stop flag set, ending");
460                    return;
461                }
462            }
463        }
464    }
465}
466
467/// Applies one control message to `downstream`, acking it, then — only
468/// for `Pause` — blocking this thread on `control_rx` alone (never
469/// touching `data_rx`) until `Resume`/`Stop`. Returns `true` once `Stop`
470/// has been handled, meaning the caller (`worker_loop`) should exit.
471fn apply_control(
472    data_rx: &Receiver<MediaBuffer>,
473    downstream: &mut Box<dyn Sink>,
474    msg: ControlMsg,
475    ack: &Sender<()>,
476    control_rx: &ControlReceiver,
477    error_reporter: &QueueErrorReporter<'_>,
478    stop: &AtomicBool,
479) -> bool {
480    pp_trace!(
481        pp_log: error_reporter.pp_log,
482        "event=control control={msg:?} phase=forwarding"
483    );
484    discard_stale_data(data_rx, msg);
485    forward_control(downstream, msg, error_reporter);
486    let is_stop = msg == ControlMsg::Stop;
487    let _ = ack.send(());
488    if is_stop {
489        return true;
490    }
491    if msg != ControlMsg::Pause {
492        return false;
493    }
494    loop {
495        // `recv_timeout` (not `recv`) so `Queue::drop` setting `stop` can
496        // still wake a worker that's paused forever with no `Resume`/
497        // `Stop` ever coming (e.g. a bare `Queue`, not reached through a
498        // `Pipeline` — see `Queue::drop`'s docs on why this state is
499        // otherwise unreachable there). Nothing else feeds this queue
500        // while paused (see the type-level docs), so there's no
501        // legitimate traffic this could ever cut off.
502        let (msg, ack) = match control_rx.rx.recv_timeout(STOP_POLL_INTERVAL) {
503            Ok(req) => (req.msg, req.ack),
504            Err(RecvTimeoutError::Timeout) => {
505                if stop.load(Ordering::Relaxed) {
506                    pp_info!(pp_log: error_reporter.pp_log, "worker: stop flag set while paused, ending");
507                    return true;
508                }
509                continue;
510            }
511            Err(RecvTimeoutError::Disconnected) => {
512                pp_info!(pp_log: error_reporter.pp_log, "worker: control channel gone while paused, ending");
513                return true; // sender gone — treat like Stop
514            }
515        };
516        pp_trace!(
517            pp_log: error_reporter.pp_log,
518            "event=control control={msg:?} phase=forwarding"
519        );
520        discard_stale_data(data_rx, msg);
521        forward_control(downstream, msg, error_reporter);
522        let is_stop = msg == ControlMsg::Stop;
523        let _ = ack.send(());
524        if is_stop {
525            return true;
526        }
527        if msg == ControlMsg::Resume {
528            return false;
529        }
530        // Another Pause while already paused: already forwarded above, keep waiting.
531    }
532}
533
534struct QueueErrorReporter<'a> {
535    bus: &'a Bus,
536    name: &'a Arc<str>,
537    pp_log: &'a PpLog,
538}
539
540impl QueueErrorReporter<'_> {
541    fn post(&self, error: crate::error::Error) {
542        self.bus.post(
543            self.pp_log,
544            BusEvent::Error {
545                element_type: ElementType::Queue,
546                name: self.name.clone(),
547                error,
548            },
549        );
550    }
551}
552
553/// Forwards control without turning one downstream failure into a stuck
554/// synchronous caller or a dead Queue worker. The request is still acked by
555/// [`apply_control`], while the failure is exposed through the same Bus path
556/// used for `consume` failures.
557fn forward_control(
558    downstream: &mut Box<dyn Sink>,
559    msg: ControlMsg,
560    error_reporter: &QueueErrorReporter<'_>,
561) {
562    if let Err(error) = downstream.control(msg) {
563        error_reporter.post(error);
564    }
565}
566
567/// Drops everything already buffered in `data_rx` without processing it —
568/// only for `Seek`. That data predates the seek point (this Queue's
569/// worker hasn't gotten to it yet, but it was read/produced before the
570/// jump), so delivering it downstream afterward would show stale
571/// frames instead of skipping straight to the new position.
572/// `Pause`/`Resume`/`Stop` leave `data_rx` alone — see the type-level
573/// docs on why that's safe (nothing feeds a paused/stopped queue in the
574/// first place).
575fn discard_stale_data(data_rx: &Receiver<MediaBuffer>, msg: ControlMsg) {
576    if matches!(msg, ControlMsg::Seek(_)) {
577        while data_rx.try_recv().is_ok() {}
578    }
579}
580
581#[cfg(test)]
582mod tests {
583    use std::{
584        sync::{
585            Arc,
586            atomic::{AtomicUsize, Ordering},
587        },
588        thread,
589        time::Duration,
590    };
591
592    use super::*;
593    use crate::bus::Bus;
594
595    /// A downstream that's slower than the producer, so a small queue
596    /// behind it actually fills up during the test.
597    struct SlowCounter {
598        pp_log: PpLog,
599        count: Arc<AtomicUsize>,
600    }
601
602    impl Element for SlowCounter {
603        fn name(&self) -> Arc<str> {
604            "slow-counter".into()
605        }
606
607        fn element_type(&self) -> ElementType {
608            ElementType::Other
609        }
610
611        fn pp_log(&self) -> &PpLog {
612            &self.pp_log
613        }
614
615        fn pp_log_mut(&mut self) -> &mut PpLog {
616            &mut self.pp_log
617        }
618    }
619
620    impl Sink for SlowCounter {
621        fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
622            if let MediaBuffer::Packet(_) = buf {
623                thread::sleep(Duration::from_millis(20));
624                self.count.fetch_add(1, Ordering::SeqCst);
625            }
626            Ok(())
627        }
628
629        fn control(&mut self, _msg: ControlMsg) -> Result<()> {
630            Ok(())
631        }
632    }
633
634    fn packet() -> MediaBuffer {
635        MediaBuffer::Packet(Arc::new(ffmpeg_next::Packet::empty()))
636    }
637
638    #[test]
639    fn block_never_drops() {
640        let count = Arc::new(AtomicUsize::new(0));
641        let sink = SlowCounter {
642            count: count.clone(),
643            pp_log: element_pp_log(ElementType::Other, "slow-counter", None),
644        };
645        let (bus, bus_rx) = Bus::new();
646
647        let mut queue = Queue::spawn_with_policy(
648            "test",
649            1,
650            Box::new(sink),
651            bus,
652            OverflowPolicy::default(),
653            None,
654        );
655        for _ in 0..10 {
656            queue.consume(packet()).unwrap();
657        }
658        queue.consume(MediaBuffer::Eos).unwrap();
659        drop(queue); // blocks until the worker drains everything and joins
660
661        assert_eq!(count.load(Ordering::SeqCst), 10);
662        assert!(!bus_rx.iter().any(|e| matches!(e, BusEvent::Dropped { .. })));
663    }
664
665    #[test]
666    fn block_with_a_finite_timeout_errors_instead_of_blocking_forever() {
667        let count = Arc::new(AtomicUsize::new(0));
668        let sink = SlowCounter {
669            count: count.clone(),
670            pp_log: element_pp_log(ElementType::Other, "slow-counter", None),
671        };
672        let (bus, _bus_rx) = Bus::new();
673
674        // Capacity 1, downstream takes 20ms/item, timeout is 5ms — pushed
675        // in a tight loop, some of these sends must outlast their own
676        // timeout instead of blocking until the worker catches up.
677        let mut queue = Queue::spawn_with_policy(
678            "test",
679            1,
680            Box::new(sink),
681            bus,
682            OverflowPolicy::Block(Duration::from_millis(5)),
683            None,
684        );
685        let mut timed_out = 0;
686        for _ in 0..10 {
687            match queue.consume(packet()) {
688                Ok(()) => {}
689                Err(_) => timed_out += 1,
690            }
691        }
692        // Eos isn't subject to the timeout (see `Sink::consume`'s own
693        // special-casing) — always goes through even after some sends
694        // above timed out.
695        queue.consume(MediaBuffer::Eos).unwrap();
696        drop(queue); // blocks until the worker drains everything and joins
697
698        assert!(
699            timed_out > 0,
700            "expected at least one send to time out against a downstream that can't keep up"
701        );
702    }
703
704    #[test]
705    fn drop_newest_drops_when_full_and_reports_on_bus() {
706        let count = Arc::new(AtomicUsize::new(0));
707        let sink = SlowCounter {
708            count: count.clone(),
709            pp_log: element_pp_log(ElementType::Other, "slow-counter", None),
710        };
711        let (bus, bus_rx) = Bus::new();
712
713        let mut queue = Queue::spawn_with_policy(
714            "test",
715            1,
716            Box::new(sink),
717            bus,
718            OverflowPolicy::DropNewest,
719            None,
720        );
721        // Pushed much faster than the 20ms/item downstream can drain a
722        // capacity-1 channel, so some of these must get dropped.
723        for _ in 0..10 {
724            queue.consume(packet()).unwrap();
725        }
726        queue.consume(MediaBuffer::Eos).unwrap(); // never dropped, even under this policy
727        drop(queue);
728
729        let processed = count.load(Ordering::SeqCst);
730        let dropped = bus_rx
731            .iter()
732            .filter(|e| matches!(e, BusEvent::Dropped { .. }))
733            .count();
734
735        assert!(
736            processed < 10,
737            "expected some packets to be dropped, but all {processed} were processed"
738        );
739        assert!(dropped > 0, "expected at least one BusEvent::Dropped");
740        assert_eq!(processed + dropped, 10);
741    }
742
743    #[test]
744    fn pause_stops_delivery_and_resume_lets_it_continue() {
745        let count = Arc::new(AtomicUsize::new(0));
746        let sink = SlowCounter {
747            count: count.clone(),
748            pp_log: element_pp_log(ElementType::Other, "slow-counter", None),
749        };
750        let (bus, _bus_rx) = Bus::new();
751
752        let mut queue = Queue::spawn_with_policy(
753            "test",
754            8,
755            Box::new(sink),
756            bus,
757            OverflowPolicy::default(),
758            None,
759        );
760        queue.control(ControlMsg::Pause).unwrap(); // blocks until the worker is actually paused
761
762        for _ in 0..3 {
763            queue.consume(packet()).unwrap();
764        }
765        // Worker is paused and not touching data_rx — nothing should have
766        // been processed yet, however long we wait.
767        thread::sleep(Duration::from_millis(100));
768        assert_eq!(count.load(Ordering::SeqCst), 0);
769
770        queue.control(ControlMsg::Resume).unwrap();
771        queue.consume(MediaBuffer::Eos).unwrap();
772        drop(queue);
773
774        assert_eq!(count.load(Ordering::SeqCst), 3);
775    }
776
777    /// Regression test: before `Queue::drop` set its own `stop` flag,
778    /// dropping a `Queue` that was never fed a `Stop` control message or
779    /// an `Eos` buffer left its worker thread parked on `recv()` with
780    /// nothing left to wake it — `drop()`'s own `handle.join()` then hung
781    /// forever. This mirrors what happens to a `.queue()`-containing
782    /// `Pipeline` that's dropped without ever being `run()`, so if this
783    /// test hangs, that fix regressed.
784    #[test]
785    fn dropping_without_stop_or_eos_does_not_hang() {
786        let count = Arc::new(AtomicUsize::new(0));
787        let sink = SlowCounter {
788            count: count.clone(),
789            pp_log: element_pp_log(ElementType::Other, "slow-counter", None),
790        };
791        let (bus, _bus_rx) = Bus::new();
792
793        let queue = Queue::spawn_with_policy(
794            "test",
795            8,
796            Box::new(sink),
797            bus,
798            OverflowPolicy::default(),
799            None,
800        );
801        drop(queue);
802    }
803
804    /// Regression test for the other half of the same bug: a worker
805    /// that's specifically inside `apply_control`'s pause loop (blocked on
806    /// `control_rx` alone, not `data_rx`) when dropped without ever
807    /// getting `Resume`/`Stop` — only reachable by pausing a bare `Queue`
808    /// directly (a `Pipeline`-owned one can't be dropped in this state,
809    /// see `Queue::drop`'s docs), but the `stop` flag has to wake this
810    /// wait loop too, not just `worker_loop`'s.
811    #[test]
812    fn dropping_while_paused_does_not_hang() {
813        let count = Arc::new(AtomicUsize::new(0));
814        let sink = SlowCounter {
815            count: count.clone(),
816            pp_log: element_pp_log(ElementType::Other, "slow-counter", None),
817        };
818        let (bus, _bus_rx) = Bus::new();
819
820        let mut queue = Queue::spawn_with_policy(
821            "test",
822            8,
823            Box::new(sink),
824            bus,
825            OverflowPolicy::default(),
826            None,
827        );
828        queue.control(ControlMsg::Pause).unwrap(); // blocks until the worker is actually paused
829        drop(queue);
830    }
831
832    #[test]
833    fn stop_is_synchronous_and_terminates_the_worker() {
834        let count = Arc::new(AtomicUsize::new(0));
835        let sink = SlowCounter {
836            count: count.clone(),
837            pp_log: element_pp_log(ElementType::Other, "slow-counter", None),
838        };
839        let (bus, _bus_rx) = Bus::new();
840
841        let mut queue = Queue::spawn_with_policy(
842            "test",
843            8,
844            Box::new(sink),
845            bus,
846            OverflowPolicy::default(),
847            None,
848        );
849        queue.consume(packet()).unwrap();
850        queue.control(ControlMsg::Stop).unwrap(); // blocks until the worker has exited
851        drop(queue); // join should return immediately — the worker already returned
852    }
853
854    /// A downstream that fails on the very first `Packet` it sees, then
855    /// behaves like `SlowCounter` for every one after.
856    struct FailFirstThenCount {
857        pp_log: PpLog,
858        count: Arc<AtomicUsize>,
859        failed_once: bool,
860    }
861
862    impl Element for FailFirstThenCount {
863        fn name(&self) -> Arc<str> {
864            "fail-first".into()
865        }
866
867        fn element_type(&self) -> ElementType {
868            ElementType::Other
869        }
870
871        fn pp_log(&self) -> &PpLog {
872            &self.pp_log
873        }
874
875        fn pp_log_mut(&mut self) -> &mut PpLog {
876            &mut self.pp_log
877        }
878    }
879
880    impl Sink for FailFirstThenCount {
881        fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
882            let MediaBuffer::Packet(_) = buf else {
883                return Ok(());
884            };
885            if !self.failed_once {
886                self.failed_once = true;
887                return Err(crate::error::Error::Other("simulated failure".into()));
888            }
889            self.count.fetch_add(1, Ordering::SeqCst);
890            Ok(())
891        }
892
893        fn control(&mut self, _msg: ControlMsg) -> Result<()> {
894            Ok(())
895        }
896    }
897
898    struct FailControl {
899        pp_log: PpLog,
900    }
901
902    impl Element for FailControl {
903        fn name(&self) -> Arc<str> {
904            "fail-control".into()
905        }
906
907        fn element_type(&self) -> ElementType {
908            ElementType::Other
909        }
910
911        fn pp_log(&self) -> &PpLog {
912            &self.pp_log
913        }
914
915        fn pp_log_mut(&mut self) -> &mut PpLog {
916            &mut self.pp_log
917        }
918    }
919
920    impl Sink for FailControl {
921        fn consume(&mut self, _buf: MediaBuffer) -> Result<()> {
922            Ok(())
923        }
924
925        fn control(&mut self, msg: ControlMsg) -> Result<()> {
926            Err(crate::error::Error::Other(format!(
927                "simulated {msg:?} failure"
928            )))
929        }
930    }
931
932    /// Regression test for the design change prompted by the `NoFreeSlot`
933    /// investigation: a `Sink::consume` failure used to end the worker
934    /// thread outright (and, transitively, everything upstream once its
935    /// data channel closed). Now it's just one dropped buffer — the
936    /// worker keeps running, later buffers still get through, and exactly
937    /// one `BusEvent::Error` shows up for the one that failed.
938    #[test]
939    fn a_failing_consume_drops_that_buffer_but_keeps_the_worker_alive() {
940        let count = Arc::new(AtomicUsize::new(0));
941        let sink = FailFirstThenCount {
942            count: count.clone(),
943            failed_once: false,
944            pp_log: element_pp_log(ElementType::Other, "fail-first", None),
945        };
946        let (bus, bus_rx) = Bus::new();
947
948        let mut queue = Queue::spawn_with_policy(
949            "test",
950            8,
951            Box::new(sink),
952            bus,
953            OverflowPolicy::default(),
954            None,
955        );
956        for _ in 0..3 {
957            queue.consume(packet()).unwrap();
958        }
959        queue.consume(MediaBuffer::Eos).unwrap();
960        drop(queue); // blocks until the worker drains everything and joins
961
962        // First packet failed (and was dropped); the other two still went
963        // through — the worker didn't die over the first one.
964        assert_eq!(count.load(Ordering::SeqCst), 2);
965        let errors = bus_rx
966            .iter()
967            .filter(|e| matches!(e, BusEvent::Error { .. }))
968            .count();
969        assert_eq!(
970            errors, 1,
971            "expected exactly one Error event, for the one buffer that failed"
972        );
973    }
974
975    /// Control failures are asynchronous worker failures just like
976    /// `consume` failures: they must be visible on the Bus, but must not
977    /// prevent Pause/Resume/Stop acknowledgements or strand the worker.
978    #[test]
979    fn failing_control_is_reported_without_blocking_the_control_cascade() {
980        let sink = FailControl {
981            pp_log: element_pp_log(ElementType::Other, "fail-control", None),
982        };
983        let (bus, bus_rx) = Bus::new();
984        let mut queue = Queue::spawn_with_policy(
985            "test",
986            1,
987            Box::new(sink),
988            bus,
989            OverflowPolicy::default(),
990            None,
991        );
992
993        queue.control(ControlMsg::Pause).unwrap();
994        queue.control(ControlMsg::Resume).unwrap();
995        queue.control(ControlMsg::Stop).unwrap();
996        drop(queue);
997
998        let errors: Vec<_> = bus_rx
999            .iter()
1000            .filter(|event| matches!(event, BusEvent::Error { .. }))
1001            .collect();
1002        assert_eq!(
1003            errors.len(),
1004            3,
1005            "Pause, Resume, and Stop failures must each be reported once"
1006        );
1007    }
1008}