1- # frozen_string_literal: false
1+ # frozen_string_literal: true
22# Timeout long-running blocks
33#
44# == Synopsis
2323# Copyright:: (C) 2000 Information-technology Promotion Agency, Japan
2424
2525module Timeout
26- VERSION = "0.2.0" . freeze
26+ VERSION = "0.2.0"
2727
2828 # Raised by Timeout.timeout when the block times out.
2929 class Error < RuntimeError
@@ -50,9 +50,88 @@ def exception(*)
5050 end
5151
5252 # :stopdoc:
53- THIS_FILE = /\A #{ Regexp . quote ( __FILE__ ) } :/o
54- CALLER_OFFSET = ( ( c = caller [ 0 ] ) && THIS_FILE =~ c ) ? 1 : 0
55- private_constant :THIS_FILE , :CALLER_OFFSET
53+ CONDVAR = ConditionVariable . new
54+ QUEUE = Queue . new
55+ QUEUE_MUTEX = Mutex . new
56+ TIMEOUT_THREAD_MUTEX = Mutex . new
57+ @timeout_thread = nil
58+ private_constant :CONDVAR , :QUEUE , :QUEUE_MUTEX , :TIMEOUT_THREAD_MUTEX
59+
60+ class Request
61+ attr_reader :deadline
62+
63+ def initialize ( thread , timeout , exception_class , message )
64+ @thread = thread
65+ @deadline = Process . clock_gettime ( Process ::CLOCK_MONOTONIC ) + timeout
66+ @exception_class = exception_class
67+ @message = message
68+
69+ @mutex = Mutex . new
70+ @done = false # protected by @mutex
71+ end
72+
73+ def done?
74+ @mutex . synchronize do
75+ @done
76+ end
77+ end
78+
79+ def expired? ( now )
80+ now >= @deadline
81+ end
82+
83+ def interrupt
84+ @mutex . synchronize do
85+ unless @done
86+ @thread . raise @exception_class , @message
87+ @done = true
88+ end
89+ end
90+ end
91+
92+ def finished
93+ @mutex . synchronize do
94+ @done = true
95+ end
96+ end
97+ end
98+ private_constant :Request
99+
100+ def self . create_timeout_thread
101+ Thread . new do
102+ requests = [ ]
103+ while true
104+ until QUEUE . empty? and !requests . empty? # wait to have at least one request
105+ req = QUEUE . pop
106+ requests << req unless req . done?
107+ end
108+ closest_deadline = requests . min_by ( &:deadline ) . deadline
109+
110+ now = 0.0
111+ QUEUE_MUTEX . synchronize do
112+ while ( now = Process . clock_gettime ( Process ::CLOCK_MONOTONIC ) ) < closest_deadline and QUEUE . empty?
113+ CONDVAR . wait ( QUEUE_MUTEX , closest_deadline - now )
114+ end
115+ end
116+
117+ requests . each do |req |
118+ req . interrupt if req . expired? ( now )
119+ end
120+ requests . reject! ( &:done? )
121+ end
122+ end
123+ end
124+ private_class_method :create_timeout_thread
125+
126+ def self . ensure_timeout_thread_created
127+ unless @timeout_thread and @timeout_thread . alive?
128+ TIMEOUT_THREAD_MUTEX . synchronize do
129+ unless @timeout_thread and @timeout_thread . alive?
130+ @timeout_thread = create_timeout_thread
131+ end
132+ end
133+ end
134+ end
56135 # :startdoc:
57136
58137 # Perform an operation in a block, raising an error if it takes longer than
@@ -83,51 +162,32 @@ def exception(*)
83162 def timeout ( sec , klass = nil , message = nil , &block ) #:yield: +sec+
84163 return yield ( sec ) if sec == nil or sec . zero?
85164
86- message ||= "execution expired" . freeze
165+ message ||= "execution expired"
87166
88167 if Fiber . respond_to? ( :current_scheduler ) && ( scheduler = Fiber . current_scheduler ) &.respond_to? ( :timeout_after )
89168 return scheduler . timeout_after ( sec , klass || Error , message , &block )
90169 end
91170
92- from = "from #{ caller_locations ( 1 , 1 ) [ 0 ] } " if $DEBUG
93- e = Error
94- bl = proc do |exception |
171+ Timeout . ensure_timeout_thread_created
172+ perform = Proc . new do |exc |
173+ request = Request . new ( Thread . current , sec , exc , message )
174+ QUEUE_MUTEX . synchronize do
175+ QUEUE << request
176+ CONDVAR . signal
177+ end
95178 begin
96- x = Thread . current
97- y = Thread . start {
98- Thread . current . name = from
99- begin
100- sleep sec
101- rescue => e
102- x . raise e
103- else
104- x . raise exception , message
105- end
106- }
107179 return yield ( sec )
108180 ensure
109- if y
110- y . kill
111- y . join # make sure y is dead.
112- end
181+ request . finished
113182 end
114183 end
184+
115185 if klass
116- begin
117- bl . call ( klass )
118- rescue klass => e
119- message = e . message
120- bt = e . backtrace
121- end
186+ perform . call ( klass )
122187 else
123- bt = Error . catch ( message , &bl )
188+ backtrace = Error . catch ( &perform )
189+ raise Error , message , backtrace
124190 end
125- level = -caller ( CALLER_OFFSET ) . size -2
126- while THIS_FILE =~ bt [ level ]
127- bt . delete_at ( level )
128- end
129- raise ( e , message , bt )
130191 end
131-
132192 module_function :timeout
133193end
0 commit comments