Those are only gems used by ManageIQ. I’m sure there are many other gems used by other
projects that still use Timeout.

Wait a second. We didn’t actually look at the contents of the Timeout.timeout block.
That’s the point, it doesn’t matter. If you hit this code at all, there’s a small
chance it can Thread#raise at a very inopportune time in code that will never
expect it!

You might be thinking, “that’s fine, this why I’m wrapping my code with a begin and a rescue or ensure
to clean things up.” It is a regular exception and we would expect it to be rescued while we are inside the begin
code. But don’t underestimate Murphy’s law. What if the exception is raised while we’re in the cleanup code,
preventing it from completing? To top it off, it will find the weirdest possible place to raise and at the most
unfortunate time, such as Friday at 5pm. I’ll repeat
the links listed above for convenience
because this is exactly what they show: Thread#raise can raise while in your ensure/rescue cleanup code.

Fun times

A “fun” bug recently reminded Beni and I of a “new” and totally awful
way Timeout.timeout can ruin your day.

Basically, he discovered that his test would sporadically hang when run in the full test suite but not when
run in isolation.

After some serious spelunking via rspec bisect and manually minimizing the reproducer by commenting out parts of
the remaining tests, he found the hang would occur when another test created a Tempfile. As soon as he put
a Tempfile.new('x') in the timeout workaround test, the test would hang “sometimes”.

If you’re still reading, you might have noticed that done is missing when the
test hangs. The done happens after Tempfile wants to close and unlink the file here.

Therefore, when it hangs, the Tempfilefinalizer never completes.

tldr; Timeout.timeout can Thread#raise while in a finalizer for another object! AWESOME.

The final straw

Wait, what’s a finalizer?

There is no guarantee when the ruby garbage collector will garbage collect an
object. ObjectSpace.define_finalizer provides a place for you to tell ruby to “run
this code when my object is to be garbage collected.” Finalizers are generally used
to cleanup things so we don’t cause memory leaks. Interrupting them is probably
not something we want to troubleshoot at 5pm on a Friday.

It’s a small list but libraries like tempfile, concurrent-ruby, ffi, actionview,
etc. are pretty common. Again, this is only in the gems that ManageIQ uses. This
list will certainly grow if you searched dependencies of other applications.

The ugly part of this story is that the finalizer and the Thread#raise are both async operations so they can happen
at nearly any time. We caught this in a test, but this could happen in development or even production and is really difficult
to reason about because the symptoms could be nearly anything.