module Timeout
Timeout long-running blocks
Synopsis
require 'timeout' status = Timeout.timeout(5) { # Something that should be interrupted if it takes more than 5 seconds... }
Description
Timeout provides a way to auto-terminate a potentially long-running operation if it hasn’t finished in a fixed amount of time.
Copyright
- Copyright
-
© 2000 Network Applied Communication Laboratory, Inc.
- Copyright
-
© 2000 Information-technology Promotion Agency, Japan
Constants
- VERSION
-
The version
Public Class Methods
Source
# File lib/timeout.rb, line 251 def self.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 state = State.instance state.ensure_timeout_thread_created perform = Proc.new do |exc| request = Request.new(Thread.current, sec, exc, message) state.queue_mutex.synchronize do state.queue << request state.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
Perform an operation in a block, raising an exception if it takes longer than sec seconds to complete.
sec-
Number of seconds to wait for the block to terminate. Any non-negative number or nil may be used, including Floats to specify fractional seconds. A value of 0 or
nilwill execute the block without any timeout. Any negative number will raise anArgumentError. klass-
ExceptionClass to raise if the block fails to terminate insecseconds. Omitting will use the default,Timeout::Error message-
Errormessage to raise withExceptionClass. Omitting will use the default, “execution expired”
Returns the result of the block if the block completed before sec seconds, otherwise raises an exception, based on the value of klass.
The exception raised to terminate the given block is the given klass, or Timeout::ExitException if klass is not given. The reason for that behavior is that Timeout::Error inherits from RuntimeError and might be caught unexpectedly by ‘rescue`. Timeout::ExitException inherits from Exception so it will only be rescued by `rescue Exception`. Note that the Timeout::ExitException is translated to a Timeout::Error once it reaches the Timeout.timeout call, so outside that call it will be a Timeout::Error.
In general, be aware that the code block may rescue the exception, and in such a case not respect the timeout. Also, the block can use ensure to prevent the handling of the exception. For those reasons, this method cannot be relied on to enforce timeouts for untrusted blocks.
If a scheduler is defined, it will be used to handle the timeout by invoking Scheduler#timeout_after.
Note that this is both a method of module Timeout, so you can include Timeout into your classes so they have a timeout method, as well as a module method, so you can call it directly as Timeout.timeout().
Ensuring the exception does not fire inside ensure blocks
When using Timeout.timeout it can be desirable to ensure the timeout exception does not fire inside an ensure block. The simplest and best way to do so it to put the Timeout.timeout call inside the body of the begin/ensure/end:
begin Timeout.timeout(sec) { some_long_operation } ensure cleanup # safe, cannot be interrupt by timeout end
If that is not feasible, e.g. if there are ensure blocks inside some_long_operation, they need to not be interrupted by timeout, and it’s not possible to move these ensure blocks outside, one can use Thread.handle_interrupt to delay the timeout exception like so:
Thread.handle_interrupt(Timeout::Error => :never) { Timeout.timeout(sec, Timeout::Error) do setup # timeout cannot happen here, no matter how long it takes Thread.handle_interrupt(Timeout::Error => :immediate) { some_long_operation # timeout can happen here } ensure cleanup # timeout cannot happen here, no matter how long it takes end }
An important thing to note is the need to pass an exception klass to Timeout.timeout, otherwise it does not work. Specifically, using +Thread.handle_interrupt(Timeout::ExitException => …)+ is unsupported and causes subtle errors like raising the wrong exception outside the block, do not use that.
Note that Thread.handle_interrupt is somewhat dangerous because if setup or cleanup hangs then the current thread will hang too and the timeout will never fire. Also note the block might run for longer than sec seconds: e.g. some_long_operation executes for sec seconds + whatever time cleanup takes.
If you want the timeout to only happen on blocking operations one can use :on_blocking instead of :immediate. However, that means if the block uses no blocking operations after sec seconds, the block will not be interrupted.
Private Instance Methods
Source
# File lib/timeout.rb, line 284 def timeout(*args, &block) Timeout.timeout(*args, &block) end