{"record":{"id":"5c3d0247cfa3c270","repo":"ruby-concurrency/concurrent-ruby","slug":"too-many-writer-threads","errorCode":null,"errorMessage":"Too many writer threads","messagePattern":"Too many writer threads","errorType":"exception","errorClass":"Concurrent::ResourceLimitError","httpStatus":null,"severity":"critical","filePath":"lib/concurrent-ruby/concurrent/atomic/read_write_lock.rb","lineNumber":169,"sourceCode":"          if waiting_writer?(c) && running_readers(c) == 1\n            @WriteLock.signal\n          end\n          break\n        end\n      end\n      true\n    end\n\n    # Acquire a write lock. Will block and wait for all active readers and writers.\n    #\n    # @return [Boolean] true if the lock is successfully acquired\n    #\n    # @raise [Concurrent::ResourceLimitError] if the maximum number of writers\n    #   is exceeded.\n    def acquire_write_lock\n      while true\n        c = @Counter.value\n        raise ResourceLimitError.new('Too many writer threads') if max_writers?(c)\n\n        if c == 0 # no readers OR writers running\n          # if we successfully swap the RUNNING_WRITER bit on, then we can go ahead\n          break if @Counter.compare_and_set(0, RUNNING_WRITER)\n        elsif @Counter.compare_and_set(c, c+WAITING_WRITER)\n          while true\n            # Now we have successfully incremented, so no more readers will be able to increment\n            #   (they will wait instead)\n            # However, readers OR writers could decrement right here, OR another writer could increment\n            @WriteLock.wait_until do\n              # So we have to do another check inside the synchronized section\n              # If a writer OR reader is running, then go to sleep\n              c = @Counter.value\n              !running_writer?(c) && !running_readers?(c)\n            end\n\n            # We just came out of a wait\n            # If we successfully turn the RUNNING_WRITER bit on with an atomic swap,","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/ruby-concurrency/concurrent-ruby/blob/0b88d5ff75f69b3740c8f0868e76f833cb2fd45d/lib/concurrent-ruby/concurrent/atomic/read_write_lock.rb#L151-L187","documentation":"Concurrent::ReadWriteLock stores waiting writers in the middle bits of its state integer (WAITING_WRITER = 1 << 15, RUNNING_WRITER = 1 << 29); when the waiting-writer count saturates, acquire_write_lock raises Concurrent::ResourceLimitError. Waiting writers drain as soon as the lock frees, so saturation means writers are permanently stuck — classically a deadlock where a reader or the running writer never releases. Note the class documentation explicitly warns that acquiring the write lock while holding the read lock deadlocks this lock.","triggerScenarios":"A thread holds the read lock and then tries to acquire the write lock on the same non-reentrant ReadWriteLock while other writers queue behind it; a writer dies or raises between manual acquire_write_lock and release_write_lock; read locks leaked so the running writer never finishes and waiting writers accumulate.","commonSituations":"Read-then-write upgrade patterns on a lock documented as non-upgradeable; skipping release on exception paths; background jobs piling onto a wedged lock until the bitfield fills.","solutions":["Never take the write lock while holding the read lock on ReadWriteLock — take the write lock once for the whole read-modify-write, or use a design that does not need upgrading.","Always release via `lock.with_write_lock { ... }` or begin/ensure pairing.","When this error appears, dump all thread backtraces (Thread.list each backtrace) — the first stuck holder is the root cause, later queuers are victims.","Keep write critical sections short and free of blocking IO so the waiting queue drains quickly."],"exampleFix":"// before — read-then-write upgrade deadlocks this lock class\nlock.with_read_lock do\n  lock.with_write_lock { cache.write(x) }  # waits for its own read lock; writers pile up\nend\n\n// after — one exclusive acquisition for read-modify-write\nlock.with_write_lock do\n  v = cache.read\n  cache.write(transform(v))\nend","handlingStrategy":"try-catch","validationCode":"Thread.list.each { |t| next if t == Thread.current } # survey holders before piling on writers","typeGuard":null,"tryCatchPattern":"begin\n  lock.with_write_lock { persist(rows) }\nrescue Concurrent::ResourceLimitError => e\n  Thread.list.each { |t| logger.error(\"#{t}: #{(t.backtrace || []).join(' | ')}\") }\n  raise\nend","preventionTips":["Never acquire the write lock while holding the read lock on the same ReadWriteLock — the class doc calls this out as deadlock.","Use with_write_lock exclusively so release is guaranteed on every path.","When one thread needs read plus write access, restructure to a single write acquisition instead of upgrading."],"tags":["concurrent-ruby","read-write-lock","deadlock","resource-limit"],"backgroundTag":"resource-limit-exceeded","analyzedSha":"0b88d5ff75f69b3740c8f0868e76f833cb2fd45d","analyzedAt":"2026-08-21T20:12:56.291Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}