tests: block the strand drain-race case's coarse rendezvous instead of spinning - #834
Merged
Merged
Conversation
…f spinning The drain-race case's per-round rendezvous (a chaser waiting for the pilot to open `gate`, and the main thread waiting for the round's `completed` count) busy-spun. Nothing blocks a spinning thread from being scheduled, so under Valgrind -- which runs one thread at a time and lets a yielding thread take the lock straight back -- the spin burns real instrumented CPU time for the whole round instead of yielding it to the actual work, stalling the case for tens of minutes. Both coarse waits now block on a condition variable instead, notified when the round's task count crosses its threshold. The fine-grained stagger loop that samples the actual few-instruction drain-and-erase window is untouched: it is not a rendezvous wait, and needs the spin's precision to land inside a window a blocking wait's wake latency would overshoot. Fixes #826 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fEUahMFF32wQLiWjbsfkc Signed-off-by: Yaraslau Tamashevich <yaraslau.tamashevich@gmail.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
tests/test_strand_race.cpp's "keeps one strand per key when a post races the drain" case synchronised its threads by spinning: four chaser threads spun on an atomicgate(yielding once every 4096 spins), and the main thread spun on an atomiccompletedcounter. Neither wait yields the CPU meaningfully to the actual work, so under Valgrind -- which runs one thread at a time and lets a yielding thread take the lock straight back -- the spin burns real instrumented CPU time for the whole round instead of letting the (slow, instrumented) work proceed. Measured on real CI runs (cited in #826): ~45 of a 54-minute job, and a separate run cancelled after >3h.This PR replaces both coarse per-round waits with a
std::condition_variableblocking wait. The fine-grained stagger loop that samples the actual few-instruction drain-and-erase race window is untouched -- it needs the spin's precision, and a blocking wait's wake latency would overshoot a window that small.As part of
/simplify,completed/gatewere also changed fromstd::atomic<int>to plainint(every access is now under the same mutex the condition variables use, so the atomics were redundant), andbody()'s notify was narrowed to fire only when a task's completion crosses the round's threshold, rather than on every one of a round'skPerRoundtasks.Verification
ctest, 1605/1605 tests) on a plain Release build, and the fixed case passes cleanly under ThreadSanitizer (36/36 assertions, 3 runs).gatepublish, while a blocking wait's OS wake latency is 1-50+ us -- so it's not obvious a blocking rendezvous still lands chaser posts inside that window. Verified empirically rather than assumed: reintroduced the historical two-step drain bug this test regresses (post()releasing_mapMtxbefore re-arming understrand->mtxonly, andscheduleNext's drain flippingrunning/erasing in two separate critical sections instead of one) in a local, uncommitted mutant ofinclude/morph/core/strand.hpp, then ran the drain-race case under TSan 10 times against both the original spin-based test and the new blocking-wait version. Both caught the reintroduced race 10/10 (a TSan data-race report plus a plain-state assertion failure each time).include/morph/core/strand.hppitself is unchanged in this PR -- the mutant was local-only, for verification, and reverted before committing.Adjacent finding (out of scope here)
tests/test_strand_race.cpp's third case ("recycles a drained strand under the key that asked for it") has a structurally identical per-round spin-wait, not covered by #826's issue text or triage. Filed as #833 rather than folded into this PR.Test plan
ctestfull suite green (1605/1605) on a plain Release build--fair-sched=yesworkaround from Move morph onto core-cpp v0.5.0: coroutine model handlers on core-cpp strands, CPM dependencies #806, since that PR is unmerged -- this fix should let the case complete without CI's own workaround)Fixes #826
🤖 Generated with Claude Code
https://claude.ai/code/session_018fEUahMFF32wQLiWjbsfkc