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

  1. Match each successful `acquire_write_lock` on the thread with exactly one `release_write_lock`, including reentrant acquisitions.
  2. Use `lock.with_write_lock { ... }` for structural pairing.
  3. Audit downgrade paths: acquire write -> acquire read -> release write, one release per acquire.
  4. 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

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


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