Re-check cooldown under lock in _maybe_grow_on_pressure

The pre-lock cooldown check used a stale _last_scale_time_usecs read.
If another thread (monitor hill-climb, concurrent enqueue) updated
the timestamp between the pre-lock check and the lock acquisition,
the function would proceed to acquire _scaling_mutex and contend
unnecessarily.  The CAS that followed would correctly reject the
stale value, so this was not a correctness bug, but it caused
avoidable lock contention on the hot enqueue path.

Add an authoritative cooldown re-check after acquiring _scaling_mutex
with a fresh timestamp read.  Replace the CAS with a plain store
since the _scaling_mutex already serializes writers.  Keep the
pre-lock speculative check as a fast-path filter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Antonio S. Musumeci 2026-04-10 01:15:21 -04:00
parent 11fafba6ae
commit 4b5b102e6f

View file

@ -669,16 +669,24 @@ ThreadPool::_maybe_grow_on_pressure()
if(!_autoscale_enabled)
return;
std::uint64_t now = _now_usecs();
std::uint64_t last = _last_scale_time_usecs.load(std::memory_order_relaxed);
if((now - last) < (std::uint64_t)_scaling_config.cooldown_usecs)
return;
// Quick check before contending on the lock. Uses a stale read
// but the authoritative re-check happens under the lock below.
{
std::uint64_t now = _now_usecs();
std::uint64_t last = _last_scale_time_usecs.load(std::memory_order_relaxed);
if((now - last) < (std::uint64_t)_scaling_config.cooldown_usecs)
return;
}
// Hold _scaling_mutex so the thread-count check and add are atomic
// with respect to other scaling operations.
mutex_lockguard(_scaling_mutex);
std::uint64_t now = _now_usecs();
std::uint64_t last = _last_scale_time_usecs.load(std::memory_order_relaxed);
if((now - last) < (std::uint64_t)_scaling_config.cooldown_usecs)
return;
std::size_t count;
{
mutex_lockguard(_threads_mutex);
@ -688,12 +696,7 @@ ThreadPool::_maybe_grow_on_pressure()
if(count >= _scaling_config.max_threads)
return;
// CAS to claim the cooldown window — only one caller wins.
// Done after the max-threads check so we don't burn the cooldown
// when the pool is already at capacity.
if(!_last_scale_time_usecs.compare_exchange_strong(last,now,
std::memory_order_relaxed))
return;
_last_scale_time_usecs.store(now,std::memory_order_relaxed);
_add_thread_scaling_locked({});
syslog(LOG_DEBUG,