{"record":{"id":"27810eabf9dd11ee","repo":"ruby-concurrency/concurrent-ruby","slug":"seconds-must-be-greater-than-zero","errorCode":null,"errorMessage":"seconds must be greater than zero","messagePattern":"seconds must be greater than zero","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"lib/concurrent-ruby/concurrent/scheduled_task.rb","lineNumber":180,"sourceCode":"    # @!visibility private\n    attr_reader :executor\n\n    # Schedule a task for execution at a specified future time.\n    #\n    # @param [Float] delay the number of seconds to wait for before executing the task\n    #\n    # @yield the task to be performed\n    #\n    # @!macro executor_and_deref_options\n    #\n    # @option opts [object, Array] :args zero or more arguments to be passed the task\n    #   block on execution\n    #\n    # @raise [ArgumentError] When no block is given\n    # @raise [ArgumentError] When given a time that is in the past\n    def initialize(delay, opts = {}, &task)\n      raise ArgumentError.new('no block given') unless block_given?\n      raise ArgumentError.new('seconds must be greater than zero') if delay.to_f < 0.0\n\n      super(NULL, opts, &nil)\n\n      synchronize do\n        ns_set_state(:unscheduled)\n        @parent = opts.fetch(:timer_set, Concurrent.global_timer_set)\n        @args = get_arguments_from(opts)\n        @delay = delay.to_f\n        @task = task\n        @time = nil\n        @executor = Options.executor_from_options(opts) || Concurrent.global_io_executor\n        self.observers = Collection::CopyOnNotifyObserverSet.new\n      end\n    end\n\n    # The `delay` value given at instantiation.\n    #\n    # @return [Float] the initial delay.","sourceCodeStart":162,"sourceCodeEnd":198,"githubUrl":"https://github.com/ruby-concurrency/concurrent-ruby/blob/0b88d5ff75f69b3740c8f0868e76f833cb2fd45d/lib/concurrent-ruby/concurrent/scheduled_task.rb#L162-L198","documentation":"ScheduledTask's delay is a relative offset in seconds, converted with #to_f and rejected when negative because a negative delay means the target time is already in the past. Note the mismatch between message and code: the check is delay.to_f < 0.0, so zero (and values like nil or 'abc' whose to_f is 0.0) is silently accepted and runs immediately. Passing an absolute Time object also slips through, since Time#to_f becomes a huge positive epoch offset.","triggerScenarios":"Computing the delay as run_at - Time.now where run_at is already past; passing '-30' (a negative numeric string); passing nil (nil.to_f == 0.0 passes, task fires immediately); passing a Time object instead of a seconds offset.","commonSituations":"Scheduling jobs at absolute timestamps taken from a DB or queue; clock skew between the app server and the system that produced the timestamp; DST transitions making a computed offset negative; config values that are absent and become nil.","solutions":["Clamp computed delays at the call site: delay = [run_at.to_f - Time.now.to_f, 0].max","Treat 'deadline already passed' as a business decision: if run_at <= Time.now, run the work synchronously or skip it, then pass the clamped delay","Reject nil explicitly before calling new if an immediate fire is not intended: raise ArgumentError, 'delay missing' if delay.nil?","Never pass a Time object; convert it to a relative offset first"],"exampleFix":"# before (raises when deadline has passed)\ntask = Concurrent::ScheduledTask.new(deadline - Time.now) { fire_alarm }\n\n# after (past deadlines run immediately)\ndelay = [[deadline - Time.now, 0].max, 0].max\ntask = Concurrent::ScheduledTask.new(delay) { fire_alarm }","handlingStrategy":"validation","validationCode":"delay = delay.to_f\ndelay = 0.0 if delay < 0 # past deadlines fire immediately\nraise ArgumentError, 'delay required' if delay.nil?\nConcurrent::ScheduledTask.new(delay) { work }","typeGuard":"def valid_delay?(v)\n  v.is_a?(Numeric) && !v.nil? && v.to_f >= 0\nend","tryCatchPattern":"begin\n  task = Concurrent::ScheduledTask.new(delay) { work }\nrescue ArgumentError => e\n  raise if e.message != 'seconds must be greater than zero'\n  delay = 0\n  task = Concurrent::ScheduledTask.new(delay) { work } # deadline already passed: run now\nend","preventionTips":["Never compute delays as raw timestamp differences without clamping to 0","Reject nil delay explicitly if immediate execution is not intended (nil.to_f passes the check)","Log schedule_time after creation to sanity-check the computed offset in production"],"tags":["ruby","concurrency","scheduled-task","argument-error","time-handling"],"backgroundTag":"schedule-time-in-past","analyzedSha":"0b88d5ff75f69b3740c8f0868e76f833cb2fd45d","analyzedAt":"2026-08-21T20:12:56.291Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}