diff --git a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/native/HangInteropTest.kt b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/native/HangInteropTest.kt index 34daef679..74ca7fd82 100644 --- a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/native/HangInteropTest.kt +++ b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/native/HangInteropTest.kt @@ -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. */ diff --git a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/native/NativeMoqRelayHarness.kt b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/native/NativeMoqRelayHarness.kt index 4b2fd6798..586991507 100644 --- a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/native/NativeMoqRelayHarness.kt +++ b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/native/NativeMoqRelayHarness.kt @@ -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." diff --git a/nestsClient/tests/hang-interop/hang-listen/src/main.rs b/nestsClient/tests/hang-interop/hang-listen/src/main.rs index 63faa0795..89b5d33e7 100644 --- a/nestsClient/tests/hang-interop/hang-listen/src/main.rs +++ b/nestsClient/tests/hang-interop/hang-listen/src/main.rs @@ -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 = None; let mut decoded: Option = 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;