Skip to content

fix(mesh): frame loss when recv_validated is cancelled mid-validation (#2458)#2745

Open
brocoppler wants to merge 1 commit into
block:mainfrom
brocoppler:fix/mesh-demo-loopback-flake
Open

fix(mesh): frame loss when recv_validated is cancelled mid-validation (#2458)#2745
brocoppler wants to merge 1 commit into
block:mainfrom
brocoppler:fix/mesh-demo-loopback-flake

Conversation

@brocoppler

Copy link
Copy Markdown

Summary

Root-causes the #2458 mesh_demo flake to a cancel-safety bug in production code and fixes it, plus one piece of endpoint hygiene the investigation surfaced.

The bug: run_demo_echo races recv_validated against its 100 ms drain tick in a select!. recv_validated reads a frame off the stream, then awaits Redis fence validation. When the tick fires inside that await, the future is dropped with the frame it already consumed — the stream then looks idle forever, the forwarding peer waits for an echo that can no longer happen, and the probe times out (the familiar 504). Loss probability per exchange ≈ validation latency ÷ 100 ms, which is why it worsens exactly when Redis is slow — i.e. under full-suite parallelism, matching #2458's "fails under cargo test, passes standalone" signature. Reproduced at 40–80 % locally, on an in-memory stream pair and live iroh endpoints alike, which rules the transport out as the cause.

Changes

  • tunnel/reliable.rs — split validate_received out of recv_validated so callers can extract frames cancel-safely and validate outside any select!/timeout region; the cancel-safety contract is documented on both.
  • mesh_boot.rsrun_demo_echo now selects only over frame extraction; decoding + fence validation run outside the cancellable region. Drain semantics unchanged.
  • buzz-relay-mesh/peer.rsIrohRecvHalf::recv_frame is now resumable: partial-read state lives in the half rather than the future (each underlying RecvStream::read await is itself cancel-safe), so racing recv_frame cannot desync the stream either.
  • buzz-relay-mesh/endpoint.rs — loopback binds disable the iroh portmapper. A loopback-bound socket can never transmit to a WAN mapping, but the gateway probe (UPnP/PCP/NAT-PMP) advertised one as a Portmapped candidate and path attempts to it fail (sendmsgEADDRNOTAVAIL) — observed live during the investigation on a NAT-PMP-capable network. Production binds (0.0.0.0) are unaffected. Regression test included (trivially green on hosts without a portmap-capable gateway).
  • api/mesh_demo.rs — the forwarded-arm round trip now runs over an in-memory MeshStream pair by default: deterministic, fast, and it reproduced the frame loss identically, making it the regression test for the fix. The live-iroh variant is kept as demo_join_forwarded_arm_round_trips_echo_live_mesh under #[ignore] for explicit transport evidence runs.

Verification

build variant runs result
pre-fix in-memory pair 10 6 fail (504)
pre-fix live iroh 5 3 fail (504)
post-fix in-memory pair 15 15 pass
post-fix live iroh (--ignored) 15 15 pass
post-fix full cargo test -p buzz-relay --lib 3 744 passed ×3

cargo clippy -p buzz-relay -p buzz-relay-mesh --lib --tests: clean. buzz-relay-mesh suite: 33 passed.

Relation to existing work

Fixes #2458

🤖 Generated with Claude Code

…e silently lost (block#2458)

run_demo_echo raced recv_validated against its 100ms drain tick in a
select!. recv_validated reads a frame off the stream and then awaits Redis
fence validation; a tick firing inside that await dropped the future and
destroyed the frame it had already consumed. The stream then looked idle
forever and the forwarding peer timed out — the block#2458 mesh_demo 504s, with
loss probability ≈ validation latency / 100ms (worse under suite load,
when Redis is slower).

- reliable: split validate_received out of recv_validated so callers can
  extract frames cancel-safely and validate outside select!/timeout;
  document the cancel-safety contract.
- mesh_boot: run_demo_echo selects only over frame extraction and
  validates outside the cancellable region.
- buzz-relay-mesh: make IrohRecvHalf::recv_frame resumable (partial-read
  state lives in the half, not the future) so racing it is actually safe.
- buzz-relay-mesh: loopback binds disable the iroh portmapper — a
  loopback socket can never use a WAN mapping, but the probe advertised
  one as a Portmapped candidate and path attempts to it fail with
  EADDRNOTAVAIL (regression test included).
- mesh_demo: the forwarded-arm round trip runs over an in-memory stream
  pair by default (deterministic; reproduced the loss just like live
  transport) with the live-iroh variant kept as an #[ignore] evidence
  smoke.

Verified: pre-fix the round trip failed ~40–80% locally (in-memory and
live alike); post-fix 15/15 in-memory + 15/15 live + 3x full
cargo test -p buzz-relay --lib green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Broc Oppler <brocoppler@gmail.com>
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.

test(relay): mesh_demo echo test flaky under parallel suite — passes standalone, fails full-suite on clean main

1 participant