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