{"record":{"id":"fe86891d80607c84","repo":"ruby-concurrency/concurrent-ruby","slug":"cannot-release-a-read-lock-which-is-not-held-fe8689","errorCode":null,"errorMessage":"Cannot release a read lock which is not held","messagePattern":"Cannot release a read lock which is not held","errorType":"exception","errorClass":"Concurrent::IllegalOperationError","httpStatus":null,"severity":"error","filePath":"lib/concurrent-ruby/concurrent/atomic/reentrant_read_write_lock.rb","lineNumber":253,"sourceCode":"        end\n      end\n      false\n    end\n\n    # Release a previously acquired read lock.\n    #\n    # @return [Boolean] true if the lock is successfully released\n    def release_read_lock\n      held = @HeldCount.value = @HeldCount.value - 1\n      rlocks_held = held & READ_LOCK_MASK\n      if rlocks_held == 0\n        c = @Counter.update { |counter| counter - 1 }\n        # If one or more writers were waiting, and we were the last reader, wake a writer up\n        if waiting_or_running_writer?(c) && running_readers(c) == 0\n          @WriteQueue.signal\n        end\n      elsif rlocks_held == READ_LOCK_MASK\n        raise IllegalOperationError, \"Cannot release a read lock which is not held\"\n      end\n      true\n    end\n\n    # Acquire a write lock. Will block and wait for all active readers and writers.\n    #\n    # @return [Boolean] true if the lock is successfully acquired\n    #\n    # @raise [Concurrent::ResourceLimitError] if the maximum number of writers\n    #   is exceeded.\n    def acquire_write_lock\n      if (held = @HeldCount.value) >= WRITE_LOCK_HELD\n        # if we already have a write (exclusive) lock, there's no need to wait\n        @HeldCount.value = held + WRITE_LOCK_HELD\n        return true\n      end\n\n      while true","sourceCodeStart":235,"sourceCodeEnd":271,"githubUrl":"https://github.com/ruby-concurrency/concurrent-ruby/blob/0b88d5ff75f69b3740c8f0868e76f833cb2fd45d/lib/concurrent-ruby/concurrent/atomic/reentrant_read_write_lock.rb#L235-L271","documentation":"`Concurrent::ReentrantReadWriteLock` packs the current thread's held read-lock count into a thread-local `@HeldCount`. `release_read_lock` decrements it; if the read bits come back as READ_LOCK_MASK (all ones) the counter underflowed — this thread held no read lock — and IllegalOperationError is raised instead of corrupting the count. Because the count is thread-local, releasing on a thread that acquired nothing raises even while other threads legitimately hold read locks.","triggerScenarios":"More `release_read_lock` calls than acquires on the current thread (e.g. a nested level popped twice); release placed in ensure on a path whose acquire was skipped by an early return or exception; release executed on a different thread/fiber than the acquire (its count there is zero).","commonSituations":"Reentrant nesting bookkeeping drifting out of sync as code paths are added; conditional acquire with unconditional release; fiber schedulers or job runners that resume work on another thread.","solutions":["Balance the count: every `acquire_read_lock` on a thread needs exactly one matching `release_read_lock` (reentrancy means N acquires need N releases).","Guard releases with a held-flag set only after a successful acquire.","Prefer `lock.with_read_lock { ... }`, whose block form keeps nesting balanced.","In cleanup code where the count is uncertain, rescue Concurrent::IllegalOperationError and continue."],"exampleFix":"// before\ndef read_through\n  @lock.acquire_read_lock unless @in_read\n  result = fetch\n  @lock.release_read_lock # raises when @in_read was true (no acquire ran)\n  result\nend\n\n// after\ndef read_through\n  @lock.with_read_lock { fetch }\nend","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"begin\n  lock.release_read_lock\nrescue Concurrent::IllegalOperationError\n  # this thread held no read lock; ignore in cleanup\nend","preventionTips":["Reentrancy bookkeeping: N acquires on a thread require N releases on that thread.","Use lock.with_read_lock { ... } which keeps nesting balanced.","Never migrate release to a different thread/fiber than the acquire."],"tags":["concurrent-ruby","reentrantreadwritelock","illegaloperationerror","lock-release"],"backgroundTag":"release-unheld-lock","analyzedSha":"0b88d5ff75f69b3740c8f0868e76f833cb2fd45d","analyzedAt":"2026-08-21T20:12:56.291Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}