{"record":{"id":"c38528c244ffddb6","repo":"ruby-concurrency/concurrent-ruby","slug":"too-many-reader-holds-on-this-thread","errorCode":null,"errorMessage":"Too many reader holds on this thread","messagePattern":"Too many reader holds on this thread","errorType":"exception","errorClass":"Concurrent::ResourceLimitError","httpStatus":null,"severity":"error","filePath":"lib/concurrent-ruby/concurrent/atomic/reentrant_read_write_lock.rb","lineNumber":164,"sourceCode":"      raise ArgumentError.new('no block given') unless block_given?\n      acquire_write_lock\n      begin\n        yield\n      ensure\n        release_write_lock\n      end\n    end\n\n    # Acquire a read lock. If a write lock is held by another thread, will block\n    # until it is released.\n    #\n    # @return [Boolean] true if the lock is successfully acquired\n    #\n    # @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","sourceCodeStart":146,"sourceCodeEnd":182,"githubUrl":"https://github.com/ruby-concurrency/concurrent-ruby/blob/0b88d5ff75f69b3740c8f0868e76f833cb2fd45d/lib/concurrent-ruby/concurrent/atomic/reentrant_read_write_lock.rb#L146-L182","documentation":"ReentrantReadWriteLock tracks per-thread holdings in a thread-local @HeldCount whose low 15 bits count this thread's outstanding read-lock holds (READ_LOCK_MASK = 32767). Reentrant reacquisition increments that counter; when the next acquire would overflow it, acquire_read_lock raises Concurrent::ResourceLimitError. In practice this means unbalanced reentrant acquisition — usually recursion or a loop that acquires more read locks than it releases.","triggerScenarios":"Recursive methods calling with_read_lock at every level so the per-thread hold count tracks stack depth past 32767; a loop on one thread calling acquire_read_lock with a release missing on one path; refactors that removed an ensure-driven release.","commonSituations":"Deep recursion over trees or graphs guarded per-level by a reentrant read lock; accidental reacquisition inside callbacks invoked under the lock; mixing manual and block lock APIs with asymmetric release counts.","solutions":["Hoist acquisition out of the recursion: wrap the whole traversal in one with_read_lock and use an internal, unlocked recursive method.","Replace manual acquire/release with with_read_lock so every acquisition is matched by exactly one ensure-driven release.","If recursion depth legitimately exceeds 32767, restructure to an iterative traversal — Ruby's stack is a hazard there anyway."],"exampleFix":"// before — one read acquire per recursion level\ndef walk(node)\n  lock.with_read_lock { visit(node); node.children.each { |c| walk(c) } }\nend\n\n// after — single acquisition for the whole traversal\ndef walk(node)\n  lock.with_read_lock { walk_locked(node) }\nend\n\ndef walk_locked(node)\n  visit(node)\n  node.children.each { |c| walk_locked(c) }\nend","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"begin\n  lock.with_read_lock { walk(node) }\nrescue Concurrent::ResourceLimitError => e\n  logger.fatal(\"per-thread read-hold overflow: #{e.message}\")\n  raise\nend","preventionTips":["Acquire reentrant locks once at the outermost layer; do not re-acquire per recursive step.","Keep acquisition and release symmetric: one with_read_lock block per acquisition.","Watch for accidental reacquisition inside callbacks invoked under the lock."],"tags":["concurrent-ruby","reentrant-lock","recursion","resource-limit"],"backgroundTag":"resource-limit-exceeded","analyzedSha":"0b88d5ff75f69b3740c8f0868e76f833cb2fd45d","analyzedAt":"2026-08-21T20:12:56.291Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}