Uh oh!
There was an error while loading. Please reload this page.
GH-48137: [C++] Restore ThreadPool state when a worker fails to start - #51107
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes exception-safety holes in Arrow’s C++ ThreadPool worker-launch path so that a failed std::thread construction cannot leave the pool in a wedged state (stale workers_ entries and inflated tasks_queued_or_running_) that would hang Shutdown() / WaitForIdle() and eventually prevent new workers from being started.
Changes:
- Make
LaunchWorkersUnlocked()erase the just-appendedworkers_entry ifstd::threadconstruction throws, then rethrow. - Move
tasks_queued_or_running_increment inSpawnReal()to after a successful worker launch attempt (keeping the launch heuristic equivalent). - Add a fork-based regression test that forces thread creation failure and asserts the pool returns to a clean state and can still shut down.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| cpp/src/arrow/util/thread_pool.h | Adds FRIEND_TEST access for the new fork-safety regression test. |
| cpp/src/arrow/util/thread_pool.cc | Restores ThreadPool internal invariants when worker thread creation throws; avoids counter leaks on launch failure. |
| cpp/src/arrow/util/thread_pool_test.cc | Adds FailedWorkerLaunch regression test that validates state restoration after forced thread creation failure. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
advitrocks9
commented
Sep 2, 2026
All three done. |
There was a problem hiding this comment.
🟡 Changes recommended
The implementation and test expectations currently conflict with the PR description’s stated exception-propagation behavior, and the new test alters a process-wide rlimit in-process rather than isolating it in a forked child (risking flakiness).
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Review effort level: Lite
| } catch (const std::exception& e) { | ||
| state_->workers_.erase(it); | ||
| return Status::UnknownError("Failed to launch worker thread: ", e.what()); | ||
| } |
| struct rlimit limit; | ||
| ASSERT_EQ(getrlimit(RLIMIT_NPROC, &limit), 0); | ||
| const rlim_t soft_limit = limit.rlim_cur; | ||
| limit.rlim_cur = 1; | ||
| if (setrlimit(RLIMIT_NPROC, &limit) != 0) { |
pitrou
commented
Sep 3, 2026
@github-actions crossbow submit -g cpp |
Revision: 0fa803a Submitted crossbow builds: ursacomputing/crossbow @ actions-b40897411d |
pitrou
left a comment
There was a problem hiding this comment.
Thanks for the update @advitrocks9 ! LGTM now.
pitrou
commented
Sep 3, 2026
(note: CI failures are unrelated) |
Rationale for this change
LaunchWorkersUnlockedappends an entry tostate_->workers_before constructing the threadthat owns it, and only the worker itself erases that entry. If the
std::threadconstructorfails, the entry stays behind with nothing left to remove it, so
Shutdownwaits forever onworkers_.empty(). The destructor takes the same path. The failure also escapedSpawnRealafter
tasks_queued_or_running_had been incremented, soWaitForIdlenever returned either,and once stale entries filled
workers_to capacity the pool stopped launching workers whileSpawnstill returned OK for tasks nothing would run.What changes are included in this PR?
LaunchWorkersUnlockedreturns aStatus. A failed thread construction erases the entry it hadreserved and returns an error, which
SpawnRealandSetCapacitypropagate. The task counter isincremented after the launch rather than before, so a failed launch cannot leak a count.
Are these changes tested?
TestThreadPool.FailedWorkerLaunchlowersRLIMIT_NPROCto 1, spawns a task, restores the softlimit, and then checks the pool reports no workers and no tasks and still shuts down. It skips on
macOS, where
RLIMIT_NPROCcounts processes rather than threads, and skips anywhere else thelowered limit does not stop thread creation, such as under root.
Are there any user-facing changes?
Yes.
Spawn,SubmitandSetCapacityused to let astd::system_errorescape when the OSrefused a new thread. They return an error
Statusnow.ThreadPool::Makeis unaffected, sinceworker threads are only started on demand and a new pool starts none.