summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBenoit Daloze <eregontp@gmail.com>2022-05-12 16:20:56 +0200
committergit <svn-admin@ruby-lang.org>2022-05-19 07:19:39 +0900
commit89fbec224d8e1fa35e82bf2712c5a5fd3dc06b83 (patch)
tree9beaa3430a7c8f91c41a8fc12f2b616c4a4332da
parent97c12c5f692d176278dd6445a751788568b54e4d (diff)
downloadruby-89fbec224d8e1fa35e82bf2712c5a5fd3dc06b83.tar.gz
[ruby/timeout] Reimplement Timeout.timeout with a single thread and a Queue
https://github.com/ruby/timeout/commit/2bafc458f1
-rw-r--r--lib/timeout.rb125
-rw-r--r--test/test_timeout.rb13
2 files changed, 101 insertions, 37 deletions
diff --git a/lib/timeout.rb b/lib/timeout.rb
index f50e706d4b..3c3cb8e066 100644
--- a/lib/timeout.rb
+++ b/lib/timeout.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
# Timeout long-running blocks
#
# == Synopsis
@@ -23,7 +23,7 @@
# Copyright:: (C) 2000 Information-technology Promotion Agency, Japan
module Timeout
- VERSION = "0.2.0".freeze
+ VERSION = "0.2.0"
# Raised by Timeout.timeout when the block times out.
class Error < RuntimeError
@@ -50,9 +50,79 @@ module Timeout
end
# :stopdoc:
- THIS_FILE = /\A#{Regexp.quote(__FILE__)}:/o
- CALLER_OFFSET = ((c = caller[0]) && THIS_FILE =~ c) ? 1 : 0
- private_constant :THIS_FILE, :CALLER_OFFSET
+ 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
+
+ class Request
+ attr_reader :deadline
+
+ def initialize(thread, timeout, exception_class, message)
+ @thread = thread
+ @deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
+ @exception_class = exception_class
+ @message = message
+
+ @mutex = Mutex.new
+ @done = false
+ end
+
+ def done?
+ @done
+ end
+
+ def expired?(now)
+ now >= @deadline and !done?
+ end
+
+ def interrupt
+ @mutex.synchronize do
+ unless @done
+ @thread.raise @exception_class, @message
+ @done = true
+ end
+ end
+ end
+
+ def finished
+ @mutex.synchronize do
+ @done = true
+ end
+ end
+ end
+ private_constant :Request
+
+ def self.ensure_timeout_thread_created
+ unless @timeout_thread
+ TIMEOUT_THREAD_MUTEX.synchronize do
+ @timeout_thread ||= 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 = Process.clock_gettime(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
+ end
+ end
+ end
# :startdoc:
# Perform an operation in a block, raising an error if it takes longer than
@@ -83,51 +153,32 @@ module Timeout
def timeout(sec, klass = nil, message = nil, &block) #:yield: +sec+
return yield(sec) if sec == nil or sec.zero?
- message ||= "execution expired".freeze
+ message ||= "execution expired"
if Fiber.respond_to?(:current_scheduler) && (scheduler = Fiber.current_scheduler)&.respond_to?(:timeout_after)
return scheduler.timeout_after(sec, klass || Error, message, &block)
end
- from = "from #{caller_locations(1, 1)[0]}" if $DEBUG
- e = Error
- bl = proc do |exception|
+ Timeout.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
+ end
begin
- x = Thread.current
- y = Thread.start {
- Thread.current.name = from
- begin
- sleep sec
- rescue => e
- x.raise e
- else
- x.raise exception, message
- end
- }
return yield(sec)
ensure
- if y
- y.kill
- y.join # make sure y is dead.
- end
+ request.finished
end
end
+
if klass
- begin
- bl.call(klass)
- rescue klass => e
- message = e.message
- bt = e.backtrace
- end
+ perform.call(klass)
else
- bt = Error.catch(message, &bl)
+ backtrace = Error.catch(&perform)
+ raise Error, message, backtrace
end
- level = -caller(CALLER_OFFSET).size-2
- while THIS_FILE =~ bt[level]
- bt.delete_at(level)
- end
- raise(e, message, bt)
end
-
module_function :timeout
end
diff --git a/test/test_timeout.rb b/test/test_timeout.rb
index 74b65f119d..58eb8421db 100644
--- a/test/test_timeout.rb
+++ b/test/test_timeout.rb
@@ -10,6 +10,18 @@ class TestTimeout < Test::Unit::TestCase
end
end
+ def test_included
+ c = Class.new do
+ include Timeout
+ def test
+ timeout(1) { :ok }
+ end
+ end
+ assert_nothing_raised do
+ assert_equal :ok, c.new.test
+ end
+ end
+
def test_yield_param
assert_equal [5, :ok], Timeout.timeout(5){|s| [s, :ok] }
end
@@ -43,6 +55,7 @@ class TestTimeout < Test::Unit::TestCase
begin
sleep 3
rescue Exception => e
+ flunk "should not see any exception but saw #{e.inspect}"
end
end
end