Skip to content

Commit 56ebd64

Browse files
committed
WIP: Handle nested timeouts call with same duration
* Prevent concurrent Thread#raise calls by tracking interruptible and interrupting states, and waiting if there is already an ongoing interrupt that we reach Request#finished.
1 parent fef9d07 commit 56ebd64

File tree

2 files changed

+88
-8
lines changed

2 files changed

+88
-8
lines changed

lib/timeout.rb

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,57 @@ def add_request(request)
140140
end
141141
private_constant :State
142142

143+
class FiberState
144+
attr_reader :mutex
145+
146+
def initialize
147+
@interruptible = false
148+
@interrupting = false
149+
@mutex = Mutex.new
150+
@condvar = ConditionVariable.new
151+
end
152+
153+
def interruptible
154+
prev = nil
155+
Sync.synchronize @mutex do
156+
prev = @interruptible
157+
@interruptible = true
158+
@condvar.signal
159+
end
160+
begin
161+
yield
162+
ensure
163+
Sync.synchronize @mutex do
164+
@interruptible = prev
165+
@condvar.signal
166+
end
167+
end
168+
end
169+
170+
def start_interrupt(request)
171+
raise unless @mutex.owned?
172+
173+
until @interruptible
174+
# We hold the Mutex so the Request can't finish,
175+
# except at this wait call, so we check right after
176+
@condvar.wait(@mutex)
177+
return false if request.done?
178+
end
179+
@interrupting = true
180+
@interruptible = false
181+
true
182+
end
183+
184+
def finished
185+
raise unless @mutex.owned?
186+
187+
if @interrupting
188+
@interrupting = false
189+
end
190+
end
191+
end
192+
private_constant :FiberState
193+
143194
class Request
144195
attr_reader :deadline
145196

@@ -149,13 +200,18 @@ def initialize(thread, timeout, exception_class, message)
149200
@exception_class = exception_class
150201
@message = message
151202

152-
@mutex = Mutex.new
153203
@done = false # protected by @mutex
204+
@fiber_state = (Thread.current[:timeout_interrupt_queue] ||= FiberState.new)
205+
@mutex = @fiber_state.mutex
206+
end
207+
208+
def interruptible(&block)
209+
@fiber_state.interruptible(&block)
154210
end
155211

156-
# Only called by the timeout thread, so does not need Sync.synchronize
157212
def done?
158-
@mutex.synchronize do
213+
return @done if @mutex.owned?
214+
Sync.synchronize @mutex do
159215
@done
160216
end
161217
end
@@ -167,16 +223,17 @@ def expired?(now)
167223
# Only called by the timeout thread, so does not need Sync.synchronize
168224
def interrupt
169225
@mutex.synchronize do
170-
unless @done
171-
@thread.raise @exception_class, @message
172-
@done = true
173-
end
226+
return if @done
227+
return unless @fiber_state.start_interrupt(self)
228+
@thread.raise @exception_class, @message
229+
@done = true
174230
end
175231
end
176232

177233
def finished
178234
Sync.synchronize @mutex do
179235
@done = true
236+
@fiber_state.finished
180237
end
181238
end
182239
end
@@ -292,7 +349,9 @@ def self.timeout(sec, klass = nil, message = nil, &block) #:yield: +sec+
292349
request = Request.new(Thread.current, sec, exc, message)
293350
state.add_request(request)
294351
begin
295-
return yield(sec)
352+
request.interruptible do
353+
yield(sec)
354+
end
296355
ensure
297356
request.finished
298357
end

repro_nesting1.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
require 'timeout'
2+
3+
begin
4+
Timeout.timeout(2) do
5+
Timeout.timeout(2) do
6+
sleep 3
7+
end
8+
end
9+
rescue Exception => e
10+
p :HERE
11+
puts ">"+e.full_message
12+
end
13+
puts
14+
15+
begin
16+
Timeout.timeout(0.1) do
17+
p sleep 3
18+
end
19+
rescue Exception => e
20+
p e.class
21+
end

0 commit comments

Comments
 (0)