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.