In Ruby, don’t use timeout()

Adam Hooper
Jul 31, 2016 · 4 min read

Ruby has a method called timeout(). It supposedly avoids long-running code. Don’t use it.

For instance, this code looks like it will take at most 10 seconds, but it could run forever:

Judging from the code, here’s what download_to_database() should do:

  • If all goes well, write the contents of a web page to the database.
  • If the HTTP request fails (e.g., server isn’t listening on port 80), throw an error.
  • If the time spent on the HTTP request plus the time spent writing to the database exceeds five seconds, throw an error and don’t write the row.

Here’s what actually happens:

  • If the time spent on the HTTP request exceeds five seconds, throw an error and don’t write the row.
  • If the time spent on the database write exceeds five seconds, block until the write finishes (potentially forever), write the row and throw an error.

Not what you expected!

While Timeout::timeout() seems intuitive, it’s complicated.

How Timeout::timeout() works

The idea is simple: Timeout::timeout(X) launches a little thread that sleeps for X seconds and then raises an error in your code.

But “raises an error in your code” is extremely complicated: it means that within a timeout() block, any line of code can throw an error. Heck, the way Timeout::timeout() is written, any line of code can throw any error.

By putting code in a Timeout::timeout() block, you change the rules. Bulletproof code is now error-prone code. The line “x = 1” can raise an ArgumentError. How do we deal with that?

The consequences

Some libraries, like Ruby’s built-in IO, are written extremely carefully: every single line of code guards against any type of error. You, too, can write your code extremely carefully … but it takes a lot of time and effort.

Most libraries aren’t crafted so meticulously. Take run-of-the-mill Ruby code, throw it in a Timeout::timeout() block, and a timeout will lead to undefined behavior.

There’s a middle ground: you can call Thread::handle_interrupt() to negate the Timeout::timeout() call. Mysql2 does this. The result is that Timeout::timeout() has no effect.

(Also, if you supply a “klass” argument to Timeout::timeout(), virtually all code that uses Thread::handle_interrupt() — including Mysql2 — will have undefined behavior. A string of timeouts can take down your web server. It happened to me; that’s why I’m writing this blog post.)

So if you use Timeout::timeout(), the code within the block is either meticulously crafted, contains bugs, or ignores the Timeout::timeout().

All those things are bad. So don’t use Timeout::timeout().

A better way

Instead, enumerate all the things that can time out and specify them. For instance:

This code is explicit and it does something different.

Get used to the “does something different” part: since calls to Timeout::timeout() are usually wrong, the only way to do something right is to do something different.

Here’s what it does:

  • If all goes well, write the contents of a web page to the database.
  • If the HTTP request fails (e.g., server isn’t listening on port 80), throw an error.
  • If we lose touch with the HTTP server for five seconds, throw an error.
  • If we lose touch with the database server for three seconds, throw an error.

(Incidentally, Net::HTTP uses Timeout::timeout() to open the HTTP connection behind the scenes. That’s the “meticulously crafted” category above. Timeout::timeout() has its place … now don’t use it ;).)

Refer to the requirements

Our Timeout::timeout() call could take an infinite amount of time writing to the database. This code fixes that error.

Technically, this “fixed” code still take infinitely long to run: Net::HTTP’s read_timeout and Mysql2's write_timeout specify a number of seconds per block; an infinite-size web page has infinitely many blocks.

Should we handle infinite-size web pages? Now we’re asking a useful question!

Always ask yourself: why do I need this timeout? If your answer is, “because this should take under five seconds,” play four-year-old kid with yourself and keep asking, “why?” Find out what you’re really worried about.

If it’s, “I worry the HTTP server will be down,” our clever use of Net::HTTP options solves that.

If it’s, “I worry the MySQL server will stop responding,” our clever use of Mysql2 options solves.

If it’s, “I worry about infinite-length web pages,” then we can solve that: delve into Net::HTTP’s innards to stop reading the response after a certain number of bytes.

If it’s, “I worry something will happen that I can’t anticipate,” that’s the halting problem. You can’t fix that. If you write Timeout::timeout() in this situation, you’re more likely to create bugs than to solve them.

Conclusion

If you ever catch yourself writing Timeout::timeout(), stop. Enumerate all the things that might take too long. Solve them one by one.

Your correct, concise solutions probably won’t employ Timeout::timeout().

Adam Hooper

Written by

Journalist, ex software engineer