Skip to content

quest(release): defer the resumed-history fix to the next cut - #7815

Merged
kixelated merged 4 commits into
mainfrom
quest/m0/backport-resume-owed-groups
Nov 4, 2026
Merged

kixelated merged 4 commits into
mainfrom
quest/m0/backport-resume-owed-groups

Conversation

@kixelated

@kixelated kixelated commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Records the maintainer's decision not to backport moq-dev/moq#7708's resumed-history fix to release. Release stays exposed to the IETF resumed-history cache loss until the next main → release cut brings the fix. That matches the earlier MKV decision in moq-dev/moq#7718.

  • Deletes quest/m0/backport-resume-owed-groups.md with quest rm, along with its m0 link.
  • Adds a note to quest/m2/release/cut.md's Plan, beside the MKV exposure, so the cut checks that both fixes reach release.

Decisions

  • Backport the resumed-history fix to release, or defer it to the next cut?
    • Backport by hand-porting demand setup and preserving finite-budget cache behavior
    • Backport and allow the finite-budget cache behavior to change
    • ✅ No backport: release stays exposed until the next main → release cut (maintainer, 2026-11-04)
  • Release a separate moq-net patch?
    • Yes, once a source fix lands
    • ✅ No: no patch release is authorized (maintainer, 2026-11-04)

Reproduction evidence

Release is exposed. At da09919e75 (moq-net 0.3.20), a retained group is lost when an IETF reader returns after idle. Lite is unaffected.

Paused-time cache-loss matrix: group 0 is cached, the reader leaves until the publisher reports it unused, groups 1 to 3 are published, and the reader resumes at group 0 with a budget that covers the history.

Version Unbounded resume Bounded resume (set_groups(0..1))
moq-transport-14 … 22 (9 drafts) ❌ returns group 3, not group 0 ❌ times out waiting for group 0
moq-lite (all 7, including lite-07-wip) ✅ retains group 0 ✅ retains group 0

In total, 18 IETF cases fail and 14 lite controls pass.

  • Bounded 1..3 history behind a fetched copy: groups 1 and 2 arrive on all nine IETF drafts. The probe starts from a copy that is already open, so it does not show that drafts 14 to 22 accept a bounded request at first subscribe.
  • Release's local cap intentionally parks at its boundary, so main's bounded EOF assertion does not apply.
  • Unchanged release suite (just rs test -p moq-net --no-fail-fast): 1,597 passed, 4 skipped.

Cause

The older model opens IETF SUBSCRIBE while it queries metadata, before the resumed reader's preferences are forwarded. origin::run_front queries a source before it splices in the reader's demand. The IETF subscriber treats that empty demand as a live join and records SUBSCRIBE_OK's Largest as its start. resume::Subscriber::poll_activate then discards the whole warm segment when that start is past the segment's edge, without checking the resumed reader's budget. Release has neither main's owed-run accounting nor its unlimited delay.max=None option, because its max_age is a concrete Duration. A backport would therefore have to hand-port demand setup rather than cherry-pick main's fix.

Reproduction probe for unchanged release

Place this at rs/moq-net/tests/resume_owed.rs in the pinned release checkout, then run just rs test -p moq-net -E 'binary(resume_owed)' --no-capture --no-fail-fast through its Nix shell. The cached-start matrix intentionally fails; the bounded-history matrix passes.

//! Release exposure probes adapted from main's owed-group resume regressions.
mod support;

use std::time::Duration;
use moq_net::{Hop, Timestamp, Version, track};
use support::harness::{MockConnectOptions, connect_mock};

const TIMEOUT: Duration = Duration::from_secs(5);
const FOREVER: Duration = Duration::from_millis((1 << 53) - 1);

fn origin(hop: u64) -> moq_net::origin::Producer {
    let mut config = moq_net::origin::Config::new(Hop::new(hop).unwrap());
    config.default_max_age = FOREVER;
    let (producer, driver) = moq_net::origin::Producer::new(config);
    tokio::spawn(support::harness::run(driver));
    producer
}

fn write(track: &track::Producer, sequence: u64) {
    let mut group = track.create_group(moq_net::group::Info { sequence }).unwrap();
    for frame in 0..2 {
        group.write_frame(Timestamp::ZERO, format!("{sequence}-{frame}").into_bytes()).unwrap();
    }
    group.finish().unwrap();
}

