fix(desktop): keep mesh allowlist on transient roster query failure#2024
Conversation
A transient relay failure (e.g. 503) during the periodic roster reconcile was collapsed into an empty roster, which reconcile_roster read as 'roster changed to empty' and restarted the mesh node down to self-only — de-admitting every other member and flapping the node (restart loop) on each relay blip. Remote consumers hit either a self-only allowlist or a node restarting mid-inference, so 'nobody else can use my shared compute'. - resolve_trusted_owner_ids now returns Result: a failed query is Err, distinct from Ok(empty) (a genuinely empty community). - Start/restore paths use resolve_trusted_owner_ids_or_self_only (fail closed to self-only is correct only at start, with no live allowlist to preserve). - reconcile_roster: on query Err, keep the current allowlist and retry next poll instead of restarting. Extracted pure roster_reconcile_action for tests. - Regression tests for the failed-query, unchanged, changed, and genuinely empty cases. Not a revert of #2000 (that consumer-admission fix stays); this hardens the same path against transient relay errors.
Review: good direction, but not yet "won't wig out on a flaky relay"This correctly fixes the 503 flap and the caller split is right (start/restore/client fail closed to self-only; only But there are three residual ways it can still thrash/tear-down under an intermittently-failing relay — and one of them is arguably more common than the 503 it fixes. An independent review (Codex, xhigh) and I both land on needs a bit more before it can claim "fixes flapping." 1. (Biggest) A successful
|
Addresses review feedback on the 503-flap fix (#1 + #2): #1 Missing membership snapshot no longer collapses to self-only. query_mesh_discovery_events returned Ok(empty) both when the relay returned an authoritative zero-member snapshot AND when no kind:13534 snapshot came back at all (a transient gap / replication lag). The relay publishes an explicit 13534 event even for a zero-member community, so absence means the query was incomplete. Added has_membership_snapshot(); a missing snapshot is now surfaced as Err so reconcile keeps the current allowlist instead of flapping the node down to self-only on a successful-but-empty response. #2 Shrink hysteresis. reconcile_roster restarted the node on the first observation of any smaller roster, so a single short-read could drop a member mid-inference. RosterReconcileAction gains AwaitConfirm: a shrink (or empty) must be observed with the same reduced roster on two consecutive polls before teardown; growth (pure additions) still applies immediately. The watcher loop carries the pending-shrink state across polls. Tests: has_membership_snapshot present/empty/missing; growth-immediate, shrink-await, shrink-confirm, shrink-reconfirm-on-change, empty-await-confirm, shrink-then-recovery. 44 mesh tests pass; clippy + fmt clean. #3 (restart rollback + mid-poll runtime-change race) tracked as fast-follow.
|
Thanks — both legit, folded in as #1 (missing snapshot → #2 (shrink hysteresis) — done. New tests: #3 (restart rollback + mid-poll runtime-change race) — keeping this PR tight as you suggested; will do it as a fast-follow with the lifecycle tests (missing-snapshot, repeated-shrink-confirm, recovery, runtime-changed-mid-query, replacement-failure-with-rollback). The pure-decision layer is now in place so the lifecycle wrapper is the remaining piece. Note: the failing Desktop E2E Integration (2/2) check is an unrelated flake — |
Shared compute consumers rejected every stock mesh-llm serving node, failing with "no live member is serving this model" even though discovery found it. Two independent transport-policy bugs, both on the CONSUMER side (a serving node never validates its own advertised endpoint, so only consumers need this): 1. Relay allowlist: IrohRelayMode::Default only accepted relays in iroh's own prod relay map. mesh-llm serving nodes advertise endpoints on mesh-llm's default public relays (*.relay.michaelneale.mesh-llm.iroh.link, see mesh-llm-host-runtime effective_relay_urls / RelayPolicy::DefaultPublic). The two sets never overlap, so relay_allowed() returned false and the serve target was filtered out of availability_from_events. Default mode now accepts iroh prod OR mesh-llm's default relays. 2. All-or-nothing endpoint validation: validate_endpoint_addr bailed on the FIRST unusable transport candidate. A stock mesh-llm endpoint advertises a relay alongside a port-0 direct IP; the port-0 candidate rejected the whole endpoint even though the relay route was usable. Transport candidates are alternatives, not all required — now accept the endpoint if at least one candidate is usable, recording why the rest were dropped. Adds regression tests using real mesh-llm relay URLs and the real relay+port-0 multi-candidate endpoint shape (the existing tests were hermetic: fake relay.example URLs and single clean-address endpoints, so neither bug was observable).
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
* origin/main: (31 commits) chore(release): release Buzz Desktop version 0.4.14 (#2036) Strip media metadata on clients and reject it at the relay (#2006) fix(desktop): restore Doctor ACP login discovery (#2017) feat(desktop): animate starter team onboarding (#2032) fix(ci): use git diff for path filtering (#2029) fix(desktop): include exe dir and ~/.local/bin in provider discovery (#2007) [codex] Hold Git concurrency permits through streaming (BUZZ-SEC-018) (#1916) [codex] Enforce shared relay admission limits (BUZZ-SEC-019) (#1917) [codex] Block banned actors from moderation commands (BUZZ-SEC-007) (#1915) [codex] Fix relay WebSocket admission limits (#1682) fix(desktop): close mention popup after an exact name + trailing space (#2030) test(desktop): harden observer archive policy regression coverage (#1994) [codex] Bind release publishers to immutable tags (BUZZ-SEC-039) (#1914) fix: resolve invite downloads by platform (#2001) Bug-bash: mention/code-span, feed titles, autocomplete, edit-mentions, draft routing, and right-click media Download (#2027) fix(desktop): keep mesh allowlist on transient roster query failure (#2024) Polish Buzz theme and sidebar (#1971) Welcome new users with a live agent team kickoff (#1998) fix(desktop): case-insensitive relay media origin match, release 0.4.13 (#2023) feat(desktop): show owners in agent hover cards (#2015) ...
Problem
Shared-compute serves locally fine, but other relay members can't use it — intermittently, in a way that "works on my machine" and no test catches.
Root cause is a transient-failure race in the periodic admission-roster reconcile (pre-existing; not introduced by #2000):
resolve_trusted_owner_idsreturned an emptyVecon any query failure (e.g. relay503), indistinguishable from a genuinely empty community.reconcile_roster(polls every 60s) restarts the mesh node wheneverfresh != current. So a transient relay blip →empty roster → restart down to self-only, de-admitting every other member.Observed live:
roster query failed; allowing only this node: relay returned 503followed by repeatedmembership roster changed; restarting mesh node.Remote consumers therefore hit either a self-only allowlist (rejected at admission) or a node restarting mid-inference (dropped connection).
Fix
resolve_trusted_owner_idsnow returnsResult<Vec<String>, String>— a failed query isErr, distinct fromOk(empty).resolve_trusted_owner_ids_or_self_only(failing closed to self-only is only correct at initial start, when there's no live allowlist to preserve).reconcile_roster: on queryErr, keep the current allowlist and retry next poll instead of restarting. Extracted a pureroster_reconcile_actionso the invariant is unit-testable without a live relay.Not a revert of #2000
#2000's consumer-admission changes stay intact. This hardens the same path against transient relay errors.
Tests
New regression tests (the ones that would have caught this):
failed_roster_query_keeps_current_allowlist— the bugunchanged_roster_is_a_noopgenuinely_changed_roster_restarts_with_fresh_ownersgenuinely_empty_roster_restarts_to_self_only—Ok(empty)still allowed to shrink to self-onlyAll 11 mesh unit tests pass;
cargo check --features mesh-llmclean.Follow-up (out of scope)
This fixes the admission flapping. Whether the published
serveTargetsreliably reach the relay for cross-member discovery is a separate thread. There is still no automated e2e covering real cross-member discovery+admission over a gated relay — every existing mesh test loopbacks, hand-copies the invite token, or skips.