ruby-concurrency/concurrent-ruby · error · Concurrent::IllegalOperationError
Cannot release a write lock which is not held
Error message
Cannot release a write lock which is not held
What it means
`Concurrent::ReentrantReadWriteLock#release_write_lock` subtracts WRITE_LOCK_HELD from the current thread's `@HeldCount`; if the write bits underflow to WRITE_LOCK_MASK the thread held no write lock and IllegalOperationError raises. Write locks are reentrant per thread — one release per acquire — and only the owning thread's count moves, so a release on any other thread (or one release too many) underflows. It is the mirror of the read-side error, plus ownership: the write count lives in the acquiring thread's HeldCount.
Source
Thrown at lib/concurrent-ruby/concurrent/atomic/reentrant_read_write_lock.rb:344
@HeldCount.value = held + WRITE_LOCK_HELD
return true
end
end
false
end
# Release a previously acquired write lock.
#
# @return [Boolean] true if the lock is successfully released
def release_write_lock
held = @HeldCount.value = @HeldCount.value - WRITE_LOCK_HELD
wlocks_held = held & WRITE_LOCK_MASK
if wlocks_held == 0
c = @Counter.update { |counter| counter - RUNNING_WRITER }
@ReadQueue.broadcast
@WriteQueue.signal if waiting_writers(c) > 0
elsif wlocks_held == WRITE_LOCK_MASK
raise IllegalOperationError, "Cannot release a write lock which is not held"
end
true
end
private
# @!visibility private
def running_readers(c = @Counter.value)
c & MAX_READERS
end
# @!visibility private
def running_readers?(c = @Counter.value)
(c & MAX_READERS) > 0
end
# @!visibility private
def running_writer?(c = @Counter.value)View on GitHub (pinned to 0b88d5ff75)
Solutions
- Match each successful `acquire_write_lock` on the thread with exactly one `release_write_lock`, including reentrant acquisitions.
- Use `lock.with_write_lock { ... }` for structural pairing.
- Audit downgrade paths: acquire write -> acquire read -> release write, one release per acquire.
- For best-effort cleanup, rescue Concurrent::IllegalOperationError rather than guessing state.
Example fix
// before
lock.acquire_write_lock
begin
update
ensure
lock.release_write_lock
end
lock.release_write_lock # extra release -> raises
// after
lock.with_write_lock { update } Defensive patterns
Strategy: try-catch
Try / catch
begin lock.release_write_lock rescue Concurrent::IllegalOperationError # this thread held no write lock; safe to continue in cleanup end
Prevention
- One release per acquire, including reentrant write acquisitions.
- Prefer lock.with_write_lock { ... }.
- Audit downgrade paths for exactly matching release counts.
When it happens
Trigger: A second `release_write_lock` after a correctly paired one on the same thread; releasing write on a thread that only acquired read locks; downgrade logic (acquire write, re-acquire read, release write) implemented with the wrong release count or on the wrong thread.
Common situations: Lock-downgrade patterns copy-pasted with an extra release; exception paths that skip acquisition but still run the ensure release; work migrated between threads between acquire and release.
Related errors
- Cannot release a read 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/2c8254477c9b7ddb.
Report an issue: GitHub.