{"record":{"id":"fed68e5bd1dce5ef","repo":"ruby-concurrency/concurrent-ruby","slug":"too-many-reader-threads-fed68e","errorCode":null,"errorMessage":"Too many reader threads","messagePattern":"Too many reader threads","errorType":"exception","errorClass":"Concurrent::ResourceLimitError","httpStatus":null,"severity":"error","filePath":"lib/concurrent-ruby/concurrent/atomic/reentrant_read_write_lock.rb","lineNumber":178,"sourceCode":"    # @raise [Concurrent::ResourceLimitError] if the maximum number of readers\n    #   or per-thread reentrant acquires is exceeded.\n    def acquire_read_lock\n      if (held = @HeldCount.value) > 0\n        raise ResourceLimitError.new('Too many reader holds on this thread') if (held & READ_LOCK_MASK) == READ_LOCK_MASK\n\n        # If we already have a lock, there's no need to wait\n        if held & READ_LOCK_MASK == 0\n          # But we do need to update the counter, if we were holding a write\n          #   lock but not a read lock\n          @Counter.update { |c| c + 1 }\n        end\n        @HeldCount.value = held + 1\n        return true\n      end\n\n      while true\n        c = @Counter.value\n        raise ResourceLimitError.new('Too many reader threads') if max_readers?(c)\n\n        # If a writer is waiting OR running when we first queue up, we need to wait\n        if waiting_or_running_writer?(c)\n          # Before going to sleep, check again with the ReadQueue mutex held\n          @ReadQueue.synchronize do\n            @ReadQueue.ns_wait if waiting_or_running_writer?\n          end\n          # Note: the above 'synchronize' block could have used #wait_until,\n          #   but that waits repeatedly in a loop, checking the wait condition\n          #   each time it wakes up (to protect against spurious wakeups)\n          # But we are already in a loop, which is only broken when we successfully\n          #   acquire the lock! So we don't care about spurious wakeups, and would\n          #   rather not pay the extra overhead of using #wait_until\n\n          # After a reader has waited once, they are allowed to \"barge\" ahead of waiting writers\n          # But if a writer is *running*, the reader still needs to wait (naturally)\n          while true\n            c = @Counter.value","sourceCodeStart":160,"sourceCodeEnd":196,"githubUrl":"https://github.com/ruby-concurrency/concurrent-ruby/blob/0b88d5ff75f69b3740c8f0868e76f833cb2fd45d/lib/concurrent-ruby/concurrent/atomic/reentrant_read_write_lock.rb#L160-L196","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before — manual pairing that leaks when read_all raises\nlock.acquire_read_lock\nrows = read_all\nlock.release_read_lock\n\n// after\nlock.with_read_lock { read_all }","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"begin\n  lock.with_read_lock { read_all }\nrescue Concurrent::ResourceLimitError => e\n  logger.fatal(\"global reader saturation, probable unbalanced acquire/release: #{e.message}\")\n  raise\nend","preventionTips":["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."],"tags":["concurrent-ruby","reentrant-lock","read-write-lock","resource-limit","lock-leak"],"backgroundTag":"resource-limit-exceeded","analyzedSha":"0b88d5ff75f69b3740c8f0868e76f833cb2fd45d","analyzedAt":"2026-08-21T20:12:56.291Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}