|
15 | 15 | #include <gtest/gtest.h> |
16 | 16 |
|
17 | 17 | #include <algorithm> |
| 18 | +#include <atomic> |
18 | 19 | #include <condition_variable> |
19 | 20 | #include <cstddef> |
20 | 21 | #include <functional> |
@@ -294,6 +295,44 @@ TEST_F(CaptureThreadPoolTest, ShutdownIsIdempotent) { |
294 | 295 | SUCCEED(); |
295 | 296 | } |
296 | 297 |
|
| 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 | + |
297 | 336 | // shutdown() releases the captured callback (and the shared_ptrs it holds) after |
298 | 337 | // joining - the load-bearing invariant behind the #441 destructor reorder. |
299 | 338 | TEST_F(CaptureThreadPoolTest, ReleasesCaptureFnAfterShutdown) { |
|
0 commit comments