Skip to content

Commit be03e88

Browse files
committed
test(fault_manager): cover concurrent enqueue during pool shutdown
Add a producer thread that hammers enqueue() while shutdown() runs on another thread - the interleaving the kRejectedShuttingDown guard exists for. Asserts the shutdown transition is observed, post-shutdown enqueues reject cleanly, and no job runs that was never accepted. Race-clean under TSan.
1 parent 60eab2f commit be03e88

1 file changed

Lines changed: 39 additions & 0 deletions

File tree

src/ros2_medkit_fault_manager/test/test_capture_thread_pool.cpp

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <gtest/gtest.h>
1616

1717
#include <algorithm>
18+
#include <atomic>
1819
#include <condition_variable>
1920
#include <cstddef>
2021
#include <functional>
@@ -294,6 +295,44 @@ TEST_F(CaptureThreadPoolTest, ShutdownIsIdempotent) {
294295
SUCCEED();
295296
}
296297

298+
// Hammer enqueue() from a producer thread while shutdown() runs concurrently -
299+
// the exact interleaving the kRejectedShuttingDown guard exists for (a fault
300+
// arriving as the destructor sets stop_). Must be race-clean under TSan: every
301+
// enqueue is cleanly accepted or rejected, never UB, and no job runs that was
302+
// never accepted.
303+
TEST_F(CaptureThreadPoolTest, ConcurrentEnqueueDuringShutdownIsClean) {
304+
std::atomic<int> ran{0};
305+
std::atomic<int> accepted{0};
306+
CaptureThreadPool pool(4, 64, QueueFullPolicy::kRejectNewest, test_logger(), [&ran](const std::string &) {
307+
ran.fetch_add(1, std::memory_order_relaxed);
308+
});
309+
310+
std::atomic<bool> saw_shutdown{false};
311+
std::thread producer([&] {
312+
for (int i = 0; i < 100000; ++i) {
313+
const EnqueueResult r = pool.enqueue("j" + std::to_string(i)).result;
314+
if (r == EnqueueResult::kAccepted) {
315+
accepted.fetch_add(1, std::memory_order_relaxed);
316+
} else if (r == EnqueueResult::kRejectedShuttingDown) {
317+
saw_shutdown.store(true);
318+
break; // pool is down; further enqueues would only ever reject
319+
}
320+
}
321+
});
322+
323+
// Race the producer: tear the pool down while it is still hammering enqueue().
324+
pool.shutdown();
325+
producer.join();
326+
327+
// The producer must have crossed the shutdown transition (else the race was not
328+
// exercised), and every post-shutdown enqueue must reject cleanly.
329+
EXPECT_TRUE(saw_shutdown.load());
330+
EXPECT_EQ(pool.enqueue("after").result, EnqueueResult::kRejectedShuttingDown);
331+
// shutdown() discards pending work, so some accepted jobs may not run - but no
332+
// job may run that was never accepted.
333+
EXPECT_LE(ran.load(), accepted.load());
334+
}
335+
297336
// shutdown() releases the captured callback (and the shared_ptrs it holds) after
298337
// joining - the load-bearing invariant behind the #441 destructor reorder.
299338
TEST_F(CaptureThreadPoolTest, ReleasesCaptureFnAfterShutdown) {

0 commit comments

Comments
 (0)