ruby-concurrency/concurrent-ruby · error · ArgumentError

interval_type must be either :fixed_delay or :fixed_rate

Error message

interval_type must be either :fixed_delay or :fixed_rate

What it means

TimerTask supports two interval semantics: :fixed_delay (default; interval counted after each run completes) and :fixed_rate (interval counted from run start). The :interval_type option is compared by identity against these two symbols, and any other value — including the strings 'fixed_delay'/'fixed_rate' — raises ArgumentError.

Source

Thrown at lib/concurrent-ruby/concurrent/timer_task.rb:303

    end

    # @!attribute [rw] timeout_interval
    # @return [Fixnum] Number of seconds the task can run before it is
    #   considered to have failed.
    def timeout_interval=(value)
      warn 'TimerTask timeouts are now ignored as these were not able to be implemented correctly'
    end

    private :post, :<<

    private

    def ns_initialize(opts, &task)
      set_deref_options(opts)

      self.execution_interval = opts[:execution] || opts[:execution_interval] || EXECUTION_INTERVAL
      if opts[:interval_type] && ![FIXED_DELAY, FIXED_RATE].include?(opts[:interval_type])
        raise ArgumentError.new('interval_type must be either :fixed_delay or :fixed_rate')
      end
      if opts[:timeout] || opts[:timeout_interval]
        warn 'TimeTask timeouts are now ignored as these were not able to be implemented correctly'
      end

      @run_now = opts[:now] || opts[:run_now]
      @interval_type = opts[:interval_type] || DEFAULT_INTERVAL_TYPE
      @task = Concurrent::SafeTaskExecutor.new(task)
      @executor = opts[:executor] || Concurrent.global_io_executor
      @running = Concurrent::AtomicBoolean.new(false)
      @age = Concurrent::AtomicFixnum.new(0)
      @value = nil

      self.observers = Collection::CopyOnNotifyObserverSet.new
    end

    # @!visibility private
    def ns_shutdown_execution

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Use the exact symbols: interval_type: :fixed_rate
  2. Normalize at the config boundary: interval_type: cfg['interval_type']&.to_sym
  3. Whitelist early: raise ArgumentError, 'bad interval_type' unless %i[fixed_delay fixed_rate].include?(type.to_sym)

Example fix

# before (string from YAML fails the symbol check)
task = Concurrent::TimerTask.new(interval_type: config['interval_type']) { tick }

# after
type = config['interval_type'].to_s.to_sym if config['interval_type']
type = :fixed_delay unless %i[fixed_delay fixed_rate].include?(type)
task = Concurrent::TimerTask.new(interval_type: type) { tick }
Defensive patterns

Strategy: validation

Validate before calling

VALID_INTERVAL_TYPES = %i[fixed_delay fixed_rate].freeze
type = opts[:interval_type]&.to_sym
type = nil unless VALID_INTERVAL_TYPES.include?(type)
Concurrent::TimerTask.new(**opts, interval_type: type).tap { |t| t.execute }

Type guard

def valid_interval_type?(v)
  %i[fixed_delay fixed_rate].include?(v.is_a?(String) ? v.to_sym : v)
end

Prevention

When it happens

Trigger: TimerTask.new(interval_type: 'fixed_delay') where the string came from YAML/JSON/ENV; typos like :fixed_interval or :fixedrate; forwarding user input unvalidated.

Common situations: Config-driven timer setup where the parser returns strings, not symbols; options copied from documentation written with quotes; version upgrades introducing the option into previously untyped config hashes.

Related errors


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