Skip to main content

Trunk

Struct Trunk 

Source
pub struct Trunk { /* private fields */ }
Available on crate feature std only.
Expand description

The sample ring: bounded, dual-retention-class, single-writer, multi-cursor. See the module docs for the design this implements and the benchmark that shaped it.

Always held as Arc<Trunk>Trunk::writer and Trunk::subscribe take self: &Arc<Self> because a TrunkWriter/SampleCursor each need to keep the shared state alive independently of the Trunk handle that created them, exactly as spikes/trunk-bench’s validated shape does.

Implementations§

Source§

impl Trunk

Source

pub fn new(config: TrunkConfig) -> Arc<Trunk>

Construct a fresh, empty Trunk.

Cannot fail and cannot panic on its configuration: every TrunkConfig capacity is a NonZeroUsize, so the one invalid value (zero — a ring that evicts every entry the instant it is pushed) is unrepresentable rather than merely rejected. See TrunkConfig’s own docs for why that replaced this method’s former five assert!s.

Source

pub fn writer(self: &Arc<Self>) -> Option<TrunkWriter>

Take the one TrunkWriter for this Trunk — the write handle for the samples + events ring group (TrunkWriter::publish/ TrunkWriter::publish_event). See One writer per ring group for the invariant this enforces (and why it does not also cover Trunk::segment_writer’s group).

Returns None on every call after the first — this ring group has exactly one writer, enforced here rather than left as a documented-only convention, because a second concurrent sample/event writer would silently interleave two unrelated publish sequences into the same ring with no way for a reader to tell them apart.

Source

pub fn segment_writer(self: &Arc<Self>) -> Option<SegmentWriter>

Take the one SegmentWriter for this Trunk — the write handle for the segments + parts ring group (SegmentWriter::publish_segment/ SegmentWriter::publish_part/SegmentWriter::note_segment_start/ SegmentWriter::set_time_anchor), independent of Trunk::writer’s group so a segmenter can hold this while the ingest driver simultaneously holds a TrunkWriter — see One writer per ring group for why the split is safe and what it does and does not guarantee across rings.

Returns None on every call after the first — this ring group has exactly one writer too, guarded by its own AtomicBool rather than Trunk::writer’s, for exactly the same reason: a second concurrent segment/part writer would silently interleave two unrelated publish sequences into the segment or part ring.

Source

pub fn subscribe(self: &Arc<Self>) -> SampleCursor

Subscribe a new SampleCursor, starting from now — the next entry TrunkWriter::publish produces after this call, not any backlog already in either ring. See Trunk::subscribe_from_backlog for the seek-to-past variant this method’s own docs used to anticipate (a consumer built after samples it needs already landed in the ring — e.g. a segmenter reacting to the same batch that announced its program — wants that one instead).

§This call is fan-out — read this before calling it per connection

spikes/trunk-bench measured writer cost as O(N) in cursor count (956 ns → 9.98 µs from 1 → 16 readers; spec §3.1) — every cursor contends the same shared lock every publish. A cursor is for a distinct consumer of the stream (a segmenter, a DVR writer, an analysis tap, one push relay) — never one per peer of a one-to-many protocol. Supported reader count is single-digit by design: LL-HLS serving a thousand viewers takes one cursor here and fans out to its viewers itself, at the layer that already holds per-viewer state anyway. Do not call this once per connection; there is no tee, broadcast channel, or per-consumer queue to reach for instead — a sample’s payload is already bytes::Bytes, so fan-out beyond this one cursor is a refcount bump the relay performs itself, not something this type needs to do for you.

Source

pub fn subscribe_from_backlog(self: &Arc<Self>) -> SampleCursor

Subscribe a new SampleCursor, starting from the oldest entry each ring currently retains instead of Trunk::subscribe’s “now” — i.e. this cursor’s first poll replays whatever backlog is still resident in the Timed ring and the Sparse ring, each independently, before catching up to the live tail.

This is the “seek-to-past variant” Trunk::subscribe’s own docs anticipated (“a later step may add a seek-to-past variant… this step does not need one”) — the step turned out to be issue #808’s segment-bridge fix: a TrunkWriter::publish batch that lands before a consumer subscribes (e.g. the very same feed call that both announces a program and publishes its first samples) is otherwise invisible to a Trunk::subscribe cursor forever, even though the samples are sitting right there in the ring.

§Replay is bounded by ring capacity, not “everything ever published”

