Repository navigation
feat(moq-net): time anchors for subscription ranges - #6074
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A subscriber now reads only the groups its own Subscription::ranges cover, resolved where it is served and again whenever they are replaced, instead of reading every group the aggregate brings in. A new Anchor::Time(pts) resolves from the cached groups' starts; an untimed track refuses it. The drop scan names only sequences inside the reader's ranges, so the SUBSCRIBE_DROP accounting can reuse it. moq-c and moq-mux stop re-resolving relative starts on their own. moq-ffi, moq-c, and the bindings carry the time anchor, and the FFI update now reports a refused one. Adds track_range_serving, which found and fixed two per-range walks per hand-out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The resolved window and its bookkeeping live behind one box so every reader that embeds a cursor stays its old size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # rs/moq-ffi/MIGRATE.md
Also document the extra group a time end may let through at the live edge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # rs/moq-net/MIGRATE.md
moq_consume_track_update queued updates for the reader task, which dropped a TimestampMismatch, so a time anchor on an untimed track silently kept the old ranges. Once subscribed, updates now go through the track's Control and fail at the call; one made before SUBSCRIBE_OK applies once it arrives and a refusal then ends the consumer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…atagrams too A copy joined mid-group stamps a later frame than the group's start, so it could convince a time end that the group started past it. A refused cursor also read and discarded datagrams instead of returning its refusal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…guide Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…vers The arrival walk passed groups below the floor without parking them, so widening the ranges (or a SUBSCRIBE_UPDATE lowering Group Start) could never serve a cached group that arrived before the ones already read. A relay could not fold a second subscription into one by lowering the start. The cursor now remembers the highest floor its walk applied. When the floor drops below it, the cached groups in between are looked up by sequence and parked, so each update costs only the groups it re-offers, and the subscriber's delivered set keeps any from going out twice. This reverses #4940's no-rewind rule; its two publisher tests now pin the new contract. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…p cap An unresolved time end covers every group handed out before its first frame, not just one. Say so on Anchor::Time and in the concept doc, and track capping it at one speculative group in quest/m1/subscribe-ranges/time-end-cap.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A local track::Subscriber hands out whole groups even when its range starts or ends mid-group. That predates this branch (reproduced on main 824179a), and the wire publishers already position each group per subscription, so it is tracked in quest/m1/subscribe-ranges/frame-bounds.md. The drift_cap comment still said ranges don't filter the subscriber; say why the edge stays uncapped by them instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # cpp/moq/MIGRATE.md
The backfill and has_unread_group read the cache by sequence, so they also picked up fetched backfill (never replayed in arrival order) and groups a route still withholds until reveal_group. Filter both to shown slots, as the arrival walk does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ngelog Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A replacement copy clamped every relative or time start to the reader's newest delivered group, so a later disjoint time range (Time(10s), say) was asked from group 0 and its own filter let through everything in between. Capping a replaced copy did the same to a time end. The Window now reports each range resolved, by index. A replacement copy is clamped from where the serving copy resolved the reader's ranges, read before that copy is capped, and a copy recomputing its own wire uses its own resolution, or resolves replaced ranges against its cache now. Only a range not resolved yet still becomes the bound itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # dart/moq/MIGRATE.md # go/wrapper/MIGRATE.md # kt/moq/MIGRATE.md # py/moq-rs/MIGRATE.md # rs/moq-c/MIGRATE.md # rs/moq-ffi/MIGRATE.md # swift/MIGRATE.md
A relative or time start a replacement copy cannot resolve yet, such as one updated while the copy is still pending, keeps its anchor instead of becoming the floor, which would read every group from there. A pending copy replaced in turn passes on the resolution it was handed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ot to A start the wire kept as an anchor can resolve below the copy's floor once it answers, and the reader never delivered the groups it skipped there, so the delivered set did not stop them. Raise the copy's local start to its floor when it answers, as start_at already does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntroduced Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # cpp/moq/MIGRATE.md # dart/moq/MIGRATE.md # go/wrapper/MIGRATE.md # kt/moq/MIGRATE.md # py/moq-rs/MIGRATE.md # rs/moq-ffi/MIGRATE.md # rs/moq-net/src/model/resume.rs # swift/MIGRATE.md
…e cache A reader no longer parks a consumer and an abort watch for every group outside its ranges or past its cap, which a narrow reader under an open aggregate accumulated per produced group. The walk passes such a group holding nothing. A move of the floor, ranges, or cap owes the spans it newly admits, and the walk re-offers what the cache still holds there through an ordered lookup, once per widening: O(log n + k) for the k groups in the span. This subsumes the lowered-floor backfill. A finished track now ends a reader with nothing left in range instead of holding it open for a cap that may rise. Drift is measured under where the ranges end as well as the local cap, so a range behind the live edge keeps its own newest group. track_range_widening replaces track_parked_abort. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4ef73d3f9d
ℹ️ 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".
A datagram filling a gap counted as the sequence having arrived, so a time end settled across it without knowing where that group starts, and the same group streaming later was read past the end. A datagram is its group's frame 0, so the starts index now holds each buffered datagram's timestamp, dropped when the datagram leaves the buffer unless the group is cached. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review (Written by Claude Opus 5.5) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3ea298982d
ℹ️ 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".
…scending A datagram is judged on its own: its timestamp against a range's times and its sequence against its absolute and resolved relative groups, never against where a time end settled. A datagram that would rewind the starts index is left out of it, so the index stays ascending for the groups a time end settles against. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Ready for maintainer review as it stands at 771c155. I've stopped iterating on Codex. After the per-datagram decision, four Codex rounds found only edge cases. Each one is fixed with a regression test that fails without it:
The JS mirror and the binding-guide docs were declined, with reasons in their threads. All 9133 workspace tests and Known residual: a stream copy of a group that arrives after its datagram has left the 64-entry send buffer, before any reader settled the end, is placed only by its arrival. Fixing that would need datagram starts kept past the buffer. (Written by Claude Opus 5.5) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 771c1558ca
ℹ️ 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".
|
Parked for maintainer review at 771c155. No more pushes, review requests, or main merges from this session. Open:
Both unresolved findings come from indexing buffered datagrams' starts, added in 3ea2989 and 771c155. If datagrams are treated as fully ephemeral, reverting that indexing removes both. It also brings back the earlier stream-copy-after-datagram finding. (Written by Claude Opus 5.5) |
A buffered datagram no longer records where its group starts. Indexing them kept a time end from settling across a sequence that arrived only as a datagram, but it needed eviction, duplicate, and ordering bookkeeping against the stream starts, each with its own edge cases. The edge it covered, a stream copy of a group already sent as a datagram arriving after the end settled, is accepted and documented at the settle walk. Datagrams are still judged per datagram by their own timestamps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nges/time-anchors
recv_datagram exists only with the async feature, so the no-default features build failed to compile the test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review (Written by Claude Opus 5.5) |
|
Codex Review: Didn't find any major issues. 🚀 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
|
Merge summary for
(Written by Claude Opus 5.5) |
|
Merging: clean (Written by Claude Opus 5.5) |
A range start or end may now name a presentation time,
track::Anchor::Time(pts), for DVR seek. A start resolves to the group containing pts. An end resolves to just past that group. An untimed track refuses either.This picks up the time anchors split out of #5929 and re-ports them onto main's
Servablewindow,next_in_range, and resumed-reader resolution. The design fixes the three open findings from that review.Design
An unsettled time end keeps forward-only pointers in
Window:known: the newest group known to start at or before pts. It advances through a track-level index of frame-0 starts, so it passes stalled groups.held: a walk over held groups. It stops at the first group that may still start (the one speculative group) or at a group starting past pts.accounted: the settle boundary. It walks held groups, drops, and ledger runs up to the first group starting past pts. When it reaches that group, the end settles asBefore(it).Per-read rescan (finding 1). Each pointer only moves forward, so a peer that leaves an early sequence missing and keeps sending makes each read scan only what arrived since. The regression test counts holdings queries behind a 2000-group gap: about 2M queries without the fix, linear with it.
Failover (finding 2).
Window::resolvedreports an unsettled time end as unresolved, not as its cap.Subscription::clampedkeeps an unresolvedTimeend instead of folding a copy's failover cap into it. The copy cursor keeps the time filter, and the reader'suntilkeeps the cap.Speculative group (finding 3). At the live edge, reads stop at the first held group that may still start. So at most one group past the end is read before its first frame shows where it starts. A reader parked on that cap wakes on the group's
poll_started, and on aWakes::watch_startthat the frame-0 hook fires when a later group starts. A held group that never shows a start (empty, aborted, or never-start) does not pin the cap. Datagrams never go through the settle walk. Each datagram is judged on its own: its timestamp against the range's time start and end, and its sequence against the range's absolute groups and the relative ones resolved so far. Datagrams stay ephemeral: a buffered datagram never records where its group starts. So a time end settles across a sequence that arrived only as a datagram, and a stream copy of that group arriving later is read even if it starts past the end. That edge is accepted and documented at the settle walk. Refusing time anchors on datagram tracks was considered and can't work. Nothing signals that a track carries datagrams, and the lite draft lets a publisher send any single-frame group as a datagram, including a copy of a group it also streams.Decisions
Public API
Breaking, with
MIGRATE.mdentries in every package:track::AnchorgainsTime(Timestamp).Control::update, with a time anchor on an untimed track fails withError::TimestampMismatch.MoqAnchorgainsTime { timestamp_us }.MoqTrackConsumer::updatenow returnsResult<(), MoqError>.TimeOverflow.moq_anchorgainstimestamp_us, read with the newMOQ_ANCHOR_TIMEkind. This is an ABI change.moq_consume_track_updatereturns a negative code when the track refuses the update, and leaves the subscription unchanged.Controlinstead of going through a channel.AnchorTime.AnchorTime..time(timestampUs:).updatenow throws, raises, or returns an error on a refused update.Wire
None. No wire carries anchors yet. The lite-07 encoding, including
Time, lands with the lite-07 ranges quest, and so does its draft text.Deferred
track::Dynamichandler. This waits for dynamic range requests, and its quest now says so.@moq/netmirror. The JS ranges quest,quest/m1/subscribe-ranges/js.md, already includesTime.Tests
a_time_end_walks_each_group_once_behind_a_missing_sequencecapping_a_replaced_copy_keeps_its_time_enda_time_end_reads_one_group_past_it_at_the_live_edgea_read_parked_on_a_time_end_wakes_as_groups_starta_datagram_is_judged_against_a_time_end_by_its_timestampa_datagram_is_judged_against_a_time_start_by_its_timestampa_settled_window_still_judges_a_datagram_by_its_timesa_waiting_window_judges_a_datagram_against_absolute_groups_tooa_time_end_settles_on_a_group_that_starts_between_its_walksan_untimed_track_refuses_time_anchorstrack_time_endsweeps 1, 8, and 64 readers over 64, 512, and 4096 groups behind a missing sequence. Throughput stays roughly flat as groups grow:The starts index is charged in
track::CACHE_OVERHEAD. A track whose group starts rewind is unsupported (#5446). Its cost stays bounded, but its own filtering may be wrong.Also fixes the
sessionbench id, which #6028 left with one argument more than its format string, so the bench failed to compile on main.(Written by Claude Opus 5.5)
🤖 Generated with Claude Code