fix(nests-tests): per-method relay reset + 2 s catalog timeout

Two further hardenings on top of the catalog-retry fix to drive
full-suite stability:

- `NativeMoqRelayHarness.resetShared()` — tears down the
  current shared relay subprocess and lets the next caller
  spawn a fresh one. ~500 ms cost per call (cargo binaries
  cached, only relay boot + UDP bind paid).
- `HangInteropTest.@BeforeTest` calls `resetShared()` so each
  scenario gets a clean relay; eliminates the per-subscriber-
  forward-queue + announce-table state that was accumulating
  across the 11 sequential tests in one JVM run.
- hang-listen catalog read per-attempt timeout bumped from
  500 ms → 2 s. Under concurrent-test load the wire round-
  trip can exceed 500 ms; longer per-attempt budget keeps the
  happy path fast (resolves on the first attempt) while
  tolerating slow handshakes.

Suite wallclock cost: ~5–6 s added (one fresh relay boot per
scenario), bringing a typical run to ~2:30. Stability gain
is the trade.

Note: full-suite stability isn't reverified in this commit —
running the suite repeatedly under three concurrent agent
worktrees (I7 reconnect, Phase 4 browser, I6 multi-listener)
caused massive resource contention. Will re-verify once the
parallel agents have completed and pushed.

https://claude.ai/code/session_01ERJPUYfdLPwZ99pr5EcEcV
This commit is contained in:
Claude
2026-05-07 00:32:41 +00:00
parent f9be7889a5
commit 706ccda677
3 changed files with 42 additions and 6 deletions
@@ -89,6 +89,15 @@ class HangInteropTest {
@BeforeTest @BeforeTest
fun gate() { fun gate() {
NativeMoqRelayHarness.assumeHangInterop() NativeMoqRelayHarness.assumeHangInterop()
// Reset the shared relay subprocess between scenarios.
// Sharing across all 11 methods in one JVM run means the
// relay's per-subscriber forward queues + announce
// tables accumulate state from prior tests, manifesting
// as intermittent catalog-cancel + sample-count flakes
// that don't reproduce in isolation. Per-method reboot
// costs ~500 ms (cargo binaries are cached) — acceptable
// for the stability gain.
NativeMoqRelayHarness.resetShared()
} }
/** I1: 5 s 440 Hz mono sine, asserted via FFT peak + ZCR. */ /** I1: 5 s 440 Hz mono sine, asserted via FFT peak + ZCR. */
@@ -153,6 +153,29 @@ class NativeMoqRelayHarness private constructor(
} }
} }
/**
* Tear down the current shared relay subprocess and start a
* fresh one. Used as a JUnit `@Before` hook by tests that
* need clean per-method relay state — under accumulated
* cross-test broadcasts / connections the relay's per-
* subscriber forward queues drift, manifesting as
* intermittent catalog-cancel and sample-count flakes that
* don't reproduce in isolation.
*
* Cost: ~500 ms per call (cargo binaries are cached, only
* the subprocess boot + UDP bind + first client handshake
* are paid). At 11 scenarios × 500 ms that's ~5.5 s added
* to the suite wallclock — acceptable trade for stability.
*/
fun resetShared() {
synchronized(sharedLock) {
shared?.let {
runCatching { it.close() }
}
shared = null
}
}
private fun doStart(): NativeMoqRelayHarness { private fun doStart(): NativeMoqRelayHarness {
check(isEnabled()) { check(isEnabled()) {
"NativeMoqRelayHarness.shared() called without -D$ENABLE_PROPERTY=true." "NativeMoqRelayHarness.shared() called without -D$ENABLE_PROPERTY=true."
@@ -168,11 +168,15 @@ async fn listen(
// Subscribe to the catalog and read the first published // Subscribe to the catalog and read the first published
// version. The catalog hook in Amethyst's // version. The catalog hook in Amethyst's
// `MoqLiteNestsSpeaker` (`setOnNewSubscriber`) can race the // `MoqLiteNestsSpeaker` (`setOnNewSubscriber`) can race the
// SUBSCRIBE bidi mid-suite — under accumulated state the // SUBSCRIBE bidi mid-suite — under accumulated relay state
// first try sometimes resolves with a "cancelled" stream // the first try sometimes resolves with "cancelled" before
// before the catalog frame arrives. Retry up to twice with // the catalog frame arrives. Retry up to 3 times with a
// a fresh subscribe; total wallclock ≤ 1 s, well inside // 2 s per-attempt timeout (6 s total worst case). Under
// the test's broadcast window. // concurrent load (multiple jvmTest workers contending for
// the relay) the first attempt's wire round-trip can
// exceed 500 ms; the longer per-attempt budget keeps the
// happy-path fast (resolves on the first attempt) while
// tolerating slow handshakes.
let info = { let info = {
let mut last_err: Option<anyhow::Error> = None; let mut last_err: Option<anyhow::Error> = None;
let mut decoded: Option<hang::Catalog> = None; let mut decoded: Option<hang::Catalog> = None;
@@ -181,7 +185,7 @@ async fn listen(
.subscribe_track(&hang::Catalog::default_track()) .subscribe_track(&hang::Catalog::default_track())
.context("subscribe catalog")?; .context("subscribe catalog")?;
let mut catalog = hang::CatalogConsumer::new(catalog_track); let mut catalog = hang::CatalogConsumer::new(catalog_track);
match tokio::time::timeout(Duration::from_millis(500), catalog.next()).await { match tokio::time::timeout(Duration::from_secs(2), catalog.next()).await {
Ok(Ok(Some(c))) => { Ok(Ok(Some(c))) => {
decoded = Some(c); decoded = Some(c);
break; break;