ruby-concurrency/concurrent-ruby · error · ArgumentError

can't set a timeout if non_block is enabled

Error message

can't set a timeout if non_block is enabled

What it means

`Concurrent::TimeoutQueue#pop(non_block = false, timeout: nil)` (RubyTimeoutQueue backs TimeoutQueue on Ruby < 3.2) supports three modes: blocking pop, non-blocking `pop(true)` (raises ThreadError 'queue empty' when empty), and timed pop via the `timeout:` keyword (returns nil on expiry). A non-blocking call cannot honour a timeout, so passing both `non_block = true` and `timeout:` raises ArgumentError before any dequeue is attempted.

Source

Thrown at lib/concurrent-ruby/concurrent/collection/ruby_timeout_queue.rb:28

        super(*args)

        @mutex = Mutex.new
        @cond_var = ConditionVariable.new
      end

      def push(obj)
        @mutex.synchronize do
          super(obj)
          @cond_var.signal
        end
      end
      alias_method :enq, :push
      alias_method :<<, :push

      def pop(non_block = false, timeout: nil)
        if non_block && timeout
          raise ArgumentError, "can't set a timeout if non_block is enabled"
        end

        if non_block
          super(true)
        elsif timeout
          @mutex.synchronize do
            deadline = Concurrent.monotonic_time + timeout
            while (now = Concurrent.monotonic_time) < deadline && empty?
              @cond_var.wait(@mutex, deadline - now)
            end
            begin
              return super(true)
            rescue ThreadError
              # still empty
              nil
            end
          end
        else

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Pick one mode: `queue.pop(timeout: 5)` for a bounded wait, or `queue.pop(true)` for try-now.
  2. In adapters, translate explicitly: `timeout ? q.pop(timeout: timeout) : q.pop(non_block)`.
  3. Model the three modes (block / try / timed) as separate methods in your wrapper so callers cannot combine flags.

Example fix

// before
item = queue.pop(true, timeout: 5) # ArgumentError

// after
item = queue.pop(timeout: 5) # wait up to 5s, nil on timeout
// or
item = queue.pop(true) # non-blocking, ThreadError when empty
Defensive patterns

Strategy: validation

Validate before calling

def pop_from(queue, non_block: false, timeout: nil)
  raise ArgumentError, 'non_block and timeout are mutually exclusive' if non_block && timeout
  timeout ? queue.pop(timeout: timeout) : queue.pop(non_block)
end

Try / catch

begin
  queue.pop(true, timeout: t)
rescue ArgumentError
  queue.pop(timeout: t) # degrade to the timed form
end

Prevention

When it happens

Trigger: `queue.pop(true, timeout: 5)`; adapters porting ::Queue's `pop(non_block)` signature that also forward a `timeout:` keyword; wrappers that always specify `timeout:` and pass through the caller's `non_block` flag. Legal: `pop`, `pop(true)`, `pop(timeout: 5)`, `pop(false, timeout: 5)`.

Common situations: Porting code between ::Queue and Concurrent::TimeoutQueue; generic queue gateway objects that combine both options; cargo-culted `pop(true, timeout: x)` copied from another library's API where the combination is legal.

Understand the failure class

Related errors


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