Skip to content

Commit 1a499a8

Browse files
committed
Make Timeout.timeout work in a trap handler on CRuby
* Fixes #17
1 parent cb2ba88 commit 1a499a8

File tree

2 files changed

+54
-3
lines changed

2 files changed

+54
-3
lines changed

lib/timeout.rb

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def ensure_timeout_thread_created
123123
# In that case, just return and let the main thread create the Timeout thread.
124124
return if @timeout_thread_mutex.owned?
125125

126-
@timeout_thread_mutex.synchronize do
126+
Sync.synchronize @timeout_thread_mutex do
127127
unless @timeout_thread&.alive?
128128
@timeout_thread = create_timeout_thread
129129
end
@@ -132,7 +132,7 @@ def ensure_timeout_thread_created
132132
end
133133

134134
def add_request(request)
135-
@queue_mutex.synchronize do
135+
Sync.synchronize @queue_mutex do
136136
@queue << request
137137
@condvar.signal
138138
end
@@ -153,6 +153,7 @@ def initialize(thread, timeout, exception_class, message)
153153
@done = false # protected by @mutex
154154
end
155155

156+
# Only called by the timeout thread, so does not need Sync.synchronize
156157
def done?
157158
@mutex.synchronize do
158159
@done
@@ -163,6 +164,7 @@ def expired?(now)
163164
now >= @deadline
164165
end
165166

167+
# Only called by the timeout thread, so does not need Sync.synchronize
166168
def interrupt
167169
@mutex.synchronize do
168170
unless @done
@@ -173,13 +175,33 @@ def interrupt
173175
end
174176

175177
def finished
176-
@mutex.synchronize do
178+
Sync.synchronize @mutex do
177179
@done = true
178180
end
179181
end
180182
end
181183
private_constant :Request
182184

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+
183205
# :startdoc:
184206

185207
# Perform an operation in a block, raising an exception if it takes longer than

test/test_timeout.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,4 +416,33 @@ def test_ractor
416416
assert_equal :ok, r
417417
end;
418418
end if defined?(::Ractor) && RUBY_VERSION >= '4.0'
419+
420+
def test_timeout_in_trap_handler
421+
# https://github.com/ruby/timeout/issues/17
422+
423+
# Test as if this was the first timeout usage
424+
kill_timeout_thread
425+
426+
rd, wr = IO.pipe
427+
428+
trap("SIGUSR1") do
429+
begin
430+
Timeout.timeout(0.1) do
431+
sleep 1
432+
end
433+
rescue Timeout::Error
434+
wr.write "OK"
435+
wr.close
436+
else
437+
wr.write "did not raise"
438+
ensure
439+
wr.close
440+
end
441+
end
442+
443+
Process.kill :USR1, Process.pid
444+
445+
assert_equal "OK", rd.read
446+
rd.close
447+
end
419448
end

0 commit comments

Comments
 (0)