Skip to content

feat(net): announce consumers report when they have synced - #5724

Merged
kixelated merged 20 commits into
mainfrom
quest/m1/announce-live
Oct 17, 2026
Merged

kixelated merged 20 commits into
mainfrom
quest/m1/announce-live

Conversation

@kixelated

@kixelated kixelated commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Implements quest/m1/announce-live: an announce consumer knows when it has synced, so "not announced" can be told from "not heard yet".

Changes

Rust (rs/moq-net)

  • The origin tracks snapshots: initial announce sets a session is still delivering. Each sits in the route trie with a subtree count, so a snapshot landing or a consumer registering touches only the path to its prefix.
  • An announce consumer is synced once every current snapshot overlapping its scope has landed and it has read the events owed at that point. "Owed" is a watermark over the coalesced queue: unseen starts and any ends, so a start and end that cancel still sync, and updates or restarts of delivered routes are not waited on.
  • lite subscriber: ANNOUNCE_INIT (lite-01/02) and ANNOUNCE_OK's active count (lite-05+) mark the boundary; lite-03/04 sync once the request is out. Anything but an ANNOUNCE (or an unknown, skipped type) inside the counted set, or the stream ending there, is ProtocolViolation.
  • IETF subscriber: the MoQ Active Count's counted NAMESPACEs mark the boundary; without it, REQUEST_OK does. NAMESPACE_DONE or a FIN inside the counted set is ProtocolViolation.
  • Bench synced sweeps (snapshots in flight) x (consumers): land and register are flat across 10..1000 on both axes.

JS (js/net)

  • announce.Consumer.synced (a Getter<boolean>), flipping once the reader asks for the event after the last owed one, so whatever it did with that event lands first.
  • The origin holds back overlapping streams while a session's request has not synced (forwardAnnounced takes a snapshot per announce request), and Reload holds the consume origin back while no session is established.
  • Lite and IETF subscribers mark the boundary as in Rust, with the same violations.
  • MoQ Active Count: declared in SETUP on draft-16+, negotiation recorded, the count sent on SUBSCRIBE_NAMESPACE's REQUEST_OK (ahead of any change), validated against the negotiation, and refused on a PUBLISH_NAMESPACE answer.

Docs: doc/concept/moq-lite.md Discovery says what synced means, which versions are exact, and the multi-source rule.

Public API

  • Rust announce::Consumer: is_synced(), poll_synced(&Waiter) (level-triggered, ready once closed), async synced(). Hidden fuzz::{Snapshot, snapshot} for the bench.
  • JS announce.Consumer.synced, also carried by Connection.announced() and Reload.announced(); announce.Producer.synced() / behind() are @internal. Ietf.activeCountFromSetup / activeCountIntoSetup, Ietf.SetupOption.ActiveCount, and Parameters.activeCount follow the hidden pattern.

Wire

No new format or draft change. JS now declares and honors the existing MoQ Active Count extension (Rust already did). Both subscribers now refuse a counted initial set that is cut short, which the drafts already require.

