timeout 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/timeout.rb +112 -39
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 405263117245d6a9c981a56bc90cad374d50547078dbcfef1cb3fecca78bcece
|
|
4
|
+
data.tar.gz: 8eaeb608b52c8f273342746ff88f7e7b9458325e4a30e9f5605d62c749022ec0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1bf761aa173a75b2545088a1304eb8e8c2bc2df0b3223af738fd2c10f2170e4b140644aa9dc036835149ff1ca2cecf866cd782560b9e3856dc63491f6b357657
|
|
7
|
+
data.tar.gz: da2ec36df76ea4dc31c532a704d3ae102eac3dc60f4091e9d7dd072144353a1a38aa818ed15d36fb8c8a675b82a9ffa9eae85475e11346faafe48f6318602af8
|
data/lib/timeout.rb
CHANGED
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
|
|
21
21
|
module Timeout
|
|
22
22
|
# The version
|
|
23
|
-
VERSION = "0.
|
|
23
|
+
VERSION = "0.6.0"
|
|
24
24
|
|
|
25
|
-
# Internal
|
|
25
|
+
# Internal exception raised to when a timeout is triggered.
|
|
26
26
|
class ExitException < Exception
|
|
27
27
|
def exception(*) # :nodoc:
|
|
28
28
|
self
|
|
@@ -54,8 +54,6 @@ module Timeout
|
|
|
54
54
|
private_constant :GET_TIME
|
|
55
55
|
|
|
56
56
|
class State
|
|
57
|
-
attr_reader :condvar, :queue, :queue_mutex # shared with Timeout.timeout()
|
|
58
|
-
|
|
59
57
|
def initialize
|
|
60
58
|
@condvar = ConditionVariable.new
|
|
61
59
|
@queue = Queue.new
|
|
@@ -83,36 +81,40 @@ module Timeout
|
|
|
83
81
|
end
|
|
84
82
|
|
|
85
83
|
def create_timeout_thread
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
84
|
+
# Threads unexpectedly inherit the interrupt mask: https://github.com/ruby/timeout/issues/41
|
|
85
|
+
# So reset the interrupt mask to the default one for the timeout thread
|
|
86
|
+
Thread.handle_interrupt(Object => :immediate) do
|
|
87
|
+
watcher = Thread.new do
|
|
88
|
+
requests = []
|
|
89
|
+
while true
|
|
90
|
+
until @queue.empty? and !requests.empty? # wait to have at least one request
|
|
91
|
+
req = @queue.pop
|
|
92
|
+
requests << req unless req.done?
|
|
93
|
+
end
|
|
94
|
+
closest_deadline = requests.min_by(&:deadline).deadline
|
|
94
95
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
now = 0.0
|
|
97
|
+
@queue_mutex.synchronize do
|
|
98
|
+
while (now = GET_TIME.call(Process::CLOCK_MONOTONIC)) < closest_deadline and @queue.empty?
|
|
99
|
+
@condvar.wait(@queue_mutex, closest_deadline - now)
|
|
100
|
+
end
|
|
99
101
|
end
|
|
100
|
-
end
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
requests.each do |req|
|
|
104
|
+
req.interrupt if req.expired?(now)
|
|
105
|
+
end
|
|
106
|
+
requests.reject!(&:done?)
|
|
104
107
|
end
|
|
105
|
-
requests.reject!(&:done?)
|
|
106
108
|
end
|
|
107
|
-
end
|
|
108
109
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
if !watcher.group.enclosed? && (!defined?(Ractor.main?) || Ractor.main?)
|
|
111
|
+
ThreadGroup::Default.add(watcher)
|
|
112
|
+
end
|
|
112
113
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
watcher.name = "Timeout stdlib thread"
|
|
115
|
+
watcher.thread_variable_set(:"\0__detached_thread__", true)
|
|
116
|
+
watcher
|
|
117
|
+
end
|
|
116
118
|
end
|
|
117
119
|
|
|
118
120
|
def ensure_timeout_thread_created
|
|
@@ -121,13 +123,20 @@ module Timeout
|
|
|
121
123
|
# In that case, just return and let the main thread create the Timeout thread.
|
|
122
124
|
return if @timeout_thread_mutex.owned?
|
|
123
125
|
|
|
124
|
-
|
|
126
|
+
Sync.synchronize @timeout_thread_mutex do
|
|
125
127
|
unless @timeout_thread&.alive?
|
|
126
128
|
@timeout_thread = create_timeout_thread
|
|
127
129
|
end
|
|
128
130
|
end
|
|
129
131
|
end
|
|
130
132
|
end
|
|
133
|
+
|
|
134
|
+
def add_request(request)
|
|
135
|
+
Sync.synchronize @queue_mutex do
|
|
136
|
+
@queue << request
|
|
137
|
+
@condvar.signal
|
|
138
|
+
end
|
|
139
|
+
end
|
|
131
140
|
end
|
|
132
141
|
private_constant :State
|
|
133
142
|
|
|
@@ -144,6 +153,7 @@ module Timeout
|
|
|
144
153
|
@done = false # protected by @mutex
|
|
145
154
|
end
|
|
146
155
|
|
|
156
|
+
# Only called by the timeout thread, so does not need Sync.synchronize
|
|
147
157
|
def done?
|
|
148
158
|
@mutex.synchronize do
|
|
149
159
|
@done
|
|
@@ -154,6 +164,7 @@ module Timeout
|
|
|
154
164
|
now >= @deadline
|
|
155
165
|
end
|
|
156
166
|
|
|
167
|
+
# Only called by the timeout thread, so does not need Sync.synchronize
|
|
157
168
|
def interrupt
|
|
158
169
|
@mutex.synchronize do
|
|
159
170
|
unless @done
|
|
@@ -164,16 +175,36 @@ module Timeout
|
|
|
164
175
|
end
|
|
165
176
|
|
|
166
177
|
def finished
|
|
167
|
-
|
|
178
|
+
Sync.synchronize @mutex do
|
|
168
179
|
@done = true
|
|
169
180
|
end
|
|
170
181
|
end
|
|
171
182
|
end
|
|
172
183
|
private_constant :Request
|
|
173
184
|
|
|
185
|
+
module Sync
|
|
186
|
+
# Calls mutex.synchronize(&block) but if that fails on CRuby due to being in a trap handler,
|
|
187
|
+
# run mutex.synchronize(&block) in a separate Thread instead.
|
|
188
|
+
def self.synchronize(mutex, &block)
|
|
189
|
+
begin
|
|
190
|
+
mutex.synchronize(&block)
|
|
191
|
+
rescue ThreadError => e
|
|
192
|
+
raise e unless e.message == "can't be called from trap context"
|
|
193
|
+
# Workaround CRuby issue https://bugs.ruby-lang.org/issues/19473
|
|
194
|
+
# which raises on Mutex#synchronize in trap handler.
|
|
195
|
+
# It's expensive to create a Thread just for this,
|
|
196
|
+
# but better than failing.
|
|
197
|
+
Thread.new {
|
|
198
|
+
mutex.synchronize(&block)
|
|
199
|
+
}.join
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
private_constant :Sync
|
|
204
|
+
|
|
174
205
|
# :startdoc:
|
|
175
206
|
|
|
176
|
-
# Perform an operation in a block, raising an
|
|
207
|
+
# Perform an operation in a block, raising an exception if it takes longer than
|
|
177
208
|
# +sec+ seconds to complete.
|
|
178
209
|
#
|
|
179
210
|
# +sec+:: Number of seconds to wait for the block to terminate. Any non-negative number
|
|
@@ -186,12 +217,18 @@ module Timeout
|
|
|
186
217
|
# Omitting will use the default, "execution expired"
|
|
187
218
|
#
|
|
188
219
|
# Returns the result of the block *if* the block completed before
|
|
189
|
-
# +sec+ seconds, otherwise
|
|
220
|
+
# +sec+ seconds, otherwise raises an exception, based on the value of +klass+.
|
|
190
221
|
#
|
|
191
|
-
# The exception
|
|
192
|
-
#
|
|
193
|
-
#
|
|
194
|
-
#
|
|
222
|
+
# The exception raised to terminate the given block is the given +klass+, or
|
|
223
|
+
# Timeout::ExitException if +klass+ is not given. The reason for that behavior
|
|
224
|
+
# is that Timeout::Error inherits from RuntimeError and might be caught unexpectedly by `rescue`.
|
|
225
|
+
# Timeout::ExitException inherits from Exception so it will only be rescued by `rescue Exception`.
|
|
226
|
+
# Note that the Timeout::ExitException is translated to a Timeout::Error once it reaches the Timeout.timeout call,
|
|
227
|
+
# so outside that call it will be a Timeout::Error.
|
|
228
|
+
#
|
|
229
|
+
# In general, be aware that the code block may rescue the exception, and in such a case not respect the timeout.
|
|
230
|
+
# Also, the block can use +ensure+ to prevent the handling of the exception.
|
|
231
|
+
# For those reasons, this method cannot be relied on to enforce timeouts for untrusted blocks.
|
|
195
232
|
#
|
|
196
233
|
# If a scheduler is defined, it will be used to handle the timeout by invoking
|
|
197
234
|
# Scheduler#timeout_after.
|
|
@@ -199,6 +236,45 @@ module Timeout
|
|
|
199
236
|
# Note that this is both a method of module Timeout, so you can <tt>include
|
|
200
237
|
# Timeout</tt> into your classes so they have a #timeout method, as well as
|
|
201
238
|
# a module method, so you can call it directly as Timeout.timeout().
|
|
239
|
+
#
|
|
240
|
+
# ==== Ensuring the exception does not fire inside ensure blocks
|
|
241
|
+
#
|
|
242
|
+
# When using Timeout.timeout it can be desirable to ensure the timeout exception does not fire inside an +ensure+ block.
|
|
243
|
+
# The simplest and best way to do so it to put the Timeout.timeout call inside the body of the begin/ensure/end:
|
|
244
|
+
#
|
|
245
|
+
# begin
|
|
246
|
+
# Timeout.timeout(sec) { some_long_operation }
|
|
247
|
+
# ensure
|
|
248
|
+
# cleanup # safe, cannot be interrupt by timeout
|
|
249
|
+
# end
|
|
250
|
+
#
|
|
251
|
+
# If that is not feasible, e.g. if there are +ensure+ blocks inside +some_long_operation+,
|
|
252
|
+
# they need to not be interrupted by timeout, and it's not possible to move these ensure blocks outside,
|
|
253
|
+
# one can use Thread.handle_interrupt to delay the timeout exception like so:
|
|
254
|
+
#
|
|
255
|
+
# Thread.handle_interrupt(Timeout::Error => :never) {
|
|
256
|
+
# Timeout.timeout(sec, Timeout::Error) do
|
|
257
|
+
# setup # timeout cannot happen here, no matter how long it takes
|
|
258
|
+
# Thread.handle_interrupt(Timeout::Error => :immediate) {
|
|
259
|
+
# some_long_operation # timeout can happen here
|
|
260
|
+
# }
|
|
261
|
+
# ensure
|
|
262
|
+
# cleanup # timeout cannot happen here, no matter how long it takes
|
|
263
|
+
# end
|
|
264
|
+
# }
|
|
265
|
+
#
|
|
266
|
+
# An important thing to note is the need to pass an exception klass to Timeout.timeout,
|
|
267
|
+
# otherwise it does not work. Specifically, using +Thread.handle_interrupt(Timeout::ExitException => ...)+
|
|
268
|
+
# is unsupported and causes subtle errors like raising the wrong exception outside the block, do not use that.
|
|
269
|
+
#
|
|
270
|
+
# Note that Thread.handle_interrupt is somewhat dangerous because if setup or cleanup hangs
|
|
271
|
+
# then the current thread will hang too and the timeout will never fire.
|
|
272
|
+
# Also note the block might run for longer than +sec+ seconds:
|
|
273
|
+
# e.g. some_long_operation executes for +sec+ seconds + whatever time cleanup takes.
|
|
274
|
+
#
|
|
275
|
+
# If you want the timeout to only happen on blocking operations one can use :on_blocking
|
|
276
|
+
# instead of :immediate. However, that means if the block uses no blocking operations after +sec+ seconds,
|
|
277
|
+
# the block will not be interrupted.
|
|
202
278
|
def self.timeout(sec, klass = nil, message = nil, &block) #:yield: +sec+
|
|
203
279
|
return yield(sec) if sec == nil or sec.zero?
|
|
204
280
|
raise ArgumentError, "Timeout sec must be a non-negative number" if 0 > sec
|
|
@@ -214,10 +290,7 @@ module Timeout
|
|
|
214
290
|
|
|
215
291
|
perform = Proc.new do |exc|
|
|
216
292
|
request = Request.new(Thread.current, sec, exc, message)
|
|
217
|
-
state.
|
|
218
|
-
state.queue << request
|
|
219
|
-
state.condvar.signal
|
|
220
|
-
end
|
|
293
|
+
state.add_request(request)
|
|
221
294
|
begin
|
|
222
295
|
return yield(sec)
|
|
223
296
|
ensure
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: timeout
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yukihiro Matsumoto
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2025-12-
|
|
10
|
+
date: 2025-12-17 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
12
|
description: Auto-terminate potentially long-running operations in Ruby.
|
|
13
13
|
email:
|