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

View File

@@ -89,6 +89,15 @@ class HangInteropTest {
@BeforeTest
fun gate() {
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. */

View File

@@ -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 {
check(isEnabled()) {
"NativeMoqRelayHarness.shared() called without -D$ENABLE_PROPERTY=true."

View File

@@ -168,11 +168,15 @@ async fn listen(
// Subscribe to the catalog and read the first published
// version. The catalog hook in Amethyst's
// `MoqLiteNestsSpeaker` (`setOnNewSubscriber`) can race the
// SUBSCRIBE bidi mid-suite — under accumulated state the
// first try sometimes resolves with a "cancelled" stream
// before the catalog frame arrives. Retry up to twice with
// a fresh subscribe; total wallclock ≤ 1 s, well inside
// the test's broadcast window.
// SUBSCRIBE bidi mid-suite — under accumulated relay state
// the first try sometimes resolves with "cancelled" before
// the catalog frame arrives. Retry up to 3 times with a
// 2 s per-attempt timeout (6 s total worst case). Under
// 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 mut last_err: Option<anyhow::Error> = None;
let mut decoded: Option<hang::Catalog> = None;
@@ -181,7 +185,7 @@ async fn listen(
.subscribe_track(&hang::Catalog::default_track())
.context("subscribe catalog")?;
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))) => {
decoded = Some(c);
break;