=======

TimeoutInterruptSingleton created for helper methods like setup and so on.

TimeoutInterrupt#timeout and TimeoutInterrupt.timeout are stubs for calling timeout of singleton-class.

Scopeable
=========

`TimeoutInterrupt.timeout` should be scopeable now.
`TimeoutInterrupt.timeout` without args will check timeouts and will raise the next timed out timeout,
needed for scopes and if many time outs at the same time occurs.
This commit is contained in:
Denis Knauf 2013-03-07 12:30:07 +01:00
parent 7608c98054
commit 6f57bb4223
3 changed files with 230 additions and 78 deletions

View file

@ -7,9 +7,62 @@ It uses POSIX's alarm and traps ALRM-signals.
Known limitations bacause of alarm and ALRM are, that you can not use alarm or trap ALRM. Known limitations bacause of alarm and ALRM are, that you can not use alarm or trap ALRM.
Scopes
======
If you need scopes with inner and outer time outs, you should know:
The first timed out Timeout will be raised:
include TimeoutInterrupt
timeout(1) { # Will be raised
timeout(10) { sleep 2 } # Will not be raised
}
If you want to know, which was raised, you need custom exceptions:
class CustomErrorWillBeRaised <Exception
end
class CustomErrorNotRaise <Exception
end
include TimeoutInterrupt
timeout( 1, CustomErrorWillBeRaised) { # Will be raised again
timeout( 10, CustomErrorNotRaise) { sleep 2 } # Will not be raised
}
Problems
========
Memory-Leaks or no clean up
---------------------------
Do not forget, syscall can have allocated memory. Do not forget, syscall can have allocated memory.
If you interrupt a call, which can not free his allocations, you will have a memory leak. If you interrupt a call, which can not free his allocations, you will have a memory leak.
So, use it only, if your process did not live any longer or if you call something, which never allocate mem If it opens a file, reads it and closes it and while it reads, a time out occurs, the file will not be closed.
So, use it only, if your process did not live any longer or if you call something, which never allocate mem or opens a file.
Every time, a process dies, all his memory will be freed and every file will be closed, so let your process die and you should be safe.
Exception-handling
------------------
Timeouts can break your exception-handling! You should not handling exception while you wait for a timeout:
include TimeoutInterrupt
timeout(1) {
begin
transaction_begin
do_something
ensure
clean_up
transaction_end
end
}
Same happens, if clean\_up will raise an exception.
And same problem you have with ruby's `Timeout.timeout`.
Copyleft Copyleft
========= =========

View file

