=======

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.
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.
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
=========

View file

@ -7,43 +7,67 @@ module FFI
end
end
module TimeoutInterrupt
def self.timeouts
@timeouts ||= {}
module TimeoutInterruptSingleton
class <<self
def timeouts thread = nil
@timeouts ||= Hash.new {|h,k| h[k] = [] }
thread = Thread.current if thread.kind_of? Thread
thread ? @timeouts[thread] : @timeouts
end
def self.alarm_trap sig
key, (at, bt) = TimeoutInterrupt.timeouts.min_by {|key,(at,bt)| at }
def alarm_trap sig
key, (at, bt, exception) = self.timeouts.min_by {|key,(at,bt,ex)| at }
return if Time.now < at
raise Timeout::Error, 'execution expired', bt
raise exception, 'execution expired', bt
end
def self.setup_timeout
if TimeoutInterrupt.timeouts.empty?
def setup
if 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
key, (at, bt) = timeouts.min_by {|key,(at,bt)| at }
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 self.timeout seconds
def timeout seconds = nil, exception = nil
return setup if seconds.nil?
seconds = seconds.to_i
raise Timeout::Error, "Timeout must be longer than '0' seconds." unless 0 < seconds
return lambda {|&e| self.timeout seconds, &e } unless block_given?
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
TimeoutInterrupt.timeouts[key] = [at, bt]
TimeoutInterrupt.setup_timeout
self.timeouts[key] = [at, bt, exception]
setup
yield
ensure
TimeoutInterrupt.timeouts.delete key
TimeoutInterrupt.setup_timeout
self.timeouts.delete key
setup
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
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
should "not interrupt a long blocking call with the old Timeout" do
def print_timeouts pre
puts "#{pre}: < #{TimeoutInterruptSingleton.timeouts.map {|k,(a,_b,_e)| "#{k.inspect}: #{a.strftime '%H:%M:%S'} (#{a-Time.now})" }.join ', '} >"
end
# 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
begin
TimeoutInterrupt.timeout(5) do
Timeout.timeout(1) 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
rescue Timeout::Error
:ok
end
end
end
assert 3 < time, "Did timeout!"
end
should "interrupt a long blocking call with the new TimeoutInterrupt" do
should "be interrupted by the new TimeoutInterrupt" do
time = Benchmark.realtime do
begin
TimeoutInterrupt.timeout(1) do
assert_raise TimeoutInterrupt::Error, "It should be timed out, why it did not raise TimeoutInterrupt::Error?" do
TimeoutInterrupt.timeout 1 do
blocking
assert false, "Should be unreachable!"
end
rescue Timeout::Error
:ok
end
end
assert 3 > time, "Did not interrupt."
end
end
should "interrupt scoped timeout, but not outer timeout" do
should "interrupt scoped timeout, but not time out the outer timeout" do
assert_no_defined_timeout_yet
begin
TimeoutInterrupt.timeout(10) do
TimeoutInterrupt.timeout(1) do
assert_raise TimeoutInterruptError, "It should be timed out, why it did not raise TimeoutInterruptError?" do
assert_nothing_raised Timeout::Error, "Oh, outer timeout was timed out. Your machine must be slow, or there is a bug" do
TimeoutInterrupt.timeout 10 do
TimeoutInterrupt.timeout 1, TimeoutInterruptError 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
assert TimeoutInterruptSingleton.timeouts.empty?, "There are timeouts defined, yet!"
end
should "clear timeouts, if not timed out, too." do
assert_no_defined_timeout_yet
TimeoutInterrupt.timeout(10) {}
assert TimeoutInterrupt.timeouts.empty?, "There are timeouts defined, yet!"
assert TimeoutInterruptSingleton.timeouts.empty?, "There are timeouts defined, yet!"
end
should "return a Proc if now block given, but do not create a timeout." do
class CustomException <Exception
end
should "raise custom exception." do
assert_raise CustomException, "Custom exceptions do not work." do
TimeoutInterrupt.timeout 1, CustomException do
sleep 2
end
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 a returned Proc with given timeout." do
should "run with once given timeout" do
assert_no_defined_timeout_yet
to = TimeoutInterrupt.timeout(10)
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