1# frozen_string_literal: false 2# Timeout long-running blocks 3# 4# == Synopsis 5# 6# require 'timeout' 7# status = Timeout::timeout(5) { 8# # Something that should be interrupted if it takes more than 5 seconds... 9# } 10# 11# == Description 12# 13# Timeout provides a way to auto-terminate a potentially long-running 14# operation if it hasn't finished in a fixed amount of time. 15# 16# Previous versions didn't use a module for namespacing, however 17# #timeout is provided for backwards compatibility. You 18# should prefer Timeout.timeout instead. 19# 20# == Copyright 21# 22# Copyright:: (C) 2000 Network Applied Communication Laboratory, Inc. 23# Copyright:: (C) 2000 Information-technology Promotion Agency, Japan 24 25module Timeout 26 # Raised by Timeout.timeout when the block times out. 27 class Error < RuntimeError 28 attr_reader :thread 29 30 def self.catch(*args) 31 exc = new(*args) 32 exc.instance_variable_set(:@thread, Thread.current) 33 ::Kernel.catch(exc) {yield exc} 34 end 35 36 def exception(*) 37 # TODO: use Fiber.current to see if self can be thrown 38 if self.thread == Thread.current 39 bt = caller 40 begin 41 throw(self, bt) 42 rescue UncaughtThrowError 43 end 44 end 45 self 46 end 47 end 48 49 # :stopdoc: 50 THIS_FILE = /\A#{Regexp.quote(__FILE__)}:/o 51 CALLER_OFFSET = ((c = caller[0]) && THIS_FILE =~ c) ? 1 : 0 52 private_constant :THIS_FILE, :CALLER_OFFSET 53 # :startdoc: 54 55 # Perform an operation in a block, raising an error if it takes longer than 56 # +sec+ seconds to complete. 57 # 58 # +sec+:: Number of seconds to wait for the block to terminate. Any number 59 # may be used, including Floats to specify fractional seconds. A 60 # value of 0 or +nil+ will execute the block without any timeout. 61 # +klass+:: Exception Class to raise if the block fails to terminate 62 # in +sec+ seconds. Omitting will use the default, Timeout::Error 63 # +message+:: Error message to raise with Exception Class. 64 # Omitting will use the default, "execution expired" 65 # 66 # Returns the result of the block *if* the block completed before 67 # +sec+ seconds, otherwise throws an exception, based on the value of +klass+. 68 # 69 # The exception thrown to terminate the given block cannot be rescued inside 70 # the block unless +klass+ is given explicitly. 71 # 72 # Note that this is both a method of module Timeout, so you can <tt>include 73 # Timeout</tt> into your classes so they have a #timeout method, as well as 74 # a module method, so you can call it directly as Timeout.timeout(). 75 def timeout(sec, klass = nil, message = nil) #:yield: +sec+ 76 return yield(sec) if sec == nil or sec.zero? 77 message ||= "execution expired".freeze 78 from = "from #{caller_locations(1, 1)[0]}" if $DEBUG 79 e = Error 80 bl = proc do |exception| 81 begin 82 x = Thread.current 83 y = Thread.start { 84 Thread.current.name = from 85 begin 86 sleep sec 87 rescue => e 88 x.raise e 89 else 90 x.raise exception, message 91 end 92 } 93 return yield(sec) 94 ensure 95 if y 96 y.kill 97 y.join # make sure y is dead. 98 end 99 end 100 end 101 if klass 102 begin 103 bl.call(klass) 104 rescue klass => e 105 bt = e.backtrace 106 end 107 else 108 bt = Error.catch(message, &bl) 109 end 110 level = -caller(CALLER_OFFSET).size-2 111 while THIS_FILE =~ bt[level] 112 bt.delete_at(level) 113 end 114 raise(e, message, bt) 115 end 116 117 module_function :timeout 118end 119 120def timeout(*args, &block) 121 warn "Object##{__method__} is deprecated, use Timeout.timeout instead.", uplevel: 1 122 Timeout.timeout(*args, &block) 123end 124 125# Another name for Timeout::Error, defined for backwards compatibility with 126# earlier versions of timeout.rb. 127TimeoutError = Timeout::Error 128class Object 129 deprecate_constant :TimeoutError 130end 131