Ruby Socket Persistence

It's surprisingly simple to persist your socket connections while restarting a Ruby program. The key lies in understand how Ruby handles file descriptors. A file descriptor is a number that represents an open file that is buffered in the OS Kernel. Now I've only tried this technique on Linux so I don't know if it works on Windows, so chime in on the comments if it does or does not work. Let's start by exploring file descriptors a little bit so we can improve our Linux knowledge. If you want immediate gratification you can skip down to the bottom-most snippet.

irb(main):001:0> require 'socket'
\=> true
irb(main):002:0> sock = TCPSocket.open("google.com",80)
\=> #<TCPSocket:0xb7c73c44>
irb(main):003:0> sock.fileno
\=> 3


note that the file number of the socket is 3. Now look at the output of lsof:

ryan@rtmlap:~$ lsof -c irb
COMMAND  PID USER   FD   TYPE  DEVICE    SIZE    NODE NAME
irb     5645 ryan  cwd    DIR     8,2   12288 1441793 /home/ryan
irb     5645 ryan  rtd    DIR     8,1    4096       2 /
irb     5645 ryan  txt    REG     8,1    3564 1177574 /usr/bin/ruby1.8
irb     5645 ryan  mem    REG     8,1   67408  539926 /lib/tls/i686/cmov/libresolv-2.7.so
irb     5645 ryan  mem    REG     8,1   38412  539915 /lib/tls/i686/cmov/libnss_files-2.7.so
irb     5645 ryan  mem    REG     8,1  254076 1208067 /usr/lib/locale/en_US.utf8/LC_CTYPE
irb     5645 ryan  mem    REG     8,1  190584  522320 /lib/libncurses.so.5.6
irb     5645 ryan  mem    REG     8,1  196560  522360 /lib/libreadline.so.5.2
irb     5645 ryan  mem    REG     8,1   17884  539913 /lib/tls/i686/cmov/libnss_dns-2.7.so
irb     5645 ryan  mem    REG     8,1   40016 1210577 /usr/lib/ruby/1.8/i486-linux/socket.so
irb     5645 ryan  mem    REG     8,1 1364388  539898 /lib/tls/i686/cmov/libc-2.7.so
irb     5645 ryan  mem    REG     8,1  149328  539906 /lib/tls/i686/cmov/libm-2.7.so
irb     5645 ryan  mem    REG     8,1   38300  539902 /lib/tls/i686/cmov/libcrypt-2.7.so
irb     5645 ryan  mem    REG     8,1    9684  539904 /lib/tls/i686/cmov/libdl-2.7.so
irb     5645 ryan  mem    REG     8,1  112354  539924 /lib/tls/i686/cmov/libpthread-2.7.so
irb     5645 ryan  mem    REG     8,1  787660 1177653 /usr/lib/libruby1.8.so.1.8.6
irb     5645 ryan  mem    REG     8,1    7552  522335 /lib/libnss_mdns4_minimal.so.2
irb     5645 ryan  mem    REG     8,1   25700   98159 /usr/lib/gconv/gconv-modules.cache
irb     5645 ryan  mem    REG     8,1   15312 1207882 /usr/lib/ruby/1.8/i486-linux/readline.so
irb     5645 ryan  mem    REG     8,1  109152  522259 /lib/ld-2.7.so
irb     5645 ryan    0u   CHR   136,1               3 /dev/pts/1
irb     5645 ryan    1u   CHR   136,1               3 /dev/pts/1
irb     5645 ryan    2u   CHR   136,1               3 /dev/pts/1
irb     5645 ryan    3u  IPv4 1897889             TCP rtmlap.local:45854->jc-in-f99.google.com:www (ESTABLISHED)

Alright, so now here's the snippet that shows persistent sockets:

#!/usr/bin/ruby
#simple_connector.rb
require 'socket'

puts "Started."

if ARGV[0] == "restart"
  sock = IO.open(ARGV[1].to_i)
  puts sock.read
  exit
else
  sock = TCPSocket.new('google.com', 80)
  sock.write("GET /\n")
end

Signal.trap("INT") do
  puts "Restarting..."
  exec("ruby simple_connector.rb restart #{sock.fileno}")
end


while true
  sleep 1
end

When the program is first run it prints 'Started.' Then it opens a socket to Google and requests the homepage. Then it waits indefinitely until it receives the INT signal (CTRL-C or kill -term PID). After that it runs exec which spawns a new program in the place of the current one. It spawns the same program, with additional command line arguments "restart fileno." When the program starts again these additional arguments make it follow the other path, which uses IO.open(file_descriptor) to open the file descriptor from the file descriptor number that we passed in on the exec line. Then it reads the socket Kernel buffer, which already contains the Google homepage.

And there you have it, a socket that persists beyond a single program.

published 2008-07-11

Questions or Feedback? Email ryan@ryantm.com or tweet @ryantm.