Timeouts are great to keep for your own code under control, but there are factors that you have less control of.
Take this example client connection:
socket = TCPSocket.new('google.com', 80)
We know that inside that constructor Ruby makes a call to
connect. Since we’re passing a hostname, rather than IP address, Ruby needs to do a DNS lookup to resolve that hostname to a unique address it can connect to.
The kicker? A slow DNS server can block your entire Ruby process. This is a bummer for multi-threaded environments.
MRI and the GIL
The standard Ruby implementation (MRI) has something called a global interpreter lock (GIL). The GIL is there for safety. It ensures that the Ruby interpreter is only ever doing one potentially dangerous thing at a time. This really comes into play in a multi-threaded environment. While one thread is active all other threads are blocked. This allows MRI to be written with safer, simpler code.
Thankfully, the GIL does understand blocking IO. If you have a thread that’s doing blocking IO (eg. a blocking
read), MRI will release the GIL and let another thread continue execution. When the blocking IO call is finished, the thread lines up for another turn to execute.
MRI is a little less forgiving when it comes to C extensions. Any time that a library uses the C extension API, the GIL blocks any other code from execution. There is no exception for blocking IO here, if a C extension is blocking on IO then all other threads will be blocked.
The key to the issue at hand here is that, out of the box, Ruby uses a C extension for DNS lookups. Hence, if that DNS lookup is blocking for a long time MRI will not release the GIL.
Thankfully, Ruby provides a solution to this in the standard library. The ‘resolv’ library provides a pure-Ruby replacement for DNS lookups. This allows MRI to release the GIL for long-blocking DNS lookups. This is a big win for multi-threaded environments.
The ‘resolv’ library has its own API, but the standard library also provides a library that monkeypatches the
Socket classes to use ‘resolv’.
require 'resolv' # the library
require 'resolv-replace' # the monkey patch
I recommend this whenever you’re doing socket programming in a multi-threaded environment.