Repository navigation
Conversation
Quest-Claim: f9d673f63dd61ec7
… goes A relay hearing a claim both over moq-lite-07 and as a translated moq-lite-06 chain filed the copy as one of its own claims, so after the publisher withdrew, every node restarted onto that copy before it retracted. The route node now publishes the lite-06 hops of the identities it has a usable route to. A lite-06 announce whose chain names one is held back, and taken up again once that route is lost, so the lite-06 path still takes over. 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>
|
Closing without merging. Maintainer decision, 2026-11-07 (/quest-spawn, design checkpoint after review round 3): ✅ drop the hold-back and delete the quest. Main already fails a lite-06 route over when the lite-07 path dies, so the quest's premise does not hold. What remains is a transient The ring-loop bug found here (two lite-06 links between lite-07 islands circle a withdrawn claim forever, on main since #8023) is being planned as its own quest. (Written by Claude Opus 5.5) |
Claims quest/m2/cluster-routing/routes/lite06-failover.md and deletes it.
Problem
The quest said a relay declines a moq-lite-06 route for an identity it already knows over moq-lite-07, and never reconsiders it. On main that is not what happens. moq-dev/moq#8023's final version guards only the relay's own routing identity, so it declines nothing else. Failover already worked on main (reproduced: D restarts onto the lite-06 route).
The gap is the other half of the quest's plan, which main never did: recognizing known identities. Without it, a relay that hears P's claim over lite-07 and again as B's translated lite-06 chain
[hop(P), B]files the copy as one of its own claims and advertises it to its lite-07 peers. Once P withdraws, every node, P included, restarts onto that stale copy before it retracts. In a triangleP -07- B -06- D -07- P, main shows[Restart, End]at P, B and D.Approach
route::node::Knownmaps the lite-06 hop (route::node::hop) of each other identity to what its usable feasible routes through unscoped neighbors can carry (Router::carried: the prefix asked of each neighbor), plus an epoch.State::flushupdates it incrementally fromRouter::changed.OriginState::known) and hands it to the node when the first lite-07 session creates it. So a lite-06 session can watch it before any node exists.Subscriber::shadowedjoins the announce path to the session origin's root, the frame the route layer keys claims in.Announced::offerchecks it first and keeps the route asHeld::Shadowed(RouteInfo). A lite-07 route that carries onlya/leaves a lite-06 copy atb/xalone. Withdrawing a claim leaves the route in place, so a withdrawal is still told apart from a lost route. AnANNOUNCE_UPDATEthat makes an attached route's chain name a known identity goes throughoffertoo.Announced::reconsiderholds back attached routes that now name a known identity and offers held-back ones that no longer do. The re-offer goes through the session limit, so a held-back route outside it becomesWithheld, and drain applies after a GOAWAY.Subscriber::chain.Choices made without asking
EndthenStart, notRestart. When the lite-07 session is lost, its routes leave the origin with it, before its node stream disconnects and the route layer learns the identity is unreachable. So the held-back copy is announced afresh. Getting aRestartwould mean either keeping the copy in the origin as a standby, which brings back the local revival on withdrawal because no one can yet tell a withdrawal from a lost route, or holding a dead session's routes. Either way readers re-request, since lite-06 mints its own instance.doc/bin/relay/cluster.mdsays "announces it afresh".live/*/public/**). Its identities are qualified anyway, so a bridge's translated hop rarely matches. Counting only unscoped routes, whose limit admits everything, settles every such case, at the cost of main's behaviour (a duplicate own claim) for copies of a scoped peer's claims. Mirroring the permit in the node would need the limit patterns there for a rare case.ROUTE_HOLD) or infeasible route does not count, so the lite-06 path also covers a route that is reconverging. The cost is a possible extra restart while Babel recovers.Impact
route::Node::randomtakes theKnownchannel,Node::newis now test-only, andRouter::carriedis added.Caused by
Overlap with #8063
moq-dev/moq#8063 does not do this failover. It touches
route/node.rs(Claim::continues, theownlineage) andAnnouncednearoffer, but notState::flush,Held, or the announce loop's poll. Expect small textual conflicts there and inorigin.rsaroundroute_node, whichever lands second.Validation
mixed_version_triangle_withdraw_retracts_once(new): P, B and D each see onlyEndon withdrawal. It fails on main with[Restart, End].mixed_version_failover_takes_up_the_lite06_route(new): cutting the P-D link announces the lite-06 copy afresh and a request succeeds. Re-linking restarts D back onto P's lite-07 instance, and a withdrawal then ends cleanly. The recovery step fails on main, which never holds the copy back.route::node::tests::known_names_a_routed_identity_where_its_route_carries(new): a stream asking forlivenames P atlive/xandlive/ybut notother/x. A withdrawal keeps the name, and dropping the stream clears it and bumps the epoch.route::node::tests::known_leaves_out_a_scoped_route(new): a scoped route names nothing, even by the qualified identity's own hop. It fails without the scope filter.mixed_version_ring_withdraw_retractsand the rest ofmesh_withdrawstill pass.test/drill/mutations/subscriber-leaks-broadcasts.patchis retargeted: its context included thePrefixRuninitializer, which gainsknown_epoch.just checkpasses on this head (12746 tests).Suggested follow-ups
P -07- B -06- D -07- E -06- F -07- B, publisher at P: after P withdraws, the claim never settles, and the test hangs at one simulated instant. Each lite-07 crossing restarts the translated chain at the claiming node's identity hop, soD -> E -> F -> B -> Dre-files it as own claims with no hop ever repeating. This PR breaks such a loop only when the relays on either side of a lite-06 link also know each other over lite-07. The same shape with old lite-06-only relays between lite-07 islands (N2 -07- N3 -06- O -06- N4 -07- N5 -06- O' -06- N2) loops too. It predates this PR (from feat(net)!: lite-07 routes through the route layer #8023) and is a correctness bug for mixed rollouts, so it is worth a quest. A likely direction is carrying a learned claim's hop history (or a count bound) across lite-07.Waiting
Knownmirrors the routed identities' shown paths, and every claim change wakes the lite-06 streams. That needs an incremental changelog to stay cheap at scale.[Restart, End]everywhere in the triangle).Check: the maintainer's pick is recorded on this PR; then
/quest-iterateimplements it and startsquest review --new.Models
Size: M
Implementer: Claude Code, claude-opus-5-5, high effort
Review
Rewritten by
quest reviewafter each round.01a1b8bc-21c7-75f1-ba0a-4b4d7531fa2d, round 3The direct scoped-neighbor case is fixed, but forwarding through an unscoped relay can still make valid legacy broadcasts unreachable.rs/moq-net/src/route/mod.rs:898:Exclude scoped identities after relay forwardingrs/moq-net/src/route/mod.rs:289:Include full permit limits in shadowing coverage;The immediate scoped-session case is fixed, but rs/moq-net/src/route/mod.rs:898 still accepts scoped identities forwarded by unscoped relays, recreating suppression outside the upstream permit.