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}