ruby-concurrency/concurrent-ruby · error · Concurrent::IllegalOperationError
Cannot release a read lock which is not held
Error message
Cannot release a read lock which is not held
What it means
`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.
Source
Thrown at lib/concurrent-ruby/concurrent/atomic/read_write_lock.rb:147
return if @Counter.compare_and_set(c, c+1)
end
end
else
break if @Counter.compare_and_set(c, c+1)
end
end
true
end
# Release a previously acquired read lock.
#
# @return [Boolean] true if the lock is successfully released
#
# @raise [Concurrent::IllegalOperationError] if no read lock is currently held.
def release_read_lock
while true
c = @Counter.value
raise IllegalOperationError, 'Cannot release a read lock which is not held' if running_readers(c) == 0
if @Counter.compare_and_set(c, c-1)
# If one or more writers were waiting, and we were the last reader, wake a writer up
if waiting_writer?(c) && running_readers(c) == 1
@WriteLock.signal
end
break
end
end
true
end
# Acquire a write lock. Will block and wait for all active readers and writers.
#
# @return [Boolean] true if the lock is successfully acquired
#
# @raise [Concurrent::ResourceLimitError] if the maximum number of writers
# is exceeded.View on GitHub (pinned to 0b88d5ff75)
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.
Example fix
// before
lock.acquire_read_lock
begin
process
ensure
cleanup
lock.release_read_lock
end
lock.release_read_lock # stray second release -> raises
// after
lock.with_read_lock { process }
cleanup Defensive patterns
Strategy: try-catch
Validate before calling
lock.release_read_lock if lock.running_readers? # no such predicate exists; rely on your own pairing instead # practical pre-check: track acquisition yourself @read_held = true after lock.acquire_read_lock; lock.release_read_lock if @read_held
Try / catch
begin lock.release_read_lock rescue Concurrent::IllegalOperationError # no reader held; safe to continue in cleanup paths end
Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Cannot release a write lock which is not held by the current
- Cannot release a read lock which is not held
- Cannot release a write lock which is not held
- Required array size too large
- Could not initialize intrinsics
AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21).
Data as JSON: /api/errors/5ef36c56f92dbb8b.
Report an issue: GitHub.