Visitar URL original
libutil: don't make ThreadPool work items wait for process() by czawistowski-nvidia · Pull Request #718 · DeterminateSystems/nix-src · GitHub
Skip to content

libutil: don't make ThreadPool work items wait for process() - #718

Open
czawistowski-nvidia wants to merge 1 commit into
DeterminateSystems:mainfrom
czawistowski-nvidia:thread-pool-early-start
Open

czawistowski-nvidia wants to merge 1 commit into
DeterminateSystems:mainfrom
czawistowski-nvidia:thread-pool-early-start

Conversation

@czawistowski-nvidia

Copy link
Copy Markdown

Motivation

ThreadPool::enqueue() counts the thread that will later call process() as a worker, even before that thread has called process(). It starts a new worker only when pending > workers + 1, so before process():

  • a single item enqueued into an idle pool doesn't run at all;
  • in general, only K−1 of K pending items run.

The header describes process() as "Execute work items until the queue is empty", and nothing in the API says enqueued work is held back until then. A caller that enqueues work and waits for it some other way (a future, a condition variable, an in-flight counter) before calling process() hangs.

As far as we can tell, no in-tree caller hits this today. Every one either calls process() right after enqueueing, or keeps enqueueing so a second item soon starts a worker. We ran into it while working on GC deletion in background threads (a follow-up we plan to send), which waits for in-flight deletions before draining the pool. There, nix-store --gc --max-freed X hung whenever the first dead path was already larger than X.

Implementation

Count the calling thread only once process() has started (state->draining, already set by process() under the same lock):

auto busyThreads = state->workers.size() + (state->draining ? 1 : 0);
if (state->pending.size() > busyThreads && state->workers.size() + 1 < maxThreads)
  • After process() has started: spawning is identical to before, so all existing fan-out is unchanged.
  • Thread cap: unchanged. workers.size() + 1 < maxThreads still reserves the caller's slot, and ThreadPool(1) still runs everything on the caller inside process().
  • Exceptions: exceptions from items that ran before process() are still rethrown by process().

Context

New unit test ThreadPool.executesWorkBeforeProcess enqueues one item into a ThreadPool(2) and waits for it before calling process(). On main the item never starts, so the test fails when its wait times out; with this change it passes.

🤖 Claude

`enqueue()` counted the thread that will call `process()` as a worker,
so it only started a new thread once there were more pending items
than workers plus one. Before `process()` is called, that thread isn't
executing anything, so a single enqueued item wasn't executed until
`process()` was called (or until more items were enqueued). This
makes it impossible to wait for the completion of an item before
calling `process()`.

Only count the calling thread once `process()` has been called, and add
a unit test.

Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 21, 2026

Copy link
Copy Markdown

Warning

Review limit reached

  • Run on-demand review

This review includes 3 billable files and costs up to $0.75.

Or wait 57 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 22a29e5c-1d32-4bc6-9ae4-1a6b3fbaf3c1
📥 Commits

Reviewing files that changed from the base of the PR and between a688fad and 07cfbfe.

📒 Files selected for processing: 3
  • src/libutil-tests/meson.build
  • src/libutil-tests/thread-pool.cc
  • src/libutil/thread-pool.cc
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant