This post originated from an RSS feed registered with Ruby Buzz
by Eric Hodel.
Original Post: Care and Feeding of Timeout.timeout
Feed Title: Segment7
Feed URL: http://blog.segment7.net/articles.rss
Feed Description: Posts about and around Ruby, MetaRuby, ruby2c, ZenTest and work at The Robot Co-op.
If you’re not careful when using Timeout.timeout you can end up with some hard to find bugs.
The first is that nesting timeouts without different timeout exception classes is very bad. Let’s say you have a process that can connect to multiple servers, but you want to give up and try the next server if it takes too long. You’d probably write something like this:
require 'timeout'
servers = [1, 2]
current_server = 0
begin
Timeout.timeout 2 do
puts "Connecting to server #{servers[current_server]}"
sleep # simulate work
end
rescue Timeout::Error
puts "Failed"
current_server += 1
retry unless current_server == servers.length
end
This is sensible code, if the work takes to long you’ll fail and move on to the next server.
But now its been a few months and you’ve added servers, but you want your application to only try for so many seconds then give up completely. Your real app would be properly factored (of course) so the bug with the simple solution wouldn’t necessarily be obvious:
require 'timeout'
servers = [1, 2]
current_server = 0
Timeout.timeout 5 do
puts 'Setting up some stuff'
sleep 4
begin
Timeout.timeout 2 do
puts "Connecting to server #{servers[current_server]}"
sleep
end
rescue Timeout::Error
puts "Failed."
current_server += 1
retry unless current_server == servers.length
end
end
When we run this code we run longer than we were supposed to:
<samp>$ time ruby t.rb
Setting up some stuff
Connecting to server 1
Failed.
Connecting to server 2
Failed.
real 0m7.044s
user 0m0.014s
sys 0m0.011s
Why seven seconds instead of five? The inner timeout block caught the out timeout block’s exception and continued doing what it was doing. This isn’t what we want, but Timeout.timeout allows you to change the raised exception:
require 'timeout'
servers = [1, 2]
current_server = 0
class ServerTimeout < Timeout::Error; end
class AppTimeout < Timeout::Error; end
Timeout.timeout 5, AppTimeout do
puts 'Setting up some stuff'
sleep 4
begin
Timeout.timeout 2, ServerTimeout do
puts "Connecting to server #{servers[current_server]}"
sleep
end
rescue ServerTimeout
puts "Failed."
current_server += 1
retry unless current_server == servers.length
end
end
So now the outer timeout can stop execution even from inside the inner timeout:
<samp>$ time ruby t.rb
Setting up some stuff
Connecting to server 1
/usr/local/lib/ruby/1.8/timeout.rb:54: execution expired (AppTimeout)
from /usr/local/lib/ruby/1.8/timeout.rb:56:in `timeout'
from t.rb:13
from /usr/local/lib/ruby/1.8/timeout.rb:56:in `timeout'
from t.rb:9
real 0m5.069s
user 0m0.022s
sys 0m0.015s</samp>
The second to watch out for is Timeout killing your rescue or ensure blocks. A timeout raised inside an ensure block will stop execution, so for critical ensure blocks you should wrap them in their own begin/end block:
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