Repository navigation
fix(net): a lite-06 viewer joining a relay reads its first group again #8563
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,36 +1,19 @@ | ||
| # [M] A lite-06 viewer joining a relay reads its first group again | ||
| # [XS] A scheduled Nightly's relay session job finishes inside its hour | ||
|
|
||
| ## Goal | ||
|
|
||
| `relay_session_join_broadcasts` and `relay_session_join_stale` finish their | ||
| moq-lite-06 cells, as they did before | ||
| [moq-dev/moq#8476](https://github.com/moq-dev/moq/pull/8476), so | ||
| `Nightly (Relay session benchmarks)` (`just gh nightly rust-relay`) finishes | ||
| inside its hour with every cell. | ||
| inside its hour with every cell, including the moq-lite-06 cells of | ||
| `relay_session_join_broadcasts` and `relay_session_join_stale`. | ||
|
|
||
| ## Plan | ||
|
|
||
| Found 2026-11-12 while timing the relay session group for | ||
| [moq-dev/moq#8555](https://github.com/moq-dev/moq/pull/8555). The first | ||
| moq-lite-06 cell of `relay_session_join_broadcasts` | ||
| (`relays=1/publishers=1/broadcasts=1/viewers=16`) and of | ||
| `relay_session_join_stale` never leaves its warm-up: the bench thread parks in | ||
| the cell's `join` loop (`rs/moq-net/benches/session/mod.rs`) at 0% CPU, | ||
| waiting on a join, watch, or the first group read that never completes. The | ||
| moq-transport and moq-lite-07 cells of the same groups finish in seconds, and | ||
| `relay_session_join_relays` finishes on every version. | ||
| The lite-06 join hang that kept those cells in their warm-up is fixed: a final | ||
| drop no longer hides a group the relay holds whole. Locally, every | ||
| `relay_session_join_*` cell finishes, the first lite-06 one in about 0.2 ms per | ||
| join. Confirm the first scheduled Nightly after the fix lands; never dispatch | ||
| Nightly. | ||
|
|
||
| `git bisect` over `c916855d16..75f00f92c8` (the 11-11 Nightly's commit, whose | ||
| lite-06 join cells finished in 0.2 s, to main on 2026-11-12) names | ||
| f3edb0b8d9 ([moq-dev/moq#8476](https://github.com/moq-dev/moq/pull/8476), "a | ||
| failed fetch below the end no longer holds it open") as the first hanging | ||
| commit. Reproduce with | ||
| `cargo bench -p moq-relay --features test-support --bench session -- --quick 'relay_session_join_broadcasts/moq-lite-06/'`. | ||
| ## Waiting | ||
|
|
||
| Find which await hangs, reproduce it as a moq-net integration test of a | ||
| lite-06 viewer joining a relay that holds history, and fix the cause. A | ||
| viewer that never receives a group it subscribed to is a product bug, not | ||
| only a bench one, so do not skip the cells. | ||
|
|
||
| Then confirm a scheduled Nightly's relay session job finishes inside its | ||
| hour; never dispatch Nightly. | ||
| - moq-dev/moq: the first scheduled Nightly on `main` after the fix lands finishes its `Relay session benchmarks` job inside the hour; check with `gh run list --workflow nightly.yml --event schedule`, then delete this quest |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| #![cfg(feature = "async")] | ||
|
|
||
| //! Viewers joining a relay that already holds a track's history, each on its own session. | ||
|
|
||
| use std::time::Duration; | ||
|
|
||
| use moq_net::{Timestamp, Version, cache, origin, track}; | ||
|
|
||
| use crate::support::harness::{MockConnectOptions, connect_mock, spawn}; | ||
|
|
||
| const TIMEOUT: Duration = Duration::from_secs(10); | ||
|
|
||
| fn origin(hop: u64) -> origin::Producer { | ||
| let mut config = origin::Config::new(std::num::NonZeroU64::new(hop).unwrap()); | ||
| config.pool = cache::Pool::unbounded(); | ||
| let (origin, driver) = origin::Producer::new(config); | ||
| spawn(driver); | ||
| origin.with_proxy(true) | ||
| } | ||
|
|
||
| /// Every viewer that joins reads the group the relay holds. Each join subscribes upstream | ||
| /// again once the last viewer left, so the publisher serves the held group again; the relay | ||
| /// refuses that copy, and a lite publisher before lite-07 then drops the group for good. | ||
| /// That drop must not hide the group the relay holds whole. | ||
| #[kio::test] | ||
| async fn each_joining_viewer_reads_the_held_group() { | ||
| for version in ["moq-lite-05", "moq-lite-06", "moq-lite-07-wip", "moq-transport-22"] { | ||
| let version: Version = version.parse().unwrap(); | ||
| let publisher = origin(1); | ||
| let relay = origin(2); | ||
| let mut options = MockConnectOptions::new(version); | ||
| options.client_publish = Some(publisher.consume()); | ||
| options.server_subscribe = Some(relay.clone()); | ||
| let _upstream = connect_mock(options).await; | ||
|
|
||
| let broadcast = publisher.route("live", origin::RouteOptions::default()).unwrap(); | ||
| let info = track::Info::default() | ||
| .with_timescale(moq_net::Timescale::MILLI) | ||
| .with_max_age(None); | ||
| let source = broadcast.broadcast().create_track("video", info).unwrap(); | ||
| let mut group = source.append_group().unwrap(); | ||
| group | ||
| .write_frame(Timestamp::from_millis(0).unwrap(), &b"x"[..]) | ||
| .unwrap(); | ||
| group.finish().unwrap(); | ||
|
|
||
| for join in 0..3 { | ||
| let viewer = origin(3 + join); | ||
| let mut options = MockConnectOptions::new(version); | ||
| options.server_publish = Some(relay.consume()); | ||
| options.client_subscribe = Some(viewer.clone()); | ||
| let downstream = connect_mock(options).await; | ||
| kio::clock::timeout(TIMEOUT, async { | ||
| let remote = viewer.consume().follow("live", None).unwrap().available().await; | ||
| let remote = remote.unwrap().expect("the broadcast resolves"); | ||
| let mut subscriber = remote.track("video").unwrap().subscribe(None).await.unwrap(); | ||
| let mut group = subscriber.recv_group().await.unwrap().expect("a group"); | ||
| assert_eq!(group.sequence, 0, "{version}: viewer {join}"); | ||
| let frame = group.read_frame().await.unwrap().expect("a frame"); | ||
| assert_eq!(&frame.payload[..], b"x", "{version}: viewer {join}"); | ||
| }) | ||
| .await | ||
| .unwrap_or_else(|_| panic!("{version}: viewer {join} never read the held group")); | ||
| drop(downstream); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
serves_end()is true for a fetched head that is still arriving, so this exemption forgets the upstream's final verdict while the only copy holding the sequence is a fill in flight. If that fill then dies, the live copy this drop aborted isFall::AbandonedforOld/Cancel/Evictederrors and stores no cut, andSlot::cutonly reads the live copy, so the track ends up with neither the group nor the drop. A reader whose range resolves at that sequence then gets nothing to start from, which is the stall shape this PR fixes; the new test finishes its head, so it stops one step short.Technical details