Jan S. wrote:
…
FYI: Eric H. 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?