From 8c5ffb6edb826587ea85f782cebbb6dbf5cb7718 Mon Sep 17 00:00:00 2001 From: mike Date: Sat, 1 Mar 2025 18:09:51 +0100 Subject: [PATCH] make park_section_no_miss.cpp test less brittle --- .../park_section_no_miss.cpp | 134 ++++++++++++------ 1 file changed, 89 insertions(+), 45 deletions(-) diff --git a/tests/park_section_no_miss/park_section_no_miss.cpp b/tests/park_section_no_miss/park_section_no_miss.cpp index ed96405..7a35a5d 100644 --- a/tests/park_section_no_miss/park_section_no_miss.cpp +++ b/tests/park_section_no_miss/park_section_no_miss.cpp @@ -2,70 +2,114 @@ #include #include +#include #include #include #include -static constexpr int NUM_ITERATIONS = 100; +static constexpr int NUM_ITERATIONS = 15; // total iterations; each has a scenario +static constexpr auto MAX_BLOCK_MS = 5000; // 5s => generous for slow/VM systems -static constexpr auto MAX_WAIT_MS = 500; +// Shared data for consumer/producer synchronization +static std::mutex g_mutex; +static std::condition_variable g_cv; -int main() { - tpark_handle_t *handle = tparkCreateHandle(); - std::atomic stop{false}; +// Let consumer tell producer "I just set the park bit" +static bool g_consumerReady = false; - // Producer thread: tries to wake the consumer after a short delay +// Let producer tell consumer "I have woken you (or tried to wake you)" +static bool g_wakeDone = false; + +// We’ll store test failures if we detect a lost wake +static std::atomic g_testFailed{false}; + +int main() +{ + // 1) Create our thread parking handle + tpark_handle_t* handle = tparkCreateHandle(); + + // 2) Producer: wakes the consumer under different timing scenarios std::thread producer([&]() { - for (int i = 0; i < NUM_ITERATIONS && !stop.load(); i++) { - // Sleep a *tiny* bit so consumer definitely has set "state=1" - // but maybe hasn't called tparkParkWait yet. - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + for (int i = 0; i < NUM_ITERATIONS && !g_testFailed.load(); i++) + { + // Acquire lock and wait until consumer sets the park bit + std::unique_lock lk(g_mutex); + g_cv.wait(lk, [] { return g_consumerReady || g_testFailed.load(); }); + if (g_testFailed.load()) break; + + // Decide how long to wait before calling wake + // scenario 0 = immediate + // scenario 1 = 100 ms + // scenario 2 = 500 ms + int scenario = i % 3; + auto sleepMs = (scenario == 0) ? 0 + : (scenario == 1) ? 100 + : 500; - // Attempt to wake — if consumer hasn't actually parked yet, - // we want to verify we do *not* lose this wake. + // Sleep some time to let the consumer possibly call parkWait + lk.unlock(); + std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs)); + lk.lock(); + + // Now call wake tparkWake(handle); + + // Indicate to the consumer "we're done calling wake" + g_wakeDone = true; + g_consumerReady = false; + g_cv.notify_one(); } }); - bool lostWake = false; - - // Consumer: in each iteration, does a "two-phase" park - for (int i = 0; i < NUM_ITERATIONS; i++) { - // 1) Begin parking (set the park bit to 1) - tparkBeginPark(handle); - - // 2) Simulate some short pause before actually calling parkWait - // The producer may call tparkWake() in this window. - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - - // 3) Actually park. But if the wake arrived "too early", - // the state is already zero and we won't block. - // For safety, let's measure how long we actually block. - auto t1 = std::chrono::steady_clock::now(); - tparkWait(handle, /*unlocked=*/true); - auto t2 = std::chrono::steady_clock::now(); - - // Check we didn't block too long - auto msBlocked = std::chrono::duration_cast(t2 - t1).count(); - if (msBlocked > MAX_WAIT_MS) { - std::cerr << "[Iteration " << i << "] Thread blocked too long (" - << msBlocked << " ms). Potential lost wake-up!\n"; - lostWake = true; - break; - } - } + // 3) Consumer: sets the park bit, signals producer, then calls parkWait + std::thread consumer([&]() { + for (int i = 0; i < NUM_ITERATIONS; i++) + { + // 3.1) "Begin Park": set bit=1 + { + std::unique_lock lk(g_mutex); + tparkBeginPark(handle); - // Signal producer to exit if we ended early - stop.store(true); + // Indicate "ready" so producer knows to do a wake + g_consumerReady = true; + g_wakeDone = false; + g_cv.notify_one(); + + // Wait until producer has definitely done a wake attempt + g_cv.wait(lk, [] { return g_wakeDone || g_testFailed.load(); }); + if (g_testFailed.load()) return; + } // unlock the mutex + + // 3.2) Now measure how long we actually block in parkWait + // Because the producer may have woken us "too early," we might skip real blocking + auto t1 = std::chrono::steady_clock::now(); + tparkWait(handle, /*unlocked=*/true); + auto t2 = std::chrono::steady_clock::now(); + + auto msBlocked = std::chrono::duration_cast(t2 - t1).count(); + if (msBlocked > MAX_BLOCK_MS) + { + std::cerr << "[Iteration " << i << "] BLOCKED too long (" + << msBlocked << " ms). Potential lost wake or deadlock.\n"; + g_testFailed.store(true); + return; + } + + // Optionally reset the bit (harmless if tparkWake already set it to 0) + tparkEndPark(handle); + } + }); producer.join(); + consumer.join(); + tparkDestroyHandle(handle); - if (lostWake) { - std::cerr << "TEST FAILED: A lost wake-up seems to have occurred.\n"; + if (g_testFailed.load()) { + std::cerr << "TEST FAILED: Lost wake or indefinite blocking scenario encountered.\n"; return EXIT_FAILURE; - } else { - std::cout << "TEST PASSED: No lost wakes in " << NUM_ITERATIONS << " iterations.\n"; } + std::cout << "TEST PASSED: No lost wakes in " << NUM_ITERATIONS + << " iterations with varying timing.\n"; return EXIT_SUCCESS; }