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