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, RequestKind},
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((request, ack)) = control_rx.try_recv() {
373 let RequestKind::Control(msg) = request else {
374 let _ = ack.send(());
375 continue;
376 };
377 if apply_control(
378 &data_rx,
379 &mut downstream,
380 msg,
381 &ack,
382 &control_rx,
383 &error_reporter,
384 &stop,
385 ) {
386 pp_info!(pp_log: &pp_log, "worker: stopped");
387 return;
388 }
389 continue;
390 }
391
392 select! {
393 recv(control_rx.rx) -> req => {
394 match req {
395 Ok(req) => {
396 let RequestKind::Control(msg) = req.kind else {
397 let _ = req.ack.send(());
398 continue;
399 };
400 if apply_control(
401 &data_rx,
402 &mut downstream,
403 msg,
404 &req.ack,
405 &control_rx,
406 &error_reporter,
407 &stop,
408 ) {
409 pp_info!(pp_log: &pp_log, "worker: stopped");
410 return;
411 }
412 }
413 Err(_) => {
414 pp_info!(pp_log: &pp_log, "worker: control channel gone, ending");
415 return; // sender (this Queue) dropped
416 }
417 }
418 }
419 recv(data_rx) -> buf => {
420 match buf {
421 Ok(buf) => {
422 let is_eos = buf.is_eos();
423 match downstream.consume(buf) {
424 Ok(()) => {
425 if is_eos {
426 pp_trace!(
427 pp_log: &pp_log,
428 "event=eos phase=completed outcome=ok"
429 );
430 bus.post(
431 &pp_log,
432 BusEvent::Eos {
433 element_type: ElementType::Queue,
434 name: name.clone(),
435 },
436 );
437 return;
438 }
439 }
440 Err(error) => {
441 if is_eos {
442 pp_trace!(
443 pp_log: &pp_log,
444 "event=eos phase=completed outcome=error error={error}"
445 );
446 }
447 // Report and move on to the next buffer —
448 // this one's dropped, but nothing else
449 // dies over it. Whoever's watching the bus
450 // decides whether the error is fatal
451 // enough to call `Pipeline::stop`.
452 error_reporter.post(error);
453 }
454 }
455 }
456 Err(_) => {
457 pp_info!(pp_log: &pp_log, "worker: producer (this Queue) gone, ending");
458 return;
459 }
460 }
461 }
462 // Only reached once neither branch above had anything ready
463 // for a whole `STOP_POLL_INTERVAL` — real traffic on either
464 // channel always wins first. See `Queue::drop`.
465 default(STOP_POLL_INTERVAL) => {
466 if stop.load(Ordering::Relaxed) {
467 pp_info!(pp_log: &pp_log, "worker: stop flag set, ending");
468 return;
469 }
470 }
471 }
472 }
473}
474
475/// Applies one control message to `downstream`, acking it, then — only
476/// for `Pause` — blocking this thread on `control_rx` alone (never
477/// touching `data_rx`) until `Resume`/`Stop`. Returns `true` once `Stop`
478/// has been handled, meaning the caller (`worker_loop`) should exit.
479fn apply_control(
480 data_rx: &Receiver<MediaBuffer>,
481 downstream: &mut Box<dyn Sink>,
482 msg: ControlMsg,
483 ack: &Sender<()>,
484 control_rx: &ControlReceiver,
485 error_reporter: &QueueErrorReporter<'_>,
486 stop: &AtomicBool,
487) -> bool {
488 pp_trace!(
489 pp_log: error_reporter.pp_log,
490 "event=control control={msg:?} phase=forwarding"
491 );
492 discard_stale_data(data_rx, msg);
493 forward_control(downstream, msg, error_reporter);
494 let is_stop = msg == ControlMsg::Stop;
495 let _ = ack.send(());
496 if is_stop {
497 return true;
498 }
499 if msg != ControlMsg::Pause {
500 return false;
501 }
502 loop {
503 // `recv_timeout` (not `recv`) so `Queue::drop` setting `stop` can
504 // still wake a worker that's paused forever with no `Resume`/
505 // `Stop` ever coming (e.g. a bare `Queue`, not reached through a
506 // `Pipeline` — see `Queue::drop`'s docs on why this state is
507 // otherwise unreachable there). Nothing else feeds this queue
508 // while paused (see the type-level docs), so there's no
509 // legitimate traffic this could ever cut off.
510 let (msg, ack) = match control_rx.rx.recv_timeout(STOP_POLL_INTERVAL) {
511 Ok(req) => {
512 let RequestKind::Control(msg) = req.kind else {
513 let _ = req.ack.send(());
514 continue;
515 };
516 (msg, req.ack)
517 }
518 Err(RecvTimeoutError::Timeout) => {
519 if stop.load(Ordering::Relaxed) {
520 pp_info!(pp_log: error_reporter.pp_log, "worker: stop flag set while paused, ending");
521 return true;
522 }
523 continue;
524 }
525 Err(RecvTimeoutError::Disconnected) => {
526 pp_info!(pp_log: error_reporter.pp_log, "worker: control channel gone while paused, ending");
527 return true; // sender gone — treat like Stop
528 }
529 };
530 pp_trace!(
531 pp_log: error_reporter.pp_log,
532 "event=control control={msg:?} phase=forwarding"
533 );
534 discard_stale_data(data_rx, msg);
535 forward_control(downstream, msg, error_reporter);
536 let is_stop = msg == ControlMsg::Stop;
537 let _ = ack.send(());
538 if is_stop {
539 return true;
540 }
541 if msg == ControlMsg::Resume {
542 return false;
543 }
544 // Another Pause while already paused: already forwarded above, keep waiting.
545 }
546}
547
548struct QueueErrorReporter<'a> {
549 bus: &'a Bus,
550 name: &'a Arc<str>,
551 pp_log: &'a PpLog,
552}
553
554impl QueueErrorReporter<'_> {
555 fn post(&self, error: crate::error::Error) {
556 self.bus.post(
557 self.pp_log,
558 BusEvent::Error {
559 element_type: ElementType::Queue,
560 name: self.name.clone(),
561 error,
562 },
563 );
564 }
565}
566
567/// Forwards control without turning one downstream failure into a stuck
568/// synchronous caller or a dead Queue worker. The request is still acked by
569/// [`apply_control`], while the failure is exposed through the same Bus path
570/// used for `consume` failures.
571fn forward_control(
572 downstream: &mut Box<dyn Sink>,
573 msg: ControlMsg,
574 error_reporter: &QueueErrorReporter<'_>,
575) {
576 if let Err(error) = downstream.control(msg) {
577 error_reporter.post(error);
578 }
579}
580
581/// Drops everything already buffered in `data_rx` without processing it —
582/// only for `Seek`. That data predates the seek point (this Queue's
583/// worker hasn't gotten to it yet, but it was read/produced before the
584/// jump), so delivering it downstream afterward would show stale
585/// frames instead of skipping straight to the new position.
586/// `Pause`/`Resume`/`Stop` leave `data_rx` alone — see the type-level
587/// docs on why that's safe (nothing feeds a paused/stopped queue in the
588/// first place).
589fn discard_stale_data(data_rx: &Receiver<MediaBuffer>, msg: ControlMsg) {
590 if matches!(msg, ControlMsg::Seek(_)) {
591 while data_rx.try_recv().is_ok() {}
592 }
593}
594
595#[cfg(test)]
596mod tests {
597 use std::{
598 sync::{
599 Arc,
600 atomic::{AtomicUsize, Ordering},
601 },
602 thread,
603 time::Duration,
604 };
605
606 use super::*;
607 use crate::bus::Bus;
608
609 /// A downstream that's slower than the producer, so a small queue
610 /// behind it actually fills up during the test.
611 struct SlowCounter {
612 pp_log: PpLog,
613 count: Arc<AtomicUsize>,
614 }
615
616 impl Element for SlowCounter {
617 fn name(&self) -> Arc<str> {
618 "slow-counter".into()
619 }
620
621 fn element_type(&self) -> ElementType {
622 ElementType::Other
623 }
624
625 fn pp_log(&self) -> &PpLog {
626 &self.pp_log
627 }
628
629 fn pp_log_mut(&mut self) -> &mut PpLog {
630 &mut self.pp_log
631 }
632 }
633
634 impl Sink for SlowCounter {
635 fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
636 if let MediaBuffer::Packet(_) = buf {
637 thread::sleep(Duration::from_millis(20));
638 self.count.fetch_add(1, Ordering::SeqCst);
639 }
640 Ok(())
641 }
642
643 fn control(&mut self, _msg: ControlMsg) -> Result<()> {
644 Ok(())
645 }
646 }
647
648 fn packet() -> MediaBuffer {
649 MediaBuffer::Packet(Arc::new(ffmpeg_next::Packet::empty()))
650 }
651
652 #[test]
653 fn block_never_drops() {
654 let count = Arc::new(AtomicUsize::new(0));
655 let sink = SlowCounter {
656 count: count.clone(),
657 pp_log: element_pp_log(ElementType::Other, "slow-counter", None),
658 };
659 let (bus, bus_rx) = Bus::new();
660
661 let mut queue = Queue::spawn_with_policy(
662 "test",
663 1,
664 Box::new(sink),
665 bus,
666 OverflowPolicy::default(),
667 None,
668 );
669 for _ in 0..10 {
670 queue.consume(packet()).unwrap();
671 }
672 queue.consume(MediaBuffer::Eos).unwrap();
673 drop(queue); // blocks until the worker drains everything and joins
674
675 assert_eq!(count.load(Ordering::SeqCst), 10);
676 assert!(!bus_rx.iter().any(|e| matches!(e, BusEvent::Dropped { .. })));
677 }
678
679 #[test]
680 fn block_with_a_finite_timeout_errors_instead_of_blocking_forever() {
681 let count = Arc::new(AtomicUsize::new(0));
682 let sink = SlowCounter {
683 count: count.clone(),
684 pp_log: element_pp_log(ElementType::Other, "slow-counter", None),
685 };
686 let (bus, _bus_rx) = Bus::new();
687
688 // Capacity 1, downstream takes 20ms/item, timeout is 5ms — pushed
689 // in a tight loop, some of these sends must outlast their own
690 // timeout instead of blocking until the worker catches up.
691 let mut queue = Queue::spawn_with_policy(
692 "test",
693 1,
694 Box::new(sink),
695 bus,
696 OverflowPolicy::Block(Duration::from_millis(5)),
697 None,
698 );
699 let mut timed_out = 0;
700 for _ in 0..10 {
701 match queue.consume(packet()) {
702 Ok(()) => {}
703 Err(_) => timed_out += 1,
704 }
705 }
706 // Eos isn't subject to the timeout (see `Sink::consume`'s own
707 // special-casing) — always goes through even after some sends
708 // above timed out.
709 queue.consume(MediaBuffer::Eos).unwrap();
710 drop(queue); // blocks until the worker drains everything and joins
711
712 assert!(
713 timed_out > 0,
714 "expected at least one send to time out against a downstream that can't keep up"
715 );
716 }
717
718 #[test]
719 fn drop_newest_drops_when_full_and_reports_on_bus() {
720 let count = Arc::new(AtomicUsize::new(0));
721 let sink = SlowCounter {
722 count: count.clone(),
723 pp_log: element_pp_log(ElementType::Other, "slow-counter", None),
724 };
725 let (bus, bus_rx) = Bus::new();
726
727 let mut queue = Queue::spawn_with_policy(
728 "test",
729 1,
730 Box::new(sink),
731 bus,
732 OverflowPolicy::DropNewest,
733 None,
734 );
735 // Pushed much faster than the 20ms/item downstream can drain a
736 // capacity-1 channel, so some of these must get dropped.
737 for _ in 0..10 {
738 queue.consume(packet()).unwrap();
739 }
740 queue.consume(MediaBuffer::Eos).unwrap(); // never dropped, even under this policy
741 drop(queue);
742
743 let processed = count.load(Ordering::SeqCst);
744 let dropped = bus_rx
745 .iter()
746 .filter(|e| matches!(e, BusEvent::Dropped { .. }))
747 .count();
748
749 assert!(
750 processed < 10,
751 "expected some packets to be dropped, but all {processed} were processed"
752 );
753 assert!(dropped > 0, "expected at least one BusEvent::Dropped");
754 assert_eq!(processed + dropped, 10);
755 }
756
757 #[test]
758 fn pause_stops_delivery_and_resume_lets_it_continue() {
759 let count = Arc::new(AtomicUsize::new(0));
760 let sink = SlowCounter {
761 count: count.clone(),
762 pp_log: element_pp_log(ElementType::Other, "slow-counter", None),
763 };
764 let (bus, _bus_rx) = Bus::new();
765
766 let mut queue = Queue::spawn_with_policy(
767 "test",
768 8,
769 Box::new(sink),
770 bus,
771 OverflowPolicy::default(),
772 None,
773 );
774 queue.control(ControlMsg::Pause).unwrap(); // blocks until the worker is actually paused
775
776 for _ in 0..3 {
777 queue.consume(packet()).unwrap();
778 }
779 // Worker is paused and not touching data_rx — nothing should have
780 // been processed yet, however long we wait.
781 thread::sleep(Duration::from_millis(100));
782 assert_eq!(count.load(Ordering::SeqCst), 0);
783
784 queue.control(ControlMsg::Resume).unwrap();
785 queue.consume(MediaBuffer::Eos).unwrap();
786 drop(queue);
787
788 assert_eq!(count.load(Ordering::SeqCst), 3);
789 }
790
791 /// Regression test: before `Queue::drop` set its own `stop` flag,
792 /// dropping a `Queue` that was never fed a `Stop` control message or
793 /// an `Eos` buffer left its worker thread parked on `recv()` with
794 /// nothing left to wake it — `drop()`'s own `handle.join()` then hung
795 /// forever. This mirrors what happens to a `.queue()`-containing
796 /// `Pipeline` that's dropped without ever being `run()`, so if this
797 /// test hangs, that fix regressed.
798 #[test]
799 fn dropping_without_stop_or_eos_does_not_hang() {
800 let count = Arc::new(AtomicUsize::new(0));
801 let sink = SlowCounter {
802 count: count.clone(),
803 pp_log: element_pp_log(ElementType::Other, "slow-counter", None),
804 };
805 let (bus, _bus_rx) = Bus::new();
806
807 let queue = Queue::spawn_with_policy(
808 "test",
809 8,
810 Box::new(sink),
811 bus,
812 OverflowPolicy::default(),
813 None,
814 );
815 drop(queue);
816 }
817
818 /// Regression test for the other half of the same bug: a worker
819 /// that's specifically inside `apply_control`'s pause loop (blocked on
820 /// `control_rx` alone, not `data_rx`) when dropped without ever
821 /// getting `Resume`/`Stop` — only reachable by pausing a bare `Queue`
822 /// directly (a `Pipeline`-owned one can't be dropped in this state,
823 /// see `Queue::drop`'s docs), but the `stop` flag has to wake this
824 /// wait loop too, not just `worker_loop`'s.
825 #[test]
826 fn dropping_while_paused_does_not_hang() {
827 let count = Arc::new(AtomicUsize::new(0));
828 let sink = SlowCounter {
829 count: count.clone(),
830 pp_log: element_pp_log(ElementType::Other, "slow-counter", None),
831 };
832 let (bus, _bus_rx) = Bus::new();
833
834 let mut queue = Queue::spawn_with_policy(
835 "test",
836 8,
837 Box::new(sink),
838 bus,
839 OverflowPolicy::default(),
840 None,
841 );
842 queue.control(ControlMsg::Pause).unwrap(); // blocks until the worker is actually paused
843 drop(queue);
844 }
845
846 #[test]
847 fn stop_is_synchronous_and_terminates_the_worker() {
848 let count = Arc::new(AtomicUsize::new(0));
849 let sink = SlowCounter {
850 count: count.clone(),
851 pp_log: element_pp_log(ElementType::Other, "slow-counter", None),
852 };
853 let (bus, _bus_rx) = Bus::new();
854
855 let mut queue = Queue::spawn_with_policy(
856 "test",
857 8,
858 Box::new(sink),
859 bus,
860 OverflowPolicy::default(),
861 None,
862 );
863 queue.consume(packet()).unwrap();
864 queue.control(ControlMsg::Stop).unwrap(); // blocks until the worker has exited
865 drop(queue); // join should return immediately — the worker already returned
866 }
867
868 /// A downstream that fails on the very first `Packet` it sees, then
869 /// behaves like `SlowCounter` for every one after.
870 struct FailFirstThenCount {
871 pp_log: PpLog,
872 count: Arc<AtomicUsize>,
873 failed_once: bool,
874 }
875
876 impl Element for FailFirstThenCount {
877 fn name(&self) -> Arc<str> {
878 "fail-first".into()
879 }
880
881 fn element_type(&self) -> ElementType {
882 ElementType::Other
883 }
884
885 fn pp_log(&self) -> &PpLog {
886 &self.pp_log
887 }
888
889 fn pp_log_mut(&mut self) -> &mut PpLog {
890 &mut self.pp_log
891 }
892 }
893
894 impl Sink for FailFirstThenCount {
895 fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
896 let MediaBuffer::Packet(_) = buf else {
897 return Ok(());
898 };
899 if !self.failed_once {
900 self.failed_once = true;
901 return Err(crate::error::Error::Other("simulated failure".into()));
902 }
903 self.count.fetch_add(1, Ordering::SeqCst);
904 Ok(())
905 }
906
907 fn control(&mut self, _msg: ControlMsg) -> Result<()> {
908 Ok(())
909 }
910 }
911
912 struct FailControl {
913 pp_log: PpLog,
914 }
915
916 impl Element for FailControl {
917 fn name(&self) -> Arc<str> {
918 "fail-control".into()
919 }
920
921 fn element_type(&self) -> ElementType {
922 ElementType::Other
923 }
924
925 fn pp_log(&self) -> &PpLog {
926 &self.pp_log
927 }
928
929 fn pp_log_mut(&mut self) -> &mut PpLog {
930 &mut self.pp_log
931 }
932 }
933
934 impl Sink for FailControl {
935 fn consume(&mut self, _buf: MediaBuffer) -> Result<()> {
936 Ok(())
937 }
938
939 fn control(&mut self, msg: ControlMsg) -> Result<()> {
940 Err(crate::error::Error::Other(format!(
941 "simulated {msg:?} failure"
942 )))
943 }
944 }
945
946 /// Regression test for the design change prompted by the `NoFreeSlot`
947 /// investigation: a `Sink::consume` failure used to end the worker
948 /// thread outright (and, transitively, everything upstream once its
949 /// data channel closed). Now it's just one dropped buffer — the
950 /// worker keeps running, later buffers still get through, and exactly
951 /// one `BusEvent::Error` shows up for the one that failed.
952 #[test]
953 fn a_failing_consume_drops_that_buffer_but_keeps_the_worker_alive() {
954 let count = Arc::new(AtomicUsize::new(0));
955 let sink = FailFirstThenCount {
956 count: count.clone(),
957 failed_once: false,
958 pp_log: element_pp_log(ElementType::Other, "fail-first", None),
959 };
960 let (bus, bus_rx) = Bus::new();
961
962 let mut queue = Queue::spawn_with_policy(
963 "test",
964 8,
965 Box::new(sink),
966 bus,
967 OverflowPolicy::default(),
968 None,
969 );
970 for _ in 0..3 {
971 queue.consume(packet()).unwrap();
972 }
973 queue.consume(MediaBuffer::Eos).unwrap();
974 drop(queue); // blocks until the worker drains everything and joins
975
976 // First packet failed (and was dropped); the other two still went
977 // through — the worker didn't die over the first one.
978 assert_eq!(count.load(Ordering::SeqCst), 2);
979 let errors = bus_rx
980 .iter()
981 .filter(|e| matches!(e, BusEvent::Error { .. }))
982 .count();
983 assert_eq!(
984 errors, 1,
985 "expected exactly one Error event, for the one buffer that failed"
986 );
987 }
988
989 /// Control failures are asynchronous worker failures just like
990 /// `consume` failures: they must be visible on the Bus, but must not
991 /// prevent Pause/Resume/Stop acknowledgements or strand the worker.
992 #[test]
993 fn failing_control_is_reported_without_blocking_the_control_cascade() {
994 let sink = FailControl {
995 pp_log: element_pp_log(ElementType::Other, "fail-control", None),
996 };
997 let (bus, bus_rx) = Bus::new();
998 let mut queue = Queue::spawn_with_policy(
999 "test",
1000 1,
1001 Box::new(sink),
1002 bus,
1003 OverflowPolicy::default(),
1004 None,
1005 );
1006
1007 queue.control(ControlMsg::Pause).unwrap();
1008 queue.control(ControlMsg::Resume).unwrap();
1009 queue.control(ControlMsg::Stop).unwrap();
1010 drop(queue);
1011
1012 let errors: Vec<_> = bus_rx
1013 .iter()
1014 .filter(|event| matches!(event, BusEvent::Error { .. }))
1015 .collect();
1016 assert_eq!(
1017 errors.len(),
1018 3,
1019 "Pause, Resume, and Stop failures must each be reported once"
1020 );
1021 }
1022}