Skip to content

test(net): pin cross-node close flapping and its residuals - #5458

Merged
kixelated merged 5 commits into
mainfrom
quest/m1/cross-node-flapping
Oct 14, 2026
Merged

kixelated merged 5 commits into
mainfrom
quest/m1/cross-node-flapping

Conversation

@kixelated

@kixelated kixelated commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

#4349: after a clean close, a client watching from another relay saw the path retracted and announced repeatedly. Main carries #4642's 300 ms hold, #5375's two-second retraction quarantine and #5391's queue dedup, but nothing pinned the client-visible case or the residuals #5375 measured.

Approach

All in rs/moq-net/tests/mesh_withdraw.rs, time-mocked:

  • Cluster: relays over latency links, a client publishing on one relay and a client watching on another (holding nothing, like a browser), plus every relay watched.
  • cross_node_close_retracts_once_{lite06,lite07}: the drill. Eight relays (the existing retracted_relay_ignores_a_stale_path graph, now STALE_PATHS), three publish/close rounds; the watching client sees exactly Start then End, and no relay revives.
  • cross_node_close_without_hold_reannounces: positive control; with the hold (and so the quarantine) at zero the client sees End, Start, End.
  • residual_slow_ring_reannounces: a six-relay ring, links under 150 ms, where an equal-cost stale copy outlives the hold (84 ms vs 446 ms); the client sees End, Start, End.
  • residual_mesh_revives: nine relays reduced from a seeded mesh; two relays revive after retracting, the client sees two toggles.
  • residual_random_meshes: fix(net): hold back a stale path to a prefix a cursor just retracted #5375's search made reproducible: 200 splitmix-seeded meshes of 8-17 relays, links 1-150 ms, pinned at 179 hunting relays, 3 reviving, 192 updates beyond their retraction (2 s in debug).
  • Settling: every drain in mesh_withdraw first awaits the new moq_net_sim::idle(), which completes once every task is parked and no timer is armed (the point the run loop used to report only as a deadlock). This replaces the quiet-period timeout, which could fire while a later quarantine release was still armed and miss it. Test origins use cache::Pool::unbounded(), since the idle-expiry sweep keeps a timer armed forever and nothing here carries media. Existing tests still pass, and every pinned sequence is unchanged.

The residual tests pin today's sequences, so a change in either direction fails and the route rewrite flips them. The quest is deleted; Routes and announces now names these tests as the #4349 acceptance.

Positive controls (same fixtures, ported to each harness):

Build drill client slow ring client 200 meshes (hunting, reviving)
release before #5376 (046bfd4ba) End, Start, End Update, End 319, 136
main before #5375 (d68bd0a7e) End, Start, End End, Start, End 504, 427
release now (42ca851af) End Update, End 230, 3
main End End, Start, End 179, 3

On main, a stale-path switch for an epochless broadcast is a Restart, and lite-06 sends that as a retraction then an announcement. The release sends an in-place update there, so on these residuals a lite-06 client sees a toggle on main that it does not see on the release.

Impact

  • None: tests, quests, and moq-net-sim (the unpublished test executor) gains idle(). No public API or wire change.

Alternatives

  • Land the residuals #[ignore]d until the rewrite passes them. Not run in CI, so a regression in the meantime goes unseen.
  • Assert bounds instead of exact sequences. That needs an arbitrary threshold and hides both regressions and partial fixes.
  • Lengthen the hold. Out of scope by the quest; it trades stale paths that live longer.

Decisions

Maintainer, 2026-10-13:

  • Residual tests land pinned to today's exact sequences and run in CI; a change in either direction fails and is re-pinned deliberately.
  • The lite-06 epochless Restart difference versus the release is accepted, with no release fix and no quest (2026-10-13). Main is correct: an epochless replacement is a new instance, so lite-06 peers restart cleanly. The release's in-place update kept them on the old route, and the next release carries Restart plus epochs.
  • The publish-time toggle (on some rings a longer path arrives first, the better route then replaces it as a Restart, and a lite-06 client sees Start, End, Start) is now a fixture requirement in quest/m1/cluster-routing/routes.md.

Follow-ups

  • With an epoch on lite-07, the random meshes show no revivals, but up to four Updates per relay before End.

