{"record":{"id":"2c8254477c9b7ddb","repo":"ruby-concurrency/concurrent-ruby","slug":"cannot-release-a-write-lock-which-is-not-held","errorCode":null,"errorMessage":"Cannot release a write lock which is not held","messagePattern":"Cannot release a write lock which is not held","errorType":"exception","errorClass":"Concurrent::IllegalOperationError","httpStatus":null,"severity":"error","filePath":"lib/concurrent-ruby/concurrent/atomic/reentrant_read_write_lock.rb","lineNumber":344,"sourceCode":"           @HeldCount.value = held + WRITE_LOCK_HELD\n          return true\n        end\n      end\n      false\n    end\n\n    # Release a previously acquired write lock.\n    #\n    # @return [Boolean] true if the lock is successfully released\n    def release_write_lock\n      held = @HeldCount.value = @HeldCount.value - WRITE_LOCK_HELD\n      wlocks_held = held & WRITE_LOCK_MASK\n      if wlocks_held == 0\n        c = @Counter.update { |counter| counter - RUNNING_WRITER }\n        @ReadQueue.broadcast\n        @WriteQueue.signal if waiting_writers(c) > 0\n      elsif wlocks_held == WRITE_LOCK_MASK\n        raise IllegalOperationError, \"Cannot release a write lock which is not held\"\n      end\n      true\n    end\n\n    private\n\n    # @!visibility private\n    def running_readers(c = @Counter.value)\n      c & MAX_READERS\n    end\n\n    # @!visibility private\n    def running_readers?(c = @Counter.value)\n      (c & MAX_READERS) > 0\n    end\n\n    # @!visibility private\n    def running_writer?(c = @Counter.value)","sourceCodeStart":326,"sourceCodeEnd":362,"githubUrl":"https://github.com/ruby-concurrency/concurrent-ruby/blob/0b88d5ff75f69b3740c8f0868e76f833cb2fd45d/lib/concurrent-ruby/concurrent/atomic/reentrant_read_write_lock.rb#L326-L362","documentation":"`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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nlock.acquire_write_lock\nbegin\n  update\nensure\n  lock.release_write_lock\nend\nlock.release_write_lock # extra release -> raises\n\n// after\nlock.with_write_lock { update }","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"begin\n  lock.release_write_lock\nrescue Concurrent::IllegalOperationError\n  # this thread held no write lock; safe to continue in cleanup\nend","preventionTips":["One release per acquire, including reentrant write acquisitions.","Prefer lock.with_write_lock { ... }.","Audit downgrade paths for exactly matching release counts."],"tags":["concurrent-ruby","reentrantreadwritelock","illegaloperationerror","lock-release","write-lock"],"backgroundTag":"release-unheld-lock","analyzedSha":"0b88d5ff75f69b3740c8f0868e76f833cb2fd45d","analyzedAt":"2026-08-21T20:12:56.291Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}