Node offloads blocking work -- filesystem calls, crypto, dns.lookup,
zlib -- to libuv's thread pool instead of running it on the event loop
thread. That pool is deliberately a FIXED size (UV_THREADPOOL_SIZE,
default 4): at most that many tasks should ever be running at once, with
anything beyond that waiting in a FIFO queue until a worker frees up.
This ThreadPool is supposed to enforce exactly that cap, and it does --
as long as tasks are submitted one at a time with nothing else queued. But
submit a BURST of tasks back to back (several submit() calls before any
complete()), and the cap silently stops applying: every submitted task
starts "running" immediately, no matter how many workers the pool actually
has.
Your task: find the bug in _dispatch and fix it. submit and
complete are already correct -- don't change them.
Example that currently fails
pool = ThreadPool(size=2)
for task_id in range(5):
pool.submit(task_id)
len(pool.running) # should be 2 -- only `size` tasks run concurrently
# currently: 5 -- _dispatch() drains the entire queue
# regardless of how many workers are already busy
pool.queue # should be [2, 3, 4] -- the rest wait their turn
# currently: [] -- everything got dispatched at once
Real-world grounding
- libuv's thread pool dispatch:
src/threadpool.c(Node vendors this same file underdeps/uv/src/threadpool.c)
Real libuv workers pull from a shared queue behind a condition variable
rather than being "dispatched to" by the submitter, and uv_queue_work
itself just appends to that queue -- but the invariant this bug violates is
the one that actually matters for anyone tuning UV_THREADPOOL_SIZE: at
most size pieces of offloaded work run concurrently, no matter how many
got submitted in a burst. Break that invariant and a flood of fs.readFile
calls stops being bounded, defeating the entire point of a fixed-size pool.