{"record":{"id":"0c916a3d75a0399d","repo":"ruby-concurrency/concurrent-ruby","slug":"timeout-must-0-0-or-more","errorCode":null,"errorMessage":"timeout must 0.0 or more","messagePattern":"timeout must 0\\.0 or more","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"lib/concurrent-ruby-edge/concurrent/channel/selector/after_clause.rb","lineNumber":11,"sourceCode":"require 'concurrent/maybe'\nrequire 'concurrent/utility/monotonic_time'\n\nmodule Concurrent\n  class Channel\n    class Selector\n\n      class AfterClause\n\n        def initialize(seconds, block)\n          raise ArgumentError.new('timeout must 0.0 or more') if seconds.to_f < 0.0\n          @end = Concurrent.monotonic_time + seconds.to_f\n          @block = block\n        end\n\n        def execute\n          if Concurrent.monotonic_time > @end\n            result = @block ? @block.call : nil\n            Concurrent::Maybe.just(result)\n          else\n            Concurrent::Maybe.nothing\n          end\n        end\n      end\n    end\n  end\nend\n","sourceCodeStart":1,"sourceCodeEnd":28,"githubUrl":"https://github.com/ruby-concurrency/concurrent-ruby/blob/0b88d5ff75f69b3740c8f0868e76f833cb2fd45d/lib/concurrent-ruby-edge/concurrent/channel/selector/after_clause.rb#L1-L28","documentation":"A selector's AfterClause arms a deadline at current monotonic time plus the given seconds. A negative delay would make the clause fire in the past, which the constructor rejects (the message 'timeout must 0.0 or more' is the library's own wording, typo included).","triggerScenarios":"sel.after(-1) or sel.timeout(-0.5) inside a Channel.select block; computed delays like deadline - Concurrent.monotonic_time that dip below zero once the deadline has already passed.","commonSituations":"Timeouts derived from remaining time (remaining = total - elapsed) that go negative on retries or slow iterations; config/ENV timeout values that use -1 as a 'disabled' sentinel; arithmetic on user-supplied durations.","solutions":["Clamp the value before registering: sel.after([remaining, 0.0].max).","Treat negative remaining as 'already timed out' and run the timeout path immediately (or register sel.default) instead of calling after.","Use nil for 'no timeout' rather than a negative sentinel, and check for it explicitly."],"exampleFix":"# before\nsel.after(deadline - Concurrent.monotonic_time)  # negative once deadline passed\n\n# after\nremaining = [deadline - Concurrent.monotonic_time, 0.0].max\nsel.after(remaining) { handle_timeout }","handlingStrategy":"validation","validationCode":"delay = Float(seconds)\nsel.after([delay, 0.0].max) { handle_timeout } unless delay.nan?","typeGuard":null,"tryCatchPattern":"begin\n  sel.after(delay) { handle_timeout }\nrescue ArgumentError\n  sel.after(0.0) { handle_timeout }  # already expired: fire immediately\nend","preventionTips":["Clamp computed remaining-time values before passing them to after/timeout.","Use nil (checked explicitly) for 'no timeout', never a negative sentinel.","Zero is valid and means 'already expired' - useful for immediate-fire defaults."],"tags":["ruby","concurrent-ruby","selector","timeout","argumenterror","monotonic-time"],"backgroundTag":"negative-timeout-value","analyzedSha":"0b88d5ff75f69b3740c8f0868e76f833cb2fd45d","analyzedAt":"2026-08-21T20:12:56.291Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}