use std::collections::{HashMap, VecDeque};
use std::future::Future;
use std::num::NonZeroUsize;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
use transmux::TrackId;
use parking_lot::{Condvar, Mutex, MutexGuard};
use crate::ids::{PartIndex, SegmentSeq};
use broadcast_common::stage::Timestamp;
use bytes::Bytes;
use event_listener::{Event, EventListener, Listener};
use thiserror::Error;
use timed_metadata::{MediaTime, PTS_HZ, TimeAnchor, TimedEvent};
pub use event_listener::EventListener as SlotFreedFuture;
use transmux::{Sample, SegmentMeta, TrackSpec};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum RetentionClass {
Timed,
Sparse,
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct TrunkConfig {
pub timed_capacity: NonZeroUsize,
pub sparse_capacity: NonZeroUsize,
pub segment_capacity: NonZeroUsize,
pub event_capacity: NonZeroUsize,
pub part_capacity: NonZeroUsize,
}
impl TrunkConfig {
pub fn new(
timed_capacity: NonZeroUsize,
sparse_capacity: NonZeroUsize,
segment_capacity: NonZeroUsize,
event_capacity: NonZeroUsize,
part_capacity: NonZeroUsize,
) -> Self {
TrunkConfig {
timed_capacity,
sparse_capacity,
segment_capacity,
event_capacity,
part_capacity,
}
}
}
struct ClassLog {
entries: VecDeque<(TrackId, Sample)>,
base: u64,
published: u64,
capacity: usize,
}
impl ClassLog {
fn new(capacity: usize) -> Self {
ClassLog {
entries: VecDeque::with_capacity(capacity),
base: 0,
published: 0,
capacity,
}
}
fn push(&mut self, track_id: TrackId, sample: Sample) {
if self.entries.len() == self.capacity {
self.entries.pop_front();
self.base += 1;
}
self.entries.push_back((track_id, sample));
self.published += 1;
}
}
const WRITER_HANDOVER_LOG_CAPACITY: usize = 32;
#[derive(Debug, Clone, Copy)]
struct WriterHandover {
timed_at: u64,
sparse_at: u64,
}
struct WriterHandoverLog {
entries: VecDeque<WriterHandover>,
base: u64,
published: u64,
capacity: usize,
}
impl WriterHandoverLog {
fn new(capacity: usize) -> Self {
WriterHandoverLog {
entries: VecDeque::with_capacity(capacity),
base: 0,
published: 0,
capacity,
}
}
fn push(&mut self, handover: WriterHandover) {
if self.entries.len() == self.capacity {
self.entries.pop_front();
self.base += 1;
}
self.entries.push_back(handover);
self.published += 1;
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SegmentEntry {
pub bytes: Bytes,
pub sequence_number: SegmentSeq,
pub duration: Duration,
pub timeline_position: Timestamp,
pub meta: SegmentMeta,
}
impl SegmentEntry {
pub fn new(
bytes: impl Into<Bytes>,
sequence_number: SegmentSeq,
duration: Duration,
timeline_position: Timestamp,
meta: SegmentMeta,
) -> Self {
SegmentEntry {
bytes: bytes.into(),
sequence_number,
duration,
timeline_position,
meta,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ArchiveOverrun {
Gap,
StallIngest,
Terminate,
}
impl Default for ArchiveOverrun {
fn default() -> Self {
ArchiveOverrun::Gap
}
}
struct PinState {
consumed: u64,
policy: ArchiveOverrun,
terminated: bool,
}
struct SegmentLog {
entries: VecDeque<SegmentEntry>,
base: u64,
published: u64,
capacity: usize,
pins: HashMap<u64, PinState>,
next_pin_id: u64,
}
impl SegmentLog {
fn new(capacity: usize) -> Self {
SegmentLog {
entries: VecDeque::with_capacity(capacity),
base: 0,
published: 0,
capacity,
pins: HashMap::new(),
next_pin_id: 0,
}
}
fn push(&mut self, entry: SegmentEntry) {
if self.entries.len() == self.capacity {
self.entries.pop_front();
self.base += 1;
}
self.entries.push_back(entry);
self.published += 1;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum EventAnchor {
Media(MediaTime),
Segment {
segment_number: SegmentSeq,
delta: u64,
},
Utc {
utc_epoch_ms: i64,
},
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct EventEntry {
pub event: TimedEvent,
pub anchor: EventAnchor,
}
struct EventLog {
entries: VecDeque<EventEntry>,
base: u64,
published: u64,
capacity: usize,
segment_starts: VecDeque<(SegmentSeq, MediaTime)>,
time_anchor: Option<TimeAnchor>,
}
impl EventLog {
fn new(capacity: usize) -> Self {
EventLog {
entries: VecDeque::with_capacity(capacity),
base: 0,
published: 0,
capacity,
segment_starts: VecDeque::with_capacity(capacity),
time_anchor: None,
}
}
fn try_resolve(&self, anchor: EventAnchor) -> EventAnchor {
match anchor {
EventAnchor::Segment {
segment_number,
delta,
} => self
.segment_starts
.iter()
.rev()
.find(|(n, _)| *n == segment_number)
.map(|(_, start)| EventAnchor::Media(MediaTime(start.0.saturating_add(delta))))
.unwrap_or(anchor),
EventAnchor::Utc { utc_epoch_ms } => self
.time_anchor
.as_ref()
.and_then(|a| epoch_ms_to_media(a, utc_epoch_ms))
.map(EventAnchor::Media)
.unwrap_or(anchor),
EventAnchor::Media(_) => anchor,
}
}
fn push(&mut self, event: TimedEvent, anchor: EventAnchor) {
let anchor = self.try_resolve(anchor);
if self.entries.len() == self.capacity {
self.entries.pop_front();
self.base += 1;
}
self.entries.push_back(EventEntry { event, anchor });
self.published += 1;
}
fn note_segment_start(&mut self, segment_number: SegmentSeq, start: MediaTime) {
if self.segment_starts.len() == self.capacity {
self.segment_starts.pop_front();
}
self.segment_starts.push_back((segment_number, start));
for entry in &mut self.entries {
if let EventAnchor::Segment {
segment_number: n,
delta,
} = entry.anchor
&& n == segment_number
{
entry.anchor = EventAnchor::Media(MediaTime(start.0.saturating_add(delta)));
}
}
}
fn set_time_anchor(&mut self, anchor: TimeAnchor) {
self.time_anchor = Some(anchor);
for entry in &mut self.entries {
if let EventAnchor::Utc { utc_epoch_ms } = entry.anchor {
if let Some(media) = epoch_ms_to_media(&anchor, utc_epoch_ms) {
entry.anchor = EventAnchor::Media(media);
}
}
}
}
}
fn epoch_ms_to_media(anchor: &TimeAnchor, utc_epoch_ms: i64) -> Option<MediaTime> {
let delta_ms = i128::from(utc_epoch_ms) - i128::from(anchor.utc_epoch_ms);
let delta_ticks = delta_ms * i128::from(PTS_HZ) / 1000;
let media = i128::from(anchor.pts_90k) + delta_ticks;
u64::try_from(media).ok().map(MediaTime)
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct PartEntry {
pub bytes: Bytes,
pub segment_number: SegmentSeq,
pub part_index: PartIndex,
pub duration: Duration,
pub independent: bool,
}
impl PartEntry {
pub fn new(
bytes: impl Into<Bytes>,
segment_number: SegmentSeq,
part_index: PartIndex,
duration: Duration,
independent: bool,
) -> Self {
PartEntry {
bytes: bytes.into(),
segment_number,
part_index,
duration,
independent,
}
}
}
struct PartRing {
q: VecDeque<PartEntry>,
#[cfg(test)]
visits: core::sync::atomic::AtomicUsize,
}
impl PartRing {
fn with_capacity(capacity: usize) -> Self {
PartRing {
q: VecDeque::with_capacity(capacity),
#[cfg(test)]
visits: core::sync::atomic::AtomicUsize::new(0),
}
}
fn len(&self) -> usize {
self.q.len()
}
fn pop_front(&mut self) -> Option<PartEntry> {
self.q.pop_front()
}
fn push_back(&mut self, e: PartEntry) {
self.q.push_back(e);
}
fn get(&self, idx: usize) -> Option<&PartEntry> {
#[cfg(test)]
self.visits
.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
self.q.get(idx)
}
#[cfg(test)]
fn iter(&self) -> impl DoubleEndedIterator<Item = &PartEntry> {
self.q.iter().inspect(|_| {
self.visits
.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
})
}
}
struct PartLog {
entries: PartRing,
base: u64,
published: u64,
capacity: usize,
by_segment: HashMap<SegmentSeq, VecDeque<u64>>,
}
impl PartLog {
fn new(capacity: usize) -> Self {
PartLog {
entries: PartRing::with_capacity(capacity),
base: 0,
published: 0,
capacity,
by_segment: HashMap::new(),
}
}
fn push(&mut self, entry: PartEntry) {
if self.entries.len() == self.capacity
&& let Some(evicted) = self.entries.pop_front()
{
if let Some(positions) = self.by_segment.get_mut(&evicted.segment_number) {
positions.pop_front();
if positions.is_empty() {
self.by_segment.remove(&evicted.segment_number);
}
}
self.base += 1;
}
self.by_segment
.entry(entry.segment_number)
.or_default()
.push_back(self.published);
self.entries.push_back(entry);
self.published += 1;
}
fn at(&self, abs: u64) -> Option<&PartEntry> {
let idx = usize::try_from(abs.checked_sub(self.base)?).ok()?;
self.entries.get(idx)
}
fn in_segment(&self, segment_number: SegmentSeq) -> Vec<PartEntry> {
self.by_segment
.get(&segment_number)
.into_iter()
.flatten()
.filter_map(|&abs| self.at(abs).cloned())
.collect()
}
fn part_bytes(&self, segment_number: SegmentSeq, part_index: PartIndex) -> Option<Bytes> {
self.by_segment
.get(&segment_number)?
.iter()
.rev()
.filter_map(|&abs| self.at(abs))
.find(|p| p.part_index == part_index)
.map(|p| p.bytes.clone())
}
}
fn ring_offset(consumed: u64, base: u64) -> Option<usize> {
usize::try_from(consumed.checked_sub(base)?).ok()
}
struct TrunkState {
timed: ClassLog,
sparse: ClassLog,
segments: SegmentLog,
events: EventLog,
parts: PartLog,
handovers: WriterHandoverLog,
tracks: Arc<[TrackSpec]>,
track_generation: u64,
}
pub struct Trunk {
state: Mutex<TrunkState>,
segment_pin_released: Condvar,
writer_taken: AtomicBool,
segment_writer_taken: AtomicBool,
writer_issued_once: AtomicBool,
progress: Event,
slot_freed: Event,
waiter_count: AtomicUsize,
#[cfg(test)]
stalled_publishers: AtomicUsize,
part_waiter_cap: usize,
}
impl Trunk {
pub fn new(config: TrunkConfig) -> Arc<Trunk> {
Arc::new(Trunk {
state: Mutex::new(TrunkState {
timed: ClassLog::new(config.timed_capacity.get()),
sparse: ClassLog::new(config.sparse_capacity.get()),
segments: SegmentLog::new(config.segment_capacity.get()),
events: EventLog::new(config.event_capacity.get()),
parts: PartLog::new(config.part_capacity.get()),
handovers: WriterHandoverLog::new(WRITER_HANDOVER_LOG_CAPACITY),
tracks: Arc::from(Vec::new()),
track_generation: 0,
}),
segment_pin_released: Condvar::new(),
writer_taken: AtomicBool::new(false),
segment_writer_taken: AtomicBool::new(false),
writer_issued_once: AtomicBool::new(false),
progress: Event::new(),
slot_freed: Event::new(),
waiter_count: AtomicUsize::new(0),
#[cfg(test)]
stalled_publishers: AtomicUsize::new(0),
part_waiter_cap: config.part_capacity.get(),
})
}
fn lock_state(&self) -> MutexGuard<'_, TrunkState> {
self.state.lock()
}
pub fn writer(self: &Arc<Self>) -> Option<TrunkWriter> {
self.writer_taken
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.ok()?;
if self.writer_issued_once.swap(true, Ordering::AcqRel) {
let mut state = self.lock_state();
let handover = WriterHandover {
timed_at: state.timed.published,
sparse_at: state.sparse.published,
};
state.handovers.push(handover);
}
Some(TrunkWriter {
trunk: Arc::clone(self),
})
}
pub fn segment_writer(self: &Arc<Self>) -> Option<SegmentWriter> {
self.segment_writer_taken
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.ok()
.map(|_| SegmentWriter {
trunk: Arc::clone(self),
})
}
pub fn subscribe(self: &Arc<Self>) -> SampleCursor {
let state = self.lock_state();
SampleCursor {
trunk: Arc::clone(self),
timed_consumed: state.timed.published,
sparse_consumed: state.sparse.published,
handovers_consumed: state.handovers.published,
subscribe_timed_at: state.timed.published,
subscribe_sparse_at: state.sparse.published,
}
}
pub fn subscribe_from_backlog(self: &Arc<Self>) -> SampleCursor {
let state = self.lock_state();
SampleCursor {
trunk: Arc::clone(self),
timed_consumed: state.timed.base,
sparse_consumed: state.sparse.base,
handovers_consumed: state.handovers.base,
subscribe_timed_at: state.timed.base,
subscribe_sparse_at: state.sparse.base,
}
}
pub fn timed_len(&self) -> usize {
self.lock_state().timed.entries.len()
}
#[cfg(test)]
pub(crate) fn sparse_len(&self) -> usize {
self.lock_state().sparse.entries.len()
}
pub fn subscribe_segments(self: &Arc<Self>) -> SegmentCursor {
let state = self.lock_state();
SegmentCursor {
trunk: Arc::clone(self),
consumed: state.segments.published,
pin_id: None,
done: false,
}
}
pub fn pin_segments(self: &Arc<Self>, on_overrun: ArchiveOverrun) -> SegmentCursor {
let mut state = self.lock_state();
let pin_id = state.segments.next_pin_id;
state.segments.next_pin_id += 1;
let consumed = state.segments.published;
state.segments.pins.insert(
pin_id,
PinState {
consumed,
policy: on_overrun,
terminated: false,
},
);
SegmentCursor {
trunk: Arc::clone(self),
consumed: 0,
pin_id: Some(pin_id),
done: false,
}
}
pub fn segment_len(&self) -> usize {
self.lock_state().segments.entries.len()
}
pub fn subscribe_events(self: &Arc<Self>) -> EventCursor {
let state = self.lock_state();
EventCursor {
trunk: Arc::clone(self),
consumed: state.events.published,
}
}
pub fn events_between(&self, from: MediaTime, to: MediaTime) -> Vec<EventEntry> {
let state = self.lock_state();
state
.events
.entries
.iter()
.filter(|e| matches!(e.anchor, EventAnchor::Media(t) if t.0 >= from.0 && t.0 < to.0))
.cloned()
.collect()
}
pub fn time_anchor(&self) -> Option<TimeAnchor> {
let state = self.lock_state();
state.events.time_anchor
}
pub fn segment_start_time(&self, seq: SegmentSeq) -> Option<MediaTime> {
let state = self.lock_state();
let log = &state.events;
log.segment_starts
.iter()
.rev()
.find(|(n, _)| *n == seq)
.map(|(_, start)| *start)
}
pub fn events_in_segment(&self, segment_number: SegmentSeq) -> Vec<EventEntry> {
let state = self.lock_state();
let log = &state.events;
let Some(chosen_idx) = log
.segment_starts
.iter()
.rposition(|(n, _)| *n == segment_number)
else {
return Vec::new();
};
let Some(&(_, start)) = log.segment_starts.get(chosen_idx) else {
return Vec::new();
};
let end = log.segment_starts.get(chosen_idx + 1).map(|&(_, s)| s.0);
log.entries
.iter()
.filter(|e| match e.anchor {
EventAnchor::Media(t) => t.0 >= start.0 && end.map(|e2| t.0 < e2).unwrap_or(true),
_ => false,
})
.cloned()
.collect()
}
#[cfg(test)]
pub(crate) fn event_len(&self) -> usize {
self.lock_state().events.entries.len()
}
pub fn part_bytes(&self, segment_number: SegmentSeq, part_index: PartIndex) -> Option<Bytes> {
self.lock_state()
.parts
.part_bytes(segment_number, part_index)
}
pub fn parts_in_segment(&self, segment_number: SegmentSeq) -> Vec<PartEntry> {
self.lock_state().parts.in_segment(segment_number)
}
pub fn part_len(&self) -> usize {
self.lock_state().parts.entries.len()
}
pub fn waiter_count(&self) -> usize {
self.waiter_count.load(Ordering::Acquire)
}
pub fn last_closed_segment(&self) -> Option<SegmentSeq> {
self.lock_state()
.segments
.entries
.back()
.map(|e| e.sequence_number)
}
pub fn tracks(&self) -> Arc<[TrackSpec]> {
Arc::clone(&self.lock_state().tracks)
}
pub fn track_generation(&self) -> u64 {
self.lock_state().track_generation
}
pub fn listen(self: &Arc<Self>) -> Option<ProgressListener> {
loop {
let current = self.waiter_count.load(Ordering::Acquire);
if current >= self.part_waiter_cap {
return None;
}
if self
.waiter_count
.compare_exchange(current, current + 1, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
break;
}
}
Some(ProgressListener {
_slot: WaiterSlot(Arc::clone(self)),
listener: self.progress.listen(),
})
}
pub fn waiter_slot_freed(&self) -> Option<EventListener> {
let listener = self.slot_freed.listen();
if self.waiter_count.load(Ordering::Acquire) < self.part_waiter_cap {
return None;
}
Some(listener)
}
}
struct WaiterSlot(Arc<Trunk>);
impl Drop for WaiterSlot {
fn drop(&mut self) {
self.0.waiter_count.fetch_sub(1, Ordering::AcqRel);
self.0.slot_freed.notify(1);
}
}
pub struct ProgressListener {
_slot: WaiterSlot,
listener: EventListener,
}
impl ProgressListener {
pub fn wait_deadline(self, deadline: std::time::Instant) -> bool {
let ProgressListener { _slot, listener } = self;
listener.wait_deadline(deadline).is_some()
}
}
impl Future for ProgressListener {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
Pin::new(&mut this.listener).poll(cx)
}
}
pub struct TrunkWriter {
trunk: Arc<Trunk>,
}
impl TrunkWriter {
pub fn publish(&self, track_id: TrackId, retention: RetentionClass, sample: Sample) {
let mut state = self.trunk.lock_state();
match retention {
RetentionClass::Timed => state.timed.push(track_id, sample),
RetentionClass::Sparse => state.sparse.push(track_id, sample),
}
}
pub fn publish_event(&self, event: TimedEvent, anchor: EventAnchor) {
let mut state = self.trunk.lock_state();
state.events.push(event, anchor);
}
pub fn set_tracks(&self, tracks: Vec<TrackSpec>) {
let mut state = self.trunk.lock_state();
state.tracks = Arc::from(tracks);
state.track_generation += 1;
drop(state);
self.trunk.progress.notify(usize::MAX);
}
}
impl Drop for TrunkWriter {
fn drop(&mut self) {
self.trunk.writer_taken.store(false, Ordering::Release);
}
}
#[cfg(not(test))]
const STALL_INGEST_MAX_WAIT: Duration = Duration::from_secs(30);
#[cfg(test)]
const STALL_INGEST_MAX_WAIT: Duration = Duration::from_secs(2);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[non_exhaustive]
#[error(
"segment sequence_number {attempted} is not greater than the last \
published one ({last_published:?}) — sequence numbers are monotonic \
across writer re-issues; see SegmentWriter::next_sequence_number"
)]
pub struct NonMonotonicSequenceNumber {
pub attempted: SegmentSeq,
pub last_published: Option<SegmentSeq>,
}
#[derive(Debug, Clone, Error)]
#[non_exhaustive]
pub enum TryPublishSegmentError {
#[error("would need to block for an ArchiveOverrun::StallIngest pin to catch up")]
WouldStall(SegmentEntry),
#[error(transparent)]
NonMonotonic(#[from] NonMonotonicSequenceNumber),
}
pub struct SegmentWriter {
trunk: Arc<Trunk>,
}
impl SegmentWriter {
pub fn publish_segment(&self, entry: SegmentEntry) -> Result<(), NonMonotonicSequenceNumber> {
let mut state = self.trunk.lock_state();
if let Some(last) = state.segments.entries.back().map(|e| e.sequence_number)
&& entry.sequence_number <= last
{
return Err(NonMonotonicSequenceNumber {
attempted: entry.sequence_number,
last_published: Some(last),
});
}
let deadline = Instant::now() + STALL_INGEST_MAX_WAIT;
loop {
if state.segments.entries.len() < state.segments.capacity {
break;
}
let oldest = state.segments.base;
let mut must_wait = false;
for pin in state.segments.pins.values_mut() {
if pin.terminated || pin.consumed > oldest {
continue;
}
match pin.policy {
ArchiveOverrun::Gap => {}
ArchiveOverrun::Terminate => pin.terminated = true,
ArchiveOverrun::StallIngest => must_wait = true,
}
}
if !must_wait {
break;
}
if Instant::now() >= deadline {
for pin in state.segments.pins.values_mut() {
if !pin.terminated
&& pin.consumed <= oldest
&& pin.policy == ArchiveOverrun::StallIngest
{
pin.terminated = true;
}
}
break;
}
#[cfg(test)]
self.trunk.stalled_publishers.fetch_add(1, Ordering::AcqRel);
self.trunk
.segment_pin_released
.wait_until(&mut state, deadline);
#[cfg(test)]
self.trunk.stalled_publishers.fetch_sub(1, Ordering::AcqRel);
}
state.segments.push(entry);
drop(state);
self.trunk.progress.notify(usize::MAX);
Ok(())
}
pub fn next_sequence_number(&self) -> SegmentSeq {
self.trunk
.lock_state()
.segments
.entries
.back()
.map(|e| e.sequence_number.saturating_next())
.unwrap_or(SegmentSeq::new(1))
}
pub fn expire_stalled_pins(&self) -> usize {
let mut state = self.trunk.lock_state();
if state.segments.entries.len() < state.segments.capacity {
return 0;
}
let oldest = state.segments.base;
let mut count = 0;
for pin in state.segments.pins.values_mut() {
if !pin.terminated
&& pin.consumed <= oldest
&& pin.policy == ArchiveOverrun::StallIngest
{
pin.terminated = true;
count += 1;
}
}
count
}
pub fn try_publish_segment(&self, entry: SegmentEntry) -> Result<(), TryPublishSegmentError> {
let mut state = self.trunk.lock_state();
if let Some(last) = state.segments.entries.back().map(|e| e.sequence_number)
&& entry.sequence_number <= last
{
return Err(TryPublishSegmentError::NonMonotonic(
NonMonotonicSequenceNumber {
attempted: entry.sequence_number,
last_published: Some(last),
},
));
}
if state.segments.entries.len() >= state.segments.capacity {
let oldest = state.segments.base;
let must_wait = state.segments.pins.values().any(|pin| {
!pin.terminated
&& pin.consumed <= oldest
&& pin.policy == ArchiveOverrun::StallIngest
});
if must_wait {
return Err(TryPublishSegmentError::WouldStall(entry));
}
for pin in state.segments.pins.values_mut() {
if !pin.terminated
&& pin.consumed <= oldest
&& pin.policy == ArchiveOverrun::Terminate
{
pin.terminated = true;
}
}
}
state.segments.push(entry);
drop(state);
self.trunk.progress.notify(usize::MAX);
Ok(())
}
pub fn publish_part(&self, entry: PartEntry) {
let mut state = self.trunk.lock_state();
state.parts.push(entry);
drop(state);
self.trunk.progress.notify(usize::MAX);
}
pub fn note_segment_start(&self, segment_number: SegmentSeq, start: MediaTime) {
let mut state = self.trunk.lock_state();
state.events.note_segment_start(segment_number, start);
}
pub fn set_time_anchor(&self, anchor: TimeAnchor) {
let mut state = self.trunk.lock_state();
state.events.set_time_anchor(anchor);
}
}
impl Drop for SegmentWriter {
fn drop(&mut self) {
self.trunk
.segment_writer_taken
.store(false, Ordering::Release);
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum SampleCursorItem {
Timed {
track_id: TrackId,
sample: Sample,
},
Sparse {
track_id: TrackId,
sample: Sample,
},
Lagged {
skipped: u64,
},
Degraded {
skipped: u64,
},
Discontinuity,
HandoversLagged {
skipped: u64,
},
}
pub struct SampleCursor {
trunk: Arc<Trunk>,
timed_consumed: u64,
sparse_consumed: u64,
handovers_consumed: u64,
subscribe_timed_at: u64,
subscribe_sparse_at: u64,
}
impl SampleCursor {
pub fn poll(&mut self) -> Option<SampleCursorItem> {
let state = self.trunk.lock_state();
if self.timed_consumed < state.timed.base {
let skipped = state.timed.base - self.timed_consumed;
self.timed_consumed = state.timed.base;
return Some(SampleCursorItem::Lagged { skipped });
}
if self.sparse_consumed < state.sparse.base {
let skipped = state.sparse.base - self.sparse_consumed;
self.sparse_consumed = state.sparse.base;
return Some(SampleCursorItem::Degraded { skipped });
}
if self.handovers_consumed < state.handovers.base {
let skipped = state.handovers.base - self.handovers_consumed;
self.handovers_consumed = state.handovers.base;
return Some(SampleCursorItem::HandoversLagged { skipped });
}
let mut pending_handover = if self.handovers_consumed < state.handovers.published {
let idx =
ring_offset(self.handovers_consumed, state.handovers.base).unwrap_or(usize::MAX);
state.handovers.entries.get(idx).copied()
} else {
None
};
while let Some(handover) = pending_handover {
if !(self.timed_consumed >= handover.timed_at
&& self.sparse_consumed >= handover.sparse_at)
{
break;
}
self.handovers_consumed += 1;
if handover.timed_at > self.subscribe_timed_at
|| handover.sparse_at > self.subscribe_sparse_at
{
return Some(SampleCursorItem::Discontinuity);
}
pending_handover = if self.handovers_consumed < state.handovers.published {
let idx = ring_offset(self.handovers_consumed, state.handovers.base)
.unwrap_or(usize::MAX);
state.handovers.entries.get(idx).copied()
} else {
None
};
}
let sparse_blocked = pending_handover.is_some_and(|h| self.sparse_consumed >= h.sparse_at);
if !sparse_blocked {
let sparse_idx =
ring_offset(self.sparse_consumed, state.sparse.base).unwrap_or(usize::MAX);
if let Some((track_id, sample)) = state.sparse.entries.get(sparse_idx) {
self.sparse_consumed += 1;
return Some(SampleCursorItem::Sparse {
track_id: *track_id,
sample: sample.clone(),
});
}
}
let timed_blocked = pending_handover.is_some_and(|h| self.timed_consumed >= h.timed_at);
if !timed_blocked {
let timed_idx =
ring_offset(self.timed_consumed, state.timed.base).unwrap_or(usize::MAX);
if let Some((track_id, sample)) = state.timed.entries.get(timed_idx) {
self.timed_consumed += 1;
return Some(SampleCursorItem::Timed {
track_id: *track_id,
sample: sample.clone(),
});
}
}
None
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum SegmentCursorItem {
Segment(SegmentEntry),
Lagged {
skipped: u64,
},
Gap {
skipped: u64,
},
Terminated,
}
pub struct SegmentCursor {
trunk: Arc<Trunk>,
consumed: u64,
pin_id: Option<u64>,
done: bool,
}
impl SegmentCursor {
pub fn poll(&mut self) -> Option<SegmentCursorItem> {
if self.done {
return None;
}
let Some(pin_id) = self.pin_id else {
let state = self.trunk.lock_state();
if self.consumed < state.segments.base {
let skipped = state.segments.base - self.consumed;
self.consumed = state.segments.base;
return Some(SegmentCursorItem::Lagged { skipped });
}
let idx = ring_offset(self.consumed, state.segments.base).unwrap_or(usize::MAX);
return if let Some(entry) = state.segments.entries.get(idx) {
self.consumed += 1;
Some(SegmentCursorItem::Segment(entry.clone()))
} else {
None
};
};
let mut state = self.trunk.lock_state();
let Some(pin) = state.segments.pins.get(&pin_id) else {
self.done = true;
return None;
};
if pin.terminated {
state.segments.pins.remove(&pin_id);
self.pin_id = None;
self.done = true;
return Some(SegmentCursorItem::Terminated);
}
let consumed = pin.consumed;
if consumed < state.segments.base {
let skipped = state.segments.base - consumed;
let base = state.segments.base;
let Some(pin) = state.segments.pins.get_mut(&pin_id) else {
debug_assert!(false, "pin_id was resolved from this same locked state");
return None;
};
pin.consumed = base;
drop(state);
self.trunk.segment_pin_released.notify_all();
return Some(SegmentCursorItem::Gap { skipped });
}
let idx = ring_offset(consumed, state.segments.base).unwrap_or(usize::MAX);
if let Some(entry) = state.segments.entries.get(idx) {
let item = entry.clone();
let Some(pin) = state.segments.pins.get_mut(&pin_id) else {
debug_assert!(false, "pin_id was resolved from this same locked state");
return None;
};
pin.consumed = pin.consumed.saturating_add(1);
drop(state);
self.trunk.segment_pin_released.notify_all();
return Some(SegmentCursorItem::Segment(item));
}
None
}
}
impl Drop for SegmentCursor {
fn drop(&mut self) {
if let Some(pin_id) = self.pin_id.take() {
let mut state = self.trunk.lock_state();
state.segments.pins.remove(&pin_id);
drop(state);
self.trunk.segment_pin_released.notify_all();
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum EventCursorItem {
Event(EventEntry),
Lagged {
skipped: u64,
},
}
pub struct EventCursor {
trunk: Arc<Trunk>,
consumed: u64,
}
impl EventCursor {
pub fn poll(&mut self) -> Option<EventCursorItem> {
let state = self.trunk.lock_state();
let log = &state.events;
if self.consumed < log.base {
let skipped = log.base - self.consumed;
self.consumed = log.base;
return Some(EventCursorItem::Lagged { skipped });
}
let idx = ring_offset(self.consumed, log.base).unwrap_or(usize::MAX);
if let Some(entry) = log.entries.get(idx) {
self.consumed += 1;
return Some(EventCursorItem::Event(entry.clone()));
}
None
}
}
#[cfg(test)]
pub(crate) fn wait_until_stalled(trunk: &Trunk, n: usize) {
let deadline = Instant::now() + Duration::from_secs(60);
while trunk.stalled_publishers.load(Ordering::Acquire) < n {
assert!(
Instant::now() < deadline,
"publisher never parked in StallIngest"
);
std::thread::yield_now();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::mpsc;
use std::thread;
use transmux::pipeline::{CodecConfig, DataCarriage};
fn opaque_track(track_id: TrackId) -> TrackSpec {
TrackSpec::new(
track_id,
90_000,
CodecConfig::Data {
stream_type: 0x06,
descriptors: Vec::new(),
carriage: DataCarriage::Pes,
},
)
}
fn nz(n: usize) -> NonZeroUsize {
NonZeroUsize::new(n).expect("test capacity must be non-zero")
}
fn sample(byte: u8, len: usize) -> Sample {
Sample::new(Bytes::from(vec![byte; len]), Some(0), Some(0), None, true)
}
fn timed_data(item: &SampleCursorItem) -> Option<(TrackId, &Sample)> {
match item {
SampleCursorItem::Timed { track_id, sample } => Some((*track_id, sample)),
_ => None,
}
}
fn segment_entry(byte: u8, seq: u32) -> SegmentEntry {
SegmentEntry::new(
Bytes::from(vec![byte; 16]),
SegmentSeq::new(seq),
Duration::from_secs(2),
Timestamp::from_nanos(u64::from(seq) * 2_000_000_000),
SegmentMeta {
discontinuous: false,
},
)
}
#[test]
fn segment_sequence_at_u32_max_saturates_and_rejects_the_next() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let writer = trunk.segment_writer().unwrap();
writer.publish_segment(segment_entry(1, u32::MAX)).unwrap();
let max = SegmentSeq::new(u32::MAX);
assert_eq!(writer.next_sequence_number(), max);
assert_eq!(max.checked_next(), None);
assert_eq!(
writer
.publish_segment(segment_entry(2, u32::MAX))
.unwrap_err(),
NonMonotonicSequenceNumber {
attempted: max,
last_published: Some(max),
}
);
assert!(matches!(
writer.publish_segment(segment_entry(3, 0)),
Err(NonMonotonicSequenceNumber { .. })
));
assert_eq!(trunk.last_closed_segment(), Some(max));
}
fn segment_data(item: &SegmentCursorItem) -> Option<&SegmentEntry> {
match item {
SegmentCursorItem::Segment(entry) => Some(entry),
_ => None,
}
}
fn drain_segments(cursor: &mut SegmentCursor, n: usize) -> Vec<SegmentCursorItem> {
let mut out = Vec::new();
for _ in 0..n {
match cursor.poll() {
Some(item) => out.push(item),
None => break,
}
}
out
}
fn drain(cursor: &mut SampleCursor, n: usize) -> Vec<SampleCursorItem> {
let mut out = Vec::new();
for _ in 0..n {
match cursor.poll() {
Some(item) => out.push(item),
None => break,
}
}
out
}
#[test]
fn multiple_cursors_see_every_sample_in_order_with_no_dup_or_loss() {
let trunk = Trunk::new(TrunkConfig::new(nz(100), nz(10), nz(4), nz(8), nz(8)));
let mut c1 = trunk.subscribe();
let mut c2 = trunk.subscribe();
let mut c3 = trunk.subscribe();
let writer = trunk.writer().unwrap();
for i in 0u8..5 {
writer.publish(TrackId::new(7), RetentionClass::Timed, sample(i, 16));
}
for cursor in [&mut c1, &mut c2, &mut c3] {
let items = drain(cursor, 5);
assert_eq!(items.len(), 5, "each cursor must see exactly 5 samples");
let bytes: Vec<u8> = items
.iter()
.map(|item| timed_data(item).unwrap().1.data[0])
.collect();
assert_eq!(bytes, vec![0, 1, 2, 3, 4], "must be in publish order");
assert!(cursor.poll().is_none(), "no extra/duplicated items");
}
}
#[test]
fn every_entry_point_survives_a_poisoned_state_lock() {
let trunk = Trunk::new(TrunkConfig::new(nz(8), nz(8), nz(8), nz(8), nz(8)));
let mut samples = trunk.subscribe();
let mut segments = trunk.subscribe_segments();
let mut events = trunk.subscribe_events();
let writer = trunk.writer().unwrap();
let segment_writer = trunk.segment_writer().unwrap();
let t2 = Arc::clone(&trunk);
let poisoner = thread::spawn(move || {
let _guard = t2.lock_state();
panic!("deliberately poisoning the trunk state lock");
});
assert!(
poisoner.join().is_err(),
"poisoner thread must have panicked"
);
assert!(
trunk.state.try_lock().is_some(),
"precondition: the lock is free (and, being parking_lot, unpoisoned) after the holder unwound"
);
writer.publish(TrackId::new(7), RetentionClass::Timed, sample(5, 4));
match samples.poll() {
Some(SampleCursorItem::Timed { track_id, sample }) => {
assert_eq!(track_id, TrackId::new(7));
assert_eq!(sample.data.as_ref(), &[5u8; 4]);
}
other => panic!("expected the published sample, got {other:?}"),
}
segment_writer.publish_segment(segment_entry(9, 1)).unwrap();
assert_eq!(trunk.last_closed_segment(), Some(SegmentSeq::new(1)));
match segments.poll() {
Some(SegmentCursorItem::Segment(e)) => {
assert_eq!(e.sequence_number, SegmentSeq::new(1))
}
other => panic!("expected segment 1, got {other:?}"),
}
segment_writer.publish_part(part_entry(3, 2, 0));
assert_eq!(
trunk
.part_bytes(SegmentSeq::new(2), PartIndex::new(0))
.unwrap()
.as_ref(),
&[3u8; 8]
);
writer.publish_event(basic_event(4), EventAnchor::Media(MediaTime(10)));
assert!(matches!(events.poll(), Some(EventCursorItem::Event(_))));
assert_eq!(trunk.events_between(MediaTime(0), MediaTime(20)).len(), 1);
drop(samples);
drop(segments);
drop(events);
drop(writer);
drop(segment_writer);
assert!(
trunk.writer().is_some(),
"slot released by Drop under poison"
);
}
#[test]
fn slow_reader_lags_but_writer_completes_regardless() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(10), nz(4), nz(8), nz(8)));
let mut slow = trunk.subscribe();
let writer = trunk.writer().unwrap();
for i in 0u8..=255u8 {
for _ in 0..4 {
writer.publish(TrackId::new(1), RetentionClass::Timed, sample(i, 8));
}
}
assert_eq!(
trunk.timed_len(),
4,
"writer unblocked: ring stayed bounded"
);
let first = slow.poll().unwrap();
assert!(
matches!(first, SampleCursorItem::Lagged { skipped: 1020 }),
"expected Lagged{{skipped: 1020}}, got {first:?}"
);
}
#[test]
fn lag_is_reported_with_an_accurate_skipped_count() {
let trunk = Trunk::new(TrunkConfig::new(nz(3), nz(10), nz(4), nz(8), nz(8)));
let mut cursor = trunk.subscribe();
let writer = trunk.writer().unwrap();
for i in 0u8..9 {
writer.publish(TrackId::new(2), RetentionClass::Timed, sample(i, 4));
}
let first = cursor.poll().unwrap();
assert!(
matches!(first, SampleCursorItem::Lagged { skipped: 6 }),
"expected Lagged{{skipped: 6}}, got {first:?}"
);
let items = drain(&mut cursor, 3);
let bytes: Vec<u8> = items
.iter()
.map(|item| timed_data(item).unwrap().1.data[0])
.collect();
assert_eq!(bytes, vec![6, 7, 8]);
assert!(cursor.poll().is_none());
}
#[test]
fn sparse_reader_loses_data_reports_degraded_distinguishable_from_timed_lagged() {
let trunk = Trunk::new(TrunkConfig::new(nz(2), nz(2), nz(4), nz(8), nz(8)));
let mut cursor = trunk.subscribe();
let writer = trunk.writer().unwrap();
for i in 0u8..5 {
writer.publish(TrackId::new(3), RetentionClass::Timed, sample(i, 4));
}
for i in 0u8..4 {
writer.publish(TrackId::new(9), RetentionClass::Sparse, sample(100 + i, 4));
}
let timed_loss = cursor.poll().unwrap();
assert!(
matches!(timed_loss, SampleCursorItem::Lagged { skipped: 3 }),
"expected ordinary Lagged{{skipped: 3}} for the Timed ring, got {timed_loss:?}"
);
let sparse_loss = cursor.poll().unwrap();
assert!(
matches!(sparse_loss, SampleCursorItem::Degraded { skipped: 2 }),
"expected escalated Degraded{{skipped: 2}} for the Sparse ring, got {sparse_loss:?}"
);
assert_ne!(
core::mem::discriminant(&timed_loss),
core::mem::discriminant(&sparse_loss),
"Lagged and Degraded must be distinct variants, not merely different field values"
);
}
#[test]
fn ring_is_bounded_under_flood_on_both_classes() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(3), nz(4), nz(8), nz(8)));
let writer = trunk.writer().unwrap();
for i in 0u32..50_000 {
writer.publish(
TrackId::new(5),
RetentionClass::Timed,
sample((i % 256) as u8, 2),
);
assert!(
trunk.timed_len() <= 4,
"Timed ring exceeded its cap mid-flood"
);
if i % 7 == 0 {
writer.publish(
TrackId::new(6),
RetentionClass::Sparse,
sample((i % 256) as u8, 2),
);
assert!(
trunk.sparse_len() <= 3,
"Sparse ring exceeded its cap mid-flood"
);
}
}
assert_eq!(trunk.timed_len(), 4);
assert_eq!(trunk.sparse_len(), 3);
}
#[test]
fn subscribe_from_backlog_replays_exact_resident_entries_both_classes() {
let trunk = Trunk::new(TrunkConfig::new(nz(100), nz(100), nz(4), nz(8), nz(8)));
let writer = trunk.writer().unwrap();
for i in 10u8..13 {
writer.publish(TrackId::new(1), RetentionClass::Timed, sample(i, 4));
}
for i in 20u8..22 {
writer.publish(TrackId::new(2), RetentionClass::Sparse, sample(i, 4));
}
let mut cursor = trunk.subscribe_from_backlog();
let sparse_items = drain(&mut cursor, 2);
let sparse_bytes: Vec<u8> = sparse_items
.iter()
.map(|item| match item {
SampleCursorItem::Sparse { sample, .. } => sample.data[0],
other => panic!("expected Sparse, got {other:?}"),
})
.collect();
assert_eq!(sparse_bytes, vec![20, 21], "exact resident Sparse backlog");
let timed_items = drain(&mut cursor, 3);
let timed_bytes: Vec<u8> = timed_items
.iter()
.map(|item| timed_data(item).unwrap().1.data[0])
.collect();
assert_eq!(
timed_bytes,
vec![10, 11, 12],
"exact resident Timed backlog"
);
assert!(
cursor.poll().is_none(),
"no extra items beyond the resident backlog"
);
writer.publish(TrackId::new(1), RetentionClass::Timed, sample(99, 4));
let live = cursor.poll().unwrap();
assert_eq!(timed_data(&live).unwrap().1.data[0], 99);
}
#[test]
fn subscribe_from_backlog_reports_lagged_when_backlog_already_overwritten() {
let trunk = Trunk::new(TrunkConfig::new(nz(3), nz(10), nz(4), nz(8), nz(8)));
let writer = trunk.writer().unwrap();
for i in 0u8..9 {
writer.publish(TrackId::new(1), RetentionClass::Timed, sample(i, 4));
}
let mut cursor = trunk.subscribe_from_backlog();
for i in 9u8..15 {
writer.publish(TrackId::new(1), RetentionClass::Timed, sample(i, 4));
}
let first = cursor.poll().unwrap();
assert!(
matches!(first, SampleCursorItem::Lagged { skipped: 6 }),
"expected Lagged{{skipped: 6}}, got {first:?}"
);
let items = drain(&mut cursor, 3);
let bytes: Vec<u8> = items
.iter()
.map(|item| timed_data(item).unwrap().1.data[0])
.collect();
assert_eq!(bytes, vec![12, 13, 14]);
assert!(cursor.poll().is_none());
}
#[test]
fn payload_is_shared_not_copied_across_cursors() {
let trunk = Trunk::new(TrunkConfig::new(nz(8), nz(8), nz(4), nz(8), nz(8)));
let mut c1 = trunk.subscribe();
let mut c2 = trunk.subscribe();
let mut c3 = trunk.subscribe();
let writer = trunk.writer().unwrap();
writer.publish(TrackId::new(4), RetentionClass::Timed, sample(0xAB, 65536));
let i1 = c1.poll().unwrap();
let i2 = c2.poll().unwrap();
let i3 = c3.poll().unwrap();
let p1 = timed_data(&i1).unwrap().1.data.as_ptr();
let p2 = timed_data(&i2).unwrap().1.data.as_ptr();
let p3 = timed_data(&i3).unwrap().1.data.as_ptr();
assert_eq!(
p1, p2,
"cursor 2's payload must be the SAME allocation as cursor 1's"
);
assert_eq!(
p2, p3,
"cursor 3's payload must be the SAME allocation as cursor 1's"
);
assert_eq!(timed_data(&i1).unwrap().1.data.len(), 65536);
}
#[test]
fn second_writer_is_refused() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let _first = trunk.writer().unwrap();
assert!(
trunk.writer().is_none(),
"a Trunk has exactly one sample/event writer"
);
}
#[test]
fn second_segment_writer_is_refused() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let _first = trunk.segment_writer().unwrap();
assert!(
trunk.segment_writer().is_none(),
"a Trunk has exactly one segment/part writer"
);
}
#[test]
fn sample_and_segment_writers_can_be_held_simultaneously() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let samples = trunk.writer().unwrap();
let segments = trunk.segment_writer().expect(
"the segment/part writer must still be takeable while the sample/event writer is held",
);
samples.publish(TrackId::new(1), RetentionClass::Timed, sample(1, 4));
segments.publish_segment(segment_entry(1, 1)).unwrap();
assert_eq!(trunk.timed_len(), 1);
assert_eq!(trunk.segment_len(), 1);
}
#[test]
fn segmenter_holds_sample_cursor_and_segment_writer_at_once() {
let trunk = Trunk::new(TrunkConfig::new(nz(8), nz(4), nz(4), nz(8), nz(8)));
let ingest = trunk.writer().unwrap();
let mut samples = trunk.subscribe();
let segmenter = trunk.segment_writer().expect(
"a segmenter must be able to take the segment/part writer \
while the ingest driver still holds the sample/event writer",
);
let mut seg_cursor = trunk.subscribe_segments();
for i in 0u8..3 {
ingest.publish(TrackId::new(1), RetentionClass::Timed, sample(i, 4));
}
let mut muxed = Vec::new();
for _ in 0..3 {
match samples.poll() {
Some(SampleCursorItem::Timed { sample, .. }) => {
muxed.push(sample.data[0]);
}
other => panic!("expected a Timed sample, got {other:?}"),
}
}
assert_eq!(muxed, vec![0, 1, 2]);
segmenter.publish_part(part_entry(0xAB, 1, 0));
segmenter.publish_segment(segment_entry(0xAA, 1)).unwrap();
let got = seg_cursor
.poll()
.expect("the segmenter's published segment must be visible");
assert_eq!(
segment_data(&got).unwrap().sequence_number,
SegmentSeq::new(1)
);
assert_eq!(
trunk.part_bytes(SegmentSeq::new(1), PartIndex::new(0)),
Some(Bytes::from(vec![0xAB; 8])),
"the segmenter's published part must be individually addressable too"
);
}
#[test]
fn subscribe_starts_from_now_not_from_history() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let writer = trunk.writer().unwrap();
writer.publish(TrackId::new(1), RetentionClass::Timed, sample(1, 4));
writer.publish(TrackId::new(1), RetentionClass::Timed, sample(2, 4));
let mut cursor = trunk.subscribe();
assert!(cursor.poll().is_none());
writer.publish(TrackId::new(1), RetentionClass::Timed, sample(3, 4));
let item = cursor.poll().unwrap();
assert_eq!(timed_data(&item).unwrap().1.data[0], 3);
}
#[test]
fn multiple_segment_cursors_see_every_segment_in_order_with_no_dup_or_loss() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(100), nz(8), nz(8)));
let mut c1 = trunk.subscribe_segments();
let mut c2 = trunk.subscribe_segments();
let mut c3 = trunk.subscribe_segments();
let writer = trunk.segment_writer().unwrap();
for i in 0u32..5 {
writer
.publish_segment(segment_entry(i as u8, i + 1))
.unwrap();
}
for cursor in [&mut c1, &mut c2, &mut c3] {
let items = drain_segments(cursor, 5);
assert_eq!(items.len(), 5, "each cursor must see exactly 5 segments");
let seqs: Vec<SegmentSeq> = items
.iter()
.map(|item| segment_data(item).unwrap().sequence_number)
.collect();
assert_eq!(
seqs,
[1, 2, 3, 4, 5].map(SegmentSeq::new),
"must be in playlist order"
);
assert!(cursor.poll().is_none(), "no extra/duplicated items");
}
}
#[test]
fn non_pinning_slow_segment_reader_lags_but_writer_completes_regardless() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let mut slow = trunk.subscribe_segments();
let writer = trunk.segment_writer().unwrap();
for i in 0u32..1024 {
writer
.publish_segment(segment_entry((i % 256) as u8, i + 1))
.unwrap();
}
assert_eq!(
trunk.segment_len(),
4,
"writer unblocked: segment log stayed bounded"
);
let first = slow.poll().unwrap();
assert!(
matches!(first, SegmentCursorItem::Lagged { skipped: 1020 }),
"expected Lagged{{skipped: 1020}}, got {first:?}"
);
}
#[test]
fn pinning_reader_receives_every_segment_while_non_pinning_reader_lags() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(2), nz(8), nz(8)));
let mut slow = trunk.subscribe_segments(); let mut archive = trunk.pin_segments(ArchiveOverrun::StallIngest); let writer = Arc::new(trunk.segment_writer().unwrap());
writer.publish_segment(segment_entry(1, 1)).unwrap();
writer.publish_segment(segment_entry(2, 2)).unwrap();
let (done_tx, done_rx) = mpsc::channel();
let blocked_writer = Arc::clone(&writer);
let handle = thread::spawn(move || {
blocked_writer.publish_segment(segment_entry(3, 3)).unwrap();
done_tx.send(()).unwrap();
});
wait_until_stalled(&trunk, 1);
assert!(
done_rx.try_recv().is_err(),
"publish_segment must still be blocked: archive has not consumed seq 1 yet"
);
let first = archive.poll().unwrap();
assert_eq!(
segment_data(&first).unwrap().sequence_number,
SegmentSeq::new(1)
);
done_rx
.recv_timeout(Duration::from_secs(60))
.expect("publish_segment must unblock once the pin advances");
handle.join().unwrap();
let second = archive.poll().unwrap();
assert_eq!(
segment_data(&second).unwrap().sequence_number,
SegmentSeq::new(2)
);
let third = archive.poll().unwrap();
assert_eq!(
segment_data(&third).unwrap().sequence_number,
SegmentSeq::new(3)
);
assert!(archive.poll().is_none());
let lag = slow.poll().unwrap();
assert!(
matches!(lag, SegmentCursorItem::Lagged { skipped: 1 }),
"expected Lagged{{skipped: 1}}, got {lag:?}"
);
let remaining: Vec<SegmentSeq> = drain_segments(&mut slow, 2)
.iter()
.map(|item| segment_data(item).unwrap().sequence_number)
.collect();
assert_eq!(remaining, [2, 3].map(SegmentSeq::new));
}
#[test]
fn pinning_is_bounded_an_unacking_consumer_cannot_grow_memory_without_limit() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let _archive = trunk.pin_segments(ArchiveOverrun::default());
let writer = trunk.segment_writer().unwrap();
for i in 0u32..50_000 {
writer
.publish_segment(segment_entry((i % 256) as u8, i + 1))
.unwrap();
assert!(
trunk.segment_len() <= 4,
"segment log exceeded its cap mid-flood despite an un-acking pinning cursor"
);
}
assert_eq!(trunk.segment_len(), 4);
}
#[test]
fn archive_overrun_gap_evicts_and_reports_gap() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(2), nz(8), nz(8)));
let mut archive = trunk.pin_segments(ArchiveOverrun::Gap);
let writer = trunk.segment_writer().unwrap();
for i in 0u32..5 {
writer
.publish_segment(segment_entry(i as u8, i + 1))
.unwrap();
}
assert_eq!(trunk.segment_len(), 2);
let gap = archive.poll().unwrap();
assert!(
matches!(gap, SegmentCursorItem::Gap { skipped: 3 }),
"expected Gap{{skipped: 3}}, got {gap:?}"
);
let remaining: Vec<SegmentSeq> = drain_segments(&mut archive, 2)
.iter()
.map(|item| segment_data(item).unwrap().sequence_number)
.collect();
assert_eq!(remaining, [4, 5].map(SegmentSeq::new));
assert!(archive.poll().is_none());
}
#[test]
fn archive_overrun_stall_ingest_actually_blocks_the_writer() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(1), nz(8), nz(8)));
let mut archive = trunk.pin_segments(ArchiveOverrun::StallIngest);
let writer = Arc::new(trunk.segment_writer().unwrap());
writer.publish_segment(segment_entry(1, 1)).unwrap();
let (done_tx, done_rx) = mpsc::channel();
let blocked_writer = Arc::clone(&writer);
let handle = thread::spawn(move || {
blocked_writer.publish_segment(segment_entry(2, 2)).unwrap();
done_tx.send(()).unwrap();
});
wait_until_stalled(&trunk, 1);
assert!(
matches!(
done_rx.try_recv(),
Err(std::sync::mpsc::TryRecvError::Empty)
),
"publish_segment must block: the pin has not consumed seq 1 yet"
);
let first = archive.poll().unwrap();
assert_eq!(
segment_data(&first).unwrap().sequence_number,
SegmentSeq::new(1)
);
done_rx
.recv_timeout(Duration::from_secs(60))
.expect("publish_segment must unblock once the pin advances");
handle.join().unwrap();
}
#[test]
fn stalled_publisher_probe_tracks_a_parked_publish_segment() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(1), nz(8), nz(8)));
let writer = Arc::new(trunk.segment_writer().unwrap());
let mut pin = trunk.pin_segments(ArchiveOverrun::StallIngest);
writer.publish_segment(segment_entry(1, 1)).unwrap();
assert_eq!(trunk.stalled_publishers.load(Ordering::Acquire), 0);
let bg = Arc::clone(&writer);
let handle = thread::spawn(move || bg.publish_segment(segment_entry(2, 2)).unwrap());
wait_until_stalled(&trunk, 1);
assert_eq!(trunk.stalled_publishers.load(Ordering::Acquire), 1);
let released_at = Instant::now();
assert!(pin.poll().is_some(), "consume seq 1: releases the pin");
handle.join().unwrap();
assert!(
released_at.elapsed() < STALL_INGEST_MAX_WAIT / 2,
"the publisher must be woken by the pin advance, not time out ({:?})",
released_at.elapsed()
);
assert_eq!(trunk.stalled_publishers.load(Ordering::Acquire), 0);
}
#[test]
fn sample_publish_proceeds_while_a_segment_publisher_is_stalled() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(1), nz(8), nz(8)));
let seg_writer = Arc::new(trunk.segment_writer().unwrap());
let writer = trunk.writer().unwrap();
let mut pin = trunk.pin_segments(ArchiveOverrun::StallIngest);
seg_writer.publish_segment(segment_entry(1, 1)).unwrap();
let bg = Arc::clone(&seg_writer);
let handle = thread::spawn(move || bg.publish_segment(segment_entry(2, 2)).unwrap());
wait_until_stalled(&trunk, 1);
writer.publish(TrackId::new(1), RetentionClass::Timed, sample(7, 4)); assert_eq!(trunk.timed_len(), 1);
assert!(pin.poll().is_some());
handle.join().unwrap();
}
#[test]
fn archive_overrun_terminate_drops_the_cursor() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(2), nz(8), nz(8)));
let mut archive = trunk.pin_segments(ArchiveOverrun::Terminate);
let writer = trunk.segment_writer().unwrap();
for i in 0u32..5 {
writer
.publish_segment(segment_entry(i as u8, i + 1))
.unwrap();
}
assert_eq!(trunk.segment_len(), 2, "writer unblocked despite Terminate");
let item = archive.poll().unwrap();
assert!(
matches!(item, SegmentCursorItem::Terminated),
"expected Terminated, got {item:?}"
);
assert!(archive.poll().is_none());
assert!(archive.poll().is_none());
writer.publish_segment(segment_entry(9, 6)).unwrap();
assert_eq!(trunk.segment_len(), 2);
}
#[test]
fn segment_bytes_are_shared_not_copied_across_cursors() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(8), nz(8), nz(8)));
let mut c1 = trunk.subscribe_segments();
let mut c2 = trunk.subscribe_segments();
let mut c3 = trunk.subscribe_segments();
let writer = trunk.segment_writer().unwrap();
writer
.publish_segment(SegmentEntry::new(
Bytes::from(vec![0xCDu8; 65536]),
SegmentSeq::new(1),
Duration::from_secs(2),
Timestamp::from_nanos(0),
SegmentMeta {
discontinuous: false,
},
))
.unwrap();
let i1 = c1.poll().unwrap();
let i2 = c2.poll().unwrap();
let i3 = c3.poll().unwrap();
let p1 = segment_data(&i1).unwrap().bytes.as_ptr();
let p2 = segment_data(&i2).unwrap().bytes.as_ptr();
let p3 = segment_data(&i3).unwrap().bytes.as_ptr();
assert_eq!(
p1, p2,
"cursor 2's segment payload must be the SAME allocation as cursor 1's"
);
assert_eq!(
p2, p3,
"cursor 3's segment payload must be the SAME allocation as cursor 1's"
);
assert_eq!(segment_data(&i1).unwrap().bytes.len(), 65536);
}
use timed_metadata::{EventKind, SourcePayload};
fn basic_event(id: u32) -> TimedEvent {
TimedEvent {
id: Some(id),
kind: EventKind::BreakStart,
at: None,
duration: None,
source: SourcePayload::Scte35 { raw: Vec::new() },
}
}
fn event_id(item: &EventCursorItem) -> Option<u32> {
match item {
EventCursorItem::Event(e) => e.event.id,
_ => None,
}
}
fn splice_insert_bytes(event_id: u32, pts_time: u64) -> Vec<u8> {
use broadcast_common::Serialize;
use scte35_splice::SpliceInfoSection;
use scte35_splice::commands::AnyCommand;
use scte35_splice::commands::splice_insert::SpliceInsert;
use scte35_splice::time::SpliceTime;
let si = SpliceInsert {
splice_event_id: event_id,
out_of_network_indicator: true,
splice_time: Some(SpliceTime::with_pts(pts_time)),
..SpliceInsert::default()
};
let section = SpliceInfoSection::new_clear(AnyCommand::SpliceInsert(si), &[]);
section.try_to_bytes().unwrap()
}
#[test]
fn events_between_returns_exactly_the_half_open_range() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let writer = trunk.writer().unwrap();
for (id, ticks) in [(1u32, 1_000u64), (2, 2_000), (3, 3_000), (4, 4_000)] {
writer.publish_event(basic_event(id), EventAnchor::Media(MediaTime(ticks)));
}
let got = trunk.events_between(MediaTime(2_000), MediaTime(4_000));
let ids: Vec<u32> = got.iter().map(|e| e.event.id.unwrap()).collect();
assert_eq!(
ids,
vec![2, 3],
"start (2_000) inclusive, end (4_000) exclusive"
);
}
#[test]
fn segment_relative_event_resolves_at_publish_time_when_boundary_already_known() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let writer = trunk.writer().unwrap();
let segment_writer = trunk.segment_writer().unwrap();
segment_writer.note_segment_start(SegmentSeq::new(3), MediaTime(300_000));
writer.publish_event(
basic_event(9),
EventAnchor::Segment {
segment_number: SegmentSeq::new(3),
delta: 1_500,
},
);
let got = trunk.events_in_segment(SegmentSeq::new(3));
assert_eq!(got.len(), 1);
assert_eq!(got[0].event.id, Some(9));
assert!(matches!(
got[0].anchor,
EventAnchor::Media(MediaTime(t)) if t == 301_500
));
}
#[test]
fn segment_relative_event_resolves_to_the_named_segment_not_whichever_is_open() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let writer = trunk.writer().unwrap();
let segment_writer = trunk.segment_writer().unwrap();
let mut cursor = trunk.subscribe_events();
writer.publish_event(
basic_event(42),
EventAnchor::Segment {
segment_number: SegmentSeq::new(2),
delta: 1_000,
},
);
segment_writer.note_segment_start(SegmentSeq::new(1), MediaTime(0));
let item = cursor.poll().unwrap();
let entry = match item {
EventCursorItem::Event(e) => e,
other => panic!("expected Event, got {other:?}"),
};
assert!(
matches!(
entry.anchor,
EventAnchor::Segment {
segment_number,
delta: 1_000,
} if segment_number == SegmentSeq::new(2)
),
"must stay pending on segment 2 — segment 1 being open must not \
resolve it against the wrong boundary: {:?}",
entry.anchor
);
assert!(
trunk.events_in_segment(SegmentSeq::new(2)).is_empty(),
"not resolved yet: must not appear under segment 2 either"
);
assert!(trunk.events_in_segment(SegmentSeq::new(1)).is_empty());
segment_writer.note_segment_start(SegmentSeq::new(2), MediaTime(90_000));
let in_seg2 = trunk.events_in_segment(SegmentSeq::new(2));
assert_eq!(in_seg2.len(), 1);
assert_eq!(in_seg2[0].event.id, Some(42));
assert!(matches!(
in_seg2[0].anchor,
EventAnchor::Media(MediaTime(t)) if t == 91_000
));
assert!(
trunk.events_in_segment(SegmentSeq::new(1)).is_empty(),
"must not ALSO appear under segment 1"
);
}
#[test]
fn reused_segment_number_resolves_against_its_most_recent_boundary() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let writer = trunk.writer().unwrap();
let segment_writer = trunk.segment_writer().unwrap();
segment_writer.note_segment_start(SegmentSeq::new(1), MediaTime(0));
segment_writer.note_segment_start(SegmentSeq::new(2), MediaTime(90_000));
segment_writer.note_segment_start(SegmentSeq::new(1), MediaTime(200_000)); segment_writer.note_segment_start(SegmentSeq::new(2), MediaTime(290_000));
writer.publish_event(
basic_event(7),
EventAnchor::Segment {
segment_number: SegmentSeq::new(1),
delta: 1_500,
},
);
let in_seg1 = trunk.events_in_segment(SegmentSeq::new(1));
assert_eq!(in_seg1.len(), 1);
assert!(
matches!(
in_seg1[0].anchor,
EventAnchor::Media(MediaTime(t)) if t == 201_500
),
"must resolve against the MOST RECENT segment-1 boundary \
(200_000 + 1_500), not the stale first one (0 + 1_500): {:?}",
in_seg1[0].anchor
);
}
#[test]
fn utc_only_event_stays_unanchored_until_a_time_anchor_arrives() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let writer = trunk.writer().unwrap();
let segment_writer = trunk.segment_writer().unwrap();
let mut cursor = trunk.subscribe_events();
writer.publish_event(
basic_event(7),
EventAnchor::Utc {
utc_epoch_ms: 5_000,
},
);
let item = cursor.poll().unwrap();
let entry = match item {
EventCursorItem::Event(e) => e,
other => panic!("expected Event, got {other:?}"),
};
assert!(
matches!(
entry.anchor,
EventAnchor::Utc {
utc_epoch_ms: 5_000
}
),
"must stay honestly unanchored — NO fabricated media time: {:?}",
entry.anchor
);
assert!(
trunk
.events_between(MediaTime(0), MediaTime(u64::MAX))
.is_empty(),
"an unanchored event must not appear in a media-time query"
);
segment_writer.set_time_anchor(TimeAnchor {
pts_90k: 0,
utc_epoch_ms: 1_000,
});
let resolved = trunk.events_between(MediaTime(0), MediaTime(u64::MAX));
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].event.id, Some(7));
assert!(
matches!(resolved[0].anchor, EventAnchor::Media(MediaTime(t)) if t == 360_000),
"expected MediaTime(360_000), got {:?}",
resolved[0].anchor
);
}
#[test]
fn a_33_bit_pts_wrap_does_not_corrupt_event_log_ordering() {
const PTS_WRAP: u64 = 1u64 << 33;
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let writer = trunk.writer().unwrap();
let mut timeline = timed_metadata::Timeline::new();
let before_wrap = splice_insert_bytes(1, PTS_WRAP - 10);
let ev1 = timeline.push_scte35(&before_wrap).unwrap();
let at1 = ev1.at.unwrap();
writer.publish_event(ev1, EventAnchor::Media(at1));
let after_wrap = splice_insert_bytes(2, 5);
let ev2 = timeline.push_scte35(&after_wrap).unwrap();
let at2 = ev2.at.unwrap();
writer.publish_event(ev2, EventAnchor::Media(at2));
assert!(
at2.0 > at1.0,
"Timeline itself must unroll monotonically: at1={}, at2={}",
at1.0,
at2.0
);
let got = trunk.events_between(MediaTime(0), MediaTime(u64::MAX));
assert_eq!(got.len(), 2);
assert_eq!(got[0].event.id, Some(1));
assert_eq!(got[1].event.id, Some(2));
assert!(
matches!(got[0].anchor, EventAnchor::Media(t) if t.0 == at1.0),
"event 1's stored anchor must equal Timeline's unrolled value \
exactly, got {:?}",
got[0].anchor
);
assert!(
matches!(got[1].anchor, EventAnchor::Media(t) if t.0 == at2.0),
"event 2's stored (post-wrap) anchor must equal Timeline's \
unrolled value exactly — not re-masked back into 33 bits, got {:?}",
got[1].anchor
);
}
#[test]
fn event_log_is_bounded_under_flood() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(3), nz(8)));
let writer = trunk.writer().unwrap();
for i in 0u32..50_000 {
writer.publish_event(basic_event(i), EventAnchor::Media(MediaTime(u64::from(i))));
assert!(
trunk.event_len() <= 3,
"event log exceeded its cap mid-flood"
);
}
assert_eq!(trunk.event_len(), 3);
}
#[test]
fn event_cursor_lag_is_reported_with_an_accurate_skipped_count_writer_never_blocks() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(3), nz(8)));
let mut cursor = trunk.subscribe_events();
let writer = trunk.writer().unwrap();
for i in 0u32..9 {
writer.publish_event(basic_event(i), EventAnchor::Media(MediaTime(u64::from(i))));
}
assert_eq!(
trunk.event_len(),
3,
"writer unblocked: event log stayed bounded"
);
let first = cursor.poll().unwrap();
assert!(
matches!(first, EventCursorItem::Lagged { skipped: 6 }),
"expected Lagged{{skipped: 6}}, got {first:?}"
);
let mut ids = Vec::new();
for _ in 0..3 {
ids.push(event_id(&cursor.poll().unwrap()).unwrap());
}
assert_eq!(ids, vec![6, 7, 8]);
assert!(cursor.poll().is_none());
}
fn round_trip_anchor() -> TimeAnchor {
TimeAnchor {
pts_90k: 900_000, utc_epoch_ms: 1_700_000_000_000, }
}
const TICKS_PER_EPOCH_MS: u64 = PTS_HZ / 1000;
const MEDIA_ROUND_TRIP_MAX_TICKS: u64 = TICKS_PER_EPOCH_MS - 1;
#[test]
fn epoch_ms_round_trip_through_media_time_is_exact() {
let anchor = round_trip_anchor();
for offset_ms in [
0i64,
1,
-1,
1_000,
-1_000,
86_400_000,
-9_000,
200_000_000_000_000,
] {
let epoch_ms = anchor.utc_epoch_ms + offset_ms;
let media = epoch_ms_to_media(&anchor, epoch_ms).unwrap();
let back = anchor.media_to_epoch_ms(media);
assert_eq!(
back, epoch_ms,
"epoch_ms -> media -> epoch_ms must be EXACT at offset {offset_ms} ms \
(media = {media:?})"
);
}
}
#[test]
fn media_round_trip_is_lossy_by_at_most_one_millisecond() {
let anchor = round_trip_anchor();
let mut worst = 0u64;
for offset_ticks in [
0i64,
1,
-1,
89,
-89,
90,
-90,
91,
-91,
18_000_000_000_000_037,
] {
let media = MediaTime((anchor.pts_90k as i64 + offset_ticks) as u64);
let epoch_ms = anchor.media_to_epoch_ms(media);
let back = epoch_ms_to_media(&anchor, epoch_ms).unwrap();
let diff = media.0.abs_diff(back.0);
assert!(
diff <= MEDIA_ROUND_TRIP_MAX_TICKS,
"media -> epoch_ms -> media lost {diff} ticks at offset \
{offset_ticks} (bound is {MEDIA_ROUND_TRIP_MAX_TICKS}, i.e. \
< 1 ms): {media:?} -> {epoch_ms} -> {back:?}"
);
worst = worst.max(diff);
}
assert_eq!(
worst, MEDIA_ROUND_TRIP_MAX_TICKS,
"the derived bound must be attained, not merely respected — \
otherwise it is a loose tolerance, not a property"
);
}
#[test]
fn epoch_before_the_timeline_origin_is_unresolvable_not_clamped() {
let anchor = round_trip_anchor();
assert_eq!(
epoch_ms_to_media(&anchor, anchor.utc_epoch_ms - 20_000),
None
);
assert_eq!(
epoch_ms_to_media(&anchor, anchor.utc_epoch_ms - 10_000),
Some(MediaTime(0))
);
assert_eq!(
epoch_ms_to_media(&anchor, anchor.utc_epoch_ms - 10_001),
None
);
let far = TimeAnchor {
pts_90k: u64::MAX,
utc_epoch_ms: 0,
};
assert_eq!(epoch_ms_to_media(&far, 1_000), None);
assert_eq!(epoch_ms_to_media(&far, i64::MIN), None);
}
#[test]
fn pre_origin_utc_event_stays_unresolved_through_the_trunk() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let writer = trunk.writer().unwrap();
let segment_writer = trunk.segment_writer().unwrap();
writer.publish_event(
basic_event(1),
EventAnchor::Utc {
utc_epoch_ms: 5_000,
},
);
segment_writer.set_time_anchor(TimeAnchor {
pts_90k: 900_000,
utc_epoch_ms: 100_000,
});
writer.publish_event(
basic_event(2),
EventAnchor::Utc {
utc_epoch_ms: 5_000,
},
);
writer.publish_event(
basic_event(3),
EventAnchor::Utc {
utc_epoch_ms: 100_000,
},
);
let got = trunk.events_between(MediaTime(0), MediaTime(u64::MAX));
assert_eq!(got.len(), 1, "only the in-range event is media-resolvable");
assert_eq!(got[0].event.id, Some(3));
assert!(matches!(
got[0].anchor,
EventAnchor::Media(MediaTime(900_000))
));
assert!(
trunk.events_between(MediaTime(0), MediaTime(1)).is_empty(),
"a pre-origin event must not be reported at the start of the stream"
);
}
fn part_entry(byte: u8, segment_number: u32, part_index: u32) -> PartEntry {
PartEntry::new(
Bytes::from(vec![byte; 8]),
SegmentSeq::new(segment_number),
PartIndex::new(part_index),
Duration::from_millis(200),
part_index == 0,
)
}
#[test]
fn part_lookups_scan_only_their_own_segment_not_the_ring() {
use core::sync::atomic::Ordering;
const SEGMENTS: u32 = 1_000;
const PARTS_PER_SEGMENT: u32 = 4;
let ring = usize::try_from(SEGMENTS * PARTS_PER_SEGMENT).unwrap();
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(ring)));
let writer = trunk.segment_writer().unwrap();
for seg in 0..SEGMENTS {
for part in 0..PARTS_PER_SEGMENT {
writer.publish_part(part_entry(
u8::try_from((seg + part) % 251).unwrap(),
seg,
part,
));
}
}
assert_eq!(trunk.part_len(), ring);
let visits = || {
trunk
.lock_state()
.parts
.entries
.visits
.load(Ordering::Relaxed)
};
let before = visits();
let newest = SEGMENTS - 1;
let parts = trunk.parts_in_segment(SegmentSeq::new(newest));
assert_eq!(
parts
.iter()
.map(|p| p.part_index.value())
.collect::<Vec<_>>(),
vec![0, 1, 2, 3]
);
assert_eq!(visits() - before, 4, "parts_in_segment work units");
let before = visits();
let bytes = trunk
.part_bytes(SegmentSeq::new(newest), PartIndex::new(3))
.expect("newest part resident");
assert_eq!(
bytes.as_ref(),
&[u8::try_from((newest + 3) % 251).unwrap(); 8]
);
assert!(
visits() - before <= 4,
"part_bytes must touch at most its own segment's parts, touched {}",
visits() - before
);
let before = visits();
assert!(
trunk
.part_bytes(SegmentSeq::new(SEGMENTS + 5), PartIndex::new(0))
.is_none()
);
assert!(
trunk
.parts_in_segment(SegmentSeq::new(SEGMENTS + 5))
.is_empty()
);
assert_eq!(visits() - before, 0);
assert_eq!(trunk.lock_state().parts.by_segment.len(), 1_000);
for part in 0..PARTS_PER_SEGMENT {
writer.publish_part(part_entry(1, SEGMENTS, part));
}
let state = trunk.lock_state();
assert_eq!(state.parts.by_segment.len(), 1_000);
assert!(!state.parts.by_segment.contains_key(&SegmentSeq::new(0)));
}
#[test]
fn part_index_tracks_eviction_and_reused_segment_numbers() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(5)));
let writer = trunk.segment_writer().unwrap();
for (seg, part, byte) in [(7, 0, 10), (7, 1, 11), (7, 2, 12), (8, 0, 20), (8, 1, 21)] {
writer.publish_part(part_entry(byte, seg, part));
}
writer.publish_part(part_entry(22, 8, 2));
writer.publish_part(part_entry(99, 7, 2));
assert_eq!(
trunk.part_bytes(SegmentSeq::new(7), PartIndex::new(0)),
None,
"evicted"
);
assert_eq!(
trunk.part_bytes(SegmentSeq::new(7), PartIndex::new(1)),
None,
"evicted"
);
let seven: Vec<(PartIndex, u8)> = trunk
.parts_in_segment(SegmentSeq::new(7))
.iter()
.map(|p| (p.part_index, p.bytes[0]))
.collect();
assert_eq!(
seven,
vec![(PartIndex::new(2), 12), (PartIndex::new(2), 99)],
"survivor then the reused run"
);
assert_eq!(
trunk
.part_bytes(SegmentSeq::new(7), PartIndex::new(2))
.unwrap()
.as_ref(),
&[99u8; 8],
"newest publish of the pair wins"
);
assert_eq!(trunk.part_len(), 5);
let state = trunk.lock_state();
let indexed: usize = state.parts.by_segment.values().map(|v| v.len()).sum();
assert_eq!(indexed, 5, "index holds exactly the resident entries");
assert_eq!(state.parts.entries.iter().count(), 5);
assert_eq!(state.parts.by_segment.len(), 2, "emptied keys are removed");
}
#[test]
fn part_is_addressable_and_readable_before_its_parent_segment_closes() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let writer = trunk.segment_writer().unwrap();
assert!(
trunk.last_closed_segment().is_none(),
"sanity check: nothing has closed yet"
);
writer.publish_part(part_entry(0xAB, 9, 0));
let bytes = trunk
.part_bytes(SegmentSeq::new(9), PartIndex::new(0))
.expect("a published part of an open segment must be addressable now");
assert_eq!(bytes, Bytes::from(vec![0xAB; 8]));
assert!(
trunk.last_closed_segment().is_none(),
"the part landed without any segment ever closing"
);
}
#[test]
fn waiter_is_woken_when_the_awaited_part_lands_and_resolves_to_it() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let writer = Arc::new(trunk.segment_writer().unwrap());
let listener = trunk.listen().expect("first registration must succeed");
assert!(
trunk
.part_bytes(SegmentSeq::new(9), PartIndex::new(0))
.is_none(),
"not published yet"
);
let bg_writer = Arc::clone(&writer);
let handle = thread::spawn(move || {
bg_writer.publish_part(part_entry(0xCC, 9, 1));
bg_writer.publish_part(part_entry(0xAB, 9, 0));
});
let woken = listener.wait_deadline(std::time::Instant::now() + Duration::from_secs(10));
assert!(
woken,
"listener must wake on publish_part, not park forever"
);
handle.join().unwrap();
let bytes = trunk
.part_bytes(SegmentSeq::new(9), PartIndex::new(0))
.expect("the awaited part must now be readable");
assert_eq!(
bytes,
Bytes::from(vec![0xAB; 8]),
"must resolve to the awaited part (index 0), not the decoy (index 1)"
);
}
#[test]
fn waiter_whose_target_never_arrives_is_bounded_not_parked_forever() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let listener = trunk.listen().unwrap();
let start = std::time::Instant::now();
let woken = listener.wait_deadline(start + Duration::from_millis(150));
let elapsed = start.elapsed();
assert!(!woken, "must report timeout, not a fabricated wake-up");
assert!(
elapsed < Duration::from_secs(60),
"must actually return at the deadline, not hang: took {elapsed:?}"
);
}
#[test]
fn writer_never_blocks_with_a_registered_never_serviced_waiter() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let writer = Arc::new(trunk.segment_writer().unwrap());
let _never_serviced = trunk.listen().unwrap();
let (done_tx, done_rx) = mpsc::channel();
let bg_writer = Arc::clone(&writer);
thread::spawn(move || {
bg_writer.publish_part(part_entry(1, 1, 0));
bg_writer.publish_segment(segment_entry(2, 1)).unwrap();
done_tx.send(()).unwrap();
});
done_rx.recv_timeout(Duration::from_secs(60)).expect(
"publish_part/publish_segment must complete promptly even with \
a live, never-serviced waiter registered",
);
}
#[test]
fn waiter_set_is_bounded_a_flood_of_listen_calls_cannot_grow_without_limit() {
let cap = nz(4).get();
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(cap)));
let mut held: Vec<ProgressListener> = Vec::new();
for _ in 0..cap {
held.push(trunk.listen().expect("must succeed up to the cap"));
}
assert_eq!(trunk.waiter_count(), cap);
assert!(
trunk.listen().is_none(),
"must refuse a registration beyond part_capacity"
);
for _ in 0..50_000 {
let l = trunk.listen();
assert!(
trunk.waiter_count() <= cap,
"waiter count exceeded part_capacity mid-flood"
);
drop(l);
}
assert_eq!(
trunk.waiter_count(),
cap,
"the held registrations are still exactly at the cap"
);
held.pop();
assert_eq!(trunk.waiter_count(), cap - 1);
assert!(
trunk.listen().is_some(),
"a released slot must be re-usable"
);
}
#[test]
fn freeing_a_waiter_slot_does_not_wake_an_unrelated_progress_waiter() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let a = trunk.listen().expect("first registration succeeds");
let b = trunk.listen().expect("second registration succeeds");
drop(a);
let woken = b.wait_deadline(std::time::Instant::now() + Duration::from_millis(50));
assert!(
!woken,
"releasing a waiter slot must not wake an unrelated `progress` \
waiter — that is the ping-pong wake-storm (a >2-waiter caller set \
would 100% CPU with no media progress)"
);
}
#[test]
fn a_parked_slot_freed_waiter_wakes_when_a_slot_is_freed() {
let cap = nz(4).get();
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(cap)));
let mut held: Vec<ProgressListener> = Vec::new();
for _ in 0..cap {
held.push(trunk.listen().expect("succeeds up to the cap"));
}
let parked = trunk
.waiter_slot_freed()
.expect("at the cap, a slot-freed future must be returned");
let holder = held.pop().expect("a held slot");
let handle = thread::spawn(move || {
drop(holder);
});
let woken = parked
.wait_deadline(std::time::Instant::now() + Duration::from_secs(10))
.is_some();
assert!(
woken,
"a released slot must wake the caller parked on `waiter_slot_freed`"
);
handle.join().unwrap();
}
#[test]
fn waiter_slot_freed_registers_before_rechecking_the_count() {
let cap = nz(4).get();
for _ in 0..200 {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(cap)));
let mut held: Vec<ProgressListener> = Vec::new();
for _ in 0..cap {
held.push(trunk.listen().expect("succeeds up to the cap"));
}
let holder = held.pop().expect("a held slot");
let t = Arc::clone(&trunk);
let handle = thread::spawn(move || {
drop(holder);
let _ = &t;
});
let fut = trunk.waiter_slot_freed();
match fut {
None => {
assert!(
trunk.listen().is_some(),
"a `None` from `waiter_slot_freed` promises a free slot"
);
}
Some(listener) => {
let woken = listener
.wait_deadline(std::time::Instant::now() + Duration::from_millis(50))
.is_some();
assert!(
woken,
"a slot freed concurrently with `waiter_slot_freed` must \
wake the returned listener (check-then-listen misses it)"
);
}
}
handle.join().unwrap();
}
}
#[test]
fn parts_remain_addressable_after_segment_close_until_ordinary_eviction_reclaims_them() {
let part_cap = nz(4).get();
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(part_cap)));
let writer = trunk.segment_writer().unwrap();
writer.publish_part(part_entry(0xAB, 1, 0));
writer.publish_segment(segment_entry(1, 1)).unwrap();
assert_eq!(
trunk.part_bytes(SegmentSeq::new(1), PartIndex::new(0)),
Some(Bytes::from(vec![0xAB; 8])),
"a just-closed segment's part must still be individually fetchable"
);
assert_eq!(trunk.last_closed_segment(), Some(SegmentSeq::new(1)));
for i in 0..part_cap as u32 {
writer.publish_part(part_entry(0xFF, 99, i));
}
assert!(
trunk
.part_bytes(SegmentSeq::new(1), PartIndex::new(0))
.is_none(),
"the part is gone once ordinary part_capacity eviction reclaims \
it — NOT because its segment closed, but because the ring's own \
bound was exceeded, exactly like every other ring in this module"
);
}
#[test]
fn fresh_trunk_has_no_tracks_and_generation_zero() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
assert_eq!(trunk.tracks().len(), 0, "nothing has ever set a track set");
assert_eq!(trunk.track_generation(), 0);
}
#[test]
fn set_tracks_replaces_the_whole_set_and_bumps_generation_by_one_per_call() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let writer = trunk.writer().unwrap();
writer.set_tracks(vec![opaque_track(TrackId::new(1))]);
assert_eq!(
trunk
.tracks()
.iter()
.map(|t| t.track_id)
.collect::<Vec<_>>(),
vec![TrackId::new(1)]
);
assert_eq!(trunk.track_generation(), 1);
writer.set_tracks(vec![
opaque_track(TrackId::new(7)),
opaque_track(TrackId::new(9)),
]);
assert_eq!(
trunk
.tracks()
.iter()
.map(|t| t.track_id)
.collect::<Vec<_>>(),
vec![TrackId::new(7), TrackId::new(9)],
"set_tracks must replace the set wholesale, not append to it"
);
assert_eq!(
trunk.track_generation(),
2,
"generation must advance by exactly one per set_tracks call"
);
}
#[test]
fn generation_is_stable_across_unrelated_activity() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let writer = trunk.writer().unwrap();
writer.set_tracks(vec![opaque_track(TrackId::new(1))]);
assert_eq!(trunk.track_generation(), 1);
for i in 0u8..10 {
writer.publish(TrackId::new(1), RetentionClass::Timed, sample(i, 4));
}
writer.publish_event(basic_event(1), EventAnchor::Media(MediaTime(0)));
assert_eq!(
trunk.track_generation(),
1,
"publishing samples/events must never bump track_generation"
);
}
#[test]
fn set_tracks_wakes_a_registered_listener() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let writer = Arc::new(trunk.writer().unwrap());
let listener = trunk.listen().expect("first registration must succeed");
let bg_writer = Arc::clone(&writer);
let handle = thread::spawn(move || {
bg_writer.set_tracks(vec![opaque_track(TrackId::new(1))]);
});
let woken = listener.wait_deadline(std::time::Instant::now() + Duration::from_secs(10));
assert!(woken, "listener must wake on set_tracks, not park forever");
handle.join().unwrap();
assert_eq!(trunk.track_generation(), 1);
}
#[test]
fn subscriber_side_panic_while_holding_the_lock_does_not_poison_later_calls() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let writer = trunk.writer().unwrap();
let trunk_for_panic = Arc::clone(&trunk);
let handle = thread::spawn(move || {
let _guard = trunk_for_panic.state.lock();
panic!("simulated panic while holding Trunk::state");
});
assert!(
handle.join().is_err(),
"setup: the thread must have panicked"
);
writer.publish(TrackId::new(1), RetentionClass::Timed, sample(1, 4));
assert_eq!(
trunk.timed_len(),
1,
"publish after the panic must take effect"
);
assert!(
trunk.state.try_lock().is_some(),
"the lock must be free after the panicking holder unwound"
);
}
#[test]
fn segment_never_overtakes_its_samples_under_a_split_lock() {
const ROUNDS: u32 = 2_000;
let trunk = Trunk::new(TrunkConfig::new(nz(4096), nz(4), nz(4096), nz(8), nz(8)));
let writer = trunk.writer().unwrap();
let seg_writer = trunk.segment_writer().unwrap();
let mut seg_samples = trunk.subscribe();
let mut watcher_samples = trunk.subscribe();
let mut watcher_segments = trunk.subscribe_segments();
let ingest = thread::spawn(move || {
for i in 0..ROUNDS {
writer.publish(
TrackId::new(1),
RetentionClass::Timed,
sample((i % 251) as u8, 4),
);
}
});
let segmenter = thread::spawn(move || {
let mut seen = 0u32;
let mut seq = 1u32;
while seq <= ROUNDS {
if let Some(SampleCursorItem::Timed { .. }) = seg_samples.poll() {
seen += 1;
if seen == seq {
seg_writer.publish_segment(segment_entry(0, seq)).unwrap();
seq += 1;
}
}
}
});
let mut delivered = 0u32;
let mut segs = 0u32;
while segs < ROUNDS {
if let Some(SegmentCursorItem::Segment(entry)) = watcher_segments.poll() {
segs += 1;
while delivered < entry.sequence_number.value() {
match watcher_samples.poll() {
Some(SampleCursorItem::Timed { .. }) => delivered += 1,
Some(_) => {}
None => panic!(
"segment {} visible but only {delivered} samples deliverable",
entry.sequence_number
),
}
}
}
}
ingest.join().unwrap();
segmenter.join().unwrap();
}
#[test]
fn try_publish_segment_lets_a_single_thread_drain_its_own_pin() {
let trunk = Trunk::new(TrunkConfig::new(nz(10), nz(10), nz(1), nz(8), nz(8)));
let writer = trunk.segment_writer().unwrap();
let mut cursor = trunk.pin_segments(ArchiveOverrun::StallIngest);
writer
.try_publish_segment(segment_entry(1, 1))
.expect("room for the first segment");
let entry2 = match writer.try_publish_segment(segment_entry(2, 2)) {
Err(TryPublishSegmentError::WouldStall(entry)) => entry,
other => panic!(
"must report WouldStall (Err) instead of silently evicting \
the un-consumed seq 1: {other:?}"
),
};
assert_eq!(
trunk.segment_len(),
1,
"seq 1 must not have been evicted yet"
);
assert!(matches!(cursor.poll(), Some(SegmentCursorItem::Segment(_))));
writer
.try_publish_segment(entry2)
.expect("must succeed once the pin has advanced past seq 1");
assert_eq!(trunk.segment_len(), 1);
assert_eq!(trunk.last_closed_segment(), Some(SegmentSeq::new(2)));
}
#[test]
fn publish_segment_blocking_path_is_bounded_not_permanent() {
let trunk = Trunk::new(TrunkConfig::new(nz(10), nz(10), nz(1), nz(8), nz(8)));
let writer = trunk.segment_writer().unwrap();
let mut cursor = trunk.pin_segments(ArchiveOverrun::StallIngest);
writer.publish_segment(segment_entry(1, 1)).unwrap();
let (done_tx, done_rx) = mpsc::channel();
let writer = Arc::new(writer);
let bg_writer = Arc::clone(&writer);
let handle = thread::spawn(move || {
bg_writer.publish_segment(segment_entry(2, 2)).unwrap();
done_tx.send(()).unwrap();
});
done_rx
.recv_timeout(Duration::from_secs(10))
.expect("publish_segment must give up and return within its bounded wait");
handle.join().unwrap();
assert_eq!(
trunk.segment_len(),
1,
"seq 1 was evicted to make room for seq 2"
);
assert_eq!(trunk.last_closed_segment(), Some(SegmentSeq::new(2)));
assert!(
matches!(cursor.poll(), Some(SegmentCursorItem::Terminated)),
"an expired StallIngest wait must terminate the pin, not \
silently gap it"
);
}
#[test]
fn writer_handover_keeps_subscribers_reading_across_a_reconnect() {
let trunk = Trunk::new(TrunkConfig::new(nz(10), nz(10), nz(4), nz(8), nz(8)));
let mut cursor = trunk.subscribe();
{
let writer_a = trunk.writer().expect("first take succeeds");
writer_a.publish(TrackId::new(1), RetentionClass::Timed, sample(0xAA, 4));
writer_a.publish(TrackId::new(1), RetentionClass::Timed, sample(0xAB, 4));
}
let writer_b = trunk
.writer()
.expect("re-issue after the first writer was dropped must succeed");
writer_b.publish(TrackId::new(1), RetentionClass::Timed, sample(0xBA, 4));
writer_b.publish(TrackId::new(1), RetentionClass::Timed, sample(0xBB, 4));
for expected in [0xAAu8, 0xAB] {
match cursor.poll() {
Some(SampleCursorItem::Timed { sample, .. }) => {
assert_eq!(sample.data.as_ref(), &[expected; 4]);
}
other => panic!("expected A's sample 0x{expected:02X}, got {other:?}"),
}
}
assert!(
matches!(cursor.poll(), Some(SampleCursorItem::Discontinuity)),
"exactly one Discontinuity must appear between A's data and B's"
);
for expected in [0xBAu8, 0xBB] {
match cursor.poll() {
Some(SampleCursorItem::Timed { sample, .. }) => {
assert_eq!(sample.data.as_ref(), &[expected; 4]);
}
other => panic!("expected B's sample 0x{expected:02X}, got {other:?}"),
}
}
assert!(
cursor.poll().is_none(),
"no extra Discontinuity or data beyond what was published"
);
}
#[test]
fn discontinuity_gates_both_classes_not_just_the_one_that_catches_up_first() {
let trunk = Trunk::new(TrunkConfig::new(nz(10), nz(10), nz(4), nz(8), nz(8)));
let mut cursor = trunk.subscribe();
{
let a = trunk.writer().expect("first take succeeds");
a.publish(TrackId::new(1), RetentionClass::Timed, sample(0xAA, 4));
}
let b = trunk
.writer()
.expect("re-issue after the first writer was dropped must succeed");
b.publish(TrackId::new(2), RetentionClass::Sparse, sample(0xBB, 4));
let mut seq = Vec::new();
while let Some(it) = cursor.poll() {
seq.push(match it {
SampleCursorItem::Timed { .. } => "A-timed",
SampleCursorItem::Sparse { .. } => "B-sparse",
SampleCursorItem::Discontinuity => "DISC",
other => panic!("unexpected item: {other:?}"),
});
}
assert_eq!(seq, vec!["A-timed", "DISC", "B-sparse"], "got {seq:?}");
}
#[test]
fn segment_writer_handover_keeps_subscriber_reading_across_a_reconnect() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(10), nz(8), nz(8)));
let mut cursor = trunk.subscribe_segments();
{
let writer_a = trunk.segment_writer().expect("first take succeeds");
writer_a.publish_segment(segment_entry(0xAA, 1)).unwrap();
writer_a.publish_segment(segment_entry(0xAB, 2)).unwrap();
}
let writer_b = trunk
.segment_writer()
.expect("re-issue after the first segment writer was dropped must succeed");
writer_b.publish_segment(segment_entry(0xBA, 3)).unwrap();
writer_b.publish_segment(segment_entry(0xBB, 4)).unwrap();
for expected_seq in 1..=4u32 {
match cursor.poll() {
Some(SegmentCursorItem::Segment(entry)) => {
assert_eq!(entry.sequence_number, SegmentSeq::new(expected_seq));
}
other => panic!("expected Segment({expected_seq}), got {other:?}"),
}
}
assert!(cursor.poll().is_none());
}
#[test]
fn writer_still_refused_while_the_first_is_alive() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let _writer_a = trunk.writer().expect("first take succeeds");
assert!(
trunk.writer().is_none(),
"a second concurrent TrunkWriter must still be refused"
);
let _segment_writer_a = trunk.segment_writer().expect("first take succeeds");
assert!(
trunk.segment_writer().is_none(),
"a second concurrent SegmentWriter must still be refused"
);
}
#[test]
fn handover_backlog_overflow_reports_handovers_lagged_not_silence() {
let trunk = Trunk::new(TrunkConfig::new(nz(1_000), nz(1_000), nz(4), nz(8), nz(8)));
let mut cursor = trunk.subscribe();
let total_takes = WRITER_HANDOVER_LOG_CAPACITY + 5;
for _ in 0..total_takes {
let w = trunk.writer().expect("reissue must keep succeeding");
w.publish(TrackId::new(1), RetentionClass::Timed, sample(0x01, 4));
}
let pushes = total_takes - 1;
let expected_skipped = (pushes - WRITER_HANDOVER_LOG_CAPACITY) as u64;
match cursor.poll() {
Some(SampleCursorItem::HandoversLagged { skipped }) => {
assert_eq!(skipped, expected_skipped);
}
other => {
panic!("expected HandoversLagged {{ skipped: {expected_skipped} }}, got {other:?}")
}
}
let mut timed_count = 0;
while let Some(item) = cursor.poll() {
if matches!(item, SampleCursorItem::Timed { .. }) {
timed_count += 1;
}
}
assert_eq!(
timed_count, total_takes,
"every published sample must still be delivered"
);
}
#[test]
fn part_bytes_reused_segment_number_returns_the_most_recent() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
{
let w = trunk.segment_writer().unwrap();
w.publish_part(PartEntry::new(
Bytes::from_static(b"old"),
SegmentSeq::new(1),
PartIndex::new(0),
Duration::from_millis(200),
true,
));
}
let w2 = trunk.segment_writer().unwrap();
w2.publish_part(PartEntry::new(
Bytes::from_static(b"new"),
SegmentSeq::new(1),
PartIndex::new(0),
Duration::from_millis(200),
true,
));
assert_eq!(
trunk
.part_bytes(SegmentSeq::new(1), PartIndex::new(0))
.unwrap()
.as_ref(),
b"new"
);
}
#[test]
fn events_in_open_reused_segment_not_hidden_by_a_stale_next_boundary() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let writer = trunk.writer().unwrap();
let sw = trunk.segment_writer().unwrap();
sw.note_segment_start(SegmentSeq::new(1), MediaTime(0));
sw.note_segment_start(SegmentSeq::new(2), MediaTime(90_000));
sw.note_segment_start(SegmentSeq::new(1), MediaTime(200_000)); writer.publish_event(
basic_event(7),
EventAnchor::Segment {
segment_number: SegmentSeq::new(1),
delta: 1_500,
},
);
assert_eq!(
trunk.events_in_segment(SegmentSeq::new(1)).len(),
1,
"open reused segment 1 must contain the event"
);
}
#[test]
fn try_publish_segment_does_not_terminate_a_pin_for_an_eviction_that_never_happened() {
let trunk = Trunk::new(TrunkConfig::new(nz(10), nz(10), nz(1), nz(8), nz(8)));
let w = trunk.segment_writer().unwrap();
let _stall = trunk.pin_segments(ArchiveOverrun::StallIngest);
let mut term = trunk.pin_segments(ArchiveOverrun::Terminate);
w.try_publish_segment(segment_entry(1, 1))
.expect("room for the first segment");
assert!(matches!(
w.try_publish_segment(segment_entry(2, 2)),
Err(TryPublishSegmentError::WouldStall(_))
));
assert_eq!(trunk.segment_len(), 1);
let it = term.poll();
assert!(
matches!(it, Some(SegmentCursorItem::Segment(_))),
"got {it:?}"
);
}
#[test]
fn expire_stalled_pins_terminates_on_demand_without_blocking() {
let trunk = Trunk::new(TrunkConfig::new(nz(10), nz(10), nz(1), nz(8), nz(8)));
let w = trunk.segment_writer().unwrap();
let mut stall = trunk.pin_segments(ArchiveOverrun::StallIngest);
assert_eq!(w.expire_stalled_pins(), 0);
w.publish_segment(segment_entry(1, 1)).unwrap();
assert!(w.try_publish_segment(segment_entry(2, 2)).is_err());
assert_eq!(
w.expire_stalled_pins(),
1,
"exactly the one blocking StallIngest pin must be terminated"
);
assert!(
matches!(stall.poll(), Some(SegmentCursorItem::Terminated)),
"the pin's cursor must see Terminated, exactly ArchiveOverrun::Terminate's own signal"
);
w.try_publish_segment(segment_entry(2, 2))
.expect("must succeed once the blocking pin has been terminated");
assert_eq!(trunk.last_closed_segment(), Some(SegmentSeq::new(2)));
}
#[test]
fn parts_in_segment_finds_open_segments_parts_even_after_a_later_segments_part_arrives() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(8), nz(8)));
let w = trunk.segment_writer().unwrap();
w.publish_part(PartEntry::new(
Bytes::from_static(b"a"),
SegmentSeq::new(1),
PartIndex::new(0),
Duration::from_millis(200),
true,
));
w.publish_part(PartEntry::new(
Bytes::from_static(b"b"),
SegmentSeq::new(1),
PartIndex::new(1),
Duration::from_millis(200),
false,
));
w.publish_part(PartEntry::new(
Bytes::from_static(b"c"),
SegmentSeq::new(2),
PartIndex::new(0),
Duration::from_millis(200),
true,
));
assert_eq!(
trunk.parts_in_segment(SegmentSeq::new(1)).len(),
2,
"the still-open segment 1's own two parts must not vanish"
);
assert_eq!(
trunk.parts_in_segment(SegmentSeq::new(2)).len(),
1,
"segment 2's own opening part must resolve independently"
);
}
#[test]
fn segment_start_time_returns_reported_start() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(4), nz(4)));
let writer = trunk.segment_writer().unwrap();
writer.note_segment_start(SegmentSeq::new(7), MediaTime(12345));
assert_eq!(
trunk.segment_start_time(SegmentSeq::new(7)),
Some(MediaTime(12345))
);
}
#[test]
fn segment_start_time_unknown_seq_is_none() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(4), nz(4)));
let writer = trunk.segment_writer().unwrap();
writer.note_segment_start(SegmentSeq::new(7), MediaTime(12345));
assert_eq!(trunk.segment_start_time(SegmentSeq::new(8)), None);
}
#[test]
fn segment_start_time_evicted_returns_none() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(2), nz(4)));
let writer = trunk.segment_writer().unwrap();
writer.note_segment_start(SegmentSeq::new(1), MediaTime(100));
writer.note_segment_start(SegmentSeq::new(2), MediaTime(200));
writer.note_segment_start(SegmentSeq::new(3), MediaTime(300));
assert_eq!(trunk.segment_start_time(SegmentSeq::new(1)), None);
assert_eq!(
trunk.segment_start_time(SegmentSeq::new(2)),
Some(MediaTime(200))
);
assert_eq!(
trunk.segment_start_time(SegmentSeq::new(3)),
Some(MediaTime(300))
);
}
#[test]
fn segment_start_time_two_segments_distinct() {
let trunk = Trunk::new(TrunkConfig::new(nz(4), nz(4), nz(4), nz(4), nz(4)));
let writer = trunk.segment_writer().unwrap();
writer.note_segment_start(SegmentSeq::new(1), MediaTime(1000));
writer.note_segment_start(SegmentSeq::new(2), MediaTime(2000));
assert_eq!(
trunk.segment_start_time(SegmentSeq::new(1)),
Some(MediaTime(1000))
);
assert_eq!(
trunk.segment_start_time(SegmentSeq::new(2)),
Some(MediaTime(2000))
);
}
}