Timeouts are all about tolerance. How long are you willing to wait for your socket to connect? To read? To write?
All of these answers are a function of your tolerance. High performance network programs typically aren’t willing to wait for operations that aren’t going to finish. It’s assumed that if your socket can’t write its data in the first 5 seconds then there’s a problem and some other behaviour should take over.
If you’ve spent any time reading Ruby code you’ve probably seen the
timeout library that comes with the standard library. Although that library tends to get used with socket programming in Ruby, I’m not even going to talk about it here because there are better ways! The
timeout library provides a general purpose timeout mechanism, but your operating system comes with socket-specific timeout mechanisms that are more performing and more intuitive.
Your operating system also offers native socket timeouts that can be set via the
RCVTIMEO socket options. But, as of Ruby 1.9, this feature is no longer functional. Due to the way that Ruby handles blocking IO in the presence of threads, it wraps all socket operations around a poll(2), which mitigates the effect of the native socket timeouts. So those are unusable too.
Ah, let’s call
IO.select old faithful, eh? So many uses.
We’ve already seen how to use
IO.select in previous chapters. Here’s how you can use it for timeouts.
require 'socket' require 'timeout' timeout = 5 # seconds Socket.tcp_server_loop(4481) do |connection| begin # Initiate the initial read(2). This is important because # it requires data be requested on the socket and circumvents # a select(2) call when there's already data available to read. connection.read_nonblock(4096) rescue Errno::EAGAIN # Monitor the connection to see if it becomes readable. if IO.select([connection], nil, nil, timeout) # IO.select will actually return our socket, but we # don't care about the return value. The fact that # it didn't return nil means that our socket is readable. retry else raise Timeout::Error end end connection.close end
I actually required
timeout in this case just to get access to that handy
As we’ve seen before
accept works very nicely with
IO.select. If you need to do a timeout around
accept it would look just like it did for
server = TCPServer.new(4481) timeout = 5 # seconds begin server.accept_nonblock rescue Errno::EAGAIN if IO.select([server], nil, nil, timeout) retry else raise Timeout::Error end end
In this case, doing a timeout around a connect works much like the other examples we’ve seen.
require 'socket' require 'timeout' socket = Socket.new(:INET, :STREAM) remote_addr = Socket.pack_sockaddr_in(80, 'google.com') timeout = 5 # seconds begin # Initiate a nonblocking connection to google.com on port 80. socket.connect_nonblock(remote_addr) rescue Errno::EINPROGRESS # Indicates that the connect is in progress. We monitor the # socket for it to become writable, signaling that the connect # is completed. # # Once it retries the above block of code it # should fall through to the EISCONN rescue block and end up # outside this entire begin block where the socket can be used. if IO.select(nil, [socket], nil, timeout) retry else raise Timeout::Error end rescue Errno::EISCONN # Indicates that the connect is completed successfully. end socket.write("ohai") socket.close
IO.select based timeout mechanisms are commonly used, even in Ruby’s standard library, and offer more stability than something like native socket timeouts.