This does not reach further back than what each ring still holds: an entry already evicted by TrunkConfig::timed_capacity/ TrunkConfig::sparse_capacity before this call is gone, exactly as it would be for any other cursor — there is no unbounded replay log behind this method, only the same fixed-size rings every other cursor reads. Concretely: this cursor starts at each ring’s current base (the oldest index still resident), not index 0, so its first poll never reports a spurious Lagged/Degraded for data that was evicted before this call — from this cursor’s point of view, “backlog” means “what the ring can show me right now”, not “what was ever published”. Both retention classes replay this way, independently: a Timed backlog and a Sparse backlog are each bounded by their own ring’s own capacity.

SampleCursorItem::Lagged/SampleCursorItem::Degraded still fire exactly as they do for a Trunk::subscribe cursor for any loss that happens after this call — falling behind the live tail once subscribed is reported in-band the same way for both kinds of cursor; only the starting position differs.

§This call is fan-out — read this before calling it per connection

Exactly Trunk::subscribe’s own fan-out warning, verbatim: writer cost is O(N) in cursor count (spikes/trunk-bench measured 956 ns → 9.98 µs from 1 → 16 readers; spec §3.1) — every cursor contends the same shared lock every publish. A cursor is for a distinct consumer of the stream, never one per peer of a one-to-many protocol. Supported reader count is single-digit by design; do not call this once per connection.

Source

pub fn timed_len(&self) -> usize

Diagnostic: entries currently resident in the Timed ring. Never exceeds TrunkConfig::timed_capacity.

Source

pub fn sparse_len(&self) -> usize

Diagnostic: entries currently resident in the Sparse ring. Never exceeds TrunkConfig::sparse_capacity.

Source

pub fn subscribe_segments(self: &Arc<Self>) -> SegmentCursor

Subscribe a new non-pinning SegmentCursor, starting from now — the same “next entry only, no backlog” rule as Trunk::subscribe, and the same single-digit-reader, one-cursor-per-distinct-consumer guidance from that method’s docs applies here verbatim (this cursor contends exactly the lock subscribe’s cursors do).

This cursor is not protected by ArchiveOverrun: if it falls behind the segment log’s ordinary TrunkConfig::segment_capacity eviction, it simply sees SegmentCursorItem::Lagged, exactly like an ordinary RetentionClass::Timed sample reader. Use this for a consumer that tolerates ordinary loss (LL-HLS window rendering, catch-up within the live window) — use Trunk::pin_segments instead for a consumer that must not miss a segment (DVR/archive).

Source

pub fn pin_segments( self: &Arc<Self>, on_overrun: ArchiveOverrun, ) -> SegmentCursor

Subscribe a new pinning SegmentCursor for a DVR/archive consumer that must not miss a segment — see The DVR contradiction for the full design story this method is the entry point for.

on_overrun is this cursor’s chosen ArchiveOverrun for the one moment its guarantee runs out: the segment log at TrunkConfig::segment_capacity, about to evict an entry this cursor has not yet consumed. There is no default parameter here on purpose — pinning is an explicit request for a stronger guarantee than Trunk::subscribe_segments gives, so the trade made when that guarantee cannot be kept is an explicit choice too, not a silent fallback (though ArchiveOverrun::default exists for a caller that affirmatively wants the same default the rest of this module uses).

Also starts from now, and also single-digit-by-design — the same fan-out rule as Trunk::subscribe and Trunk::subscribe_segments applies; a pinning cursor is exactly as expensive per publish as any other.

Source

pub fn segment_len(&self) -> usize

Diagnostic: entries currently resident in the segment log. Never exceeds TrunkConfig::segment_capacity — true even with an un-acking pinning cursor attached, which is exactly the property The DVR contradiction’s “pinning is bounded” claim means.

Source

pub fn subscribe_events(self: &Arc<Self>) -> EventCursor

Subscribe a new EventCursor over the event log, starting from now — the same “next entry only, no backlog” rule as Trunk::subscribe/Trunk::subscribe_segments, and the same single-digit-reader, one-cursor-per-distinct-consumer guidance applies here verbatim (this cursor contends exactly the lock every other cursor does).

A streaming consumer — e.g. a playback scheduler that wants every event as it resolves — wants this. A point-in-time query — “what has resolved for segment N” (a manifest renderer), or “what resolved between T1 and T2” (that same scheduler, replaying its window) — wants Trunk::events_in_segment/Trunk::events_between instead; both read the same log, just as a snapshot rather than a moving position. See The event log.

Source

pub fn events_between(&self, from: MediaTime, to: MediaTime) -> Vec<EventEntry>