Checks

  • just rs test -p moq-net: 2003 passed.
  • Rebased onto fix(net): retire epochless handles only once their Restart is deliverable #5443 and later main: mesh_withdraw passes with every pinned sequence unchanged, before and after the switch to idle().
  • just check on the final head: 6286 tests passed, plus moq-net-sim's new idle_waits_for_every_timer. One rerun hit a wall-clock flake in moq-cli::export::an_export_failure_with_the_broadcast_up_exits_without_lingering, which passed alone and in the other run; this PR touches no code it runs.

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

@kixelated

Copy link
Copy Markdown
Collaborator Author

Outcome: the quest is done. The drill passes on main, and the positive controls fail on the release before #5376 and on main before #5375. The residuals are pinned, and the quest is deleted in favor of Routes and announces.

Open decisions for the maintainer:

  1. How the residual tests land. Recommended: pin the exact sequences we see today (this PR), so they run in CI and any change fails loudly. The alternatives are #[ignore] until the rewrite passes them, or asserting bounds.
  2. Epochless Restart on lite-06. On main, a stale-path switch is a Restart, which a lite-06 client sees as a retraction and then an announcement. The release sends an in-place update in the same spot. So once main ships, these residuals show up as toggles at the client. Recommended: plan a quest to decide this before the next release: accept it, or lower an epochless restart to an update on lite-06 sessions. The rewrite does not remove this case, because Routes keeps epochless source changes as Restart.

(Written by Claude Opus 5.5)

@kixelated
kixelated marked this pull request as ready for review October 13, 2026 21:40
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-14T03:08:31.275540Z ae7beba Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 333d7cdb2f

ℹ️ 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".

Comment thread rs/moq-net/tests/mesh_withdraw.rs Outdated
}

/// A link between two relays, by index, and its one-way latency in ms.
type Link = ((usize, usize), u64);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Store link latency as a Duration

These fixtures depend on exact timing to reproduce route hunting, but Link stores the latency as an untyped u64 and only assigns milliseconds at the call site, making a future fixture or consumer able to silently interpret the value in another unit. Use Duration in Link and construct the constants with Duration::from_millis so the unit is encoded where each value is declared.

AGENTS.md reference: rs/AGENTS.md:L36-L37

Useful? React with 👍 / 👎.

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.

Agreed: Link now carries a Duration, and the tables use a const fn ms so the unit sits at each value (ee9da29).

(Written by Claude Opus 5.5)

@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 commit: 333d7cd against ab09ae8.

No actionable correctness bugs found in the test/quest changes. The zero-hold watching client, deterministic latency fixtures, repeated close rounds, and quiet window beyond the quarantine provide useful regression coverage. Pinning the known residual sequences while leaving #4349 closure with the routing rewrite is a sound, clearly documented direction.

Coverage limit: mesh_withdraw.rs:518–524 pins counts of affected relays, not the number of toggles on each. Extra toggles on an already-affected relay can leave (179, 3) unchanged; per-seed event counts would strengthen that sweep. The reduced fixtures already assert exact sequences.

Verification: GitHub-only static review. No tests or historical positive controls run independently; the reported test counts and timings are author results.

@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 3c3e9d71-a6bb-4683-ba6f-6202c1878226
📥 Commits

Reviewing files that changed from the base of the PR and between 333d7cd and ae7beba.

📒 Files selected for processing: 3
  • quest/m1/README.md
  • rs/moq-net/sim/src/lib.rs
  • rs/moq-net/tests/mesh_withdraw.rs
💤 Files with no reviewable changes: 1
  • quest/m1/README.md

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 0 remain after this review.

Walkthrough

The simulator adds an idle() future that completes after tasks settle and no timer or source deadline remains. Mesh-withdrawal tests use simulator idle and add coverage for repeated closes, stale-path re-announcements, fixed topologies, and 200 seeded random meshes. Routing quest notes describe residual test cases and closure criteria. The Cross-node flapping quest document and its links are removed.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to ae7be

The close and residual tests can merge without the separate live-replacement fixture; no actionable merge-blocking issue remains.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 79.31% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 29 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Title check Passed The title clearly summarizes the main change: adding tests that pin cross-node close flapping and residual route behavior.
Description check Passed The description directly explains the tested regressions, simulator changes, quest updates, test results, and maintainer decisions.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
✨ Simplify code
  • Commit to this branch
  • Create a new PR
  • 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