Iteration

  • Rebased onto main: the synced integration test moved into rs/moq-net/tests/integration/ (test: link each crate's integration tests into one binary #5689), the snapshot's origin handle is a SendCell, and the JS Reload test uses @moq/clock's Milli.
  • Fixed a regression CI caught: cluster_unknown::unknown_publisher_does_not_flap_across_a_lite_cluster_triangle failed in about half of runs on this branch (0/10 on main). Counting a snapshot in flight took the cursor state's write guard, which wakes every waiter, so a relay woke each overlapping announce feed twice per connecting session. That perturbed the mesh enough for a shorter path to land after a longer one and flap as a held restart. Counting now goes through a read guard and only the last landing wakes (holding_back_wakes_nobody covers it); the triangle passes 12/12.
  • Codex review of b21aa2abd, all three findings fixed with regression tests that fail without them:
    • A forwarded request synced as it opens (draft-14/15 at the root with no Solicit) never released its origin snapshot; open now checks synced.peek() after subscribing.
    • Connection.announced() (pool) and Reload.announced() without a consume origin never synced. Both now mirror their upstream, behind while enabled with nothing delivering (decision 5's rule).
    • An ACTIVE_COUNT on a PUBLISH_NAMESPACE answer now closes the transport with PROTOCOL_VIOLATION instead of a clean close.
  • Codex review of ba587064d, both findings fixed:
    • The wake fix left a gap between zeroing the wait count and owing the queue where another thread could read synced early. The last landing now does both under one write guard.
    • The JS subscriber closed cleanly over SUBSCRIBE_NAMESPACE violations (Active Count mismatch, a cut-short set, and the existing malformed advertisement). It now closes with PROTOCOL_VIOLATION; the tests assert the code.
  • Codex review of b78ed255e: a snapshot beneath a mount's hole held the handle back. A cursor now judges a snapshot as it judges a route there (not in a hole, nameable through its mount).
  • Codex review of be5ee5906: a snapshot under a hidden segment held default readers (Rust and JS), and an ACTIVE_COUNT too large to read slipped past the advertisement's violation close. Both fixed with tests.
  • Codex review of 5cd60c262: a handle switching upstreams while synced stayed synced over the old upstream's retractions. Pool and Reload now share one pump (pumpAnnounced) that retracts synchronously at teardown and owes those ends.
  • Codex review of c9ec7e8c9: a teardown with a snapshot in flight reported synced before the queue drained. The teardown now owes the queue like the last landing.
  • Merged main (Migrate network timing onto moq-clock #5681's moq_clock, feat(net)!: let live announcements renew their auth #5695's auth rename) and renamed caught up to synced everywhere, per the maintainer: is_synced / poll_synced / synced(), JS synced, the bench, the integration test, and the docs and draft prose.

Checks

  • Passed on 7b88bcd8f: just check (scoped to the branch) and just test interop --all. 4770abc23 and e168fb270 only merge main again (test-file and wire.ts conflicts, both sides kept). e168fb270 also adds the // pending: /quest/m1/public-model/answer.md markers refactor(net): narrow the model and mark what sessions reach #5794 now requires on the session-reached snapshot items (Rust Snapshot, snapshot, landed; JS announce.Producer.synced / behind, wire.OriginProducer.snapshot). just check passes on it.
  • The lite cluster triangle stressed 10/10 on the final origin code.

Decisions

Iterated unattended, so each took the recommended option. The maintainer can override any of them before merge.

  1. Naming: ✅ synced (the maintainer's pick) / caught_up / caughtUp / live as the quest said ("live" already means a track's live edge).
  2. Multi-source rule: ✅ wait for every current overlapping snapshot, and a new session (reconnect, second feed) takes sync back until it lands / synced once and stays.
  3. poll_synced: ✅ level-triggered (ready whenever synced, and once closed) / edge-triggered.
  4. Unknown lite announce types inside a counted set: ✅ neither count nor cut, so a future type does not break older subscribers / refuse as a violation.
  5. JS Reload hold: ✅ while enabled with no session, including before a URL is set and after giving up, mirroring expect() / only while a connection attempt is in flight.

Follow-ups

  • Lazy discovery hand-over: let a narrower request take over from a broad one once it reports synced on an exact-boundary version (needs the consumer to say whether its boundary is exact).
  • js/watch: show offline once the announcement stream has synced without the broadcast.
  • Rust Follow / moq-ffi and bindings: expose synced.
  • A JS announced benchmark: origin.announced re-diffs the whole table per wake.
  • Suspected on main too, only rarer: in a redundant mesh, a shorter path to an UNKNOWN-hop publisher landing after a longer one is a held restart, which lite-06 and older render as an end and a start, so viewers see a flap. Worth a dedicated reproduction.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

@kixelated

Copy link
Copy Markdown
Collaborator Author

Paper trail: just test interop --all passed on the current head ("interop: all checks passed"), along with the scoped just check. The PR stays a draft for the maintainer's call on the open decisions in the description; recommendations are listed with each.

(Written by Claude Opus 5.5)

@kixelated

Copy link
Copy Markdown
Collaborator Author

#5689 merged: moq-net, moq-relay, moq-tokio, and moq-uring now link their integration tests into one binary under tests/integration/. When you rebase, move rs/moq-net/tests/caught_up.rs into that crate's tests/integration/ and add a mod line to its main.rs. Until then it fails to compile, because mod support; now lives under integration/.

(Written by Claude Opus 5.5)

kixelated and others added 7 commits October 16, 2026 20:12
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sessions hold a snapshot on the origin from the moment they request
announcements until the peer's initial set is in: ANNOUNCE_OK's count on
lite-05+, ANNOUNCE_INIT on lite-01/02, MoQ Active Count on moq-transport,
and the request or answer as a best effort elsewhere. An announce consumer
is caught up once no snapshot overlapping its scope is in flight and it has
read every event queued before the last one landed. A counted set cut short
is a protocol violation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
announce.Consumer.caughtUp flips once the source's initial announcements
are read: counted on lite-05+ (ANNOUNCE_OK active) and on moq-transport
with MoQ Active Count, which JS now declares, counts as a publisher, and
validates. A counted set cut short is a protocol violation. Older versions
catch up on the answer or the request, as a best effort.

An origin stream waits on every session still delivering an overlapping
initial set, and a Reload holds it back while no session is established,
so a reconnect takes caught up back until the new set lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lock

Move the caught-up integration test into tests/integration (#5689), hold the
snapshot's origin handle in a SendCell like the rest of the origin, and use
@moq/clock's Milli in the Reload test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Counting a snapshot in flight took the cursor state's write guard, which wakes
every waiter on it. A relay therefore woke each overlapping announce feed twice
per session that connected, and that perturbation made the lite cluster triangle
in cluster_unknown flap (about half of runs, never on main). Count through a
read guard instead and wake only on the last landing, the one change a
caught-up waiter can see.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated force-pushed the quest/m1/announce-live branch from 4e7e4df to b21aa2a Compare October 17, 2026 04:41
@kixelated

Copy link
Copy Markdown
Collaborator Author

Paper trail for head b21aa2abd: just test interop --all passed ("interop: all checks passed"), along with the scoped just check. The lite cluster triangle in cluster_unknown failed in about half of runs before the wake fix and passes 12/12 after it.

(Written by Claude Opus 5.5)

@kixelated
kixelated marked this pull request as ready for review October 17, 2026 04:47
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 17, 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-17T07:32:09.691640Z 7252571 New commits
ℹ️ 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.

@coderabbitai

coderabbitai Bot commented Oct 17, 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 19 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
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 25aa5718-9b64-4386-a3fd-744628a79ac7
📥 Commits

Reviewing files that changed from the base of the PR and between 433f9a0 and e168fb2.

📒 Files selected for processing: 43
  • doc/concept/moq-lite.md
  • doc/concept/standard.md
  • drafts/draft-lcurley-moq-active-count.md
  • js/net/src/announced.test.ts
  • js/net/src/announced.ts
  • js/net/src/connection/accept.ts
  • js/net/src/connection/connect.ts
  • js/net/src/connection/forward.test.ts
  • js/net/src/connection/forward.ts
  • js/net/src/connection/handshake.ts
  • js/net/src/connection/pool.test.ts
  • js/net/src/connection/pool.ts
  • js/net/src/connection/reload.test.ts
  • js/net/src/connection/reload.ts
  • js/net/src/ietf/active_count.ts
  • js/net/src/ietf/connection.ts
  • js/net/src/ietf/index.ts
  • js/net/src/ietf/parameters.ts
  • js/net/src/ietf/publisher.test.ts
  • js/net/src/ietf/publisher.ts
  • js/net/src/ietf/subscriber.test.ts
  • js/net/src/ietf/subscriber.ts
  • js/net/src/lite/subscriber.test.ts
  • js/net/src/lite/subscriber.ts
  • js/net/src/origin.ts
  • js/net/src/wire.ts
  • quest/m1/README.md
  • quest/m1/announce-live.md
  • quest/m1/lazy-discovery/rust.md
  • rs/moq-net/Cargo.toml
  • rs/moq-net/benches/synced.rs
  • rs/moq-net/src/fuzz.rs
  • rs/moq-net/src/ietf/active_count.rs
  • rs/moq-net/src/ietf/peer.rs
  • rs/moq-net/src/ietf/request.rs
  • rs/moq-net/src/ietf/session.rs
  • rs/moq-net/src/ietf/subscriber.rs
  • rs/moq-net/src/lite/publisher.rs
  • rs/moq-net/src/lite/subscriber.rs
  • rs/moq-net/src/model/mod.rs
  • rs/moq-net/src/model/origin.rs
  • rs/moq-net/tests/integration/main.rs
  • rs/moq-net/tests/integration/synced.rs
  • 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

Comment @coderabbitai help to get the list of available commands.

@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: b21aa2abd3

ℹ️ 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/connection/forward.ts Outdated
Comment thread js/net/src/announced.ts Outdated
Comment thread js/net/src/ietf/publisher.ts Outdated
kixelated and others added 2 commits October 16, 2026 22:28
A forwarded request already caught up as it opens (draft-14/15 at the root with
no Solicit) never released its origin snapshot, since the listener only sees
later changes. Connection.announced() and a Reload without a consume origin
pumped events but never caught up. They now mirror their upstream, behind while
enabled with nothing delivering, as the consume origin's hold is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The session closed cleanly and the advertiser swallowed the violation, so the
peer never saw PROTOCOL_VIOLATION.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@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: ba587064db

ℹ️ 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/ietf/subscriber.ts
Comment thread rs/moq-net/src/model/origin.rs Outdated
kixelated and others added 2 commits October 16, 2026 23:01
Zeroing the wait count under a read guard and owing the queue under a later
write guard left a gap where another thread could read caught up before the
owed set was in. The last landing now does both under the write guard; earlier
landings still only decrement, waking nobody.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A miscounted or cut-short Active Count set, like a malformed advertisement,
closed the session cleanly, so the peer never saw PROTOCOL_VIOLATION.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@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: b78ed255e7

ℹ️ 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 rs/moq-net/src/model/origin.rs Outdated
A cursor waited on any snapshot its patterns overlapped, even one beneath a
subtree a mount shadows, so an unrelated session could hold the handle back.
It now judges a snapshot as it judges a route there: not in a hole, and
nameable through its mount.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@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: be5ee59064

ℹ️ 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/origin.ts Outdated
Comment thread rs/moq-net/src/model/origin.rs
Comment thread js/net/src/ietf/publisher.ts Outdated
kixelated and others added 2 commits October 16, 2026 23:44
A default reader waited on a session's initial set under a hidden segment,
though it lists none of those routes. Rust and JS now rule such a snapshot out
the way they hide its routes; a top-up feed's rule depends on the rest of the
path, so it never rules a prefix out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reading the count throws before the violation close ran, so the advertisement
was dropped and the session stayed up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@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: 5cd60c262f

ℹ️ 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/connection/pool.ts Outdated
Switching a handle's upstream while caught up left the downstream caught up:
the retractions landed after the new upstream was mirrored, and Producer's
caughtUp() is a no-op until behind(). Pool and Reload now share one pump that
retracts synchronously at teardown, goes behind first so even a parked reader
takes the ends as owed, and mirrors caught up only from an open upstream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@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: c9ec7e8c93

ℹ️ 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 rs/moq-net/src/model/origin.rs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated and others added 2 commits October 17, 2026 07:12
# Conflicts:
#	js/net/src/ietf/publisher.test.ts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title feat(net): announce consumers report when they have caught up feat(net): announce consumers report when they have synced Oct 17, 2026
@kixelated

Copy link
Copy Markdown
Collaborator Author

Head 7b88bcd8f10dd4da574f3d471e2f1c15200c6db1 merges main (#5681's moq_clock, #5695's auth rename) and renames caught up to synced everywhere, per the maintainer's decision 1: Rust is_synced / poll_synced / synced(), JS announce.Consumer.synced, the synced bench and integration test, and the docs and active-count draft prose. Decisions 2 to 5 stand. No behavior change since the clean Codex review of 725257112. just check and just test interop --all pass on this head.

(Written by Claude Opus 5.5)

@kixelated
kixelated enabled auto-merge October 17, 2026 14:45
# Conflicts:
#	js/net/src/lite/subscriber.test.ts
@kixelated
kixelated added this pull request to the merge queue Oct 17, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 17, 2026
@kixelated
kixelated enabled auto-merge October 17, 2026 16:49
@kixelated
kixelated added this pull request to the merge queue Oct 17, 2026
Merged via the queue into main with commit 866e727 Oct 17, 2026
17 checks passed
@kixelated
kixelated deleted the quest/m1/announce-live branch October 17, 2026 17:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant