Bound _remove_thread_locked wait to avoid indefinite _scaling_mutex hold

_remove_thread_locked enqueues a self-exit task and blocks on the
future while holding _scaling_mutex. If all workers are busy with
long-running tasks, this blocks indefinitely — starving the monitor
thread (and any concurrent add_thread/set_threads) since they all
contend on _scaling_mutex.

Replace the unbounded wait with a 5-second timed wait. On timeout,
return -EBUSY so callers can retry or report the condition. The
enqueued self-exit task will still be picked up eventually and the
thread will remove itself when a worker becomes free.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Antonio S. Musumeci 2026-04-09 21:51:50 -04:00
parent 8baf274cde
commit 0bef28d154

View file

@ -26,6 +26,7 @@
#include <algorithm>
#include <atomic>
#include <cerrno>
#include <chrono>
#include <csignal>
#include <cstring>
#include <future>
@ -731,6 +732,11 @@ ThreadPool::_remove_thread_locked()
// via promise. The worker loop checks _tl_should_exit after
// running the func and exits cleanly - no separate poison pill
// needed, so exactly one thread is removed per call.
//
// The future wait is bounded: if no worker picks up the task
// within 5 seconds (e.g. all workers running long tasks), bail
// with -EBUSY rather than blocking indefinitely while holding
// _scaling_mutex and starving the monitor thread.
auto promise = std::make_shared<std::promise<void>>();
auto func =
@ -742,7 +748,10 @@ ThreadPool::_remove_thread_locked()
};
_queue.enqueue_unbounded(std::move(func));
promise->get_future().wait();
auto status = promise->get_future().wait_for(std::chrono::seconds(5));
if(status == std::future_status::timeout)
return -EBUSY;
return 0;
}