Comment @coderabbitai help to get the list of available commands.

@kixelated
kixelated force-pushed the quest/m1/cross-node-flapping branch from ee9da29 to 276cdd1 Compare October 13, 2026 21:55
@kixelated

Copy link
Copy Markdown
Collaborator Author

@codex review

Since the last review: Link latency is a Duration, residual_random_meshes also pins the updates sent beyond each retraction (192), the publish-time toggle fixture is now required in quest/m1/cluster-routing/routes.md, and the branch is rebased onto #5443 with every pinned sequence unchanged.

(Written by Claude Opus 5.5)

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 276cdd18c3

ℹ️ 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".

@kixelated

Copy link
Copy Markdown
Collaborator Author

@codex review

Please review the final head 276cdd1.

(Written by Claude Opus 5.5)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 276cdd18c3

ℹ️ 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".

Comment thread rs/moq-net/tests/mesh_withdraw.rs Outdated
async fn drain(watched: &mut mpsc::UnboundedReceiver<(String, Kind)>) -> HashMap<String, Vec<Kind>> {
let mut updates = HashMap::<String, Vec<Kind>>::new();
while let Ok(Some((prefix, kind))) = moq_net_sim::timeout(Duration::from_secs(1), watched.next()).await {
while let Ok(Some((prefix, kind))) = moq_net_sim::timeout(Duration::from_secs(3), watched.next()).await {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Replace the timeout with a true mesh-settle condition

When a stale announcement is quarantined on successive relays, it can be scheduled more than three simulated seconds after this watcher’s last event because each quarantine lasts two seconds in addition to link and update-hold delays. This timeout then wins while later simulator timers remain armed; since Cluster::close drains the client and relays sequentially and never revisits earlier receivers, a delayed Start can remain unread and the tests can incorrectly report a final End or zero revival. Drive the mesh to deterministic global convergence before draining every receiver instead of treating three seconds of inactivity as convergence.

AGENTS.md reference: AGENTS.md:L16-L16

Useful? React with 👍 / 👎.

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.

Agreed, fixed in ae7beba. moq_net_sim::idle() completes once every task is parked and no timer or source is armed, the point the run loop used to report only as a deadlock. Each drain in mesh_withdraw now awaits it first, then reads every receiver without blocking, so a release armed after a quarantine always lands before anything is read. The quiet-period timeout is gone.

The test origins use cache::Pool::unbounded(): the idle-expiry sweep re-arms every 15 s, so the mesh never went idle otherwise, and nothing here carries media. Every pinned sequence holds unchanged.

(Written by Claude Opus 5.5)

kixelated and others added 5 commits October 13, 2026 19:23
A client publishing on one relay closes a broadcast, and a client watching
from another relay must see one retraction. The drill reproduces #4349's
retract/announce/retract on the release before the retraction quarantine
and passes on main; a zero-hold control shows it still detects the toggle.

Residual counterexamples pin what main does today: a stale path outlasting
the 300 ms hold on a six-relay ring, a revival the quarantine misses on nine
relays, and 179 hunting relays (3 reviving) over 200 seeded random meshes.
The route rewrite must bring each to a single retraction.

Drains now wait out the two-second quarantine, so a late revival is seen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dates

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
moq_net_sim::idle completes once every task is parked and nothing is armed,
the point the run loop used to report only as a deadlock. mesh_withdraw
waits for it before draining any receiver, so a stale path released after a
quarantine can no longer land after its relay was read. Test origins use an
unbounded cache pool: the idle-expiry sweep would otherwise stay armed forever.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated force-pushed the quest/m1/cross-node-flapping branch from 276cdd1 to ae7beba Compare October 14, 2026 03:05
@kixelated

Copy link
Copy Markdown
Collaborator Author

@codex review

Head ae7beba: drains now wait for moq_net_sim::idle() (global quiescence on mocked time) instead of a quiet-period timeout, and the branch is rebased onto main. Every pinned sequence is unchanged.

(Written by Claude Opus 5.5)

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: ae7bebacd9

ℹ️ 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".

@kixelated
kixelated added this pull request to the merge queue Oct 14, 2026
Merged via the queue into main with commit cc501c3 Oct 14, 2026
9 checks passed
@kixelated
kixelated deleted the quest/m1/cross-node-flapping branch October 14, 2026 03:43
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