ruby-concurrency/concurrent-ruby · error · Concurrent::IllegalOperationError
Cannot release a write lock which is not held by the current
Error message
Cannot release a write lock which is not held by the current thread
What it means
`Concurrent::ReadWriteLock` records the write-lock owner in `@Writer`; `release_write_lock` does a compare_and_set of `Thread.current` to nil and raises IllegalOperationError when it fails. That means the calling thread is not the current write owner: released without acquiring, released twice, or — most commonly — acquired on one thread (often a pool thread) and released on another. The lock's `write_locked?` predicate only reports that some thread holds it, not which.
Source
Thrown at lib/concurrent-ruby/concurrent/atomic/read_write_lock.rb:208
c = @Counter.value
break if !running_writer?(c) && !running_readers?(c) && @Counter.compare_and_set(c, c+RUNNING_WRITER-WAITING_WRITER)
end
break
end
end
@Writer.set(Thread.current)
true
end
# Release a previously acquired write lock.
#
# @return [Boolean] true if the lock is successfully released
#
# @raise [Concurrent::IllegalOperationError] if the write lock is not held
# by the current thread.
def release_write_lock
unless @Writer.compare_and_set(Thread.current, nil)
raise IllegalOperationError, 'Cannot release a write lock which is not held by the current thread'
end
c = @Counter.update { |counter| counter - RUNNING_WRITER }
@ReadLock.broadcast
@WriteLock.signal if waiting_writers(c) > 0
true
end
# Queries if the write lock is held by any thread.
#
# @return [Boolean] true if the write lock is held else false`
def write_locked?
@Counter.value >= RUNNING_WRITER
end
# Queries whether any threads are waiting to acquire the read or write lock.
#
# @return [Boolean] true if any threads are waiting for a lock else falseView on GitHub (pinned to 0b88d5ff75)
Solutions
- Keep acquire and release on the same thread — move the whole critical section into that thread.
- Use `lock.with_write_lock { ... }` so the pair is structural, not manual.
- When ownership must cross calls, track the owning Thread yourself and release only from it.
- In cleanup paths where ownership is unknown, rescue Concurrent::IllegalOperationError and log; do not blindly retry.
Example fix
// before
worker = Thread.new { lock.acquire_write_lock; do_work }
worker.join
lock.release_write_lock # raises: @Writer is the worker thread
// after
worker = Thread.new do
lock.acquire_write_lock
begin
do_work
ensure
lock.release_write_lock
end
end
worker.join Defensive patterns
Strategy: try-catch
Validate before calling
@write_owner = Thread.current # record at acquire time lock.release_write_lock if @write_owner == Thread.current
Try / catch
begin lock.release_write_lock rescue Concurrent::IllegalOperationError logger.debug 'write lock not held by this thread' end
Prevention
- Acquire and release on the same thread; keep the pair inside one method with ensure.
- Prefer lock.with_write_lock { ... }.
- write_locked? means 'held by any thread' — do not use it as an ownership check.
- Track the owning Thread when the critical section crosses call boundaries.
When it happens
Trigger: Acquiring inside a worker/pool thread (`Concurrent::Future.execute { lock.acquire_write_lock; work }`) and releasing from the main thread after join; a double release on one thread; releasing in ensure after an exception aborted the acquire mid-way.
Common situations: Refactoring block-scoped locking into manual acquire/release across method or thread boundaries; work handed to a thread pool while the coordinator releases; cleanup code run in a different fiber/thread than the critical section.
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
- 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/b3b136a60282c9f3.
Report an issue: GitHub.