From 27aabae935e7732a0146123cb26572636b5152cc Mon Sep 17 00:00:00 2001 From: Joe Rivera Date: Fri, 2 Oct 2026 12:09:06 -0500 Subject: [PATCH] test(state): fix apply-barrier race in MultiProcessIpc.BidirectionalReplication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test used wait_for_received(1) as the sync point before reading the replicated value from the tree. But wait_for_received counts frames DECODED by the reader thread, not frames APPLIED to the tree, and inbound frames are ingested on pump_all(). So the child checked `server_val == "from_server"` while only an earlier frame (or nothing) had been applied, captured a stale value, and exited 12 ("child did not receive server's value") — intermittently (~2/20 locally; it surfaced in the recipe lane). The parent's `got_client` check had the identical latent race. Fix: on both sides, poll the ACTUAL condition — pumping (pump_all/flush) so received frames are applied — until the expected value appears or a 5s deadline passes, instead of trusting the frame-decode counter. No product code changed. Verified: 100/100 runs of BidirectionalReplication pass. --- src/cvc/tests/state_multiprocess_ipc_test.cpp | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/src/cvc/tests/state_multiprocess_ipc_test.cpp b/src/cvc/tests/state_multiprocess_ipc_test.cpp index 0930bea50..bc9069dd5 100644 --- a/src/cvc/tests/state_multiprocess_ipc_test.cpp +++ b/src/cvc/tests/state_multiprocess_ipc_test.cpp @@ -174,9 +174,24 @@ TEST(MultiProcessIpcIntegration, BidirectionalReplication) { ct.pump_all(); ct.flush(); - // Wait for server's value. - ct.wait_for_received(1, std::chrono::milliseconds(5000)); - bool ok = ca.root()("server_val").value() == "from_server"; + // Wait for (and APPLY) the server's value. wait_for_received counts frames DECODED by the + // reader thread, NOT frames applied to the tree — and inbound frames are ingested on pump_all — + // so checking the tree right after wait_for_received could read a stale value, and the child + // intermittently exited 12 ("did not receive server's value"). Poll the actual condition, + // pumping so received frames are applied. + bool ok = false; + { + const auto dl = std::chrono::steady_clock::now() + std::chrono::milliseconds(5000); + while (std::chrono::steady_clock::now() < dl) { + ct.pump_all(); + ct.flush(); + if (ca.root()("server_val").value() == "from_server") { + ok = true; + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + } // Keep pumping so server receives ours. for (int i = 0; i < 40; ++i) { @@ -210,9 +225,21 @@ TEST(MultiProcessIpcIntegration, BidirectionalReplication) { st.pump_all(); st.flush(); - // Wait for client's value. - st.wait_for_received(1, std::chrono::milliseconds(5000)); - bool got_client = sa.root()("client_val").value() == "from_client"; + // Wait for (and APPLY) the client's value — same apply-barrier race as the child above: poll the + // actual condition, pumping so received frames are ingested, instead of trusting the frame count. + bool got_client = false; + { + const auto dl = std::chrono::steady_clock::now() + std::chrono::milliseconds(5000); + while (std::chrono::steady_clock::now() < dl) { + st.pump_all(); + st.flush(); + if (sa.root()("client_val").value() == "from_client") { + got_client = true; + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + } // Keep pumping so child gets our value. for (int i = 0; i < 80; ++i) {