@ -7,43 +7,67 @@ module FFI
end end
end end
module TimeoutInterrupt module TimeoutInterruptSingleton
def self.timeouts class <<self
@timeouts ||= {} def timeouts thread = nil
end @timeouts ||= Hash.new {|h,k| h[k] = [] }
thread = Thread.current if thread.kind_of? Thread
def self.alarm_trap sig thread ? @timeouts[thread] : @timeouts
key, (at, bt) = TimeoutInterrupt.timeouts.min_by {|key,(at,bt)| at }
return if Time.now < at
raise Timeout::Error, 'execution expired', bt
end
def self.setup_timeout
if TimeoutInterrupt.timeouts.empty?
Signal.trap( 'ALRM') {}
FFI::LibC.alarm 0
else
key, (at, bt) = TimeoutInterrupt.timeouts.min_by {|key,(at,bt)| at }
secs = (at - Time.now).to_i+1
TimeoutInterrupt.alarm_trap if 1 > secs
Signal.trap 'ALRM', &TimeoutInterrupt.method( :alarm_trap)
FFI::LibC.alarm secs
end end
end
def self.timeout seconds def alarm_trap sig
seconds = seconds.to_i key, (at, bt, exception) = self.timeouts.min_by {|key,(at,bt,ex)| at }
raise Timeout::Error, "Timeout must be longer than '0' seconds." unless 0 < seconds return if Time.now < at
return lambda {|&e| self.timeout seconds, &e } unless block_given? raise exception, 'execution expired', bt
at = Time.now + seconds end
key, bt = Random.rand( 2**64-1), Kernel.caller
begin def setup
TimeoutInterrupt.timeouts[key] = [at, bt] if timeouts.empty?
TimeoutInterrupt.setup_timeout Signal.trap( 'ALRM') {}
yield FFI::LibC.alarm 0
ensure else
TimeoutInterrupt.timeouts.delete key key, (at, bt) = timeouts.min_by {|key,(at,bt)| at }
TimeoutInterrupt.setup_timeout secs = (at - Time.now)
alarm_trap 14 if 0 > secs
Signal.trap 'ALRM', &method( :alarm_trap)
FFI::LibC.alarm secs.to_i+1
end
end
def timeout seconds = nil, exception = nil
return setup if seconds.nil?
seconds = seconds.to_i
exception ||= TimeoutInterrupt::Error
raise exception, "Timeout must be longer than '0' seconds." unless 0 < seconds
unless block_given?
return lambda {|&e|
raise exception, "Expect a lambda." unless e
timeout seconds, exception, &e
}
end
at = Time.now + seconds
key, bt = Random.rand( 2**64-1), Kernel.caller
begin
self.timeouts[key] = [at, bt, exception]
setup
yield
ensure
self.timeouts.delete key
setup
end
end end
end end
end end
module TimeoutInterrupt
class Error < Timeout::Error
end
def self.timeout seconds = nil, exception = nil, &e
TimeoutInterruptSingleton.timeout seconds, exception, &e
end
def timeout seconds = nil, exception = nil, &e
TimeoutInterruptSingleton.timeout seconds, exception, &e
end
end

View file

