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::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.
Source
Thrown at lib/concurrent-ruby/concurrent/atomic/reentrant_read_write_lock.rb:253
end
end
false
end
# Release a previously acquired read lock.
#
# @return [Boolean] true if the lock is successfully released
def release_read_lock
held = @HeldCount.value = @HeldCount.value - 1
rlocks_held = held & READ_LOCK_MASK
if rlocks_held == 0
c = @Counter.update { |counter| counter - 1 }
# If one or more writers were waiting, and we were the last reader, wake a writer up
if waiting_or_running_writer?(c) && running_readers(c) == 0
@WriteQueue.signal
end
elsif rlocks_held == READ_LOCK_MASK
raise IllegalOperationError, "Cannot release a read lock which is not held"
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.
def acquire_write_lock
if (held = @HeldCount.value) >= WRITE_LOCK_HELD
# if we already have a write (exclusive) lock, there's no need to wait
@HeldCount.value = held + WRITE_LOCK_HELD
return true
end
while trueView on GitHub (pinned to 0b88d5ff75)
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.
Example fix
// before
def read_through
@lock.acquire_read_lock unless @in_read
result = fetch
@lock.release_read_lock # raises when @in_read was true (no acquire ran)
result
end
// after
def read_through
@lock.with_read_lock { fetch }
end Defensive patterns
Strategy: try-catch
Try / catch
begin lock.release_read_lock rescue Concurrent::IllegalOperationError # this thread held no read lock; ignore in cleanup end
Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- Cannot release a write lock which is not held
- Cannot release a read lock which is not held
- Cannot release a write lock which is not held by the current
- 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/fe86891d80607c84.
Report an issue: GitHub.