Skip to main content

media_pp/core/
queue.rs

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