Skip to content

feat(net)!: announcements carry availability; a let-go is offline, not END - #8375

Merged
kixelated merged 13 commits into
mainfrom
quest/m2/cluster-routing/routes/availability
Nov 10, 2026
Merged

kixelated merged 13 commits into
mainfrom
quest/m2/cluster-routing/routes/availability

Conversation

@kixelated

@kixelated kixelated commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

Announcements now report availability. When this side lets go of a route, readers get a distinct offline event instead of the origin's END. This covers Rust, js/net, and every binding. Finishes quest/m2/cluster-routing/routes/availability.md and deletes it, building on the Listing API. The maintainer's decision moves to the routes epic as item 16.

A token-scoped lite-07 subscriber now asks its peer for the one region its scope allows, rather than for everything. The peer's floors can then certify the request, so a clean finish still ENDs across a relay instead of going offline.

Decisions; then /quest-iterateimplements it and runs review round 4 withquest review --new` if the approach changed.

Decisions

  • ✅ Option 1: a scoped lite-07 subscriber narrows its ANNOUNCE_REQUEST to its scope's single region, where its limit and its subscribe grant meet. It reopens the request when that region changes, so a widening renewal isn't lost. (Rejected: floors carrying regions on the wire, which reverses epic decision 10; landing with the finish lanes accepting Offline.) Maintainer, 2026-11-09, via /quest-unblock.
  • ✅ During a reopen's handover, the old request gets its own announce budget: it moves what it holds onto a separate counter with the same cap, so the session's count belongs wholly to the new request. A peer can make the session hold at most twice the cap, and only during one handover; a peer flooding the old request fails it on its own budget. (Rejected: dropping the old request at once on every region change, which flickers claims offline; holding a client's first request until its credential is answered and dropping the old one on later renewals.) Maintainer, 2026-11-09, via /quest-unblock. Recorded as item 17 in the routes epic README.
  • Done in this PR, not a separate prerequisite PR. On main every unserve still reads as END, so narrowing alone changes nothing a reader sees. Its tests would pass there with or without it. Here, the finish lanes and auth::lite_07_a_scoped_withdrawal_ends fail without it.

How the narrowing works

  • Permit::region() is the one prefix where the peer's grant and this side's limit meet, or None if there are several or none.
  • Each AnnouncePrefix puts the discovery prefix on the wire, narrowed to that region when the region lies beneath it. On lite-07 only, since only lite-07 has floors.
  • When the subscribe permit changes, the driver reopens any request whose region moved. The new request keeps the old one running until its own SYNCED. That way the node lets go only of what the new request no longer carries. An old request that never reached its run loop has served nothing, so it is dropped at once.
  • At the reopen, the old request's Routed::hand_over moves its slots onto Slots::split, a fresh counter with the session's cap.
  • The lite-07 draft gains one SHOULD under Floors, plus a changelog line. There is no wire change.

Public API impact (breaking, no wire change)

  • Rust: RouteKind::Offline; announce::Event::{Unavailable, Available, Offline} beside Present/Absent; RouteInfo::available().
  • JS: listing event kinds unavailable | available | offline, route kind offline, RouteInfo.available.
  • FFI: MoqAnnounceEvent::{Unavailable, Available, Offline}, MoqRouteInfo.available. C ABI: MOQ_ANNOUNCE_KIND_{OFFLINE,UNAVAILABLE,AVAILABLE}, moq_route_info.available. Go, Python, Kotlin, Swift and Dart aliases and docs are mirrored.

Impact / migration

An exhaustive match on announce::Event or RouteKind needs the new arms. To keep the old behavior, treat Offline like Absent/End and ignore Available/Unavailable.

Choices made without asking

  • Kept the name Absent and did not rename it to Ended.
  • Availability rides on Update/RouteInfo. The listing turns flips into Available/Unavailable.
  • A session drop, an auth limit loss, or a narrower interest goes offline. moq-transport ends a route only on NAMESPACE_DONE or a clean stream close.
  • An unavailable route ranks with draining routes.
  • moq-dev/moq#8369 landed first, so Offline is now a RouteKind variant beside Start/Update/Restart/End. Merged main to pick it up.
  • If the scope changes again before a replacement request is synced, the request before it is let go at once. Only one handover is in flight at a time.
  • A client learns the grant for its connection credential from the AUTH_OK, which may land after its first request goes out. That first request is then reopened at the region once the answer arrives.
  • Review round 1: the JS moq-transport subscriber now mirrors Rust. NAMESPACE_DONE ends a namespace. A reflected namespace, or a SUBSCRIBE_NAMESPACE stream that goes away (even by the peer's FIN, which withdraws nothing per a_lost_namespace_stream_closes_the_broadcast), goes offline. A PUBLISH_NAMESPACE ends when the peer ends its request. So a moqt-18 graceful close now reads as offline in connection/close.test.ts, while lite-07 still ends.

Checks

On head c8902ab874 (the handover budget):

  • just check: pass, check c included. Earlier heads of this branch failed check c the same way a clean origin/main did: the C bindgen refused MoqInstance's exported Debug ("the C backend does not support exported traits on objects yet"), which came in with moq-dev/moq#8366. That failure is pre-existing and nothing here changes it; it did not reproduce in this run.
  • just test interop --all: pass. Main quarantines the finish scenario (moq-dev/moq#8407), so its 45 lanes are skipped, as are the existing JS lite-07 skips.
  • routed::tests::a_handover_counts_the_replacement_alone (a handover near the cap keeps the session) and routed::tests::a_handover_caps_the_old_request (a peer flooding the old request is still capped) both fail with round 3's give-back in place of the private budget.

On head 3320df4e1c, before that quarantine reached this branch, just test interop --all passed with all 45 finish X -> Y lanes, and with narrowing disabled finish rust -> rust failed again, so the narrowing is what fixes it.

New tests: auth::lite_07_a_scoped_withdrawal_ends, auth::lite_07_a_scope_change_reopens_without_letting_go, auth::lite_07_a_scope_change_at_the_announce_cap_keeps_the_session, auth::tests::the_region_is_where_grant_and_limit_meet, and the two handover tests above. Each fails with its fix removed.

Suggested follow-ups

  • The C bindgen failure on MoqInstance above. A separate quest is being planned for it; it is not this PR's change.

Caused by

None: this is a feature.

Models

Size: L
Implementer: Claude Code, Opus 5.5, unknown effort
Implementer: Claude Code, claude-opus-5-5, high effort (iterate, L)
Implementer: Claude Code, claude-opus-5-5, high effort (iterate, L)

(Written by Opus 5.5 with high)

🤖 Generated with Claude Code

Review

Rewritten by quest review after each round.

  • Reviewer: Codex, gpt-6.1-sol, medium effort
  • Session: 01a1c340-5a55-7111-87e2-2c56f4f1f33f, round 4
  • Head: c8902ab
  • Base: origin/main at beb9013
  • Score: 10/10, The separate capped handover counter fixes duplicate charging for incoming claims, and the incremental changes introduce no remaining findings.
  • Findings: none
  • The last round's findings, at 3320df4:
    • Fixed: medium, rs/moq-net/src/lite/subscriber/routed.rs:308: Extend handover credit to claims arriving on the predecessor; Routed::hand_over moves existing slot guards and future acquisitions onto a separate Slots::split counter with the same cap, leaving replacement credit unaffected by predecessor updates.

kixelated and others added 5 commits November 9, 2026 07:18
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 Announcement availability feat(net)!: announcements carry availability; a let-go is offline, not END Nov 9, 2026
…certification

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated and others added 5 commits November 9, 2026 16:44
…ing/routes/availability

# Conflicts:
#	rs/moq-cli/tests/integration/instances.rs
#	rs/moq-cli/tests/integration/rtsp.rs
#	rs/moq-gst/src/source/imp.rs
#	rs/moq-net/benches/announce.rs
#	rs/moq-net/src/auth.rs
#	rs/moq-net/src/ietf/publisher.rs
#	rs/moq-net/src/ietf/subscriber.rs
#	rs/moq-net/src/lite/publisher.rs
#	rs/moq-net/src/lite/replay.rs
#	rs/moq-net/src/model/announced.rs
#	rs/moq-net/src/model/follow.rs
#	rs/moq-net/src/model/origin.rs
#	rs/moq-net/src/route/node.rs
#	rs/moq-net/tests/integration/announce_covering_route.rs
#	rs/moq-net/tests/integration/announce_to_serve.rs
#	rs/moq-net/tests/integration/auth.rs
#	rs/moq-net/tests/integration/claim_continues.rs
#	rs/moq-net/tests/integration/dial_split_horizon.rs
#	rs/moq-net/tests/integration/extensions.rs
#	rs/moq-net/tests/integration/instance.rs
#	rs/moq-net/tests/integration/lazy_discovery.rs
#	rs/moq-net/tests/integration/mount.rs
#	rs/moq-net/tests/integration/multi_cdn.rs
#	rs/moq-net/tests/integration/session_panic.rs
#	rs/moq-net/tests/integration/support/mod.rs
#	rs/moq-net/tests/integration/synced.rs
#	rs/moq-relay/src/cluster.rs
#	rs/moq-relay/src/internal.rs
#	rs/moq-relay/tests/drills.rs
#	rs/moq-relay/tests/integration/auth_lifetime.rs
#	rs/moq-relay/tests/integration/cluster_idle.rs
#	rs/moq-relay/tests/integration/cluster_unknown.rs
#	rs/moq-relay/tests/integration/connection.rs
#	rs/moq-relay/tests/integration/embed.rs
#	rs/moq-relay/tests/integration/memory.rs
#	rs/moq-relay/tests/integration/runtime_uring.rs
#	rs/moq-relay/tests/integration/runtime_workers.rs
#	rs/moq-relay/tests/integration/smoke.rs
#	rs/moq-relay/tests/integration/tenant_matrix.rs
#	rs/moq-stats/src/aggregate.rs
#	rs/moq-stats/src/consume.rs
#	rs/moq-stats/src/produce.rs
#	rs/moq-tokio/tests/integration/broadcast.rs
A token-scoped session asked its peer for everything, which its scope did not
cover, so the peer's floors certified nothing and a clean finish reached readers
through a relay as offline. The subscriber now narrows each ANNOUNCE_REQUEST to
the one region where its limit and subscribe grant meet, and reopens it when
that region changes, keeping the old request until the new one is synced.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t losses go offline

A replacement request takes slots for the claims its predecessor still holds,
so it keeps the predecessor only while the session has room for both, and lets
it go first otherwise. The JS moq-transport subscriber ends a namespace only on
NAMESPACE_DONE or the peer finishing a PUBLISH_NAMESPACE, as Rust does; a
reflected namespace or a request that went away goes offline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A wider replacement can need more slots than its predecessor held, so budgeting
a duplicate of the old set was not enough. The predecessor now gives back its
slots at the handover: the replacement gets the whole cap, and what the old one
keeps stays bounded by the cap it took it under.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing/routes/availability

# Conflicts:
#	dart/moq_ffi/lib/src/moq.dart
#	rs/moq-c/src/api.rs
#	rs/moq-c/src/origin.rs
#	rs/moq-c/src/test.rs
#	rs/moq-ffi/src/origin.rs
#	rs/moq-net/src/lite/subscriber/routed.rs
During a scoped lite-07 subscriber's ANNOUNCE_REQUEST handover, the old
request moves what it holds onto a separate counter with the session's
cap, so the session's count belongs to the replacement alone. A peer can
hold at most twice the cap, during one handover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review November 10, 2026 05:10
@coderabbitai

coderabbitai Bot commented Nov 10, 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 6 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: 19941380-0847-4993-8d9c-f6e1dd0c36d7
Commits

Reviewing files that changed from the base of the PR and between ac3cb8c and c8902ab.

Files selected for processing: 107

  • dart/moq/lib/src/aliases.dart
  • dart/moq/lib/src/client.dart
  • dart/moq_ffi/lib/src/moq.dart
  • doc/concept/identity.md
  • doc/lib/go/index.md
  • doc/lib/kt/index.md
  • doc/lib/py/index.md
  • doc/lib/rs/moq-net.md
  • drafts/draft-lcurley-moq-lite.md
  • go/wrapper/README.md
  • go/wrapper/origin.go
  • go/wrapper/types.go
  • js/moq-boy/src/element.tsx
  • js/net/api/index.api.md
  • js/net/examples/discovery.ts
  • js/net/src/announced.test-util.ts
  • js/net/src/announced.test.ts
  • js/net/src/announced.ts
  • js/net/src/connection/close.test.ts
  • js/net/src/connection/forward.test.ts
  • js/net/src/connection/forward.ts
  • js/net/src/connection/reload.test.ts
  • js/net/src/connection/reload.ts
  • js/net/src/hop.ts
  • js/net/src/ietf/peer-violations.test.ts
  • js/net/src/ietf/subscriber.test.ts
  • js/net/src/ietf/subscriber.ts
  • js/net/src/internal.ts
  • js/net/src/origin.test.ts
  • js/net/src/origin.ts
  • js/net/src/wire.ts
  • js/room/src/room.test.ts
  • js/room/src/room.ts
  • js/watch/src/broadcast.ts
  • js/watch/src/echo.test.ts
  • kt/README.md
  • kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Aliases.kt
  • kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Moq.kt
  • py/moq-rs/README.md
  • py/moq-rs/examples/announced.py
  • py/moq-rs/moq/__init__.py
  • py/moq-rs/moq/origin.py
  • quest/m2/cluster-routing/routes/README.md
  • quest/m2/cluster-routing/routes/availability.md
  • quest/m2/cluster-routing/routes/js.md
  • rs/hang/examples/subscribe.rs
  • rs/moq-bench/src/connection.rs
  • rs/moq-c/src/api.rs
  • rs/moq-c/src/origin.rs
  • rs/moq-c/src/test.rs
  • rs/moq-cli/src/announced.rs
  • rs/moq-cli/src/archive.rs
  • rs/moq-cli/src/report.rs
  • rs/moq-cli/tests/integration/instances.rs
  • rs/moq-cli/tests/integration/rtsp.rs
  • rs/moq-ffi/src/origin.rs
  • rs/moq-gst/src/source/imp.rs
  • rs/moq-net/benches/announce.rs
  • rs/moq-net/src/auth.rs
  • rs/moq-net/src/ietf/publisher.rs
  • rs/moq-net/src/ietf/subscriber.rs
  • rs/moq-net/src/lite/publisher.rs
  • rs/moq-net/src/lite/replay.rs
  • rs/moq-net/src/lite/subscriber.rs
  • rs/moq-net/src/lite/subscriber/routed.rs
  • rs/moq-net/src/model/announced.rs
  • rs/moq-net/src/model/follow.rs
  • rs/moq-net/src/model/origin.rs
  • rs/moq-net/src/route/node.rs
  • rs/moq-net/src/session.rs
  • rs/moq-net/tests/integration/auth.rs
  • rs/moq-net/tests/integration/claim_continues.rs
  • rs/moq-net/tests/integration/dial_split_horizon.rs
  • rs/moq-net/tests/integration/extensions.rs
  • rs/moq-net/tests/integration/instance.rs
  • rs/moq-net/tests/integration/lazy_discovery.rs
  • rs/moq-net/tests/integration/mount.rs
  • rs/moq-net/tests/integration/multi_cdn.rs
  • rs/moq-net/tests/integration/session_panic.rs
  • rs/moq-net/tests/integration/support/mod.rs
  • rs/moq-net/tests/integration/synced.rs
  • rs/moq-relay/src/cluster.rs
  • rs/moq-relay/src/internal.rs
  • rs/moq-relay/tests/drills.rs
  • rs/moq-relay/tests/integration/auth_lifetime.rs
  • rs/moq-relay/tests/integration/cluster_idle.rs
  • rs/moq-relay/tests/integration/cluster_unknown.rs
  • rs/moq-relay/tests/integration/connection.rs
  • rs/moq-relay/tests/integration/embed.rs
  • rs/moq-relay/tests/integration/memory.rs
  • rs/moq-relay/tests/integration/runtime_uring.rs
  • rs/moq-relay/tests/integration/runtime_workers.rs
  • rs/moq-relay/tests/integration/smoke.rs
  • rs/moq-relay/tests/integration/tenant_matrix.rs
  • rs/moq-room/src/room.rs
  • rs/moq-stats/src/aggregate.rs
  • rs/moq-stats/src/consume.rs
  • rs/moq-stats/src/produce.rs
  • rs/moq-tokio/examples/clock.rs
  • rs/moq-tokio/src/origin.rs
  • rs/moq-tokio/src/server.rs
  • rs/moq-tokio/tests/integration/backend.rs
  • rs/moq-tokio/tests/integration/broadcast.rs
  • rs/moq-webrtc/src/lib.rs
  • swift/Sources/Moq/Aliases.swift
  • swift/Sources/Moq/Origin.swift
  • test/drill/mutations/relay-withdraws-lost-publisher.patch
  • 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

@kixelated
kixelated added this pull request to the merge queue Nov 10, 2026

@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 head: c8902ab87491f923f442dbcb0c341447f4ac7c57

Three findings:

  1. [P2] Share handover credit across the session’s predecessors — rs/moq-net/src/lite/subscriber/routed.rs:311–316. Each hand_over() creates an independent full-cap counter. Discovery intentionally retains overlapping requests: watch a, then root, then change the permit region to a/b. Both requests reopen and retain predecessors. A peer can withhold replacement SYNCED and fill both old counters to C while replacements share another C, retaining 3C slots rather than the intended 2C session bound; more overlapping prefixes multiply it further. Preserve separate handover credit, but share one capped predecessor budget across the session. Add an overlapping-request regression, rather than testing one predecessor alone.

  2. [P2] Preserve Offline when a scoped cursor falls back to a covering route — rs/moq-net/src/model/origin.rs:1753–1757. With a scoped, covering cursor rooted at room/alice, an exact forwarded route can have Some([]) captures while the broader room fallback has None. Losing the exact route’s session correctly passes Gone::Offline into the update, but this capture-change branch replaces it with Gone::End. Readers receive End/Absent even though the publisher never withdrew. Preserve the actual removal reason when replacing the captured identity; extend capture_change_retracts_before_reannouncing_a_presented_prefix with a lost forwarded route.

  3. [P2] Classify scoped JS teardown through the same terminal transition — js/net/src/origin.ts:1695–1698. When a session receives routes through a scoped Producer, closing that scoped handle while keeping the parent origin open invokes the raw callbacks registered by #hold, bypassing close(lost). Received entries retain lost = false, so the parent announcement reader emits end/public absent instead of offline. Later route closure cannot repair this because ended is already true. Put the reason in the shared release transition: received-route teardown defaults to offline, while explicit wire end selects withdrawal. Cover scoped-handle closure in a regression test.

Direction: keep the agreed narrowed-request handover, but enforce its bound at session scope and preserve terminal provenance across every release path.

Verification: GitHub-only static review of the changes, relevant callers, bindings, and tests. No builds, tests, or interoperability runs were executed.

(written by unknown OpenAI model)

Merged via the queue into main with commit f8078c3 Nov 10, 2026
12 of 14 checks passed
@kixelated
kixelated deleted the quest/m2/cluster-routing/routes/availability branch November 10, 2026 05:34
@pullfrog

pullfrog Bot commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

This run was cancelled before it finished.

Pullfrog  | View workflow run | via Pullfrog

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