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