Ruby’s Timeout
If you think you’ve been around the block a few times and know your ins-and-outs of Ruby’s funkiest details, here’s a quick Ruby quiz for you: on MRI, what does this piece of code print out?
require 'timeout'
def do_stuff
sleep(2)
puts "done sleeping"
rescue => e
puts 'boom!'
ensure
puts 'all done'
end
begin
Timeout::timeout(1) do
do_stuff
end
rescue Timeout::Error
puts 'timed out'
end
If you answered timed out
, that’s not a bad start. After all, the #timeout
method is what raises the error, right? Or… no, actually. Ruby’s timeout method actually works by kicking off a new thread, which sleeps for the timeout duration and then raises an exception on the original execution thread, at whatever point in the code it happens to be at. The stack trace will read as if it came from a random line in your code (which can be surprising to the uninitiated).
Okay, so it prints out boom!
then all done
, right? Nope. The rescue block in #do_stuff
isn’t able to rescue the timeout error, because – contrary to what the docs imply – the timeout exception actually raises a Timeout::ExitException
, which is not a StandardError
and thus is not rescuable without rescuing Exception.
So although the rescue doesn’t catch the exception, the ensure block will still trigger. Which means the correct answer to my quiz is…
all done
timed out
But wait, there’s more! #
Let’s mix things up a bit. Given what I’ve explained, see if you can guess what this prints out:
require 'timeout'
def do_stuff
sleep(2)
puts "done sleeping"
rescue => e
puts 'boom!'
ensure
puts 'all done'
return 1 # This is the only addition!
end
begin
Timeout::timeout(1) do
do_stuff
end
rescue Timeout::Error
puts 'timed out'
end
If you’re at a loss to how this could be different, take a second to think about what it even means to have an ensure block return a value. An exception should halt execution, right? But returning a value means execution continues. Sounds like a paradox to me, and it’s one Ruby resolves by… dropping the exception on the floor (see Les Hill’s post on the topic for more detail). So what actually prints out is just:
all done
And you have no idea anything even timed out, except that whatever you wanted to happen probably didn’t happen.
Doing things sanely #
Timeouts are useful. But if you want to avoid unnecessary headaches with them, here are a few tips:
- Tell Timeout to raise your own error class, so you can control its behavior. Passing a second argument, e.g.
Timeout::timeout(2, MyCodeIsTooSlowError
, will make the exception raised aMyCodeIsTooSlowError
which you can explicitly rescue or not rescue in your code. The added clarity of knowing which timeout block timed out might be useful, too. - Avoid
ensure; return
whenever possible. Instead, use abegin; rescue; end; return
pattern, so that you and future programmers are aware that all exceptions are being swallowed and ignored. - Consider using Rubinius, which has a consistent and less-surprising approach to timeouts.