{"record":{"id":"2e40e6cb953fa4f1","repo":"ruby-concurrency/concurrent-ruby","slug":"min-threads-cannot-be-less-than-default-min-po-2e40e6","errorCode":null,"errorMessage":"`min_threads` cannot be less than #{DEFAULT_MIN_POOL_SIZE}","messagePattern":"`min_threads` cannot be less than #(.+?)","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb","lineNumber":158,"sourceCode":"      deprecated \"#prune_pool has no effect and will be removed in next the release, see https://github.com/ruby-concurrency/concurrent-ruby/pull/1082.\"\n    end\n\n    private\n\n    # @!visibility private\n    def ns_initialize(opts)\n      @min_length      = opts.fetch(:min_threads, DEFAULT_MIN_POOL_SIZE).to_i\n      @max_length      = opts.fetch(:max_threads, DEFAULT_MAX_POOL_SIZE).to_i\n      @idletime        = opts.fetch(:idletime, DEFAULT_THREAD_IDLETIMEOUT).to_i\n      @max_queue       = opts.fetch(:max_queue, DEFAULT_MAX_QUEUE_SIZE).to_i\n      @synchronous     = opts.fetch(:synchronous, DEFAULT_SYNCHRONOUS)\n      @fallback_policy = opts.fetch(:fallback_policy, :abort)\n\n      raise ArgumentError.new(\"`synchronous` cannot be set unless `max_queue` is 0\") if @synchronous && @max_queue > 0\n      raise ArgumentError.new(\"#{@fallback_policy} is not a valid fallback policy\") unless FALLBACK_POLICIES.include?(@fallback_policy)\n      raise ArgumentError.new(\"`max_threads` cannot be less than #{DEFAULT_MIN_POOL_SIZE}\") if @max_length < DEFAULT_MIN_POOL_SIZE\n      raise ArgumentError.new(\"`max_threads` cannot be greater than #{DEFAULT_MAX_POOL_SIZE}\") if @max_length > DEFAULT_MAX_POOL_SIZE\n      raise ArgumentError.new(\"`min_threads` cannot be less than #{DEFAULT_MIN_POOL_SIZE}\") if @min_length < DEFAULT_MIN_POOL_SIZE\n      raise ArgumentError.new(\"`min_threads` cannot be more than `max_threads`\") if min_length > max_length\n\n      @pool                 = [] # all workers\n      @ready                = [] # used as a stash (most idle worker is at the start)\n      @queue                = [] # used as queue\n      # @ready or @queue is empty at all times\n      @scheduled_task_count = 0\n      @completed_task_count = 0\n      @largest_length       = 0\n      @workers_counter      = 0\n      @ruby_pid             = $$ # detects if Ruby has forked\n    end\n\n    # @!visibility private\n    def ns_limited_queue?\n      @max_queue != 0\n    end\n","sourceCodeStart":140,"sourceCodeEnd":176,"githubUrl":"https://github.com/ruby-concurrency/concurrent-ruby/blob/0b88d5ff75f69b3740c8f0868e76f833cb2fd45d/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb#L140-L176","documentation":"Raised by RubyThreadPoolExecutor#ns_initialize when the :min_threads option, after `.to_i` coercion, is less than DEFAULT_MIN_POOL_SIZE (which is 0 in this version of concurrent-ruby). The pool cannot honor a request to keep fewer than zero threads alive, so construction fails immediately with ArgumentError. Because the value is fetched with `opts.fetch(:min_threads, 0).to_i`, any object that coerces to a negative Integer (e.g. -1, -2.9, \"-5\") triggers it.","triggerScenarios":"Concurrent::ThreadPoolExecutor.new(min_threads: -1); passing a numeric string like min_threads: '-5' (String#to_i keeps the sign); computing the value from config such as worker_count - 2 when worker_count is 1; floats like -1.5 (to_i gives -1).","commonSituations":"Config-driven pool sizing where min_threads comes from ENV vars or YAML without range validation; formulas that can go negative under load or small deployments; teams porting Java ThreadPoolExecutor code where negative values were silently clamped.","solutions":["Set min_threads to 0 or a positive Integer, or omit it entirely (the default is 0, which creates threads on demand)","Validate the value at the config boundary: parse with Integer(...) and reject anything below 0 with a clear configuration error before constructing the pool","If the value arrives as a string, coerce and range-check it explicitly instead of relying on the library's internal .to_i"],"exampleFix":"# before\npool = Concurrent::ThreadPoolExecutor.new(min_threads: ENV.fetch('MIN_THREADS').to_i)\n\n# after\nmin = Integer(ENV.fetch('MIN_THREADS', 0))\nraise ArgumentError, \"MIN_THREADS must be >= 0, got #{min}\" if min < 0\npool = Concurrent::ThreadPoolExecutor.new(min_threads: min)","handlingStrategy":"validation","validationCode":"min = Integer(ENV.fetch('MIN_THREADS', 0))\nraise ArgumentError, \"min_threads must be >= 0\" if min < 0\npool = Concurrent::ThreadPoolExecutor.new(min_threads: min)","typeGuard":"->(v) { v.is_a?(Integer) && v >= 0 }","tryCatchPattern":"begin\n  Concurrent::ThreadPoolExecutor.new(min_threads: min)\nrescue ArgumentError => e\n  raise ConfigError, \"thread pool config rejected: #{e.message}\"\nend","preventionTips":["Centralize pool construction in one factory so size validation lives in a single place","Parse numeric config with Integer(...) at load time, not with to_i at use time","Add config schema checks (range assertions) that run at boot, not on first request"],"tags":["ruby","concurrency","thread-pool","argument-validation","configuration"],"backgroundTag":"thread-pool-misconfiguration","analyzedSha":"0b88d5ff75f69b3740c8f0868e76f833cb2fd45d","analyzedAt":"2026-08-21T20:12:56.291Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}