diff --git a/quest/m0/README.md b/quest/m0/README.md index fcfeb086b2..124aaf1494 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -25,6 +25,9 @@ Routing: Wildcard landed in #4403, so a service claims the prefix it could serve instead of enumerating broadcasts. Serving the relay's ingested-only view (`origin::Consumer::local()`) to localhost workers belongs to moq.pro's edge, which embeds moq-relay; it moved there on 2026-09-28. +Pools of claim workers (transcoders) also need the relay to forget a front +nobody reads, and demand that doesn't flap on a viewer's first subscribe +(both found 2026-10-07). Interop: Fastly's moq-relay-interop report (run of 2026-09-23, build 7ee2b02) was triaged against `main` on 2026-10-07. Its SETUP, UNSUBSCRIBE @@ -60,6 +63,9 @@ a published `@moq/watch` break. - [Subgroup at object 0](/quest/m0/ietf-first-object-zero.md) - a draft-18 subgroup with FIRST_OBJECT clear that starts at object 0 is read whole - [Request caps](/quest/m0/request-caps.md) - lite message sizes, IETF request IDs, and per-session announces and subscriptions are bounded - [Prefix route fronts](/quest/m0/prefix-route-fronts.md) - a prefix route cannot be made to mint one front per requested path +- [TRACK stream demand](/quest/m0/track-stream-demand.md) - a lite-05+ subscribe shows the publisher one `used` edge, not a flap between TRACK and SUBSCRIBE +- [Idle fronts](/quest/m0/idle-fronts.md) - a front nobody has read for the linger ends with its per-path state, so a standing claim stops accumulating fronts and sources +- [Upstream position regression](/quest/m0/largest-regression.md) - a relay copy that sees upstream's largest group go backwards ends instead of serving the old instance's cache - [qmux credit](/quest/m0/qmux-credit.md) - qmux returns connection credit for dropped and stopped streams and delivers its close frame, on both lines - [web-transport releases the qmux fixes](/quest/m0/qmux-credit-upstream.md) - waiting on moq-dev/web-transport#412 and #413 to merge and ship, which qmux credit bumps to - [Broadcast epochs](/quest/m0/broadcast-epoch/README.md) - every first-party publisher that can restart mints a fresh route epoch, the newest wins a path, and only routes with the same epoch resume a subscription diff --git a/quest/m0/broadcast-epoch/README.md b/quest/m0/broadcast-epoch/README.md index 1cdf653a59..6c3c6544a8 100644 --- a/quest/m0/broadcast-epoch/README.md +++ b/quest/m0/broadcast-epoch/README.md @@ -54,6 +54,9 @@ Decided: explicit `Restart` announce event tells players to follow, through [Restart](/quest/m0/broadcast-epoch/restart.md). When the newest goes and an older one is still live, the older one wins again as a new broadcast. +- On lite-07 a claim's answer will carry the served broadcast's own epoch + ([Claim-served epochs](/quest/m0/broadcast-epoch/claim-epochs.md), planned + 2026-10-07), which adds it to TRACK_INFO. - A catalog `broadcast` reference by name follows the newest epoch, since a path cannot name one. - Every first-party publisher that can restart mints its own: the apps, @@ -95,4 +98,5 @@ then. MoQ Pro's VOD `storage.json` moves from the same seed to its own epoch. - [Publish catalog restart](/quest/m0/broadcast-epoch/publish-catalog-restart.md) - `@moq/publish` never reuses catalog group numbers under one name and epoch after a re-announce - [TS restart](/quest/m0/broadcast-epoch/ts-restart.md) - a signalled backward TS discontinuity finishes the broadcast and continues the same input under a fresh epoch - [Bindings](/quest/m0/broadcast-epoch/bindings.md) - moq-ffi and every wrapper expose the epoch and let a publisher announce one +- [Claim-served epochs](/quest/m0/broadcast-epoch/claim-epochs.md) - a claim's answer names the instance that served it, so a restarted output is never spliced and a per-output epoch costs no restart - [Stats totals and prefix tracks](/quest/m0/broadcast-epoch/stats-split.md) - the same release retires the per-path stats maps for totals and on-demand prefix tracks (decided 2026-10-05) diff --git a/quest/m0/broadcast-epoch/claim-epochs.md b/quest/m0/broadcast-epoch/claim-epochs.md new file mode 100644 index 0000000000..ffe32fdf1a --- /dev/null +++ b/quest/m0/broadcast-epoch/claim-epochs.md @@ -0,0 +1,94 @@ +# [L] A claim's answer carries its broadcast's epoch + +## Goal + +A broadcast a worker serves in answer to a prefix claim carries its own +epoch, and a relay learns it from the answer. Two things follow for a pool +of claim workers (transcoders) behind relays: + +- A worker that closes an output and serves the path again is a new instance, + so a relay never splices the closed instance's cached groups into it. +- A per-output epoch costs no cut on the first view. + +Today a claim names no instance, a relay's session answers a request under a +claim with a placeholder source before anything goes upstream, and no answer +on the wire names an instance, so the relay's front always resolves without +an epoch. A worker that announces its output's exact path with a fresh epoch +to get one restarts the first viewer: the front resolved through the claim, +and the epoch route wins the path and is announced as a `Restart` +([Restart](/quest/m0/broadcast-epoch/restart.md)). A front on a drained +worker no longer needs this: under Restart a request joins a front only while +its route still wins, on every version (re-scoped 2026-10-07). lite-07 only; older versions +and moq-transport keep today's behavior. + +## Plan + +Decisions (2026-10-07, proposed for the maintainer): + +- TRACK_INFO gains an `Epoch`: the instance that answered, or empty. Each hop + answers with the epoch of the front it served from, so it carries through + relay chains. SUBSCRIBE_OK doesn't need it: the relay fills the learned + epoch into later TRACK, SUBSCRIBE and FETCH, and the publisher's existing + refusal of another epoch catches a restart. +- The epoch lives on the broadcast, since a broadcast is an instance, and + `Request::accept` reads it from the accepted broadcast rather than taking + it as an argument. +- A front adopts the epoch it learns. Routes without an epoch never splice + (maintainer, 2026-10-08, in [Restart](/quest/m0/broadcast-epoch/restart.md)): + a route change, including a per-path winner change under a prefix pool, + reaches downstream as a `Restart` of the route, which unsets the cached + copy of every broadcast nested under it. What that + can't see is a worker restarting an output under the same claim route; + there the learned epoch is what a downstream copy names on re-subscribe, + and a mismatch is refused. +- A refusal of a front's learned epoch means that instance is gone: the front + ends, and a request that joined it re-resolves onto a fresh front (a new + TRACK_INFO exchange) instead of failing. Only readers the old instance was + serving see a cut. +- A front someone reads stays on its instance even when a cheaper route + appears, as Restart's sticky subscriptions already do: moving it would + start a second instance at the path. A worker that wants its viewers off + closes its outputs, and they re-request. +- No dedicated "broadcast closed" message: with the epoch, the next request + re-resolves anyway, and an exact announcement already gets ANNOUNCE_END. +- The draft's "the Epoch of the route it resolved" and "the route it would + serve from has another Epoch" become broadcast-scoped; as written, a + request naming an epoch is refused while the best route is the claim, even + though that instance is serving. Update the concept doc's epoch section and + this line's README to match. +- A worker using this should not also announce its output's exact path with + the epoch: that announcement and TRACK_INFO race on separate streams, and + the announcement still wins new requests over the claim front and is + announced as a `Restart`. +- A worker drains feed by feed by closing the outputs it wants off: their + readers re-request and land on the cheapest worker as a new instance, one + short cut per feed instead of withdrawing the claim and cutting everything. +- Every link from the worker to the relays that serve viewers must speak + lite-07, since the epoch is learned and named hop by hop. A viewer's own + version should not matter, because the join rule and the refusal live at + relays and workers; confirm it, including that `@moq/net`'s track cache + never hands a returning viewer the old instance's group. + +This is a lite-07 wire change, so it lands before the version is cut. + +Verification: a relay integration test with two claim workers (mocked time). +A worker restarting an output within the linger delivers the new instance from +its first group, with nothing stale. A worker answering with a per-output +epoch serves its first viewer without a cut. A worker closing a path while +it is read sends the re-request to the cheaper worker as a new instance. A +request joining an unread front whose worker restarted its output reaches the +new instance without an error. A lite-06 or IETF viewer behind a lite-07 +worker-to-relay path, with the old instance's groups still cached, receives +only the new instance's groups after the worker restarts the output with a +new epoch. +`just test interop --all`. + +Public API: Rust and JS, the broadcast's epoch and what `accept` does with +it. Wire: lite-07 TRACK_INFO gains `Epoch`. + +## Related + +- [Restart](/quest/m0/broadcast-epoch/restart.md) - the join rule and sticky subscriptions this builds on +- [Idle fronts](/quest/m0/idle-fronts.md) - ends an unread front and its per-path state after the linger on every version +- [Upstream position regression](/quest/m0/largest-regression.md) - catches a restart on versions without this field +- [Finalize moq-lite-07](/quest/m1/lite07-finalize.md) - waits on this wire change diff --git a/quest/m0/broadcast-epoch/restart.md b/quest/m0/broadcast-epoch/restart.md index 846b89d634..bf7ae6bc45 100644 --- a/quest/m0/broadcast-epoch/restart.md +++ b/quest/m0/broadcast-epoch/restart.md @@ -25,6 +25,24 @@ decide. A copy resumes or splices only across routes with an identical epoch, so the old copy's cached groups never reach a subscriber of the new source. +Relays keep caching between subscriptions: a `Restart` is the cache +invalidation. It unsets the relay's copy for that path, so the next +subscription goes upstream to the new route (maintainer, 2026-10-08: "a +Restart unsets the cache; the next subscription goes upstream to the new +route"). + +`Restart` is route-level only, on every version, lite-07 included +(maintainer, 2026-10-08: "Restart is route-level only; nested broadcasts +under the longest-matching route restart"). A `Restart` names a route, the +announced path or prefix, as ANNOUNCE does. A subscriber applies it to every +broadcast whose longest matching route is that one: it unsets each cached +copy, and the next subscribe re-resolves upstream. Under a prefix pool the +advertised winner is ranked by the prefix while requests are ranked by the +requested path, so a path can move to another worker while the prefix's +winner stays. The relay sends that as a `Restart` of the prefix route (such +as `pool`), and every nested broadcast under it re-resolves; a viewer whose +path kept its worker just resubscribes to the same source. + ## Plan Decided in planning (2026-10-07, from #4970's review): @@ -88,7 +106,8 @@ Decided in planning (2026-10-07, from #4970's review): relay retires that copy for new requests and opens a fresh one, never joining or resuming into the old copy. - **Wire.** lite-07 (`moq-lite-07-wip`) gains an explicit restart message on - the announce stream. Older lite versions and moq-transport send + the announce stream, naming a route like ANNOUNCE does, never a nested + path. Older lite versions and moq-transport send `ANNOUNCE_END` then `ANNOUNCE_START` for a source change, a new rule for a relay's winner moving between entries (the draft only says so for a publisher replacing its own epoch), and a receiver delivers an end and @@ -140,8 +159,12 @@ old route: no `Restart` and new requests stay on the old route until it is withdrawn, then `Restart` and a re-request lands on the replacement. With mocked time, a lite-06 relay chain where an epochless publisher restarts: the downstream relay's cached groups from the old instance never reach a new -subscriber mixed with the new instance's groups. Run `just drafts check` and -`just test interop --all`. +subscriber mixed with the new instance's groups. With mocked time, on lite-06 +and lite-07, a prefix pool of epoch-less workers announced as `pool` behind +two relays, where one path's winner changes under an unchanged prefix winner: +the downstream relay gets a `Restart` of `pool`, drops its cached copies of +the nested paths, and the next subscribes reach the right workers, the moved +path on its new worker and the rest on theirs. Run `just drafts check` and `just test interop --all`. Docs: update `doc/concept/moq-lite.md` (publisher epochs) and `doc/lib/{rs,js}` announce sections inline, plus `doc/bin/rtmp.md`, diff --git a/quest/m0/idle-fronts.md b/quest/m0/idle-fronts.md new file mode 100644 index 0000000000..2c6d8d96d4 --- /dev/null +++ b/quest/m0/idle-fronts.md @@ -0,0 +1,101 @@ +# [M] An unread front ends after its linger + +## Goal + +A relay ends an origin front once nothing has read it for `track::IDLE_LINGER`, +and the per-path state it holds goes with it, so a standing prefix claim +doesn't keep a front, its driver task, and a session placeholder source for +every path ever requested under it. Today a front lives until its route +leaves. Done when, after the linger, the relay holds no front for an unread +path P and the session no source for it. + +Found with a pool of transcode workers claiming one prefix with +`origin::Producer::dynamic`: a drained worker kept serving a path to new +viewers through a relay. That routing half is +[Restart](/quest/m0/broadcast-epoch/restart.md)'s (re-scoped 2026-10-07): a +request joins a front only while its route still wins, so a drained claim's +front takes no newcomers. A replaced front still keeps serving its +subscribers until its route goes, so this quest reclaims it once unread. + +Non-goal: a worker that closes an output and serves the same path again +restarting at group 0. That reuses a name for different content, which is the +publisher's bug (see Plan). + +## Plan + +Facts from `main` (`rs/moq-net/src/model/origin.rs`, `front.rs`, +`lite/subscriber.rs`): + +- A front resolved without an epoch stays on its first route (`pick`, #4942). + `request()` joins any live front whose epoch matches the best route's until + [Restart](/quest/m0/broadcast-epoch/restart.md) lands; after it, a front + whose route lost takes no newcomers but stays for its subscribers. +- Under a claim, the session answers a request with a placeholder source it + creates on the spot (`poll_serve`), kept in the announced route's sources + until the claim is withdrawn or the session closes. moq-lite has no message + for "the broadcast served under this prefix closed", so the relay never sees + `SourceClosed` when the worker closes its output. In-process (no relay) the + front's source is the worker's broadcast, `SourceClosed` fires, and the next + request re-resolves; that's why the bug needs a relay. +- Nothing ends a front for having no readers. Its unread tracks park and are + forgotten after `IDLE_LINGER`, but the front and its driver task stay. +- Each distinct path requested under a claim therefore leaves a placeholder + source and serve state on the session, and a front on the relay, for as + long as the claim stands. + +Decisions (2026-10-07): + +- End every front, epoch or not, once all its tracks are forgotten and no + consumer holds its broadcast. Ending only after the linger keeps the cache + window for a returning viewer, needs no wire change, and doesn't conflict + with the draft's Resume text, which pins subscriptions, not idle fronts. + A front serving a local source forgets an unread track outright, so it ends + as soon as it goes unread; reaching a local source again is free. +- A request must never join a front that is ending: the front leaves the + origin's front table under the lock before it ends (or `request()` treats an + ending front as gone), so a newcomer gets a fresh front rather than a + broadcast that closes at once. +- A front still waiting for coverage + ([Front parking](/quest/m1/origin-front-parks.md)) is not idle and stays. +- The session drops a placeholder source once it has no consumers left, so + per-path state under a claim goes with its fronts instead of piling up until + the claim leaves. The route's served cache hands one source to every front + for the path (a plain front and a peer's filtered front can share it), so + the trigger is the source losing its last consumer, not one front ending. + Withdrawing the claim or closing the session still closes the source at + once, as today, and in-flight consumers drain on their own. The session + keeps that state in more than one place; all of it goes. +- This also reclaims the filtered front a peer session leaves behind (moved + here from [Front parking](/quest/m1/origin-front-parks.md)): a hop in a + covering route chain gets its own filtered front, which today lives as long + as the route, one per peer session that both publishes and subscribes or + reconnects with a fresh hop. +- A claim worker that re-serves a path after closing it is a new instance. + Document that in `doc/concept/moq-lite.md` Resume: such a worker keeps its + group sequence going, across its own source restarting too (mirroring an + upstream sequence that goes back to 0 isn't enough), or gives each output its own epoch once + [Claim-served epochs](/quest/m0/broadcast-epoch/claim-epochs.md) lands. + Without that, a viewer returning within the + linger gets the old instance's cached latest group and then nothing until + the new instance's sequence passes it. + +Verification: a relay integration test with two `dynamic` claims on their own +sessions (mocked time): read P, leave, close the output, drain the serving +claim, advance past the linger, and check that the relay holds no front for P +and the first session no source for P. A `front.rs` +unit test for the end condition, including a parked front that must stay, +and an origin test of a request racing a front's end (like `front.rs`'s +`a_reader_racing_the_forget_keeps_the_track`, one level up). The front count +returns to the plain front after a peer session closes. + +Public API: none expected. Wire: none. + +## Related + +- [Restart](/quest/m0/broadcast-epoch/restart.md) - a request joins a front only while its route still wins, the routing half of the same report +- [Claim-served epochs](/quest/m0/broadcast-epoch/claim-epochs.md) - on lite-07, a worker's restarted output is a new instance and never spliced +- [Upstream position regression](/quest/m0/largest-regression.md) - fails loud on the stale splice where the answer shows it +- [Prefix route fronts](/quest/m0/prefix-route-fronts.md) - bounds how many fronts a prefix route can mint at once; this reclaims idle ones +- [Front parking](/quest/m1/origin-front-parks.md) - a front waiting for coverage must survive this; the filtered-front leak moved here from it +- [Route wakes](/quest/m1/route-wakes.md) - indexes fronts per route, so an ended front must drop its entries +- [Broadcast epochs](/quest/m0/broadcast-epoch/README.md) - restarted publishers mint a fresh epoch, the documented fix for a re-served path diff --git a/quest/m0/largest-regression.md b/quest/m0/largest-regression.md new file mode 100644 index 0000000000..73aa85d5d1 --- /dev/null +++ b/quest/m0/largest-regression.md @@ -0,0 +1,47 @@ +# [S] A relay copy fails loud when upstream's position goes backwards + +## Goal + +A relay copy that subscribes upstream again and hears that upstream's largest +group is below the newest group it has cached treats it as a reused name: the +copy ends with an error, its source closes so the front ends, and the readers +it was serving re-request. A new request arriving as the front ends gets a +fresh front without an error. Today the copy keeps its stale live floor (`set_live` in +`rs/moq-net/src/model/track.rs`), so a viewer returning within the linger to a +publisher that restarted at group 0 gets the old instance's cached group and +then nothing until the new sequence passes it. + +Covers lite-07 and moq-transport, whose answers carry the largest position. +lite-05 and 06 can't tell, and keep waiting on the floor. + +## Plan + +Found while scoping [Idle fronts](/quest/m0/idle-fronts.md) (2026-10-07): +under a prefix claim the relay never sees the worker close a broadcast, so a +restart at the same path reaches a lingering copy unannounced. A publisher +that restarts its group sequence under the same name is buggy, and this makes +that bug visible instead of a silent stall. A change of route can't cause +it: every route change, including a per-path winner change under a prefix +pool, reaches downstream as a `Restart` of the route, which unsets the cached +copy of every broadcast nested under it +(maintainer, 2026-10-08, in [Restart](/quest/m0/broadcast-epoch/restart.md)). +Only a restart behind an unchanged route reaches a copy unannounced. + +A lower largest group alone doesn't prove a restart: a same-epoch resume onto +a replica that has fallen behind reports one legitimately, and must keep +waiting as today. The copy ends only when the answer comes from the route it +was last served from without an epoch, where no cross-route resume happens, +or names a different epoch than the copy holds. Check for any other +legitimate case before relying on that split. + +Verification: mocked time, a publisher restarting a path at group 0 within +the linger. The returning reader gets an error and then the new instance from +group 0, never the old group. A same-epoch standby behind the cached copy +keeps the copy and resumes it. + +Public API: none. Wire: none. + +## Related + +- [Claim-served epochs](/quest/m0/broadcast-epoch/claim-epochs.md) - prevents the splice on lite-07 by naming the instance +- [Coalesce dynamic tracks](/quest/m1/2991-net-coalesce-dynamic-tracks-and-preserve-sequences-across.md) - keeps sequences going when a dynamic track's producer is replaced within one broadcast diff --git a/quest/m0/track-stream-demand.md b/quest/m0/track-stream-demand.md new file mode 100644 index 0000000000..cc51eb3558 --- /dev/null +++ b/quest/m0/track-stream-demand.md @@ -0,0 +1,76 @@ +# [M] Demand holds across a subscription's TRACK and SUBSCRIBE + +## Goal + +On moq-lite 05 and later, a publisher sees one `used` edge when a viewer +subscribes, not `used`, `unused`, `used`. Today demand drops for one round +trip per hop between the TRACK request and the SUBSCRIBE that follows it. A +publisher that stops work on `broadcast::Demand::unused`, which is what +`Demand` documents itself for, closes its output on every first viewer and is +asked to create it again a few milliseconds later; a transcoder tears down its +encoders and waits for the next keyframe. It happens under a prefix claim and +for an exact announce alike. lite-04 and moq-transport don't do this. Rust, +JS, and the lite draft. + +## Plan + +Facts from `main`: + +- The publisher answers TRACK with a track `query()`, which counts as demand + on the track and its broadcast, and drops it once TRACK_INFO is written + (`lite/publisher.rs`, `TrackInfoServe`). At the publisher's front it + arrives as an `Asked` with no subscription to hold; the `Asked::sub` + comment covers moq-transport's case, not this one. +- The subscriber sends SUBSCRIBE only after reading TRACK_INFO. At a relay, + the upstream SUBSCRIBE waits for the downstream reader's own SUBSCRIBE, so + the gap grows by a round trip per hop. JS subscribers also await TRACK_INFO + before SUBSCRIBE. +- A relay's own copy flaps too, but `track::IDLE_LINGER` hides that from + everything downstream. Only an end publisher sees the raw edge. + +Decisions (2026-10-07): + +- An open TRACK stream counts as interest. The publisher keeps its query + until the requester FINs or resets the TRACK stream. The subscriber FINs it + once its SUBSCRIBE stream has its first response (the start, a group, an + end, or a reset), or once its own demand goes (an info-only query). The hold + chains across hops, so demand stays continuous end to end. It is backward + compatible: an older subscriber FINs at once, as today. +- Holding only until SUBSCRIBE is sent isn't enough: QUIC doesn't order the + two streams, and a relay that handles the TRACK FIN before the SUBSCRIBE + parks the track and drops its copy at once, letting go upstream. Waiting for + the first response costs one stream held for about a round trip. +- A held TRACK stream is demand without a subscription, so it counts against + the per-session subscription cap from + [Request caps](/quest/m0/request-caps.md); a peer can't hold more interest + than it could by subscribing. A held TRACK and the SUBSCRIBE for the same + track on the same session share one slot: the SUBSCRIBE takes over the + TRACK's reservation instead of needing a second, so a session at the cap + can still turn its held TRACKs into subscriptions (decided 2026-10-08 from + review). +- Not pipelining TRACK and SUBSCRIBE, which the draft already allows: it + still races, and every hop would have to buffer frames until TRACK_INFO. +- Consumers never debounce demand; the docs promise clean edges. +- Scope: Rust and JS, plus one sentence in the draft's Track Stream section. + Run `just test interop --all`. + +Verification: a `moq-net` integration test on the simulated network (10 ms +latency) asserting exactly one `used` edge and no `unused` while the reader +stays subscribed, on lite-05, 06, and 07, direct and through one relay. It +fails on `main` today. The relay case controls the ordering so the +downstream TRACK FIN is handled before its SUBSCRIBE. A boundary test fills +the per-session subscription cap with held TRACK streams, turns each into a +live SUBSCRIBE with no `unused` edge, and checks that one more TRACK is +refused. JS counterparts for the JS side. + +Public API: none. Wire: semantics only (holding the TRACK stream open), no new +fields. + +## Required + +- [Request caps](/quest/m0/request-caps.md) - the per-session subscription cap a held TRACK stream counts against + +## Related + +- [Idle fronts](/quest/m0/idle-fronts.md) - found in the same transcode-pool report +- [#4225](https://github.com/moq-dev/moq/pull/4225) - holds a lite subscription's demand in the same publisher code; expect a conflict diff --git a/quest/m1/js-session-parity.md b/quest/m1/js-session-parity.md index 6685eb0ea2..0b34db3a07 100644 --- a/quest/m1/js-session-parity.md +++ b/quest/m1/js-session-parity.md @@ -30,4 +30,3 @@ Test each against the Rust behavior, with mocked time. ## Related - [JS request window](/quest/m1/js-request-window.md) - JS grants request IDs as requests close, the other half of JS request limits -- [Stats epochs](/quest/m0/broadcast-epoch/stats-epoch.md) - #4904, which the resolved-epoch check follows diff --git a/quest/m1/lite07-finalize.md b/quest/m1/lite07-finalize.md index 8ea5082f90..162fe04964 100644 --- a/quest/m1/lite07-finalize.md +++ b/quest/m1/lite07-finalize.md @@ -74,6 +74,7 @@ is published; older versions are unchanged. - [Live media time](/quest/m1/subscribe-live-time.md) - SUBSCRIBE_OK carries the publisher's live media time - [Untimed lite-07](/quest/m1/lite-untimed.md) - an untimed track crosses the wire untimed - [One route cost](/quest/m1/route-cost.md) - ANNOUNCE carries one cost +- [Claim-served epochs](/quest/m0/broadcast-epoch/claim-epochs.md) - TRACK_INFO carries the epoch of the instance that answered ## Related diff --git a/quest/m1/origin-front-parks.md b/quest/m1/origin-front-parks.md index 57161e3e31..f7266e7747 100644 --- a/quest/m1/origin-front-parks.md +++ b/quest/m1/origin-front-parks.md @@ -25,16 +25,9 @@ benchmark swept over requesters and route-table churn would show whether the re-mint is a real slope or noise, and it is the same benchmark that would show the replacement is cheaper. -Also reclaim the filtered front a peer session leaves behind (folded in -2026-10-05). #4922 keyed fronts by `Horizon::effective`, so viewers share the -plain front, but a hop that appears in a covering route chain still gets its -own filtered front, and that front lives as long as the route. A peer that -both publishes and subscribes, or reconnects with a fresh hop, leaves one -behind per peer session. #4922 scoped it out because it scales with peer -sessions, not viewers. Test that the front count returns to the plain front -after the peer session closes. If this quest moves to m2, split this leftover -into its own m1 quest: it is a leak, not a question of whether the retry loop -costs anything. +The filtered front a peer session leaves behind, folded in here on +2026-10-05, moved to [Idle fronts](/quest/m0/idle-fronts.md) on 2026-10-07: +it ends once unread like any other front. Public API: no signature change expected. `routed_broadcast` and `request_broadcast` keep their contracts; only where the waiting happens diff --git a/quest/m1/route-wakes.md b/quest/m1/route-wakes.md index b573b29c11..1a2ac40d3d 100644 --- a/quest/m1/route-wakes.md +++ b/quest/m1/route-wakes.md @@ -49,11 +49,19 @@ Decisions: them when it closes. The lookup walks descendants but skips subtrees whose waiting set is empty, keeping cost in woken fronts rather than served paths. -- A serving front follows the best route, per the wildcard line (decided - 2026-10-04, replacing Stay). A join or re-price must rehash every front - below the prefix, since rendezvous moves exactly the paths the changed - route now wins; only those fronts re-select. A leave wakes only the fronts - the leaver served or was requesting through. +- A serving front follows the best route among those it may resume across + (decided 2026-10-04, replacing Stay; narrowed 2026-10-07 after #4942 and + [Restart](/quest/m0/broadcast-epoch/restart.md)): routes with its epoch. A + front resolved without one, or replaced by a newer epoch, stays on its route + for its subscribers until that route goes and never splices onto another + (maintainer, 2026-10-08); new requests take a fresh front on the winner + instead of waking it. An idle front (every track + forgotten, no consumer holding its broadcast, not waiting for coverage) + ends instead of moving + ([Idle fronts](/quest/m0/idle-fronts.md)). A join or re-price must rehash + every epoch front below the prefix, since rendezvous moves exactly the paths + the changed route now wins; only those fronts re-select. A leave wakes only + the fronts the leaver served or was requesting through. - Builds on fronts keyed by effective exclusion, so the index hangs off the final front identity. `origin-front-parks.md` replaces the `routed_broadcast` retry loop, one of the watch consumers here; whichever lands second adapts it.