The somewhat peculiar behavior of Ruby's Thread#raise
Here's something that's perhaps not entirely obvious: when you call Thread#raise
, the exception will be raised at whatever point of execution that thread happens to be at.
require 'thread'
t = Thread.new{
sleep 0.1
sleep 0.1
sleep 0.1
sleep 0.1
sleep 0.1
}
sleep rand(4) * 0.1
t.raise
t.join
output
➔ ruby thread_raise.rb
thread_raise.rb:6:in `sleep': unhandled exception
from thread_raise.rb:6:in `block in <main>'
➔ ruby thread_raise.rb
thread_raise.rb:7:in `sleep': unhandled exception
from thread_raise.rb:7:in `block in <main>'
➔ ruby thread_raise.rb
thread_raise.rb:5:in `sleep': unhandled exception
from thread_raise.rb:5:in `block in <main>'
➔ ruby thread_raise.rb
thread_raise.rb:5:in `sleep': unhandled exception
from thread_raise.rb:5:in `block in <main>'
See how the stack trace in the exception shows a different line of code each time? It's understandable that it would behave like this -- I can't think of any other behavior that would make more sense. But it should only be used when the code inside the thread knows that it's being called from a particular outer context, and that the outer context might have its own problems that it then tells the thread about. Example:
def do_jobs
begin
while true
# do interesting things
end
rescue NoMoreDiskSpace
# tidy things up
end
end
t = Thread.new(do_jobs)
while true
if disk_is_full
t.raise(NoMoreDiskSpace)
end
end
Unfortunately, Ruby's timeout library uses Thread#raise
, even though it is frequently used around enormous amounts of arbitrary code and libraries that might have no idea that they are being called within Timeout#timeout
and can't be expected to elegantly handle a timeout event at every single point in the code. Furthermore, this inner code might have blocks that call top-level Exception
, which will then catch the exception that Timeout
sends, handle it in a way it wasn't written for, and not raise it back up to the Timeout.timeout
invocation. I'll elaborate on this issue more in future posts.