diff options
| author | Koichi Sasada <ko1@atdot.net> | 2025-12-05 01:49:31 +0900 |
|---|---|---|
| committer | git <svn-admin@ruby-lang.org> | 2025-12-05 18:32:13 +0000 |
| commit | ec28bd75a869c2e525388582370239a9ff8c19e7 (patch) | |
| tree | 7532d0fd1eb596fdc9a63b07e851e4b81d23d915 | |
| parent | 65dbd571c1e0aae3b0919ae6e64704726f18b265 (diff) | |
[ruby/timeout] support Ractor
1. Introduce State to store all status.
2. Store State instance to the Ractor local storage if possible
3. Make `GET_TIME` (Method object) shareable if possible
3 is supporeted Ruby 4.0 and later, so the Rator support is works
only on Ruby 4.0 and later.
https://github.com/ruby/timeout/commit/54ff671c6c
| -rw-r--r-- | lib/timeout.rb | 166 | ||||
| -rw-r--r-- | test/test_timeout.rb | 20 |
2 files changed, 127 insertions, 59 deletions
diff --git a/lib/timeout.rb b/lib/timeout.rb index f173d028a3..2bf3e75146 100644 --- a/lib/timeout.rb +++ b/lib/timeout.rb @@ -44,12 +44,107 @@ module Timeout end # :stopdoc: - CONDVAR = ConditionVariable.new - QUEUE = Queue.new - QUEUE_MUTEX = Mutex.new - TIMEOUT_THREAD_MUTEX = Mutex.new - @timeout_thread = nil - private_constant :CONDVAR, :QUEUE, :QUEUE_MUTEX, :TIMEOUT_THREAD_MUTEX + + # We keep a private reference so that time mocking libraries won't break + # Timeout. + GET_TIME = + if defined?(Ractor.make_shareable) + begin + Ractor.make_shareable(Process.method(:clock_gettime)) + rescue # failed on Ruby 3.4 + Process.method(:clock_gettime) + end + else + Process.method(:clock_gettime) + end + + private_constant :GET_TIME + + class State + attr_reader :condvar, :queue, :queue_mutex # shared with Timeout.timeout() + + def initialize + @condvar = ConditionVariable.new + @queue = Queue.new + @queue_mutex = Mutex.new + + @timeout_thread = nil + @timeout_thread_mutex = Mutex.new + end + + if defined?(Ractor.store_if_absent) && + defined?(Ractor.shareble?) && Ractor.shareable?(GET_TIME) + + # Ractor support if + # 1. Ractor.store_if_absent is available + # 2. Method object can be shareable (4.0~) + + Ractor.store_if_absent :timeout_gem_state do + State.new + end + + def self.instance + Ractor[:timeout_gem_state] + end + + ::Timeout::RACTOR_SUPPORT = true # for test + else + @GLOBAL_STATE = State.new + + def self.instance + @GLOBAL_STATE + end + end + + def create_timeout_thread + watcher = Thread.new do + requests = [] + while true + until @queue.empty? and !requests.empty? # wait to have at least one request + req = @queue.pop + requests << req unless req.done? + end + closest_deadline = requests.min_by(&:deadline).deadline + + now = 0.0 + @queue_mutex.synchronize do + while (now = GET_TIME.call(Process::CLOCK_MONOTONIC)) < closest_deadline and @queue.empty? + @condvar.wait(@queue_mutex, closest_deadline - now) + end + end + + requests.each do |req| + req.interrupt if req.expired?(now) + end + requests.reject!(&:done?) + end + end + + if !watcher.group.enclosed? && (!defined?(Ractor.main?) || Ractor.main?) + ThreadGroup::Default.add(watcher) + end + + watcher.name = "Timeout stdlib thread" + watcher.thread_variable_set(:"\0__detached_thread__", true) + watcher + end + + def ensure_timeout_thread_created + unless @timeout_thread&.alive? + # If the Mutex is already owned we are in a signal handler. + # 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 + unless @timeout_thread&.alive? + @timeout_thread = create_timeout_thread + end + end + end + end + end + + private_constant :State class Request attr_reader :deadline @@ -91,55 +186,6 @@ module Timeout end private_constant :Request - def self.create_timeout_thread - watcher = Thread.new do - requests = [] - while true - until QUEUE.empty? and !requests.empty? # wait to have at least one request - req = QUEUE.pop - requests << req unless req.done? - end - closest_deadline = requests.min_by(&:deadline).deadline - - now = 0.0 - QUEUE_MUTEX.synchronize do - while (now = GET_TIME.call(Process::CLOCK_MONOTONIC)) < closest_deadline and QUEUE.empty? - CONDVAR.wait(QUEUE_MUTEX, closest_deadline - now) - end - end - - requests.each do |req| - req.interrupt if req.expired?(now) - end - requests.reject!(&:done?) - end - end - ThreadGroup::Default.add(watcher) unless watcher.group.enclosed? - watcher.name = "Timeout stdlib thread" - watcher.thread_variable_set(:"\0__detached_thread__", true) - watcher - end - private_class_method :create_timeout_thread - - def self.ensure_timeout_thread_created - unless @timeout_thread and @timeout_thread.alive? - # If the Mutex is already owned we are in a signal handler. - # 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 - unless @timeout_thread and @timeout_thread.alive? - @timeout_thread = create_timeout_thread - end - end - end - end - private_class_method :ensure_timeout_thread_created - - # We keep a private reference so that time mocking libraries won't break - # Timeout. - GET_TIME = Process.method(:clock_gettime) - private_constant :GET_TIME - # :startdoc: # Perform an operation in a block, raising an error if it takes longer than @@ -178,12 +224,14 @@ module Timeout return scheduler.timeout_after(sec, klass || Error, message, &block) end - ensure_timeout_thread_created + state = State.instance + state.ensure_timeout_thread_created + perform = Proc.new do |exc| request = Request.new(Thread.current, sec, exc, message) - QUEUE_MUTEX.synchronize do - QUEUE << request - CONDVAR.signal + state.queue_mutex.synchronize do + state.queue << request + state.condvar.signal end begin return yield(sec) diff --git a/test/test_timeout.rb b/test/test_timeout.rb index e367df757c..233f54eb82 100644 --- a/test/test_timeout.rb +++ b/test/test_timeout.rb @@ -280,4 +280,24 @@ class TestTimeout < Test::Unit::TestCase }.join end; end + + def test_ractor + assert_separately(%w[-rtimeout -W0], <<-'end;') + r = Ractor.new do + Timeout.timeout(1) { 42 } + end.value + + assert_equal 42, r + + r = Ractor.new do + begin + Timeout.timeout(0.1) { sleep } + rescue Timeout::Error + :ok + end + end.value + + assert_equal :ok, r + end; + end if Timeout.const_defined?(:RACTOR_SUPPORT) end |
