Skip to content

fix(net): ordered reads wait only on groups a subscription still owes; max_delay defaults to 0 - #7403

Merged
moq-bot[bot] merged 15 commits into
mainfrom
quest/m2/ordered-owed-wait
Oct 31, 2026
Merged

moq-bot[bot] merged 15 commits into
mainfrom
quest/m2/ordered-owed-wait

Conversation

@kixelated

@kixelated kixelated commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Finishes quest/m2/ordered-owed-wait.md. track::Ordered now waits for a missing group only while the track still owes it, within the reader's max_delay. With no budget it waits until the hole resolves. This PR also makes max_delay default to 0 on every subscription path in JS and the bindings. No wire change.

Summary

  • Owed: TrackState::owed finds the lowest group in a gap that a received copy's live feed may still land. It reuses the rule may_land already applies, now factored into find_landable. A group is not owed when:
    • its arrival was recorded (later eviction, an abort with a reclaimed slot, or a datagram on that sequence doesn't matter);
    • it was dropped (SUBSCRIBE_DROP or a cut);
    • it is below the declared start, or below the first arrival if no start was declared;
    • it is past the end or outside the reader's window;
    • it is in a gap the arrival ledger has folded.
  • Wait: in 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.
  • Budgets:
    • A zero budget (the default) waits for nothing.
    • A budget narrowed mid-wait ends the wait.
    • An unlimited budget (None) waits until the hole resolves. There is no grace cap: the ledger::GRACE special case this PR first added is gone.
  • Docs: Ordered's rustdoc gains a # Gaps section, 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 with SUBSCRIBE_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 JS Ordered and Subscription.maxDelay docs say the same, and note that the JS reader skips gaps today.
  • Defaults (see the audit below):
    • JS: an omitted or undefined maxDelay means 0 again, and null is the explicit "no limit". This reverts moq-dev/moq#7077's "omitted means unlimited" before it ships. A new internal helper, maxDelayOf, carries null to the wire as the largest readable value, as Rust does for None.
    • FFI: MoqSubscription.max_delay has no default. Python's wrapper defaults it to timedelta(0).
  • Tests all use the pool's stepped clock, so they are deterministic:
    • a reordered group is delivered in order;
    • a local track still skips gaps;
    • the untimed case: with groups 0 and 2..10, the reader wakes at exactly the budget;
    • the media-time deadline;
    • the not-owed cases;
    • zero, narrowed, and unlimited budgets (an unlimited reader still waits after an hour, then reads [1, 2] once group 1 lands);
    • the review regressions;
    • JS: omitted, {} and default() are all 0, and null reaches back to the publisher's max age.

Decisions

  1. What "owed" means on a local track.

    • ✅ Only subscription-fed copies owe groups; local tracks keep skipping gaps. (Maintainer, 2026-10-30.)
    • Count a local producer's unreleased sequences as owed.
  2. An ordered reader with no budget. This replaces the earlier "1 s cap", "refuse unlimited", and "a day-long budget for history" entries.

    • ✅ Allowed: Rust None, JS null, 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 keep None. (Maintainer, final, 2026-10-30.)
    • Refuse unlimited on Ordered (implemented, then reverted).
    • Cap the wait at ledger::GRACE (the original implementation, removed).
  3. Gap caveat.

    • ✅ Document that an unlimited or long ordered budget only avoids blocking where gaps are signalled (moq-lite 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.
  4. max_delay defaults to 0.

    • ✅ The default stays 0: Subscription::default() and Subscription.default() are 0, and a JS field that is omitted or undefined means 0. null is valid in JS as the explicit unlimited. (Maintainer clarifications, 2026-10-30.)
  5. FFI MoqSubscription.max_delay default.

    • ✅ Required, with no default. The decision was "default to 0, or required if uniffi can't express it". UniFFI 0.32 refuses any literal default on a Duration field ("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.
  6. Scope.

    • ✅ The JS and FFI default fixes go in this PR.
    • ✅ Leave fetch options (Rust group::Fetch, JS fetchGroup, FFI MoqFetchGroupOptions) at publisher retention, since a fetch names one group.
    • ✅ Leave IETF's absent MAX_AGE meaning unlimited (wire semantics, ietf/publisher.rs:37).
  7. Snapshot latest() at an owed hole. On a received track with a nonzero or unlimited max_delay, moq_json::snapshot::Consumer::latest() drains Ordered::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.

    • ✅ Land with the documented caveat (poll_latest rustdoc, 1aad05c) and plan a follow-up quest: a new Ordered read that skips owed holes while keeping the reader's bounds, with snapshot latest() moved onto it. (Taken overnight on the maintainer's standing authorization, 2026-10-31.)
    • Fix it in this PR: new public moq-net API threaded through 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:

Surface Omitted means Notes
Rust Subscription::default() 0 None is the explicit unlimited
JS Subscription.default(), subscribe(), subscribe({}), maxDelay: undefined 0 fixed here: {} was unlimited since #7077 (unreleased); null is unlimited
FFI moq_subscription_defaults() 0
FFI MoqSubscription built field by field required fixed here: was null (unlimited). Kotlin and Swift callers must now name it
Python moq.Subscription() 0 fixed here: was None
Dart MoqSubscription() null (unlimited) not fixable here: the Dart generator emits a nullable field as an optional named parameter
Go Subscription{} nil (unlimited) not fixed: *time.Duration's zero value; needs a type change
C moq_subscription zeroed unlimited (max_delay_present false) not fixed: needs an ABI change
Fetch options (Rust, JS, FFI) publisher retention kept by decision 6

maxAge: the JS Subscription type has no maxAge; it is refused at runtime. Info.maxAge (publisher retention) is omitted-means-unlimited in JS, and Rust's Info::max_age is too. Both are out of scope here and noted for the defaults-audit quest.

ledger::GRACE findings

Not removed in this PR. No wire use is small and safe to remove.

  • IETF session tail (fixed 1 s; ietf/subscriber.rs:803, Settle at :3807; JS ietf/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.
  • moq-lite session tail (max_delay, else 1 s; lite/subscriber.rs:6841,6766,8009,8037; JS lite/subscriber.ts:1109): from lite-05, publishers send SUBSCRIBE_DROP for 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-side (the arrival-ledger fold, held fetches, resume; model/track.rs:1893-1907, resume.rs:520; JS track.ts:1585): grace() returns max_delay whenever 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

  • Bindings break: Kotlin and Swift Subscription(...) must name maxDelay. Python's bare Subscription() now means 0 instead of unlimited. MIGRATE notes are updated in moq-ffi, Kotlin, Swift, Python, and JS.
  • JS: the unreleased fix(bindings)!: null max delay means no limit, with a default subscription constructor #7077 behavior is reverted. Its MIGRATE entry is replaced by "maxDelay: null sets no subscriber limit". The API report changes maxDelay?: Milli to Milli | null.

Choices made without asking

Suggested follow-ups

  • Dart, Go, and C max_delay zero-value defaults are still unlimited (see the audit).
  • moq-lite tail: replace the grace with SUBSCRIBE_DROP accounting on lite-05+ (see the GRACE findings).
  • JS Ordered parity: it skips gaps instead of waiting for owed groups.
  • Ordered read that skips owed holes for snapshot latest() (decision 7).

Checks

  • quest gate just check origin/main at fcf617a: pass.
  • At 1aad05c (doc-only changes since fcf617a): just check fcf617a58c passed rs (4959 tests), py, and c. kt and cpp were cancelled by the shell time limit on the loaded host.
  • cargo test -p moq-net --all-features --lib: 2688 passed. bun test in js/net: 2648 passed.

Review

  • Reviewer: Codex gpt-6.1-sol, medium effort, via quest review --base origin/main, session 01a190e0-69af-7511-8748-a868e88e5c30 (new session, rounds counted from 1).
  • Round 1 (fa8d606): 1 finding, snapshot latest() waits at owed holes. Refuted: bounded by the opt-in budget, no in-repo caller affected.
  • Round 2 (fa8d606): the same finding. The reviewer rebutted: it breaks latest()'s documented contract. Documented in 1aad05c.
  • Round 3 (1aad05c): the same finding. The reviewer wants the behavior fixed, not documented. Accepted under decision 7: land with the documented caveat and plan the follow-up Ordered read.
  • Earlier, on fcf617a (Codex gpt-6-astra, --deep, session 01a18ee5-…): 0 findings. Before that, on the pre-decision heads (Codex gpt-6.1-sol): two findings at 3ecbd21, both fixed; 0 at dda86df.

Online review threads

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 5 commits October 30, 2026 08:28
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>
@kixelated kixelated changed the title Ordered reads wait only on owed groups fix(net): ordered reads wait only on groups a subscription still owes Oct 30, 2026
kixelated and others added 5 commits October 30, 2026 13:35
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>
kixelated and others added 2 commits October 30, 2026 14:59
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>
@kixelated kixelated changed the title fix(net): ordered reads wait only on groups a subscription still owes fix(net): ordered reads wait only on groups a subscription still owes; max_delay defaults to 0 Oct 30, 2026
@kixelated
kixelated marked this pull request as ready for review October 30, 2026 22:21
@coderabbitai

coderabbitai Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You'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.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ae1e9667-1ac0-4c97-b25d-f7e9e3adc354
Commits

Reviewing files that changed from the base of the PR and between b7ad3da and 28e9732.

Files selected for processing: 28
  • dart/moq_ffi/lib/src/moq.dart
  • doc/lib/js/net.md
  • doc/lib/kt/index.md
  • doc/lib/py/index.md
  • doc/lib/swift/index.md
  • js/net/MIGRATE.md
  • js/net/api/index.api.md
  • js/net/src/ietf/subscriber.ts
  • js/net/src/internal.ts
  • js/net/src/lite/subscriber.ts
  • js/net/src/track.test.ts
  • js/net/src/track.ts
  • kt/moq/MIGRATE.md
  • kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Aliases.kt
  • kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt
  • py/moq-rs/MIGRATE.md
  • py/moq-rs/moq/_records.py
  • py/moq-rs/tests/test_local.py
  • quest/m2/README.md
  • quest/m2/ordered-owed-wait.md
  • rs/moq-ffi/MIGRATE.md
  • rs/moq-ffi/src/consumer.rs
  • rs/moq-json/src/snapshot/consumer.rs
  • rs/moq-net/src/model/subscription.rs
  • rs/moq-net/src/model/track.rs
  • swift/MIGRATE.md
  • swift/Sources/Moq/Aliases.swift
  • swift/Tests/MoqTests/SmokeTests.swift
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

Share

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T22:23:46.614941Z fcf617a Draft marked ready
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread js/net/src/track.ts Resolved

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — the core ordered-owed logic, the JS null/0 semantics, 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_land is refactored onto find_landable with an unchanged truth table, and a new TrackState::owed / Cursor::poll_owed makes 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.maxDelay is Milli | null; null means unlimited and omitted/undefined means 0. NO_MAX_DELAY/maxDelayOf move to internal.ts and feed the lite Subscribe/Update and IETF update wire.
  • Bindings/defaults: MoqSubscription.max_delay drops its uniffi default (required in Kotlin/Swift), Python's wrapper defaults to timedelta(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 null default.

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.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Comment thread rs/moq-ffi/MIGRATE.md Resolved
kixelated and others added 2 commits October 30, 2026 23:42
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>
@kixelated
kixelated enabled auto-merge (squash) October 31, 2026 08:42
@moq-bot
moq-bot Bot merged commit 5183699 into main Oct 31, 2026
1 check passed
@moq-bot
moq-bot Bot deleted the quest/m2/ordered-owed-wait branch October 31, 2026 16:39
kixelated added a commit that referenced this pull request Oct 31, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant