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, RequestKind},
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((request, ack)) = control_rx.try_recv() {
373            let RequestKind::Control(msg) = request else {
374                let _ = ack.send(());
375                continue;
376            };
377            if apply_control(
378                &data_rx,
379                &mut downstream,
380                msg,
381                &ack,
382                &control_rx,
383                &error_reporter,
384                &stop,
385            ) {
386                pp_info!(pp_log: &pp_log, "worker: stopped");
387                return;
388            }
389            continue;
390        }
391
392        select! {
393            recv(control_rx.rx) -> req => {
394                match req {
395                    Ok(req) => {
396                        let RequestKind::Control(msg) = req.kind else {
397                            let _ = req.ack.send(());
398                            continue;
399                        };
400                        if apply_control(
401                            &data_rx,
402                            &mut downstream,
403                            msg,
404                            &req.ack,
405                            &control_rx,
406                            &error_reporter,
407                            &stop,
408                        ) {
409                            pp_info!(pp_log: &pp_log, "worker: stopped");
410                            return;
411                        }
412                    }
413                    Err(_) => {
414                        pp_info!(pp_log: &pp_log, "worker: control channel gone, ending");
415                        return; // sender (this Queue) dropped
416                    }
417                }
418            }
419            recv(data_rx) -> buf => {
420                match buf {
421                    Ok(buf) => {
422                        let is_eos = buf.is_eos();
423                        match downstream.consume(buf) {
424                            Ok(()) => {
425                                if is_eos {
426                                    pp_trace!(
427                                        pp_log: &pp_log,
428                                        "event=eos phase=completed outcome=ok"
429                                    );
430                                    bus.post(
431                                        &pp_log,
432                                        BusEvent::Eos {
433                                            element_type: ElementType::Queue,
434                                            name: name.clone(),
435                                        },
436                                    );
437                                    return;
438                                }
439                            }
440                            Err(error) => {
441                                if is_eos {
442                                    pp_trace!(
443                                        pp_log: &pp_log,
444                                        "event=eos phase=completed outcome=error error={error}"
445                                    );
446                                }
447                                // Report and move on to the next buffer —
448                                // this one's dropped, but nothing else
449                                // dies over it. Whoever's watching the bus
450                                // decides whether the error is fatal
451                                // enough to call `Pipeline::stop`.
452                                error_reporter.post(error);
453                            }
454                        }
455                    }
456                    Err(_) => {
457                        pp_info!(pp_log: &pp_log, "worker: producer (this Queue) gone, ending");
458                        return;
459                    }
460                }
461            }
462            // Only reached once neither branch above had anything ready
463            // for a whole `STOP_POLL_INTERVAL` — real traffic on either
464            // channel always wins first. See `Queue::drop`.
465            default(STOP_POLL_INTERVAL) => {
466                if stop.load(Ordering::Relaxed) {
467                    pp_info!(pp_log: &pp_log, "worker: stop flag set, ending");
468                    return;
469                }
470            }
471        }
472    }
473}
474
475/// Applies one control message to `downstream`, acking it, then — only
476/// for `Pause` — blocking this thread on `control_rx` alone (never
477/// touching `data_rx`) until `Resume`/`Stop`. Returns `true` once `Stop`
478/// has been handled, meaning the caller (`worker_loop`) should exit.
479fn apply_control(
480    data_rx: &Receiver<MediaBuffer>,
481    downstream: &mut Box<dyn Sink>,
482    msg: ControlMsg,
483    ack: &Sender<()>,
484    control_rx: &ControlReceiver,
485    error_reporter: &QueueErrorReporter<'_>,
486    stop: &AtomicBool,
487) -> bool {
488    pp_trace!(
489        pp_log: error_reporter.pp_log,
490        "event=control control={msg:?} phase=forwarding"
491    );
492    discard_stale_data(data_rx, msg);
493    forward_control(downstream, msg, error_reporter);
494    let is_stop = msg == ControlMsg::Stop;
495    let _ = ack.send(());
496    if is_stop {
497        return true;
498    }
499    if msg != ControlMsg::Pause {
500        return false;
501    }
502    loop {
503        // `recv_timeout` (not `recv`) so `Queue::drop` setting `stop` can
504        // still wake a worker that's paused forever with no `Resume`/
505        // `Stop` ever coming (e.g. a bare `Queue`, not reached through a
506        // `Pipeline` — see `Queue::drop`'s docs on why this state is
507        // otherwise unreachable there). Nothing else feeds this queue
508        // while paused (see the type-level docs), so there's no
509        // legitimate traffic this could ever cut off.
510        let (msg, ack) = match control_rx.rx.recv_timeout(STOP_POLL_INTERVAL) {
511            Ok(req) => {
512                let RequestKind::Control(msg) = req.kind else {
513                    let _ = req.ack.send(());
514                    continue;
515                };
516                (msg, req.ack)
517            }
518            Err(RecvTimeoutError::Timeout) => {
519                if stop.load(Ordering::Relaxed) {
520                    pp_info!(pp_log: error_reporter.pp_log, "worker: stop flag set while paused, ending");
521                    return true;
522                }
523                continue;
524            }
525            Err(RecvTimeoutError::Disconnected) => {
526                pp_info!(pp_log: error_reporter.pp_log, "worker: control channel gone while paused, ending");
527                return true; // sender gone — treat like Stop
528            }
529        };
530        pp_trace!(
531            pp_log: error_reporter.pp_log,
532            "event=control control={msg:?} phase=forwarding"
533        );
534        discard_stale_data(data_rx, msg);
535        forward_control(downstream, msg, error_reporter);
536        let is_stop = msg == ControlMsg::Stop;
537        let _ = ack.send(());
538        if is_stop {
539            return true;
540        }
541        if msg == ControlMsg::Resume {
542            return false;
543        }
544        // Another Pause while already paused: already forwarded above, keep waiting.
545    }
546}
547
548struct QueueErrorReporter<'a> {
549    bus: &'a Bus,
550    name: &'a Arc<str>,
551    pp_log: &'a PpLog,
552}
553
554impl QueueErrorReporter<'_> {
555    fn post(&self, error: crate::error::Error) {
556        self.bus.post(
557            self.pp_log,
558            BusEvent::Error {
559                element_type: ElementType::Queue,
560                name: self.name.clone(),
561                error,
562            },
563        );
564    }
565}
566
567/// Forwards control without turning one downstream failure into a stuck
568/// synchronous caller or a dead Queue worker. The request is still acked by
569/// [`apply_control`], while the failure is exposed through the same Bus path
570/// used for `consume` failures.
571fn forward_control(
572    downstream: &mut Box<dyn Sink>,
573    msg: ControlMsg,
574    error_reporter: &QueueErrorReporter<'_>,
575) {
576    if let Err(error) = downstream.control(msg) {
577        error_reporter.post(error);
578    }
579}
580
581/// Drops everything already buffered in `data_rx` without processing it —
582/// only for `Seek`. That data predates the seek point (this Queue's
583/// worker hasn't gotten to it yet, but it was read/produced before the
584/// jump), so delivering it downstream afterward would show stale
585/// frames instead of skipping straight to the new position.
586/// `Pause`/`Resume`/`Stop` leave `data_rx` alone — see the type-level
587/// docs on why that's safe (nothing feeds a paused/stopped queue in the
588/// first place).
589fn discard_stale_data(data_rx: &Receiver<MediaBuffer>, msg: ControlMsg) {
590    if matches!(msg, ControlMsg::Seek(_)) {
591        while data_rx.try_recv().is_ok() {}
592    }
593}
594
595#[cfg(test)]
596mod tests {
597    use std::{
598        sync::{
599            Arc,
600            atomic::{AtomicUsize, Ordering},
601        },
602        thread,
603        time::Duration,
604    };
605
606    use super::*;
607    use crate::bus::Bus;
608
609    /// A downstream that's slower than the producer, so a small queue
610    /// behind it actually fills up during the test.
611    struct SlowCounter {
612        pp_log: PpLog,
613        count: Arc<AtomicUsize>,
614    }
615
616    impl Element for SlowCounter {
617        fn name(&self) -> Arc<str> {
618            "slow-counter".into()
619        }
620
621        fn element_type(&self) -> ElementType {
622            ElementType::Other
623        }
624
625        fn pp_log(&self) -> &PpLog {
626            &self.pp_log
627        }
628
629        fn pp_log_mut(&mut self) -> &mut PpLog {
630            &mut self.pp_log
631        }
632    }
633
634    impl Sink for SlowCounter {
635        fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
636            if let MediaBuffer::Packet(_) = buf {
637                thread::sleep(Duration::from_millis(20));
638                self.count.fetch_add(1, Ordering::SeqCst);
639            }
640            Ok(())
641        }
642
643        fn control(&mut self, _msg: ControlMsg) -> Result<()> {
644            Ok(())
645        }
646    }
647
648    fn packet() -> MediaBuffer {
649        MediaBuffer::Packet(Arc::new(ffmpeg_next::Packet::empty()))
650    }
651
652    #[test]
653    fn block_never_drops() {
654        let count = Arc::new(AtomicUsize::new(0));
655        let sink = SlowCounter {
656            count: count.clone(),
657            pp_log: element_pp_log(ElementType::Other, "slow-counter", None),
658        };
659        let (bus, bus_rx) = Bus::new();
660
661        let mut queue = Queue::spawn_with_policy(
662            "test",
663            1,
664            Box::new(sink),
665            bus,
666            OverflowPolicy::default(),
667            None,
668        );
669        for _ in 0..10 {
670            queue.consume(packet()).unwrap();
671        }
672        queue.consume(MediaBuffer::Eos).unwrap();
673        drop(queue); // blocks until the worker drains everything and joins
674
675        assert_eq!(count.load(Ordering::SeqCst), 10);
676        assert!(!bus_rx.iter().any(|e| matches!(e, BusEvent::Dropped { .. })));
677    }
678
679    #[test]
680    fn block_with_a_finite_timeout_errors_instead_of_blocking_forever() {
681        let count = Arc::new(AtomicUsize::new(0));
682        let sink = SlowCounter {
683            count: count.clone(),
684            pp_log: element_pp_log(ElementType::Other, "slow-counter", None),
685        };
686        let (bus, _bus_rx) = Bus::new();
687
688        // Capacity 1, downstream takes 20ms/item, timeout is 5ms — pushed
689        // in a tight loop, some of these sends must outlast their own
690        // timeout instead of blocking until the worker catches up.
691        let mut queue = Queue::spawn_with_policy(
692            "test",
693            1,
694            Box::new(sink),
695            bus,
696            OverflowPolicy::Block(Duration::from_millis(5)),
697            None,
698        );
699        let mut timed_out = 0;
700        for _ in 0..10 {
701            match queue.consume(packet()) {
702                Ok(()) => {}
703                Err(_) => timed_out += 1,
704            }
705        }
706        // Eos isn't subject to the timeout (see `Sink::consume`'s own
707        // special-casing) — always goes through even after some sends
708        // above timed out.
709        queue.consume(MediaBuffer::Eos).unwrap();
710        drop(queue); // blocks until the worker drains everything and joins
711
712        assert!(
713            timed_out > 0,
714            "expected at least one send to time out against a downstream that can't keep up"
715        );
716    }
717
718    #[test]
719    fn drop_newest_drops_when_full_and_reports_on_bus() {
720        let count = Arc::new(AtomicUsize::new(0));
721        let sink = SlowCounter {
722            count: count.clone(),
723            pp_log: element_pp_log(ElementType::Other, "slow-counter", None),
724        };
725        let (bus, bus_rx) = Bus::new();
726
727        let mut queue = Queue::spawn_with_policy(
728            "test",
729            1,
730            Box::new(sink),
731            bus,
732            OverflowPolicy::DropNewest,
733            None,
734        );
735        // Pushed much faster than the 20ms/item downstream can drain a
736        // capacity-1 channel, so some of these must get dropped.
737        for _ in 0..10 {
738            queue.consume(packet()).unwrap();
739        }
740        queue.consume(MediaBuffer::Eos).unwrap(); // never dropped, even under this policy
741        drop(queue);
742
743        let processed = count.load(Ordering::SeqCst);
744        let dropped = bus_rx
745            .iter()
746            .filter(|e| matches!(e, BusEvent::Dropped { .. }))
747            .count();
748
749        assert!(
750            processed < 10,
751            "expected some packets to be dropped, but all {processed} were processed"
752        );
753        assert!(dropped > 0, "expected at least one BusEvent::Dropped");
754        assert_eq!(processed + dropped, 10);
755    }
756
757    #[test]
758    fn pause_stops_delivery_and_resume_lets_it_continue() {
759        let count = Arc::new(AtomicUsize::new(0));
760        let sink = SlowCounter {
761            count: count.clone(),
762            pp_log: element_pp_log(ElementType::Other, "slow-counter", None),
763        };
764        let (bus, _bus_rx) = Bus::new();
765
766        let mut queue = Queue::spawn_with_policy(
767            "test",
768            8,
769            Box::new(sink),
770            bus,
771            OverflowPolicy::default(),
772            None,
773        );
774        queue.control(ControlMsg::Pause).unwrap(); // blocks until the worker is actually paused
775
776        for _ in 0..3 {
777            queue.consume(packet()).unwrap();
778        }
779        // Worker is paused and not touching data_rx — nothing should have
780        // been processed yet, however long we wait.
781        thread::sleep(Duration::from_millis(100));
782        assert_eq!(count.load(Ordering::SeqCst), 0);
783
784        queue.control(ControlMsg::Resume).unwrap();
785        queue.consume(MediaBuffer::Eos).unwrap();
786        drop(queue);
787
788        assert_eq!(count.load(Ordering::SeqCst), 3);
789    }
790
791    /// Regression test: before `Queue::drop` set its own `stop` flag,
792    /// dropping a `Queue` that was never fed a `Stop` control message or
793    /// an `Eos` buffer left its worker thread parked on `recv()` with
794    /// nothing left to wake it — `drop()`'s own `handle.join()` then hung
795    /// forever. This mirrors what happens to a `.queue()`-containing
796    /// `Pipeline` that's dropped without ever being `run()`, so if this
797    /// test hangs, that fix regressed.
798    #[test]
799    fn dropping_without_stop_or_eos_does_not_hang() {
800        let count = Arc::new(AtomicUsize::new(0));
801        let sink = SlowCounter {
802            count: count.clone(),
803            pp_log: element_pp_log(ElementType::Other, "slow-counter", None),
804        };
805        let (bus, _bus_rx) = Bus::new();
806
807        let queue = Queue::spawn_with_policy(
808            "test",
809            8,
810            Box::new(sink),
811            bus,
812            OverflowPolicy::default(),
813            None,
814        );
815        drop(queue);
816    }
817
818    /// Regression test for the other half of the same bug: a worker
819    /// that's specifically inside `apply_control`'s pause loop (blocked on
820    /// `control_rx` alone, not `data_rx`) when dropped without ever
821    /// getting `Resume`/`Stop` — only reachable by pausing a bare `Queue`
822    /// directly (a `Pipeline`-owned one can't be dropped in this state,
823    /// see `Queue::drop`'s docs), but the `stop` flag has to wake this
824    /// wait loop too, not just `worker_loop`'s.
825    #[test]
826    fn dropping_while_paused_does_not_hang() {
827        let count = Arc::new(AtomicUsize::new(0));
828        let sink = SlowCounter {
829            count: count.clone(),
830            pp_log: element_pp_log(ElementType::Other, "slow-counter", None),
831        };
832        let (bus, _bus_rx) = Bus::new();
833
834        let mut queue = Queue::spawn_with_policy(
835            "test",
836            8,
837            Box::new(sink),
838            bus,
839            OverflowPolicy::default(),
840            None,
841        );
842        queue.control(ControlMsg::Pause).unwrap(); // blocks until the worker is actually paused
843        drop(queue);
844    }
845
846    #[test]
847    fn stop_is_synchronous_and_terminates_the_worker() {
848        let count = Arc::new(AtomicUsize::new(0));
849        let sink = SlowCounter {
850            count: count.clone(),
851            pp_log: element_pp_log(ElementType::Other, "slow-counter", None),
852        };
853        let (bus, _bus_rx) = Bus::new();
854
855        let mut queue = Queue::spawn_with_policy(
856            "test",
857            8,
858            Box::new(sink),
859            bus,
860            OverflowPolicy::default(),
861            None,
862        );
863        queue.consume(packet()).unwrap();
864        queue.control(ControlMsg::Stop).unwrap(); // blocks until the worker has exited
865        drop(queue); // join should return immediately — the worker already returned
866    }
867
868    /// A downstream that fails on the very first `Packet` it sees, then
869    /// behaves like `SlowCounter` for every one after.
870    struct FailFirstThenCount {
871        pp_log: PpLog,
872        count: Arc<AtomicUsize>,
873        failed_once: bool,
874    }
875
876    impl Element for FailFirstThenCount {
877        fn name(&self) -> Arc<str> {
878            "fail-first".into()
879        }
880
881        fn element_type(&self) -> ElementType {
882            ElementType::Other
883        }
884
885        fn pp_log(&self) -> &PpLog {
886            &self.pp_log
887        }
888
889        fn pp_log_mut(&mut self) -> &mut PpLog {
890            &mut self.pp_log
891        }
892    }
893
894    impl Sink for FailFirstThenCount {
895        fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
896            let MediaBuffer::Packet(_) = buf else {
897                return Ok(());
898            };
899            if !self.failed_once {
900                self.failed_once = true;
901                return Err(crate::error::Error::Other("simulated failure".into()));
902            }
903            self.count.fetch_add(1, Ordering::SeqCst);
904            Ok(())
905        }
906
907        fn control(&mut self, _msg: ControlMsg) -> Result<()> {
908            Ok(())
909        }
910    }
911
912    struct FailControl {
913        pp_log: PpLog,
914    }
915
916    impl Element for FailControl {
917        fn name(&self) -> Arc<str> {
918            "fail-control".into()
919        }
920
921        fn element_type(&self) -> ElementType {
922            ElementType::Other
923        }
924
925        fn pp_log(&self) -> &PpLog {
926            &self.pp_log
927        }
928
929        fn pp_log_mut(&mut self) -> &mut PpLog {
930            &mut self.pp_log
931        }
932    }
933
934    impl Sink for FailControl {
935        fn consume(&mut self, _buf: MediaBuffer) -> Result<()> {
936            Ok(())
937        }
938
939        fn control(&mut self, msg: ControlMsg) -> Result<()> {
940            Err(crate::error::Error::Other(format!(
941                "simulated {msg:?} failure"
942            )))
943        }
944    }
945
946    /// Regression test for the design change prompted by the `NoFreeSlot`
947    /// investigation: a `Sink::consume` failure used to end the worker
948    /// thread outright (and, transitively, everything upstream once its
949    /// data channel closed). Now it's just one dropped buffer — the
950    /// worker keeps running, later buffers still get through, and exactly
951    /// one `BusEvent::Error` shows up for the one that failed.
952    #[test]
953    fn a_failing_consume_drops_that_buffer_but_keeps_the_worker_alive() {
954        let count = Arc::new(AtomicUsize::new(0));
955        let sink = FailFirstThenCount {
956            count: count.clone(),
957            failed_once: false,
958            pp_log: element_pp_log(ElementType::Other, "fail-first", None),
959        };
960        let (bus, bus_rx) = Bus::new();
961
962        let mut queue = Queue::spawn_with_policy(
963            "test",
964            8,
965            Box::new(sink),
966            bus,
967            OverflowPolicy::default(),
968            None,
969        );
970        for _ in 0..3 {
971            queue.consume(packet()).unwrap();
972        }
973        queue.consume(MediaBuffer::Eos).unwrap();
974        drop(queue); // blocks until the worker drains everything and joins
975
976        // First packet failed (and was dropped); the other two still went
977        // through — the worker didn't die over the first one.
978        assert_eq!(count.load(Ordering::SeqCst), 2);
979        let errors = bus_rx
980            .iter()
981            .filter(|e| matches!(e, BusEvent::Error { .. }))
982            .count();
983        assert_eq!(
984            errors, 1,
985            "expected exactly one Error event, for the one buffer that failed"
986        );
987    }
988
989    /// Control failures are asynchronous worker failures just like
990    /// `consume` failures: they must be visible on the Bus, but must not
991    /// prevent Pause/Resume/Stop acknowledgements or strand the worker.
992    #[test]
993    fn failing_control_is_reported_without_blocking_the_control_cascade() {
994        let sink = FailControl {
995            pp_log: element_pp_log(ElementType::Other, "fail-control", None),
996        };
997        let (bus, bus_rx) = Bus::new();
998        let mut queue = Queue::spawn_with_policy(
999            "test",
1000            1,
1001            Box::new(sink),
1002            bus,
1003            OverflowPolicy::default(),
1004            None,
1005        );
1006
1007        queue.control(ControlMsg::Pause).unwrap();
1008        queue.control(ControlMsg::Resume).unwrap();
1009        queue.control(ControlMsg::Stop).unwrap();
1010        drop(queue);
1011
1012        let errors: Vec<_> = bus_rx
1013            .iter()
1014            .filter(|event| matches!(event, BusEvent::Error { .. }))
1015            .collect();
1016        assert_eq!(
1017            errors.len(),
1018            3,
1019            "Pause, Resume, and Stop failures must each be reported once"
1020        );
1021    }
1022}