Every currently-resolved (EventAnchor::Media) event whose media time falls in the half-open range [from, to) — start inclusive, end exclusive. An entry still Segment/Utc-anchored never appears here: it has no honest media time yet, and fabricating one to satisfy this query would be exactly B1 — see The event log.

Source

pub fn events_in_segment(&self, segment_number: u32) -> Vec<EventEntry>

Every currently-resolved event whose media time falls within segment segment_number’s span: [start_N, start_{N+1}) once SegmentWriter::note_segment_start has reported the next segment’s start too, else [start_N, ∞) (the segment is still open — nothing yet says where it ends). Returns nothing for a segment_number this trunk has never reported a start for: there is no span to contain anything, and an unresolved EventAnchor::Segment entry targeting it is not returned either, for the same B1 reason Trunk::events_between documents.

Source

pub fn event_len(&self) -> usize

Diagnostic: entries currently resident in the event log. Never exceeds TrunkConfig::event_capacity.

Source

pub fn part_bytes(&self, segment_number: u32, part_index: u32) -> Option<Bytes>

A live part’s bytes by (segment_number, part_index) — the direct, &self-shaped query a ServedEgress implementation needs to answer “does this part exist right now”, exactly the shape Trunk::events_between/Trunk::events_in_segment already give the event log rather than forcing a caller to drain a cursor into a self-maintained cache. See The live-part log for why a part answers Some here for as long as it has not been evicted by TrunkConfig::part_capacity’s ordinary bound — including after its parent segment has closed.

Source

pub fn parts_in_segment(&self, segment_number: u32) -> Vec<PartEntry>

Every currently-resident part of segment segment_number, in publish order — the part-log counterpart of Trunk::events_in_segment, letting a caller derive “how many parts does the open segment have so far” (RFC 8216bis’s _HLS_part blocking-reload condition) without a cursor.

Source

pub fn part_len(&self) -> usize

Diagnostic: entries currently resident in the live-part log. Never exceeds TrunkConfig::part_capacity.

Source

pub fn waiter_count(&self) -> usize

Diagnostic: currently-outstanding ProgressListener registrations (from Trunk::listen, not yet dropped). Never exceeds TrunkConfig::part_capacity — see The reader-wake primitive.

Source

pub fn last_closed_segment(&self) -> Option<u32>

The sequence number of the most-recently-closed segment (the newest SegmentWriter::publish_segment call), or None if no segment has closed yet. Distinguishes “closed” (a whole, fetchable SegmentEntry) from merely “has live parts” — RFC 8216bis §6.2.5.2’s bare-_HLS_msn blocking-reload condition needs exactly this distinction (mirrors hls_runtime::server::MediaStore::last_closed_segment_seq, which this method lets a ServedEgress stop duplicating).

Source

pub fn tracks(&self) -> Arc<[TrackSpec]>

This program’s current complete track set — see TrunkWriter::set_tracks for how it is set/replaced. Empty until the first set_tracks call (a freshly-minted Trunk announces no tracks yet). Stored as Arc<[TrackSpec]>, so this is a cheap Arc clone (a refcount bump), never a Vec copy, however many tracks the program carries.

Source

pub fn track_generation(&self) -> u64

Bumped by exactly one on every TrunkWriter::set_tracks call (including one that happens to set an identical set to what was already there — this counts calls, not distinct sets). Lets a consumer detect “the track set may have changed” by comparing two u64s rather than diffing two Vec<TrackSpec>s — cheap regardless of how many tracks a program carries. 0 until the first set_tracks call.

Source

pub fn listen(self: &Arc<Self>) -> Option<ProgressListener>

Register for the next part/segment-close notification — see The reader-wake primitive.

Returns None once TrunkConfig::part_capacity concurrent registrations are already outstanding — the caller must not wait in that case (there is no slot to wait in); it should fall back to an immediate re-poll or answer its request as unavailable now, exactly as a caller must once crate::egress::AwaitPolicy itself has expired. Register before re-checking the condition you are waiting onevent_listener’s standard idiom, and the same ordering hls_runtime::server::MediaStore::listen’s own docs require — otherwise a notify racing your check can be missed.

Auto Trait Implementations§

§

impl !Freeze for Trunk

§

impl RefUnwindSafe for Trunk

§

impl Send for Trunk

§

impl Sync for Trunk

§

impl Unpin for Trunk

§

impl UnsafeUnpin for Trunk

§

impl UnwindSafe for Trunk

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.