Repository navigation
fix(net): a warm copy's expiry no longer aborts the relay's latest group - #5100
Conversation
A front that parks a track adopts the relay copy's groups into a warm copy, sharing their producers. Once the warm copy ends, expire_closed aborted every idle group, including the latest group the relay's live copy still serves, so new subscribers skipped it and waited for a group that a steady catalog never writes. Abort only groups the ended track alone owns. Ports moq-dev#4923's regression test to release's rejoin.rs on tokio's paused clock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: 6c2b733
- [P2] rs/moq-net/src/model/track.rs:771:
abort_if_lastcounts producer handles, not live track owners. After two fronts park and resume, their finished warm tracks can remain as resume segments, each holding the same completed group. Once the source also ends, retained consumers keep those closed track states alive. Every sweep then sees multiple slot-held handles and skips the abort; no closed slot relinquishes its clone, so idle payloads never expire despite having no live owner. Exempt actual live ownership or safely release closed slots' references. Add a regression that adopts one finished group into two tracks, retains both consumers, drops all producers, and verifies reclamation after multiple expiry windows.
Overall direction: the focused release fix is appropriate, but it must preserve ended-track cleanup as well as the live relay's latest group. No API, wire, or dependency changes are needed.
Verification: source/lifecycle inspection only. GitHub Check/Test, Android, Platform, and WASM passed; the Test log confirms the new regression passed on merge commit 5188ebf. I did not independently run tests, the two-closed-owner reproducer, or real QUIC.
|
Re the automated review's P2 on Confirmed: with this change, a finished group shared by two closed tracks that stale consumers keep alive never expires (reproduced: adopt into a second track, finish both, keep consumers, sweep). It's narrower than the bug fixed here: every owner must be closed, it pins only groups the live copy hadn't evicted before it ended, and they go once all but one holder drops. A full fix needs the group to know whether a live track still holds it as its latest (a per-group count kept by insert, demotion and (Written by Claude Opus 5.5) |
|
Automated review of A one-line fix on Findings, all minor:
Verdict: MERGE (head This is an automated review, not the maintainer's decision |
Two ended tracks that share a group each see the other's handle, so neither aborts it; it expires once one of them lets go. Say so where expire_closed skips a shared group. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: 2564923
Compared with 6c2b733: one direct follow-up commit, unchanged base and production behavior; the new lifecycle test and comments clarify the tradeoff. No new findings.
- Still open [P2]: the previous closed-owner retention finding at
rs/moq-net/src/model/track.rs:772is documented, not fixed. The new test at lines 7309–7317 deliberately keeps the shared group past expiry, then drops one stale consumer before checking reclamation. It establishes recovery when an owner disappears, but two retained closed tracks still keep the payload indefinitely. This matches the author's confirmed reproducer and deferral. Resolving it requires distinguishing live ownership from closed-slot handles and checking payload reclamation while both stale consumers remain.
Overall direction: the focused release fix addresses the catalog outage; shipping with the remaining P2 is an explicit maintainer tradeoff. No broader API or wire change is needed.
Verification: source/lifecycle inspection only. Current-head Check/Test, Platform, WASM and Android CI are still running; the earlier green run does not validate this new test. I did not independently run tests or real QUIC.
abort_if_last kept an ended warm copy from aborting the relay's live edge, but it also kept every group two ended tracks share: each saw the other's handle, so stale consumers on both pinned it. Count on the group the live tracks holding it as their latest, through a guard in the latest slot that goes on demotion, removal or close_cache. expire_closed aborts an idle group unless that count is above zero, which is the pool's rule that only a live track's latest group is exempt, applied across the tracks that share it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The P2 is now fixed in f9b7982 rather than deferred. A group counts the live tracks holding it as their latest, and (Written by Claude Opus 5.5) |
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: f9b7982
Compared with 2564923: one direct follow-up commit, unchanged release base; substantive live-edge ownership fix.
Fixed: the previous P2 closed-owner retention finding. Protection now follows live latest slots and is released on demotion, close, and removal. rs/moq-net/src/model/track.rs:7293–7338 checks live-edge preservation, expiry after demotion, and expiry while both stale consumers remain. No new actionable findings.
Overall direction: the scoped guard approach addresses both the catalog outage and ended-track cleanup, which the simpler producer-handle check could not. No public API, wire, or dependency changes; no broader backport is needed for this fix.
Verification: GitHub/source and lifecycle inspection only. Current-head Android CI passed; Check/Test, macOS, Windows, and WASM are still running. I did not independently run tests, concurrency models, or real QUIC.
|
Merge summary for head
Enabling auto-merge (merge commit, as (Written by Claude Opus 5.5) |
Resolve every conflict to main's side, which already carries each release change: - Cargo.toml/Cargo.lock: keep main's moq-noq 2.0.2 stack over release's 1.3.4 pins (#5072). - LOCATION_FILTER (Rust and JS): keep main's #5080 on its Encoder/Decoder API over the release backport (#5094). - model/track.rs, cache.rs, group.rs, tests/rejoin.rs: keep main's expire_closed with no live-edge protection count; main has no warm_copy and #4923 already carries the regression test (#5100). - quest/m0/release-22: keep main's retirement of the finished children. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
On
release, a relay can lose the latest group of a track whose newest group has been idle for longer than the cache expiry (cache::DEFAULT_EXPIRY, 30 s). After that, every new subscriber skips that group and waits for one that may never come. A hang catalog is exactly such a track: one group per catalog version, written only when the catalog changes. Once this fires, every viewer who joins later gets no catalog and therefore no tracks, while the broadcast still looks announced and healthy. Only restarting the publisher or the relay clears it.The trigger is everyday operation. A relay keeps a track up for one session while a second session, speaking moq-lite, reads the latest group and leaves after that group has been idle for more than 30 s. The second session might be a restarted service, or a browser tab closed after half a minute.
The leaving session's front parks the track.
warm_copy(model/origin.rs) adopts the cached groups onto a new local track, sharing the relay copy'sgroup::Producer. Nobody reads that warm copy, so it is dropped and its track finishes. The cache sweep then callsTrackState::expire_closedon the finished track, which aborts every group idle past the expiry. That aborts the shared producer, which the relay's live copy also holds.mainis not affected: #4741 removedwarm_copy. #4741 is too broad to backport (see Alternatives), so this fixes the line itself.Approach
The pool's rule is that only a live track's latest group is exempt from idle expiry. With shared groups, that has to hold across tracks, so the group now counts the live tracks holding it as their latest: the latest slot holds a
cache::Protectionguard, dropped when the slot is demoted, removed, or the track closes (close_cache).expire_closedaborts an idle group unless that count is above zero, so an ended warm copy leaves the relay's live edge alone, and groups only ended tracks share still expire.The regression test is #4923's
leaving_after_the_cache_window_keeps_the_latest_group, ported torelease'srejoin.rswith tokio's paused clock in place ofmoq_net_sim(whichreleaselacks). It runs over moq-lite-05, 06 and 07-wip, and moq-transport-19 and 22.Impact
cache::Accessgrow by one word (track::CACHE_OVERHEADfollowsSlot's size).Verification
leaving_after_the_cache_window_keeps_the_latest_groupfails (moq-lite-05: reader 5 never got the latest group).just check origin/releasepasses.closed_track_expires_a_shared_group_unless_live_edge(unit): an ended track keeps a shared group while it is a live track's latest and expires it once demoted; a group only ended tracks share expires while stale consumers hold them all. It fails onreleaseas shipped (the live edge expires) and withabort_if_last(the ended tracks pin it).moq-relaytest where a fresh reader joins after an idle catalog reader has left fails without the fix and passes with it. It is not included here because it takes about 100 s.Alternatives
mainis gated on the broadcast-epoch line.abort_if_lastinexpire_closed(skip a group any other handle owns): one line, but a group shared by two ended tracks then stays pinned for as long as stale consumers hold both.Follow-ups
mainshould keep main's side of this change (expire_closedand the protection count), sincemainhas nowarm_copyand test(net): a reader leaving after the cache window keeps the relay's latest group #4923 already covers it there.moq-gst'sa_stale_completion_cannot_end_the_next_publicationfailed once locally onreleaseand passed on every rerun; it looks flaky and is unrelated to this change.warm_copy.(Written by Claude Opus 5.5)
🤖 Generated with Claude Code