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}