Jan Svitok wrote:
...
FYI: Eric Hodel has written a post about nested timeouts:
http://blog.segment7.net/articles/2006/04/11/care-and-feeding-of-timeout-timeout
The second part of the article points out a potential danger of the interaction between timeouts and rescue/ensure. But the proposed solution may not be good advice.
The danger is that a timeout exception may fire during an ensure clause, preventing the ensure clause (and any necessary cleanup code within it) from finishing, which could cause resource leaks or other problems.
The solution proposed in the article is to wrap code within the ensure clause in a begin..end block to handle timeout exceptions, like this:
require 'timeout'
Timeout.timeout 2 do
begin
puts "Allocating the thingy..."
sleep 1
raise RuntimeError, 'Oh no! Something went wrong!'
ensure
# Since we might time out, hold onto the timeout we caught
# so we can re-raise it when we're done cleaning up.
timeout = nil
begin # we really need to clean up
puts "Cleaning up after the thingy..."
sleep 2
puts "Cleaned up after the thingy!"
rescue Timeout::Error => e
puts "Timed out! Trying again!"
timeout = e # save that timeout then retry
retry
end
# Raise the timeout so we time out all the way to the top.
raise timeout unless timeout.nil?
end
end
However, that solution only reduces the chance of the timeout interfering with the ensure clause. Suppose the timeout fires while the main thread is executing the line "timeout = nil". (It's impossible in this example, but you can get it to happen by putting a "sleep 5" just after this line.) Then the inner begin..end clause doesn't catch the timeout, and the cleanup doesn't happen. In general, unless you are very sure about the timings of your code, you cannot guarantee that the timeout won't fire at the wrong time. So it's a race condition.
Another problem is that the "retry" may cause the cleanup to happen twice, if the timeout fires just when cleanup is finishing. That may lead to other problems (such as closing a file twice and generating another exception).
A solution in this case (but not in general) is to move the ensure block *outside* of the timeout block:
require 'timeout'
begin
Timeout.timeout 2 do
puts "Allocating the thingy..."
sleep 1
raise RuntimeError, 'Oh no! Something went wrong!'
end
rescue Timeout::Error => e
puts "timed out"
ensure
puts "Cleaning up after the thingy..."
sleep 2
puts "Cleaned up after the thingy!"
end
The effect of this is to kill the timeout thread before the ensure block starts (since the timeout block has been exited). Also, the ensure code will be called exactly once (assuming there are no other interrupts).
But this refactoring technique cannot be used if the ensure clause occurs in some nested method call. This has been noted before:
http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/113417
For example:
require 'timeout'
def do_some_work
puts "do_some_work is starting"
sleep 1
raise 'exception in do_some_work'
ensure
puts "cleaning up resources in do_some_work"
sleep 2
puts "cleaned up resources in do_some_work"
end
Timeout.timeout 2 do
do_some_work
end
This outputs:
do_some_work is starting
cleaning up resources in do_some_work
/usr/local/lib/ruby/1.8/timeout.rb:54:in `do_some_work': execution expired (Timeout::Error)
The cleanup never finishes.
We could modify do_some_work to handle timeouts, as Eric did in his article, but then there would still be the race condition that would (depending on timing) allow the timeout to kill the ensure clause anyway. And there's still the retry problem (causing multiple executions of cleanup code). And it's not easy to modify every library method in this way.
I wish I had an easy answer to this problem, but I don't. Are timeout and ensure inherently incompatible?
···
--
vjoel : Joel VanderWerf : path berkeley edu : 510 665 3407