Classes
======= 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:
parent
7608c98054
commit
6f57bb4223
3 changed files with 230 additions and 78 deletions
55
README.md
55
README.md
|
@ -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
|
||||
=========
|
||||
|
|
|
@ -7,43 +7,67 @@ module FFI
|
|||
end
|
||||
end
|
||||
|
||||
module TimeoutInterrupt
|
||||
def self.timeouts
|
||||
@timeouts ||= {}
|
||||
end
|
||||
|
||||
def self.alarm_trap sig
|
||||
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
|
||||
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
|
||||
end
|
||||
|
||||
def self.timeout seconds
|
||||
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?
|
||||
at = Time.now + seconds
|
||||
key, bt = Random.rand( 2**64-1), Kernel.caller
|
||||
begin
|
||||
TimeoutInterrupt.timeouts[key] = [at, bt]
|
||||
TimeoutInterrupt.setup_timeout
|
||||
yield
|
||||
ensure
|
||||
TimeoutInterrupt.timeouts.delete key
|
||||
TimeoutInterrupt.setup_timeout
|
||||
def alarm_trap sig
|
||||
key, (at, bt, exception) = self.timeouts.min_by {|key,(at,bt,ex)| at }
|
||||
return if Time.now < at
|
||||
raise exception, 'execution expired', bt
|
||||
end
|
||||
|
||||
def setup
|
||||
if timeouts.empty?
|
||||
Signal.trap( 'ALRM') {}
|
||||
FFI::LibC.alarm 0
|
||||
else
|
||||
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 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
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
time = Benchmark.realtime do
|
||||
begin
|
||||
TimeoutInterrupt.timeout(5) do
|
||||
Timeout.timeout(1) 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
|
||||
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
|
||||
assert false, "Should be unreachable!"
|
||||
end
|
||||
end
|
||||
rescue Timeout::Error
|
||||
:ok
|
||||
end
|
||||
assert 3 > time, "Did not interrupt."
|
||||
end
|
||||
assert 3 < time, "Did timeout!"
|
||||
end
|
||||
|
||||
should "interrupt a long blocking call with the new TimeoutInterrupt" do
|
||||
time = Benchmark.realtime do
|
||||
begin
|
||||
TimeoutInterrupt.timeout(1) do
|
||||
blocking
|
||||
should "interrupt scoped timeout, but not time out the outer timeout" do
|
||||
assert_no_defined_timeout_yet
|
||||
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
|
||||
end
|
||||
assert 3 > time, "Did not interrupt."
|
||||
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!"
|
||||
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
|
||||
assert_no_defined_timeout_yet
|
||||
assert TimeoutInterrupt.timeout(10).kind_of?( Proc), "Did not return a Proc."
|
||||
class CustomException <Exception
|
||||
end
|
||||
|
||||
should "run a returned Proc with given timeout." do
|
||||
assert_no_defined_timeout_yet
|
||||
to = TimeoutInterrupt.timeout(10)
|
||||
called = false
|
||||
to.call { called = true }
|
||||
assert called, "Did not called."
|
||||
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 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
|
||||
|
|
Loading…
Reference in a new issue