test: add threadpool Start-Stop race coverage
What changed, and why it matters
This commit only adds a new automated test to Bitcoin Core. It does not change any production code. The test checks that the thread pool can safely handle a Start() call happening while Stop() is still running, without getting stuck in a deadlock. It is a defensive test, not a fix for a known live bug.
No action required. Review the test for correctness and consider whether the production ThreadPool implementation already handles the race condition the test exercises, since the commit only adds coverage and does not patch behavior.
Security signals we found
Concurrency/deadlock test added for thread pool Start/Stop race
Test expects specific exception message for interrupted/stopping state
No changes to non-test source files
Evidence from the diff
The diff adds a single BOOST_AUTO_TEST_CASE named start_mid_stop_does_not_deadlock to src/test/threadpool_tests.cpp. The test starts a ThreadPool, blocks all workers with a semaphore, calls Stop() on a separate thread, waits until WorkersCount() drops to 0 (indicating Stop() has taken ownership and is joining), then calls Start() again. It expects either a successful restart or an exception with message “Thread pool has been interrupted or is stopping”. No production code is modified, and the commit message describes it purely as test coverage.
Changed components
src/test/threadpool_tests.cppInspect captured patch +35 / −0
diff --git a/src/test/threadpool_tests.cpp b/src/test/threadpool_tests.cpp
index aee9a722..d956c205 100644
--- a/src/test/threadpool_tests.cpp
+++ b/src/test/threadpool_tests.cpp
@@ -37,6 +37,7 @@ struct ThreadPoolFixture {
// 8) Submit task when all threads are busy, stop pool and verify task gets executed.
// 9) Congestion test; create more workers than available cores.
// 10) Ensure Interrupt() prevents further submissions.
+// 11) Start() must not cause a deadlock when called during Stop().
BOOST_FIXTURE_TEST_SUITE(threadpool_tests, ThreadPoolFixture)
#define WAIT_FOR(futures) \
@@ -316,4 +317,38 @@ BOOST_AUTO_TEST_CASE(interrupt_blocks_new_submissions)
BOOST_CHECK_EQUAL(threadPool.WorkersCount(), 0);
}
+// Test 11, Start() must not cause a deadlock when called during Stop()
+BOOST_AUTO_TEST_CASE(start_mid_stop_does_not_deadlock)
+{
+ ThreadPool threadPool(POOL_NAME);
+ threadPool.Start(NUM_WORKERS_DEFAULT);
+
+ // Keep all workers busy so Stop() gets stuck waiting for them to finish during join()
+ std::counting_semaphore<> workers_blocker(0);
+ const auto blocking_tasks = BlockWorkers(threadPool, workers_blocker, NUM_WORKERS_DEFAULT);
+
+ std::thread stopper_thread([&threadPool] { threadPool.Stop(); });
+
+ // Stop() takes ownership of the workers before joining them, so WorkersCount()
+ // hits 0 the moment Stop() is waiting for them to join. That is our signal
+ // to call Start() right into the middle of the joining phase.
+ while (threadPool.WorkersCount() != 0) {
+ std::this_thread::yield(); // let the OS breathe so it can switch context
+ }
+ // Now we know for sure the stopper thread is hanging while workers are still alive.
+ // Restart the pool and resume workers so the stopper thread can proceed.
+ // This will throw an exception only if the pool handles Start-Stop race properly,
+ // otherwise it will proceed and hang the stopper_thread.
+ try {
+ threadPool.Start(NUM_WORKERS_DEFAULT);
+ } catch (std::exception& e) {
+ BOOST_CHECK_EQUAL(e.what(), "Thread pool has been interrupted or is stopping");
+ }
+ workers_blocker.release(NUM_WORKERS_DEFAULT);
+ WAIT_FOR(blocking_tasks);
+
+ // If Stop() is stuck, joining the stopper thread will deadlock
+ stopper_thread.join();
+}
+
BOOST_AUTO_TEST_SUITE_END()
Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.