123456789_123456789_123456789_123456789_123456789_

Module: Timeout

Relationships & Source Files
Namespace Children
Modules:
Classes:
Exceptions:
Defined in: lib/timeout.rb

Overview

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

© 2000 Network Applied Communication Laboratory, Inc.

Copyright

© 2000 Information-technology Promotion Agency, Japan

Constant Summary

Class Method Summary

Instance Method Summary

Class Method Details

.timeout(sec, klass = nil, message = nil, &block)

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 nil will execute the block without any timeout. Any negative number will raise an ArgumentError.

klass

Exception Class to raise if the block fails to terminate in sec seconds. Omitting will use the default, Timeout::Error

message

::Timeout::Error message to raise with Exception Class. 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 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().

Ensuring the exception does not fire inside ensure blocks

When using 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 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, 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.

Raises:

  • (ArgumentError)
[ GitHub ]

  
# File 'lib/timeout.rb', line 278

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.add_request(request)
    begin
      return yield(sec)
    ensure
      request.finished
    end
  end

  if klass
    perform.call(klass)
  else
    Error.handle_timeout(message, &perform)
  end
end

Instance Method Details

#timeout(*args, &block) (private)

[ GitHub ]

  
# File 'lib/timeout.rb', line 308

private def timeout(*args, &block)
  Timeout.timeout(*args, &block)
end