Repository navigation
fix(net): ordered reads wait only on groups a subscription still owes; max_delay defaults to 0 - #7403
Conversation
Quest-Claim: 8703fa9af8fad0f6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces the 1 s ledger::GRACE cap on an unbounded owed-group wait: with no budget, one hole the track owes but never resolves would hold every later group forever, so Ordered::update, its Control, and a read all refuse a max_delay of None with Error::Unsupported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…names it JS: an omitted Subscription.maxDelay is the default zero again, reverting #7077's omitted-means-unlimited before it ships; null is refused. FFI: UniFFI can't default a Duration to 0, so MoqSubscription.max_delay has no default and Kotlin and Swift callers must name it. Python's wrapper defaults it to zero. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eat documented moq-archive, moq-json snapshot, and moq-c tests that read a whole track in order now pass a day-long max_delay instead of None. Rust and JS docs note a long ordered budget is only safe where gaps are signalled (moq-lite SUBSCRIBE_DROP, IETF ordered FETCH). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… gap caveat documented" This reverts commit c1b26ce.
Final maintainer decision: unlimited stays valid on Ordered (Rust None, JS null, FFI null) and waits with no grace cap; the default stays zero. Drops the refusal, restores JS null as the explicit unlimited, and documents that an unlimited or long ordered budget can block over a moq-transport SUBSCRIBE, which signals no gaps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 58 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fcf617a58c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: fcf617a58c177e8af982dea752b2c564d155abd0.
No actionable bugs found in the changed code. The direction is sound: reuse the existing landability rules, bound ordered waiting with the existing drift budget, and register both track-change and ledger-fold wakeups rather than adding another timeout policy. The JS omitted/undefined-versus-null distinction and explicit FFI constructor requirement match the documented migration.
Reviewed the owed-gap path and surrounding cursor/drift/closure logic, JS subscription normalization and wire call sites, binding changes, and added regressions. The documented JS ordered-reader parity and Dart/Go/C default differences remain outside this implementation. Verification was static only; I did not run tests or independently verify the reported test results.
There was a problem hiding this comment.
ℹ️ No critical issues — the core ordered-owed logic, the JS
null/0semantics, and the bindings change all check out. One minor doc-accuracy suggestion inline.
Reviewed changes
- Rust core (
rs/moq-net/src/model/track.rs):may_landis refactored ontofind_landablewith an unchanged truth table, and a newTrackState::owed/Cursor::poll_owedmakes an ordered read on a received copy wait, within its drift budget, for a gap the live feed still owes — skipping drops, datagrams, evicted/aborted, below-start, and folded gaps, and skipping everything on a local track. Unlimited (None) waits until the hole resolves; zero waits for nothing. - JS (
js/net):Subscription.maxDelayisMilli | null;nullmeans unlimited and omitted/undefinedmeans 0.NO_MAX_DELAY/maxDelayOfmove tointernal.tsand feed the lite Subscribe/Update and IETF update wire. - Bindings/defaults:
MoqSubscription.max_delaydrops its uniffi default (required in Kotlin/Swift), Python's wrapper defaults totimedelta(0), and MIGRATE/doc updates land across moq-ffi, kt, swift, py, js, and dart. - Tests: stepped-clock Rust coverage for reorder, the not-owed cases, and zero/narrowed/unlimited budgets; a JS test for the omitted vs
nulldefault.
I verified the new wait cannot lose a wakeup (Cursor::poll registers on the TrackState consumer, woken by every write and close, plus watch_tail and the ledger fold deadline), that maxDelayOf always receives a value already materialized by subscriptionDefaults, and that no binding has a generation or ABI break from the uniffi default removal.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
Dart, Go, and C can still omit it, which means no subscriber limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dget Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drop Required entries and links to quests that merged PRs finished since this branch was cut (cli-modes #7261, mesh/sign #7092, ordered-owed-wait #7403, obs-multitrack #7333, mpeg2-open-gop-export #7365, workflow-security #7317), linking the PR where the prose needs it. Complete the README index bullets that were cut mid-sentence, and fold the CodeRabbit plan findings into their quests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Finishes
quest/m2/ordered-owed-wait.md.track::Orderednow waits for a missing group only while the track still owes it, within the reader'smax_delay. With no budget it waits until the hole resolves. This PR also makesmax_delaydefault to 0 on every subscription path in JS and the bindings. No wire change.Summary
TrackState::owedfinds the lowest group in a gap that a received copy's live feed may still land. It reuses the rulemay_landalready applies, now factored intofind_landable. A group is not owed when:SUBSCRIBE_DROPor a cut);Cursor::poll_next_with, a candidate above an owed hole parks until the hole lands, the track learns it won't, or the reader's budget gives up on it (poll_drifted, the same rule that skips stale groups). The reader is parked on every track change and on the ledger's fold deadline.None) waits until the hole resolves. There is no grace cap: theledger::GRACEspecial case this PR first added is gone.Ordered's rustdoc gains a# Gapssection, replacing "a gap costs nothing". It carries the caveat that an unlimited or long ordered budget only avoids blocking where gaps are signalled: moq-lite withSUBSCRIBE_DROP, or an ordered moq-transport FETCH. Over a moq-transport SUBSCRIBE, an ordered reader can block at a hole until the track ends. The JSOrderedandSubscription.maxDelaydocs say the same, and note that the JS reader skips gaps today.undefinedmaxDelaymeans 0 again, andnullis the explicit "no limit". This reverts moq-dev/moq#7077's "omitted means unlimited" before it ships. A new internal helper,maxDelayOf, carriesnullto the wire as the largest readable value, as Rust does forNone.MoqSubscription.max_delayhas no default. Python's wrapper defaults it totimedelta(0).[1, 2]once group 1 lands);{}anddefault()are all 0, andnullreaches back to the publisher's max age.Decisions
What "owed" means on a local track.
An ordered reader with no budget. This replaces the earlier "1 s cap", "refuse unlimited", and "a day-long budget for history" entries.
None, JSnull, and FFI null mean unlimited, and an unlimited ordered reader waits at an owed hole until it resolves. No grace cap, because grace periods are unwanted. Archive and history readers keepNone. (Maintainer, final, 2026-10-30.)Ordered(implemented, then reverted).ledger::GRACE(the original implementation, removed).Gap caveat.
SUBSCRIBE_DROP, IETF ordered FETCH). Over a moq-transport SUBSCRIBE it can block until the track ends. Gap detection is planned separately by the maintainer and is not built here.max_delaydefaults to 0.Subscription::default()andSubscription.default()are 0, and a JS field that is omitted orundefinedmeans 0.nullis valid in JS as the explicit unlimited. (Maintainer clarifications, 2026-10-30.)FFI
MoqSubscription.max_delaydefault.Durationfield ("field max_delay of type Duration can't have a default value of type integer", in every generator).moq_subscription_defaults()still gives 0, and null stays the explicit unlimited.Scope.
group::Fetch, JSfetchGroup, FFIMoqFetchGroupOptions) at publisher retention, since a fetch names one group.MAX_AGEmeaning unlimited (wire semantics,ietf/publisher.rs:37).Snapshot
latest()at an owed hole. On a received track with a nonzero or unlimitedmax_delay,moq_json::snapshot::Consumer::latest()drainsOrdered::poll_next_group, so it waits for an owed lower group (up to the budget, or until the gap resolves) even when a newer snapshot is buffered. Every in-repo snapshot reader uses the zero default and is unaffected.poll_latestrustdoc, 1aad05c) and plan a follow-up quest: a newOrderedread that skips owed holes while keeping the reader's bounds, with snapshotlatest()moved onto it. (Taken overnight on the maintainer's standing authorization, 2026-10-31.)Cursor::poll_next_with,Subscriber::poll_group, and the resume layer's multi-copy peek (model/resume.rs).Defaults audit
Every subscription/reader default for
max_delay, as of this head:Subscription::default()Noneis the explicit unlimitedSubscription.default(),subscribe(),subscribe({}),maxDelay: undefined{}was unlimited since #7077 (unreleased);nullis unlimitedmoq_subscription_defaults()MoqSubscriptionbuilt field by fieldmoq.Subscription()NoneMoqSubscription()Subscription{}*time.Duration's zero value; needs a type changemoq_subscriptionzeroedmax_delay_presentfalse)maxAge: the JSSubscriptiontype has nomaxAge; it is refused at runtime.Info.maxAge(publisher retention) is omitted-means-unlimited in JS, and Rust'sInfo::max_ageis too. Both are out of scope here and noted for the defaults-audit quest.ledger::GRACEfindingsNot removed in this PR. No wire use is small and safe to remove.
ietf/subscriber.rs:803,Settleat :3807; JSietf/subscriber.ts:1320): after PUBLISH_DONE, a Stream Count of 0 or unknown, or a stream reset before its header, leaves nothing to count. The draft itself prescribes a timeout. Removing it needs reliable reset or a protocol change.max_delay, else 1 s;lite/subscriber.rs:6841,6766,8009,8037; JSlite/subscriber.ts:1109): from lite-05, publishers sendSUBSCRIBE_DROPfor every gap, before SUBSCRIBE_END/FIN, so the tail could wait for accounting alone. That needs version gating (lite-01..04 have no DROP), explicit handling of lost-datagram holes on lite-05/06, and a guard against a peer that never accounts. It is a medium-sized follow-up in both languages.model/track.rs:1893-1907,resume.rs:520; JStrack.ts:1585):grace()returnsmax_delaywhenever it is nonzero, so only the zero-budget case uses the 1 s fallback. Dropping it would fold gaps at once, so a few-ms reorder would be judged lost. The better route is to take gap verdicts from the session (SUBSCRIBE_DROP, the settled tail) once gap detection lands.Impact
Subscription(...)must namemaxDelay. Python's bareSubscription()now means 0 instead of unlimited. MIGRATE notes are updated in moq-ffi, Kotlin, Swift, Python, and JS.maxDelay: nullsets no subscriber limit". The API report changesmaxDelay?: MillitoMilli | null.Choices made without asking
Ordered. The default zero budget waits on nothing, somoq-jsonsnapshot readers only differ on received tracks with a nonzero budget.Suggested follow-ups
max_delayzero-value defaults are still unlimited (see the audit).SUBSCRIBE_DROPaccounting on lite-05+ (see the GRACE findings).Orderedparity: it skips gaps instead of waiting for owed groups.latest()(decision 7).Checks
quest gate just check origin/mainat fcf617a: pass.just check fcf617a58cpassedrs(4959 tests),py, andc.ktandcppwere cancelled by the shell time limit on the loaded host.cargo test -p moq-net --all-features --lib: 2688 passed.bun testin js/net: 2648 passed.Review
gpt-6.1-sol, medium effort, viaquest review --base origin/main, session01a190e0-69af-7511-8748-a868e88e5c30(new session, rounds counted from 1).latest()waits at owed holes. Refuted: bounded by the opt-in budget, no in-repo caller affected.latest()'s documented contract. Documented in 1aad05c.Orderedread.gpt-6-astra,--deep, session01a18ee5-…): 0 findings. Before that, on the pre-decision heads (Codexgpt-6.1-sol): two findings at 3ecbd21, both fixed; 0 at dda86df.Online review threads
rs/moq-ffi/MIGRATE.md:45, overbroad "required" claim): fixed in fa8d606. Only Kotlin and Swift requiremax_delay; Dart, Go, and C can still omit it.js/net/src/track.ts:2947, JS owed-gap parity): not taken here. The maintainer has planned it asquest/m1/js-ordered-owed.mdin quest: plan Delay on subscriptions, JS owed waits, audio reach-back, lite drop-count tails, and an SPS recheck #7426; thread resolved.(Written by Claude Opus 5.5)
🤖 Generated with Claude Code