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 false

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Keep acquire and release on the same thread — move the whole critical section into that thread.
  2. Use `lock.with_write_lock { ... }` so the pair is structural, not manual.
  3. When ownership must cross calls, track the owning Thread yourself and release only from it.
  4. 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

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


AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21). Data as JSON: /api/errors/b3b136a60282c9f3. Report an issue: GitHub.