Repository navigation
fix(mux): redundant TS failover survives a killed importer behind bufferbloat - #6113
Conversation
A relay resets a superseded group with the Old stream code, which reaches the subscriber as Error::Stream(StreamError::Old). The snapshot and window consumers only matched the local Error::Old, so a TS export behind a slow link died with "json: old" when the relay reset its catalog group. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A missing group's monotonic budget started when the read cursor reached it. Once a source stopped (a killed importer whose route outlives it until the QUIC idle timeout), the newest content stopped advancing, so a backlog with every other group missing drained one budget per hole. The TS export's jitter clock read that metered trickle as a source far slower than real time and failed a stitching export before it could switch back to the surviving importer. A hole is now late from when the first group past it arrived, in both the Rust and JS container consumers. The redundant failover test also prints the exporter's stderr when it exits early. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A killed importer sends no close, so the relay keeps its route until the idle timeout. A short one, as a redundant ingest would run, keeps the outage the failover tests wait out to seconds. 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>
…2/redundant-failover-drift # Conflicts: # quest/m1/test-flakes-2/README.md
|
Warning Review limit reachedYou'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 8 minutes. View limit details
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. Comment |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
# Conflicts: # quest/m1/audit-test-cost.md
# Conflicts: # quest/m1/test-flakes-2/README.md
|
Merged origin/main again (a587c85). The only conflict was Checks: (Written by Claude Opus 5.5) |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged origin/main (245a378).
(Written by Claude Opus 5.5) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 245a378edc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
# Conflicts: # quest/m0/redundant-failover-drift.md # quest/m1/audit-compile-time.md # quest/m1/audit-test-cost.md
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1b5621eef8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… backlog Each pending group now carries the earliest arrival among it and the groups past it. Arrivals only move forward, so an insert copies it from the next group and nothing below changes; a hole reads it from the head in O(1) rather than scanning every pending group per hole, which a sparse backlog from a peer made quadratic. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merge summary for 6b5fd8e:
(Written by Claude Opus 5.5) |
|
Re-queued at the same head (6b5fd8e). The 05:39Z removal was manual with no recorded reason: no merge-group run had started for this PR, no new review finding or failing check followed, and no open PR supersedes it. Main's six newer commits merge cleanly (test merge passes (Written by Claude Opus 5.5) |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Finishes and deletes
quest/m1/test-flakes-2/redundant-failover-drift.md.Summary
export::redundant_importers_switch_through_bufferbloat_on_lite_07had two independent causes behind the same "stitching export exited" message. Both were found from the exporter's stderr and per-frame jitter timing on failing runs.max_delay= delay/2 = 1.5 s) started when the read cursor reached it, so the backlog drained one group per 1.5 s: the log showsmedia skipped upstream ... skipped=120msexactly 1.502 s apart.jitter::Bufferread that paced trickle as a source running at 0.17x and failed (-2148785 ppm,-6508448 ppm). The jitter behaves to its documented contract (moq-dev/moq#5490, moq-dev/moq#5714): a backlog read at once gives one floor. The metering upstream of it was the bug.rs/moq-mux/src/container/consumer.rsandjs/hang/src/container/consumer.ts, which moq-dev/moq#5999 introduced together.json: old. When a relay resets a catalog group, the reset arrives asError::Stream(StreamError::Old).moq_json::snapshot::Consumeronly treated the localError::Old/Evicted/Lagged/GroupTooLargeas a lost group, so the export failed. moq-dev/moq#6204 fixes the same match in the moq-json snapshot and window consumers and moq-flate's snapshot consumer, classifying byStreamError::from, so this PR no longer carries its own copy of that fix.--quic-idle-timeout 2s --quic-keep-alive 500ms. The failover test also prints the exporter's stderr when it exits early.quest/m0/redundant-failover-drift.md; that copy and itsquest/m0/README.mdentry are deleted, and the references inquest/m1/test-flakes-2/README.mdandquest/m1/audit-compile-time.mdnow point at this PR (quest/m1/audit-test-cost.mdfinished on main).None of the quest's three options was the cause. Option 1 (jitter) would have undone #5490's fail-fast for sources far off real time; option 2 (follower stale-media switching) belongs to route health.
Public API / wire impact
Tests
Each regression test failed on the original code (checked by restoring the old source) and passes with the fix:
moq-mux container::consumer::tests::holes_behind_a_stalled_source_share_one_budget: on the old code it read["gap", "frame 2"]after one budget instead of all three groups.consumer.holes.test.ts: "the holes a stalled source left behind share one budget".a_hole_is_late_from_the_earliest_arrival_past_it(Rust) and "a hole is late from the earliest arrival past it" (JS): a group arriving below an earlier one inherits that arrival. Each fails without the copy on insert.Checks run:
ppm, onejson: old). With both fixes and the default idle timeout, it passed 12 of 12.just check: passed (4,286 + 516 + 216 + 123 + 10 Rust tests, JS tests, lint). It caught rustfmt and Biome formatting, fixed in this push.quest check: passed.cargo nextest -p moq-mux -p moq-cli(1778 passed), moq-mux clippy, js/hang tests (278 passed),tscand Biome pass.tsc, andquest checkpass.cargo nextest -p moq-mux -p moq-cli(1777 passed), the three ignored redundant-importer tests 3 of 3 under--stress-count 3(bufferbloat 29.9 to 35.2 s), js/hang tests (277 passed), andquest checkall pass.nix flake checkcannot run on this host (the github-runner image build needs user namespaces the sandbox refuses); the diff touches no Nix.Bufferbloat test duration
It still runs about 30 s. moq-dev/moq#6192 has since moved the real-time export tests behind
#[ignore]into the nightlyjust rs cli-exportleg, so this cost no longer lands onjust check. Timed per phase on one run: first output 4.9 s, first switch 15.6 s, second 21.2 s, third 33.1 s, with 3 to 8 s viewer output gaps at each switch. The downlink is shaped to 160 kbit/s against a roughly 400 kbit/s source, so every switch waits for the new importer's keyframe group to cross a link below the media rate. That cost is the scenario itself, and the test runs real subprocesses on wall time, so a paused clock does not apply. Shortening it means a faster link, which weakens the bufferbloat coverage, so it stays.Decisions (confirmed by the maintainer)
StreamError::from, so this PR dropped its inline arms.Suggested follow-ups
(Written by Claude Opus 5.5)
🤖 Generated with Claude Code