module Timeout
def self.create_timeout_thread
def self.create_timeout_thread watcher = Thread.new do requests = [] while true until QUEUE.empty? and !requests.empty? # wait to have at least one request req = QUEUE.pop requests << req unless req.done? end closest_deadline = requests.min_by(&:deadline).deadline now = 0.0 QUEUE_MUTEX.synchronize do while (now = GET_TIME.call(Process::CLOCK_MONOTONIC)) < closest_deadline and QUEUE.empty? CONDVAR.wait(QUEUE_MUTEX, closest_deadline - now) end end requests.each do |req| req.interrupt if req.expired?(now) end requests.reject!(&:done?) end end ThreadGroup::Default.add(watcher) unless watcher.group.enclosed? watcher.name = "Timeout stdlib thread" watcher.thread_variable_set(:"\0__detached_thread__", true) watcher end
def self.ensure_timeout_thread_created
def self.ensure_timeout_thread_created unless @timeout_thread and @timeout_thread.alive? TIMEOUT_THREAD_MUTEX.synchronize do unless @timeout_thread and @timeout_thread.alive? @timeout_thread = create_timeout_thread end end end end
def timeout(sec, klass = nil, message = nil, &block) #:yield: +sec+
a module method, so you can call it directly as Timeout.timeout().
Timeout into your classes so they have a #timeout method, as well as
Note that this is both a method of module Timeout, so you can include
Scheduler#timeout_after.
If a scheduler is defined, it will be used to handle the timeout by invoking
method cannot be relied on to enforce timeouts for untrusted blocks.
ensure to prevent the handling of the exception. For that reason, this
the block unless +klass+ is given explicitly. However, the block can use
The exception thrown to terminate the given block cannot be rescued inside
+sec+ seconds, otherwise throws an exception, based on the value of +klass+.
Returns the result of the block *if* the block completed before
Omitting will use the default, "execution expired"
+message+:: Error message to raise with Exception Class.
in +sec+ seconds. Omitting will use the default, Timeout::Error
+klass+:: Exception Class to raise if the block fails to terminate
Any negative number will raise an ArgumentError.
value of 0 or +nil+ will execute the block without any timeout.
or nil may be used, including Floats to specify fractional seconds. A
+sec+:: Number of seconds to wait for the block to terminate. Any non-negative number
+sec+ seconds to complete.
Perform an operation in a block, raising an error if it takes longer than
def timeout(sec, klass = nil, message = nil, &block) #:yield: +sec+ return yield(sec) if sec == nil or sec.zero? raise ArgumentError, "Timeout sec must be a non-negative number" if 0 > sec message ||= "execution expired" if Fiber.respond_to?(:current_scheduler) && (scheduler = Fiber.current_scheduler)&.respond_to?(:timeout_after) return scheduler.timeout_after(sec, klass || Error, message, &block) end Timeout.ensure_timeout_thread_created perform = Proc.new do |exc| request = Request.new(Thread.current, sec, exc, message) QUEUE_MUTEX.synchronize do QUEUE << request CONDVAR.signal end begin return yield(sec) ensure request.finished end end if klass perform.call(klass) else Error.handle_timeout(message, &perform) end end