feat(moq-net): shared RAM LRU group cache (5s/unbounded default) + FFI Cache surface#1899
Open
kixelated wants to merge 7 commits into
Open
feat(moq-net): shared RAM LRU group cache (5s/unbounded default) + FFI Cache surface#1899kixelated wants to merge 7 commits into
kixelated wants to merge 7 commits into
Conversation
Add a `Cache` handle that retains old groups in RAM beyond a track's latest one, bounded by a shared byte budget and wall-clock age. Attach it at the origin, broadcast, or track level (cascading: track over broadcast over origin); clone the handle to share one budget across many tracks. Eviction is LRU by wall-clock last-access time (when a group was last served), not by media timestamp or arrival order. A group is dropped once it exceeds `max_age` since its last access, or once the shared total exceeds `max_bytes` (least-recently-accessed first). A track's current max_sequence group is never handed to the cache, so a live subscriber can always grab it. Default behavior changes to latest-group-only: with no `Cache` attached a track keeps only its latest group, replacing dev's arrival-window retention as the local policy. The wire `TrackInfo::cache` Duration field is intentionally left intact (its removal is a separate VOD discussion); the shared `Cache` is a local-only retention object that supersedes it as the retention driver. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EviJwrDw3XZ9ZgESJGua28
6 tasks
Expose the shared RAM LRU Cache through the FFI/C surface as a builder, and remove the now-vestigial `TrackInfo::cache` field. FFI (moq-ffi / libmoq): - New `MoqCache` (uniffi Object) + `MoqCacheConfig` (Record: max_bytes, max_age_ms), with `MoqCache::new`, `clone_handle`, `is_clone`. - `MoqBroadcastProducer::with_cache(&MoqCache)` and `MoqOriginProducer::with_cache(&MoqCache)` builder methods, mirroring moq-net's `with_cache`. A bare `MoqBroadcastProducer::new()` attaches its own default cache (64 MiB / DEFAULT_CACHE age) so an in-process consumer that lags the publisher does not lose superseded groups under the latest-only default. - libmoq C: `moq_cache_create(max_bytes, max_age_ms)`, `moq_cache_close`, `moq_publish_with_cache(broadcast, cache)`; `moq_publish_create` attaches a default cache. - Removes the `cache_ms` field from `MoqTrackInfo` (breaking; retention is a Cache policy now). moq-net: - Removes `TrackInfo::cache: Duration`, `with_cache(Duration)`, and its serde helpers. The field was local-only (never on either wire protocol); retention is governed by the shared Cache. `DEFAULT_CACHE` stays as the canonical default retention window. - `clamp_stale` now derives from the attached Cache's `max_age`; with no cache (latest-only), the stale window collapses to `Duration::ZERO` since nothing beyond the latest group is kept. - Adds `pub(crate) Cache::max_age()`. Bindings + docs (Cross-Package Sync): - py/moq-rs, swift, kt, go wrappers expose the Cache surface and `with_cache`, and drop `cache_ms`. - js/net: drops the local-only `cache` field from `TrackInfo` (also never on its wire codec); local retention falls back to the `DEFAULT_CACHE_MS` window. - doc/concept: reconciles the moq-lite retention prose (local cache policy, not a wire hint). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EviJwrDw3XZ9ZgESJGua28
The meta track's createTrack literal still set `cache`, which no longer exists on TrackInfo after retention became a local Cache policy. Removing it; the track falls back to the default local retention window like every other track. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EviJwrDw3XZ9ZgESJGua28
…ribe tests These in-process tests publish several groups and only then create a consumer that reads back the history. Under the latest-group-only default they lost all but the final group. Attach an unbounded Cache to the broadcast/track so the history is retained, mirroring production where the broadcast carries a cache. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EviJwrDw3XZ9ZgESJGua28
Revert the latest-group-only default to a backwards-compatible per-broadcast
cache: every BroadcastProducer (and every bare TrackProducer) now installs its
own default Cache with a 5-second age window and no byte cap, cascaded onto its
tracks. A late subscriber can replay the last few seconds, matching the
historical behavior. Each broadcast gets its own default instance, not a shared
global; an explicit with_cache at the origin/broadcast/track level still
overrides it and can share one budget across many.
cache::Config::default() is now { max_age: DEFAULT_CACHE (5s), max_bytes: 0 },
where max_bytes == 0 means no byte cap (eviction by age alone) and
max_age == Duration::ZERO disables retention (latest group only). clamp_stale
bounds a subscriber's stale window by the cache's max_age as before.
FFI/C: moq_publish_create (libmoq) and MoqBroadcastProducer::new (moq-ffi) no
longer attach a bespoke 64 MiB cache; they inherit moq-net's 5s default. The
MoqCache / with_cache handle stays as the opt-in for a custom window, a byte
budget, or a shared budget. MoqCacheConfig defaults to 5000 ms / 0 bytes.
Ripple: py/swift/kt/go wrapper docs updated (Go gains DefaultCacheConfig since a
zero-valued struct would otherwise mean latest-only); doc/concept/layer/moq-lite
describes the 5s default. js/net already implemented the 5s window. Reverts the
test cache-attaches from the prior latest-only commit; those tests now pass
unmodified under the default.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EviJwrDw3XZ9ZgESJGua28
…ru-dev # Conflicts: # rs/moq-net/src/model/broadcast.rs # rs/moq-net/src/model/track.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a single shared RAM LRU cache for groups, configurable at the origin, broadcast, or track level (cascading: track over broadcast over origin). A
Cachehandle is cheaply cloneable andArc-backed: everything attached with the same handle draws from onemax_bytesbudget and onemax_age; two distinctCacheinstances are independent.BroadcastProducerinstalls its own defaultCachewithmax_age = DEFAULT_CACHE(5 seconds) and no byte cap, cascaded onto its tracks. A late subscriber can replay the last few seconds, matching the historical behavior. Each broadcast gets its own instance, not a shared global. An explicitwith_cacheat any level overrides it and can share one budget across many.max_agesince last access, or the shared total exceedsmax_bytes(least-recently-accessed first). Usesweb_async::time::Instant.max_bytes == 0means no byte cap (eviction by age alone);max_age == Duration::ZEROdisables retention (latest group only); the currentmax_sequencegroup is never handed to the cache, so it's never evicted out from under a live subscriber.Public API additions
moq-net
cache::Config—#[non_exhaustive],Default({ max_age: DEFAULT_CACHE (5s), max_bytes: 0 }),with_max_bytes/with_max_age; fieldsmax_bytes: u64,max_age: Duration.Cache(re-exported flat) —#[non_exhaustive],Cache::new(Config),Cache::is_clone; cheaply cloneable.pub(crate) Cache::max_age().TrackProducer::with_cache(self, Cache) -> Self,BroadcastProducer::with_cache,OriginProducer::with_cache(override the default).GroupProducer::cached_size(&self) -> u64,GroupProducer::is_aborted(&self) -> bool.moq-ffi / libmoq (Cache builder surface)
MoqCache(Object) +MoqCacheConfig(Record:max_bytes,max_age_ms);MoqCache::new,clone_handle,is_clone.MoqCacheConfigdefaults to5000 ms/0bytes.MoqBroadcastProducer::with_cache(&MoqCache),MoqOriginProducer::with_cache(&MoqCache)(builder, mirroring moq-net). BareMoqBroadcastProducer::new()inherits moq-net's 5s default (no bespoke FFI cache); pass oneMoqCacheto several broadcasts to share a budget or cap RAM.moq_cache_create(max_bytes, max_age_ms),moq_cache_close(cache),moq_publish_with_cache(broadcast, cache);moq_publish_createinherits the 5s default.moq.hregenerates via cbindgen (no signature change).Breaking changes → correctly targets
devMoqTrackInfo.cache_msremoved from the FFI surface (andcache_ms/cachefrom every binding); retention is now aCachepolicy.TrackInfo::cache: Durationremoved fromrs/moq-net(andwith_cache(Duration)+ its serde +evict_expired). This field was local-only — never carried on either wire protocol (verified: neither the moq-liteTrackInfocodec nor moq-transport frames acachefield; dev reconstructs it locally fromDEFAULT_CACHE). So this is an API/local-policy change, not a wire-format change.DEFAULT_CACHE(5s) stays as the canonical default retention window, now the defaultcache::Config::max_age.The default retention behavior is unchanged vs. dev (still ~5s of history), so this is not a behavior regression for existing publish→subscribe flows; it's an API reshape (config-driven
Cacheinstead of aTrackInfofield).Supersedes #1898 (built against
main).clamp_stale semantics (rebased on the Cache)
clamp_staleno longer reads the removedTrackInfo::cache; it derives from the attachedCache:stale.min(cache.max_age())— a subscriber can't wait for a late group longer than the cache keeps it. A zero-age (latest-only) cache, or no cache at all, clamps toDuration::ZERO.Duration::ZEROis left untouched by themin.Cross-package sync
DEFAULT_CACHE_MS = 5000, age-based prune, no byte cap) — matches the Rust default; dropped the local-onlycachefield fromTrackInfo(also never on its wire codec).bun test+tsc -b+ biome clean.layer/moq-lite.mdretention prose describes the 5s-per-broadcast default and the explicit-cache knobs.Cachesurface +with_cache, dropcache_ms, and document the 5s/no-byte-cap default. Go gainsDefaultCacheConfig()since a zero-valuedCacheConfigwould otherwise mean a zero window (latest-only) rather than the 5s default.doc/lib/{py,swift,kt,go,c}reference neithercache_msnor a TRACK_INFOcachefield, so they need no edits.Folded-in fixes (from #1898 CI + review)
RUSTDOCFLAGS="-D warnings" cargo docpasses.#[non_exhaustive]onCache(not justConfig).max_ageis evicted on read, not revived. All read paths.read_framebumps LRU recency likerecv_group/get_group.cached_size()so a superseded group that grows via late frames is counted at its grown size.Test plan (re-run post-merge with #1900)
cargo test -p moq-net --lib— 417 passed (incl.default_retains_recent_groups,default_evicts_after_window,zero_age_keeps_only_latest_group,stale_clamped_to_default_window,stale_clamped_to_zero_for_latest_only, and the cache/eviction suite)cargo test -p moq-mux --lib— 288 passed (publish→subscribe tests pass unmodified under the 5s default)cargo test -p moq-audio(roundtrip + lib) andcargo test -p moq-bench— pass unmodifiedcargo build -p moq-ffi -p libmoqclean;cargo test -p libmoq— 25 passed; the lonesession_connect_and_closefailure is environmental to the sandbox (real QUIC socket bind / IPv6[::]:0blocked,Os code 97), unrelated to this changecargo testfor hang / moq-loc / moq-msf / moq-json / moq-token — pass (validated pre-merge; dev's auto-merged changes only)cargo fmt/cargo clippyclean on moq-net/moq-ffi/libmoq/moq-mux (only a pre-existing moq-native dependency warning)bun test,tsc -b, biome check clean🤖 Generated with Claude Code
https://claude.ai/code/session_01EviJwrDw3XZ9ZgESJGua28
(Written by Claude)