{"record":{"id":"5ef36c56f92dbb8b","repo":"ruby-concurrency/concurrent-ruby","slug":"cannot-release-a-read-lock-which-is-not-held","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/read_write_lock.rb","lineNumber":147,"sourceCode":"              return if @Counter.compare_and_set(c, c+1)\n            end\n          end\n        else\n          break if @Counter.compare_and_set(c, c+1)\n        end\n      end\n      true\n    end\n\n    # Release a previously acquired read lock.\n    #\n    # @return [Boolean] true if the lock is successfully released\n    #\n    # @raise [Concurrent::IllegalOperationError] if no read lock is currently held.\n    def release_read_lock\n      while true\n        c = @Counter.value\n        raise IllegalOperationError, 'Cannot release a read lock which is not held' if running_readers(c) == 0\n\n        if @Counter.compare_and_set(c, c-1)\n          # If one or more writers were waiting, and we were the last reader, wake a writer up\n          if waiting_writer?(c) && running_readers(c) == 1\n            @WriteLock.signal\n          end\n          break\n        end\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.","sourceCodeStart":129,"sourceCodeEnd":165,"githubUrl":"https://github.com/ruby-concurrency/concurrent-ruby/blob/0b88d5ff75f69b3740c8f0868e76f833cb2fd45d/lib/concurrent-ruby/concurrent/atomic/read_write_lock.rb#L129-L165","documentation":"`Concurrent::ReadWriteLock#release_read_lock` decrements the running-reader count and raises IllegalOperationError when that count is already zero — it refuses to underflow. Read locks have no owner tracking, so any thread may release, but only while at least one reader holds the lock. Typical causes: a double release, releasing on a code path that never acquired, or releasing from a thread that never participated while no other reader exists.","triggerScenarios":"Calling `lock.release_read_lock` in both a method body and its ensure block (double release); a conditional `acquire_read_lock` branch paired with an unconditional release; early-return refactor that skips the acquire but still hits the release. Prefer the built-in `with_read_lock { ... }` block form which guarantees pairing.","commonSituations":"Manual lock management copied from examples; ensure-block cleanup where the exception occurred before acquire; multiple exit paths added over time so release gets reached twice.","solutions":["Audit call sites so every `release_read_lock` pairs with exactly one successful `acquire_read_lock` on the same path.","Set a `held = true` flag only after acquire succeeds, and release in ensure only when `held`.","Replace manual pairs with `lock.with_read_lock { ... }`, which acquires and releases for you.","For best-effort cleanup where state is unknown, rescue Concurrent::IllegalOperationError and log."],"exampleFix":"// before\nlock.acquire_read_lock\nbegin\n  process\nensure\n  cleanup\n  lock.release_read_lock\nend\nlock.release_read_lock # stray second release -> raises\n\n// after\nlock.with_read_lock { process }\ncleanup","handlingStrategy":"try-catch","validationCode":"lock.release_read_lock if lock.running_readers? # no such predicate exists; rely on your own pairing instead\n# practical pre-check: track acquisition yourself\n@read_held = true after lock.acquire_read_lock; lock.release_read_lock if @read_held","typeGuard":null,"tryCatchPattern":"begin\n  lock.release_read_lock\nrescue Concurrent::IllegalOperationError\n  # no reader held; safe to continue in cleanup paths\nend","preventionTips":["Use lock.with_read_lock { ... } instead of manual acquire/release.","Set a held flag only after acquire returns and release in ensure only when set.","Never release on paths that did not acquire."],"tags":["concurrent-ruby","readwritelock","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"}