In Ruby, don’t use timeout()
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().