ruby-concurrency/concurrent-ruby · error · Concurrent::ResourceLimitError
Too many reader threads
Error message
Too many reader threads
What it means
ReentrantReadWriteLock keeps global state in @Counter whose low 15 bits count active readers across all threads (MAX_READERS = 32767); each thread's first read hold bumps the global counter. acquire_read_lock raises Concurrent::ResourceLimitError when that packed reader field is full. Because reentrancy lets a single thread's leak grow the global count on its own, this almost always means unbalanced acquire/release somewhere.
Source
Thrown at lib/concurrent-ruby/concurrent/atomic/reentrant_read_write_lock.rb:178
# @raise [Concurrent::ResourceLimitError] if the maximum number of readers
# or per-thread reentrant acquires is exceeded.
def acquire_read_lock
if (held = @HeldCount.value) > 0
raise ResourceLimitError.new('Too many reader holds on this thread') if (held & READ_LOCK_MASK) == READ_LOCK_MASK
# If we already have a lock, there's no need to wait
if held & READ_LOCK_MASK == 0
# But we do need to update the counter, if we were holding a write
# lock but not a read lock
@Counter.update { |c| c + 1 }
end
@HeldCount.value = held + 1
return true
end
while true
c = @Counter.value
raise ResourceLimitError.new('Too many reader threads') if max_readers?(c)
# If a writer is waiting OR running when we first queue up, we need to wait
if waiting_or_running_writer?(c)
# Before going to sleep, check again with the ReadQueue mutex held
@ReadQueue.synchronize do
@ReadQueue.ns_wait if waiting_or_running_writer?
end
# Note: the above 'synchronize' block could have used #wait_until,
# but that waits repeatedly in a loop, checking the wait condition
# each time it wakes up (to protect against spurious wakeups)
# But we are already in a loop, which is only broken when we successfully
# acquire the lock! So we don't care about spurious wakeups, and would
# rather not pay the extra overhead of using #wait_until
# After a reader has waited once, they are allowed to "barge" ahead of waiting writers
# But if a writer is *running*, the reader still needs to wait (naturally)
while true
c = @Counter.valueView on GitHub (pinned to 0b88d5ff75)
Solutions
- Use with_read_lock/with_write_lock block forms everywhere; each block performs exactly one acquire and one guaranteed release.
- Audit for unbalanced release counts: the reentrant lock requires one release per acquisition, not one per thread.
- Instrument the lock (log thread id plus acquire/release pairs) to find the leaking caller.
- Bound the number of threads taking read locks (e.g. run work through a fixed ThreadPoolExecutor) if genuine reader concurrency is the issue.
Example fix
// before — manual pairing that leaks when read_all raises
lock.acquire_read_lock
rows = read_all
lock.release_read_lock
// after
lock.with_read_lock { read_all } Defensive patterns
Strategy: try-catch
Try / catch
begin
lock.with_read_lock { read_all }
rescue Concurrent::ResourceLimitError => e
logger.fatal("global reader saturation, probable unbalanced acquire/release: #{e.message}")
raise
end Prevention
- Release exactly once per acquisition — the reentrant lock counts holds, not threads.
- Prefer with_read_lock/with_write_lock so pairing is automatic.
- Run workers through a bounded ThreadPoolExecutor to cap how many threads can hold read locks simultaneously.
When it happens
Trigger: A thread takes its first read hold via with_read_lock and never returns from the block (infinite loop); manual acquire_read_lock on many threads without matching releases; releasing fewer times than acquiring so counts ratchet upward.
Common situations: Background worker threads each leaking a read lock per job over time; long-running loops inside read critical sections; migration from plain ReadWriteLock where release accounting differs.
Related errors
- Too many reader threads
- Too many writer threads
- Too many reader holds on this thread
- Too many writer threads
- no block given
AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21).
Data as JSON: /api/errors/fed68e5bd1dce5ef.
Report an issue: GitHub.