pub struct Trunk { /* private fields */ }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
impl Trunk
Sourcepub fn new(config: TrunkConfig) -> Arc<Trunk> ⓘ
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.
Sourcepub fn writer(self: &Arc<Self>) -> Option<TrunkWriter>
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.
Sourcepub fn segment_writer(self: &Arc<Self>) -> Option<SegmentWriter>
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.
Sourcepub fn subscribe(self: &Arc<Self>) -> SampleCursor
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.
Sourcepub fn subscribe_from_backlog(self: &Arc<Self>) -> SampleCursor
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.
Sourcepub fn timed_len(&self) -> usize
pub fn timed_len(&self) -> usize
Diagnostic: entries currently resident in the Timed ring. Never
exceeds TrunkConfig::timed_capacity.
Sourcepub fn sparse_len(&self) -> usize
pub fn sparse_len(&self) -> usize
Diagnostic: entries currently resident in the Sparse ring. Never
exceeds TrunkConfig::sparse_capacity.
Sourcepub fn subscribe_segments(self: &Arc<Self>) -> SegmentCursor
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).
Sourcepub fn pin_segments(
self: &Arc<Self>,
on_overrun: ArchiveOverrun,
) -> SegmentCursor
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.
Sourcepub fn segment_len(&self) -> usize
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.
Sourcepub fn subscribe_events(self: &Arc<Self>) -> EventCursor
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.
Sourcepub fn events_between(&self, from: MediaTime, to: MediaTime) -> Vec<EventEntry>
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.
Sourcepub fn events_in_segment(&self, segment_number: u32) -> Vec<EventEntry>
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.
Sourcepub fn event_len(&self) -> usize
pub fn event_len(&self) -> usize
Diagnostic: entries currently resident in the event log. Never
exceeds TrunkConfig::event_capacity.
Sourcepub fn part_bytes(&self, segment_number: u32, part_index: u32) -> Option<Bytes>
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.
Sourcepub fn parts_in_segment(&self, segment_number: u32) -> Vec<PartEntry>
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.
Sourcepub fn part_len(&self) -> usize
pub fn part_len(&self) -> usize
Diagnostic: entries currently resident in the live-part log. Never
exceeds TrunkConfig::part_capacity.
Sourcepub fn waiter_count(&self) -> usize
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.
Sourcepub fn last_closed_segment(&self) -> Option<u32>
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).
Sourcepub fn tracks(&self) -> Arc<[TrackSpec]> ⓘ
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.
Sourcepub fn track_generation(&self) -> u64
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.
Sourcepub fn listen(self: &Arc<Self>) -> Option<ProgressListener>
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
on — event_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.