Timeouts

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.

Unusable Options

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 SNDTIMEO and 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.

What’s left?

IO.select

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 Timeout::Error constant.

Accept Timeout

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 read.

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

Connect Timeout

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

These IO.select based timeout mechanisms are commonly used, even in Ruby’s standard library, and offer more stability than something like native socket timeouts.