Skip to content

fix(bus): synchronous handler registration to de-flake round-trip tests - #59

Merged
hartsock merged 1 commit into
mainfrom
issue-52/deflake-mdns-roundtrip-tests
Jul 14, 2026
Merged

fix(bus): synchronous handler registration to de-flake round-trip tests#59
hartsock merged 1 commit into
mainfrom
issue-52/deflake-mdns-roundtrip-tests

Conversation

@hartsock

Copy link
Copy Markdown
Member

What this PR does

Hardens the #52 de-flake by fixing the root-cause nondeterminism the
direct-dial round-trip tests were papering over with a fixed sleep.

Bus::handle_requests registered its handler on a detached tokio::spawn
and returned before that task had run. A request dialed to the topic in the
gap found no handler in Inbox::dispatch_request, which silently returns
Ok(None) (no reply) — the asker just times out. On a loaded CI runner the
sleep(200ms) crutch can lose that race; that is precisely the flake class
#52 set out to eliminate.

  • inbox.rs: the handlers map only ever holds its lock for a brief
    get().cloned() / insert() — never across an .await — so it does not
    need tokio::sync::RwLock. Switch it to std::sync::RwLock, make
    register_handler a synchronous fn, and add a handler_count()
    diagnostic accessor.
  • bus.rs: handle_requests now installs the handler before it returns
    (no spawn, no window). Drops the 4x yield_now() crutch in the in-memory
    round-trip unit test.
  • tests: remove the now-needless sleep(200ms) from the deterministic
    direct-dial round-trips (bus_roundtrip.rs, mcp_stdio.rs).

Test plan

  • New regression test handle_requests_registers_synchronously_no_spawn_race
    (agent-mesh-bus/src/bus.rs): asserts the handler is registered the instant
    handle_requests returns, with no yield between. Verified it
    deterministically fails on the old spawn-based code (handler_count 0
    vs 1) and passes now.
  • Stress-ran the sleep-removed tests to confirm the synchronous path is
    stable without the timing crutch:
    • request_reply_roundtrip_via_direct_dial_no_mdns — 30/30
    • mcp_server_direct_addr_round_trips_to_live_responder — 30/30
    • request_reply_roundtrip_over_in_memory_transport (yields removed) — 30/30
  • just check (workspace fmt + clippy -D warnings + test) and just cov-ci
    (--fail-under-lines 75) green locally via the pre-push hook. pyo3
    feature build clippy-clean.

Out of scope

Refs #52 — de-flake portion. The issue's feature scope appears already
complete on main; leaving the close decision to a human rather than
auto-closing.

…rips

`Bus::handle_requests` registered its handler on a detached `tokio::spawn`
and returned before the spawned task had run. A request dialed to that
topic in the intervening window found no handler in
`Inbox::dispatch_request`, which silently returns `Ok(None)` (no reply) —
so the asker just timed out. The direct-dial round-trip tests papered
over the race with a fixed `sleep(200ms)` (and the in-memory unit test
with four `yield_now()`s): a timing crutch that can still lose the race
on a loaded CI runner.

Root-cause fix: the `handlers` map only ever holds its lock for a brief
`get().cloned()` / `insert()` (never across an `.await`), so it does not
need `tokio::sync::RwLock`. Switch it to `std::sync::RwLock`, make
`Inbox::register_handler` synchronous, and have `handle_requests` install
the handler before it returns. No spawn, no window, no sleep.

- inbox.rs: `handlers` -> `std::sync::RwLock`; `register_handler` is now a
  sync `fn`; add `handler_count()` diagnostic accessor.
- bus.rs: `handle_requests` registers synchronously; drop the 4x
  `yield_now()` crutch in the in-memory round-trip test.
- Remove the now-needless `sleep(200ms)` from the deterministic
  direct-dial round-trips (bus_roundtrip.rs, mcp_stdio.rs).

Regression test `handle_requests_registers_synchronously_no_spawn_race`
asserts the handler is registered the instant `handle_requests` returns,
with no yield in between. On a current-thread runtime this deterministically
fails on the old spawn-based code (count 0) and passes now (count 1).

Refs #52 (de-flake portion).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@hartsock hartsock added the risk:low Low-risk change: scoped, tested, no CI/build/hook edits label Jul 13, 2026
@hartsock
hartsock merged commit 6b40b99 into main Jul 14, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:low Low-risk change: scoped, tested, no CI/build/hook edits

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant