1
- # frozen_string_literal: false
1
+ # frozen_string_literal: true
2
2
# Timeout long-running blocks
3
3
#
4
4
# == Synopsis
23
23
# Copyright:: (C) 2000 Information-technology Promotion Agency, Japan
24
24
25
25
module Timeout
26
- VERSION = "0.2.0" . freeze
26
+ VERSION = "0.2.0"
27
27
28
28
# Raised by Timeout.timeout when the block times out.
29
29
class Error < RuntimeError
@@ -50,9 +50,79 @@ def exception(*)
50
50
end
51
51
52
52
# :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
71
+ end
72
+
73
+ def done?
74
+ @done
75
+ end
76
+
77
+ def expired? ( now )
78
+ now >= @deadline and !done?
79
+ end
80
+
81
+ def interrupt
82
+ @mutex . synchronize do
83
+ unless @done
84
+ @thread . raise @exception_class , @message
85
+ @done = true
86
+ end
87
+ end
88
+ end
89
+
90
+ def finished
91
+ @mutex . synchronize do
92
+ @done = true
93
+ end
94
+ end
95
+ end
96
+ private_constant :Request
97
+
98
+ def self . ensure_timeout_thread_created
99
+ unless @timeout_thread
100
+ TIMEOUT_THREAD_MUTEX . synchronize do
101
+ @timeout_thread ||= 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
+ end
125
+ end
56
126
# :startdoc:
57
127
58
128
# Perform an operation in a block, raising an error if it takes longer than
@@ -83,51 +153,32 @@ def exception(*)
83
153
def timeout ( sec , klass = nil , message = nil , &block ) #:yield: +sec+
84
154
return yield ( sec ) if sec == nil or sec . zero?
85
155
86
- message ||= "execution expired" . freeze
156
+ message ||= "execution expired"
87
157
88
158
if Fiber . respond_to? ( :current_scheduler ) && ( scheduler = Fiber . current_scheduler ) &.respond_to? ( :timeout_after )
89
159
return scheduler . timeout_after ( sec , klass || Error , message , &block )
90
160
end
91
161
92
- from = "from #{ caller_locations ( 1 , 1 ) [ 0 ] } " if $DEBUG
93
- e = Error
94
- bl = proc do |exception |
162
+ Timeout . ensure_timeout_thread_created
163
+ perform = Proc . new do |exc |
164
+ request = Request . new ( Thread . current , sec , exc , message )
165
+ QUEUE_MUTEX . synchronize do
166
+ QUEUE << request
167
+ CONDVAR . signal
168
+ end
95
169
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
- }
107
170
return yield ( sec )
108
171
ensure
109
- if y
110
- y . kill
111
- y . join # make sure y is dead.
112
- end
172
+ request . finished
113
173
end
114
174
end
175
+
115
176
if klass
116
- begin
117
- bl . call ( klass )
118
- rescue klass => e
119
- message = e . message
120
- bt = e . backtrace
121
- end
177
+ perform . call ( klass )
122
178
else
123
- bt = Error . catch ( message , &bl )
179
+ backtrace = Error . catch ( &perform )
180
+ raise Error , message , backtrace
124
181
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 )
130
182
end
131
-
132
183
module_function :timeout
133
184
end
0 commit comments