async fn next(sub: &mut track::Subscriber) -> u64 {
    let mut group = sub.recv_group().await.unwrap().expect("a group");
    let sequence = group.sequence;
    for frame in 0..2 {
        assert_eq!(group.read_frame().await.unwrap().unwrap().payload.as_ref(), format!("{sequence}-{frame}").as_bytes());
    }
    assert!(group.read_frame().await.unwrap().is_none());
    sequence
}

fn old_ietf(version: Version) -> bool {
    matches!(version.to_string().as_str(), "moq-transport-14" | "moq-transport-15" | "moq-transport-16" | "moq-transport-17" | "moq-transport-18" | "moq-transport-19")
}

async fn cached_start(version: Version, bounded: bool) {
    let publisher = origin(1);
    let broadcast = publisher.create_broadcast("bcast").unwrap();
    let track = broadcast.create_track("video", track::Info::default().with_max_age(FOREVER)).unwrap();
    broadcast.announce(Default::default()).unwrap();
    write(&track, 0);
    let client = origin(2);
    let mut options = MockConnectOptions::new(version);
    options.server_publish = Some(publisher.consume());
    options.client_subscribe = Some(client.clone());
    let pair = connect_mock(options).await;
    let consumer = client.consume();
    consumer.routed("bcast").await.unwrap();
    let remote = consumer.request_broadcast("bcast").await.unwrap();
    let copy = remote.track("video").unwrap();
    let mut live = copy.subscribe(None).await.unwrap();
    assert_eq!(next(&mut live).await, 0);
    drop((live, copy));
    eprintln!("{version} bounded={bounded}: waiting for idle");
    track.unused().await.unwrap();
    eprintln!("{version} bounded={bounded}: idle");
    for sequence in 1..4 { write(&track, sequence); }
    let copy = remote.track("video").unwrap();
    let mut subscription = track::Subscription::default().with_max_age(FOREVER).with_start(track::Position::group(0));
    if bounded { subscription = subscription.with_end(track::Position::group(1)); }
    eprintln!("{version}: resubscribing");
    let mut sub = match copy.subscribe(subscription).await {
        Ok(sub) => sub,
        Err(moq_net::Error::Unsupported) if bounded && old_ietf(version) => return,
        Err(err) => panic!("subscribe: {err}"),
    };
    if bounded { sub.set_groups(0..1); }
    eprintln!("{version} bounded={bounded}: reading cached zero");
    assert_eq!(next(&mut sub).await, 0, "the resumed read retains its cached start");
    drop((sub, pair, copy, remote, client, track, broadcast, publisher));
}

async fn bounded_after_fetch(version: Version) {
    let publisher = origin(1);
    let broadcast = publisher.create_broadcast("bcast").unwrap();
    let track = broadcast.create_track("video", track::Info::default().with_max_age(FOREVER)).unwrap();
    broadcast.announce(Default::default()).unwrap();
    for sequence in 0..2 { write(&track, sequence); }
    let client = origin(2);
    let mut options = MockConnectOptions::new(version);
    options.server_publish = Some(publisher.consume());
    options.client_subscribe = Some(client.clone());
    let pair = connect_mock(options).await;
    let consumer = client.consume();
    consumer.routed("bcast").await.unwrap();
    let remote = consumer.request_broadcast("bcast").await.unwrap();
    let copy = remote.track("video").unwrap();
    let mut fetched = copy.fetch_group(0, None).await.unwrap();
    for frame in 0..2 {
        assert_eq!(fetched.read_frame().await.unwrap().unwrap().payload.as_ref(), format!("0-{frame}").as_bytes());
    }
    assert!(fetched.read_frame().await.unwrap().is_none());
    for sequence in 2..4 { write(&track, sequence); }
    let subscription = track::Subscription::default().with_max_age(FOREVER).with_groups(1..3);
    eprintln!("{version}: resubscribing");
    let mut sub = match copy.subscribe(subscription).await {
        Ok(sub) => sub,
        Err(moq_net::Error::Unsupported) if old_ietf(version) => return,
        Err(err) => panic!("subscribe: {err}"),
    };
    sub.set_groups(1..3);
    let mut got = vec![next(&mut sub).await, next(&mut sub).await];
    got.sort_unstable();
    assert_eq!(got, [1, 2], "bounded history below Largest");
    drop((sub, pair, copy, remote, client, track, broadcast, publisher));
}

