-
Notifications
You must be signed in to change notification settings - Fork 3
kithara file
Documentation reviewed from source revision 19ca073f2. This records the documented contract at that revision; it is not a new runtime validation. API and usage · All crates.
kithara-file owns FileError, FileEvent, and TotalBytesSource. FileEvent::Opened carries the canonical kithara-stream AudioCodec and ContainerFormat values directly.
FileConfig<S>::for_src(src) -> File<S> (StreamType marker) -> internal FileCoord, which splits by
source: FileSrc::Local reads through AssetStore<S> with an absolute ResourceKey;
FileSrc::Remote runs an internal pull-driven FilePeer emitting FetchCmd batches to the
shared dl::Downloader. The AssetStore<S> owns one pending resource per ResourceKey: consumer
demand, the canonical reader/writer, and writer epochs stay in that storage state, while File
drives HTTP through a writer epoch capability. FileSource (impl kithara_stream::Source) wraps FileCoord; Stream<File<S>>
(Read + Seek) wraps FileSource. FileSource is synchronous: every async concern (HTTP fetch,
body streaming, finalization) belongs to the Downloader through FilePeer, and it holds the
PeerHandle from Downloader::register for its whole lifetime — dropping the last handle cancels
in-flight fetches.
store: AssetStore<S> and pools: PoolRegion<S> are both required configuration fields. They use
the same closed schema S: HasPool<u8>; FileConfig<S> keeps cheap clones of both. Remote source,
storage, and fallback transport therefore obtain byte buffers through one facade and one shared
hard budget. Local sources keep the same explicit contract even though they skip the downloader.
There is no global or lazily inferred pool.
Stream<File<S>>::Read + Seek goes through FileSource::wait_range / read_at.
-
wait_range(_, Some(_))is the audio-worker probe: checks phase once, returnsSourceError::WaitBudgetExceededfor missing in-range bytes instead of blocking.wait_range(_, None)is the off-RT adapter path, delegating to the storage wait until bytes, EOF, failure, or cancel resolves. The source token is a per-wait cancel authority: it wakes a blocked adapter without cancelling the shared asset or another follower. A flushing seek short-circuits toWaitOutcome::Interruptedbefore any demand update. - The probe clamps oversized read-ahead at a known length, so a fully cached file queried with
0..read_aheadwhereread_ahead > lenisReady, not need-data. -
Eofrequires both that the range starts at or past the known length and that the resource isCommitted. An announced-but-uncommitted length yieldsWaiting; an in-range range not yet written returnsWaitBudgetExceeded(→Pending/ need-data) so the reader holds rather than terminating. See kithara-stream contracts "End-of-stream contract". - Known length blends the announced total (
FileCoord::total_bytes, seeded from response metadata) with the committedAssetReader::len(), announced first. When a commit has no explicit final length, the committed reader length is canonical. A ranged response takes its full total fromContent-Range; its sliceContent-Lengthis never treated as the resource total. If a server ignores the initial bounded range and returns a full200withoutContent-Range, that full response'sContent-Lengthis the resource total.
FilePeer targets one fetch at a time, and it targets it at the reader cursor
(kithara_stream::Source::position). Stream<File<S>>::probe_seek moves that cursor and arms the
peer precisely so the peer re-targets around the new position.
- The next fetch starts at the first gap at or after the cursor. Only when nothing is missing ahead of the cursor does the peer fall back to the first gap from byte 0. That second lookup is not a state-resolution fallback: both lookups are correct answers to different questions, and the order is the priority. The listener is blocked on the bytes under the cursor, so those come first; the earlier span still has to land for the resource to commit, so it is filled next. Drop the cursor lookup and a forward seek waits for the whole skipped span; drop the byte-0 lookup and a seeked-over span never lands and the resource never commits.
- A fetch streams forward from where it started, so a cursor that sits inside the fetch's span
past the bytes it has landed can no longer be served by it — that fetch would deliver the whole
skipped span first. The peer cancels it. Stored bytes decide where the fetch has got to (the
first gap at or after its start), not a write offset the fetch publishes: that offset lands
after
write_at, and the landing is what wakes the reader, so a reader that consumed the bytes in between sat ahead of the offset without being ahead of the fetch and got its own fetch cancelled. Cancellation relinquishes the writer epoch, the completion path wakes the peer, and the next plan re-elects a writer and anchors on the new cursor. A fetch whose span ends before the cursor is a backfill: it starts behind the cursor by construction and never promised it anything, so it runs to completion. - A plan that finds no gap over a known extent commits the resource from the peer. A cancelled fetch relinquishes without committing, and when it had already landed everything its replacement has nothing to fetch; parking there would leave every consumer waiting on a complete resource.
- The cursor steers nothing until the resource extent is known, and nothing once it sits outside it. A range request needs an extent: before the first response answers how long the resource is, a cursor past the end is indistinguishable from one inside it, and anchoring there would both ask for bytes that do not exist and cancel the one request that reports the real length. Both cases leave the head fetch to establish the resource. This is why a seek past the end of a not-yet-sized resource still terminates.
- Because fetches follow the cursor, stored bytes are sparse and
FileCoord::set_download_posreports the running fetch's write cursor, not a contiguous prefix.
FileSrc::Local(path) requires an existing absolute path, opens it through AssetStore with an
absolute ResourceKey, skips all network activity, and yields a FileSource with no peer and no
downloader. Media bytes are read in place: never copied under the cache directory, and no layout
callback applies to that absolute key. Derived resources differ —
kithara_play::ResourceConfig::asset_key describes the same path as AssetSource::Local, selects
the File layout, and mints an ordinary relative key such as the default
analysis/track.analysis, so a custom File layout governs local-track analysis and other
derived artifacts while the original media file stays untouched. FileSrc::Remote:
- Opening returns as soon as the asset claim succeeds;
Content-Length/Content-Typearrive later with the first response, solen()isNoneuntil then.AcquisitionResult::Readymeans already committed (no remote fetch);Pendingattaches a reader, resource lease, and optional elected writer to theAssetStore-owned session. Followers attach to that session and never acquire or reactivate the backing resource themselves. - If a sibling
AssetStoreinstance holds the atomic-chunked tmp for the same canonical path,createpolls every 10 ms until that sibling commits or drops, or returns cancellation when its own work token fires. The loop is wrapped in#[kithara::hang_watchdog]and ticks the watchdog only while the tmp's length is unchanged, so a live sibling never panics and only a stale tmp from a crashed process does. - Fetch selection follows the cursor-first contract above; downloaded coverage may be sparse. Backpressure runs
through the consumer-demand entry shared between
FileCoord::read_pos_handle()and the resource lease fromAssetStore::attach_pending_resource, bounded bylook_ahead_bytes. Before a missing range yields or blocks, File raises that consumer's monotonic requested-end floor; this immediate demand overrides the bounded prefetch frontier, includinglook_ahead_bytes = 0. The lease's one-shot reader Waker uses onlyWeak<FileInner>. On writes it rearms before waking the audio worker; on a terminal commit it settles file metadata and derived indexes before that wake. Completion work therefore stays outside the audio worker's real-time read path, and the Waker cannot pin a dropped source through the pending-resource state. Non-blocking audio probes wait for that commit and raise their demand to the whole resource, so the real-time decoder reads only the immutable committed snapshot. Blocking file readers may still consume published partial ranges. - Only the elected writer epoch issues GETs.
poll_nextregisters its peer Waker before the readiness/election check, leaves it armed onPending, and clears that exact registration beforeReady. A stale writer handle is removed and dropped outside the File mutex before the peer attempts promotion. Writer handoff changes only the epoch; the canonical writer and partial bytes stay in the same session, so two consumers of one URL share one GET whenever no handoff is needed. -
FilePeerholdsWeak<FileInner>;FileSourceowns the strong lifetime. InsideFileInner, the asset reader is declared before the demand lease, so the reader drops first. The last source drop synchronously retires the exact session, while the lease'sAssetStoreclone pins disk-checkpoint ownership through the remote lifetime. - Each fetch uses a child of the writer cancel token. Source cancellation cancels only that fetch
child; the peer holds non-owning wake guards for both source and session cancellation. Either
cancellation wakes a parked peer before its next readiness check. Session cancellation ends the
peer and forbids writer re-election. Source cancellation that races after a valid response has
landed every advertised byte commits the canonical resource; otherwise it relinquishes only this
source's epoch.
NetError::Cancellednever emitsFileEvent::Error. A fatal error, or an initial zero-progress error, fails the current epoch. Other transient completions keep partial coverage active, but may commit when every advertised byte already landed. Late stale callbacks cannot write, commit, fail, or publish File events for a successor. The peer clears its in-flight flag only after the completion callback settles the epoch, so a concurrent registry poll cannot issue a replacement GET during terminal cleanup. - HTTP range upper bounds are exclusive internally and inclusive on the wire. Ranged responses
must identify the requested start, interval, and numeric representation total with a valid
Content-Range; a full200that ignores an initial range is accepted only withContent-Length. A resumed response withoutContent-Range, a mismatched interval, an unknown*representation total, or an unknown-size bounded full response fails before any byte is written.
FileConfig<S> is the one configuration struct this crate has: the tunables and the per-call
wiring live in it together. #[derive(Patch)] generates FileConfigPatch beside it, and apply
writes only the fields the document names, leaving the rest of FileConfig standing.
extension: Option<String> and look_ahead_bytes: Option<u64> are already optional on the
config, so a document names a plain value under extension / look_ahead_bytes, not
Some(value). tmp_claim_poll_interval is read through
serde(with = "humantime_serde::option"), so a document writes 25ms rather than a raw
millisecond count.
kithara-app's file: section is that document's spelling. It types into FileConfigPatch,
main hands it to AppConfig::file, and every track is opened with that value:
kithara_play::ResourceConfig carries the patch — no FileConfig can exist before a track does —
and kithara-play/src/resource/build.rs applies it to the FileConfig it builds. The one field
the document does not have the last word on is extension: the file branch resolves it as the
per-call format hint, then the extension derived from the source path, then whatever the document
named — the two ahead of it identify the track being opened, which is more specific than a
crate-wide default.
src, discriminator, and headers are per-stream input, not crate-wide policy: they name what
is being fetched and how, not a tunable default. store, pools, bus, cancel, and downloader
are live handles or S-typed values a document has no way to name, the same reasoning
kithara-hls::HlsConfig applies to its own wiring fields.
Cache identity: naming is owned by the layout registered for the File marker in the shared
AssetStore. The stream binds AssetSource::Remote { url, discriminator } through
store.scope::<File>() and mints one AssetResource::Source; its extension comes from the
explicit config.extension hint, then the final URL-path extension, and finally bin, accepting
only a short all-ASCII-alphanumeric candidate, lower-cased. The default layout stores the file at
<asset_root>/track/track.<ext>, root hash over the canonical URL without query or fragment,
folding in the explicit discriminator when present. Query parameters are never an implicit
identity for direct files. A higher layer may register any AssetLayout for File;
kithara_play::policy::QueryIdentityLayout is the built-in domain-aware option selecting stable
identity parameters while ignoring rotating signatures and expiry values. Layouts are registered
once through AssetStore::builder(pools.clone()).layouts, and every FileConfig<S> holds cheap
clones of that same store handle and region.