@ -11,70 +11,145 @@ class TestRubyTimeoutInterrupt < Test::Unit::TestCase
end end
def assert_no_defined_timeout_yet def assert_no_defined_timeout_yet
assert TimeoutInterrupt.timeouts.empty?, "For testing, no timeout should be defined, yet!" assert TimeoutInterruptSingleton.timeouts.empty?, "For testing, no timeout should be defined, yet!"
end end
should "not interrupt a long blocking call with the old Timeout" do def print_timeouts pre
time = Benchmark.realtime do puts "#{pre}: < #{TimeoutInterruptSingleton.timeouts.map {|k,(a,_b,_e)| "#{k.inspect}: #{a.strftime '%H:%M:%S'} (#{a-Time.now})" }.join ', '} >"
begin end
TimeoutInterrupt.timeout(5) do
Timeout.timeout(1) do # For testing raising scoped Timeout.
class TimeoutError < Exception
end
# For testing raising scoped TimeoutInterrupt.
class TimeoutInterruptError < Exception
end
context "Long really blocking calls" do
should "not be interrupted by the old Timeout" do
time = Benchmark.realtime do
assert_nothing_raised TimeoutError, "Unexpected time out. Your Ruby implementation can time out with old Timeout? You need not TimeoutInterrupt. But it is ok. You can ignore this Error. :)" do
assert_raise TimeoutInterruptError, "Ohoh. TimeoutInterrupt should be raised." do
TimeoutInterrupt.timeout 5, TimeoutInterruptError do
Timeout.timeout 1, TimeoutError do
blocking
assert false, "Should be unreachable!"
end
end
end
end
end
assert 3 < time, "Did timeout!"
end
should "be interrupted by the new TimeoutInterrupt" do
time = Benchmark.realtime do
assert_raise TimeoutInterrupt::Error, "It should be timed out, why it did not raise TimeoutInterrupt::Error?" do
TimeoutInterrupt.timeout 1 do
blocking blocking
assert false, "Should be unreachable!" assert false, "Should be unreachable!"
end end
end end
rescue Timeout::Error
:ok
end end
assert 3 > time, "Did not interrupt."
end end
assert 3 < time, "Did timeout!"
end end
should "interrupt a long blocking call with the new TimeoutInterrupt" do should "interrupt scoped timeout, but not time out the outer timeout" do
time = Benchmark.realtime do assert_no_defined_timeout_yet
begin assert_raise TimeoutInterruptError, "It should be timed out, why it did not raise TimeoutInterruptError?" do
TimeoutInterrupt.timeout(1) do assert_nothing_raised Timeout::Error, "Oh, outer timeout was timed out. Your machine must be slow, or there is a bug" do
blocking TimeoutInterrupt.timeout 10 do
TimeoutInterrupt.timeout 1, TimeoutInterruptError do
Kernel.sleep 2
end
assert false, "Should be unreachable!" assert false, "Should be unreachable!"
end end
rescue Timeout::Error
:ok
end end
end end
assert 3 > time, "Did not interrupt." assert TimeoutInterruptSingleton.timeouts.empty?, "There are timeouts defined, yet!"
end
should "interrupt scoped timeout, but not outer timeout" do
assert_no_defined_timeout_yet
begin
TimeoutInterrupt.timeout(10) do
TimeoutInterrupt.timeout(1) do
Kernel.sleep 2
end
assert false, "Should be unreachable!"
end
rescue Timeout::Error
:ok
end
assert TimeoutInterrupt.timeouts.empty?, "There are timeouts defined, yet!"
end end
should "clear timeouts, if not timed out, too." do should "clear timeouts, if not timed out, too." do
assert_no_defined_timeout_yet assert_no_defined_timeout_yet
TimeoutInterrupt.timeout(10) {} TimeoutInterrupt.timeout(10) {}
assert TimeoutInterrupt.timeouts.empty?, "There are timeouts defined, yet!" assert TimeoutInterruptSingleton.timeouts.empty?, "There are timeouts defined, yet!"
end end
should "return a Proc if now block given, but do not create a timeout." do class CustomException <Exception
assert_no_defined_timeout_yet
assert TimeoutInterrupt.timeout(10).kind_of?( Proc), "Did not return a Proc."
end end
should "run a returned Proc with given timeout." do should "raise custom exception." do
assert_no_defined_timeout_yet assert_raise CustomException, "Custom exceptions do not work." do
to = TimeoutInterrupt.timeout(10) TimeoutInterrupt.timeout 1, CustomException do
called = false sleep 2
to.call { called = true } end
assert called, "Did not called." end
end
context "A prepared timeout (Proc)" do
should "be returned by calling timeout without a block" do
assert_no_defined_timeout_yet
assert TimeoutInterrupt.timeout(10).kind_of?( Proc), "Did not return a Proc."
end
should "run with once given timeout" do
assert_no_defined_timeout_yet
to = TimeoutInterrupt.timeout 10
called = false
to.call { called = true }
assert called, "Did not called."
end
should "raise custom exception" do
assert_raise CustomException, "Custom exceptions do not work." do
prepared = TimeoutInterrupt.timeout 1, CustomException
prepared.call { sleep 2 }
end
end
should "not be scopeable, without manualy setup after rescue and 2 time outs at once" do
prepared = TimeoutInterrupt.timeout 1
assert_no_defined_timeout_yet
called = false
prepared.call do
assert_raise TimeoutInterrupt::Error, 'It should time out after one second, but it did not.' do
prepared.call { 2; sleep 2 }
end
called = true
end
assert called, "It's true, it should be called, also if not expected."
end
should "be scopeable, with manualy setup after rescue, also if 2 time outs at once." do
prepared = TimeoutInterrupt.timeout 1
assert_no_defined_timeout_yet
prepared.call do
assert_raise TimeoutInterrupt::Error, 'It should time out after one second, but it did not.' do
prepared.call { sleep 2 }
end
assert_raise TimeoutInterrupt::Error, 'Manualy called timeout setup did not raise.' do
TimeoutInterrupt.timeout
end
assert true, "Should never be reached."
end
end
end
class IncludeModuleTest
include TimeoutInterrupt
def please_timeout after
timeout after do
sleep after+10
end
end
end
context "Included module" do
should "provide timeout too" do
assert_raise TimeoutInterrupt::Error, "Included timeout can not be used?" do
IncludeModuleTest.new.please_timeout 2
end
end
end end
end end