#[tokio::test(start_paused = true)]
async fn resumed_cached_start_matrix() {
    let mut failures = Vec::new();
    for version in moq_net::Versions::all().iter().copied().chain(["moq-lite-07-wip".parse().unwrap()]) {
        for bounded in [false, true] {
            let result = tokio::spawn(async move {
                tokio::time::timeout(TIMEOUT, cached_start(version, bounded)).await.expect("case timed out");
            }).await;
            eprintln!("{version} bounded={bounded}: {result:?}");
            if result.is_err() { failures.push(format!("{version} bounded={bounded}")); }
        }
    }
    assert!(failures.is_empty(), "{failures:?}");
}

#[tokio::test(start_paused = true)]
async fn resumed_bounded_history_matrix() {
    let mut failures = Vec::new();
    for draft in 14..=22 {
        let version: Version = format!("moq-transport-{draft}").parse().unwrap();
        let result = tokio::spawn(async move {
            tokio::time::timeout(TIMEOUT, bounded_after_fetch(version)).await.expect("case timed out");
        }).await;
        eprintln!("{version}: {result:?}");
        if result.is_err() { failures.push(version); }
    }
    assert!(failures.is_empty(), "{failures:?}");
}

Impact

  • Public API and wire: unchanged. Only quest Markdown changes.
  • No version bump, tag, or release.

Validation

  • quest check --base origin/main: 600 documents ok.
  • quest gate nix develop --command just ci origin/main: passed, including check markdown and check quest.

Review

Codex gpt-6.1-sol (medium effort) reviewed cb01b83863 against origin/main with quest review (round 1): no findings.

Follow-ups

None. The release cut carries the exposure.

(Written by Claude Opus 5.5)

kixelated and others added 2 commits November 3, 2026 21:14
@kixelated kixelated changed the title Release resumes owed groups or proves it doesn't need to docs(quest): reproduce release resumed-history cache loss Nov 4, 2026
kixelated and others added 2 commits November 3, 2026 22:44
The maintainer chose no backport (2026-11-04): release stays exposed to the
IETF resumed-history cache loss until the next main to release cut brings
#7708. Delete the backport quest and note the exposure beside the
MKV one in the cut's Plan so the cut checks both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title docs(quest): reproduce release resumed-history cache loss quest(release): defer the resumed-history fix to the next cut Nov 4, 2026
@kixelated
kixelated marked this pull request as ready for review November 4, 2026 06:48
@coderabbitai

coderabbitai Bot commented Nov 4, 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 35 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: 2a427a7d-bb0f-4548-b470-b043cc9847e7
Commits

Reviewing files that changed from the base of the PR and between 17a81d6 and cb01b83.

Files selected for processing: 3
  • quest/m0/README.md
  • quest/m0/backport-resume-owed-groups.md
  • quest/m2/release/cut.md
  • 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

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

  • Deleted the resume-owed-groups quest — removes quest/m0/backport-resume-owed-groups.md and its line in quest/m0/README.md's Required list.
  • Recorded the deferral at the cut — adds a note to quest/m2/release/cut.md saying the next main → release cut must ship both #7234's MKV fix and #7708's resumed-history fix, noting releases (moq-net 0.3.20) exposure on drafts 14 to 22 and the 2026-11-04 no-backport decision.

Verified there are no remaining references to the deleted quest path anywhere in the tree, and that the exposure wording matches the reproduction matrix in the PR body (9 IETF drafts fail, lite controls pass). The deletion departs from the quest's own "not exposed" branch, but the cut note records an explicit maintainer decision, so the record is consistent.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

@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 cb01b83. No actionable issue found in recording the maintainer's decision to defer the resumed-history fix. The release-cut plan retains the IETF exposure and names the main fix to carry into release; the completed backport quest is removed from the index.

Verification: GitHub-only static review of the quest changes, linked fixes and recorded decision. I did not reproduce the reported release matrix or run the checks.

(Written by review (OpenAI))

@kixelated
kixelated added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit 7abc018 Nov 4, 2026
11 checks passed
@kixelated
kixelated deleted the quest/m0/backport-resume-owed-groups branch November 4, 2026 07:40
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