[ruby/timeout] Make Timeout.timeout work in a trap handler on CRuby

* Fixes https://github.com/ruby/timeout/issues/17

https://github.com/ruby/timeout/commit/1a499a8f96
This commit is contained in:
Benoit Daloze 2025-12-11 14:16:49 +01:00 committed by git
parent 9865048a34
commit b49ff7cc70
2 changed files with 54 additions and 3 deletions

View File

@ -123,7 +123,7 @@ module Timeout
# In that case, just return and let the main thread create the Timeout thread.
return if @timeout_thread_mutex.owned?
@timeout_thread_mutex.synchronize do
Sync.synchronize @timeout_thread_mutex do
unless @timeout_thread&.alive?
@timeout_thread = create_timeout_thread
end
@ -132,7 +132,7 @@ module Timeout
end
def add_request(request)
@queue_mutex.synchronize do
Sync.synchronize @queue_mutex do
@queue << request
@condvar.signal
end
@ -153,6 +153,7 @@ module Timeout
@done = false # protected by @mutex
end
# Only called by the timeout thread, so does not need Sync.synchronize
def done?
@mutex.synchronize do
@done
@ -163,6 +164,7 @@ module Timeout
now >= @deadline
end
# Only called by the timeout thread, so does not need Sync.synchronize
def interrupt
@mutex.synchronize do
unless @done
@ -173,13 +175,33 @@ module Timeout
end
def finished
@mutex.synchronize do
Sync.synchronize @mutex do
@done = true
end
end
end
private_constant :Request
module Sync
# Calls mutex.synchronize(&block) but if that fails on CRuby due to being in a trap handler,
# run mutex.synchronize(&block) in a separate Thread instead.
def self.synchronize(mutex, &block)
begin
mutex.synchronize(&block)
rescue ThreadError => e
raise e unless e.message == "can't be called from trap context"
# Workaround CRuby issue https://bugs.ruby-lang.org/issues/19473
# which raises on Mutex#synchronize in trap handler.
# It's expensive to create a Thread just for this,
# but better than failing.
Thread.new {
mutex.synchronize(&block)
}.join
end
end
end
private_constant :Sync
# :startdoc:
# Perform an operation in a block, raising an exception if it takes longer than

View File

@ -416,4 +416,33 @@ class TestTimeout < Test::Unit::TestCase
assert_equal :ok, r
end;
end if defined?(::Ractor) && RUBY_VERSION >= '4.0'
def test_timeout_in_trap_handler
# https://github.com/ruby/timeout/issues/17
# Test as if this was the first timeout usage
kill_timeout_thread
rd, wr = IO.pipe
trap("SIGUSR1") do
begin
Timeout.timeout(0.1) do
sleep 1
end
rescue Timeout::Error
wr.write "OK"
wr.close
else
wr.write "did not raise"
ensure
wr.close
end
end
Process.kill :USR1, Process.pid
assert_equal "OK", rd.read
rd.close
end
end