Category: ruby

The Trouble With Socket Timeout

Hi. We’re currently upgrading a Ruby driver at our platform at work. At the socket level, the old version of this driver uses IO.select, which boils down to the OS’s select system call. A tried and true solution, working as expected on any scenario: it waits for a certain time, if the time runs out it simply returns nothing and resumes execution. So if a client connects to a server and it stops responding but doesn’t close the connection, the client can decide what to do with that. Here’s an example of that:

#server.rb
require 'socket'

delay = 5

server = TCPServer.new 2000

loop do
  client = server.accept
  puts "#{Time.now} > Client arrived. Sleeping for #{delay}s."
  sleep delay
  puts "#{Time.now} > Done, replying."
  client.puts "Done. Bye!"
  client.close
end
#client-io-select.rb
require 'socket'

host = '127.0.0.1'
port = 2000
timeout = 2

s = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
s.connect(Socket.pack_sockaddr_in(port, host))

rs, = IO.select([s], [], [], timeout)
if rs
  puts rs[0].read(1000)
else
  puts 'Timeout'
end

s.close

Run the server, and then run client-io-select.rb. As expected, it will timeout after 2s while the server is deliberately sleeping for 5s. Change the client timeout to 6s and it will print the server reply. The new version of the driver changed that implementation in favour of setting the timeout value as an option of the socket, as specified in the socket man page and other places. So instead of using IO.select, it’s using Socket’s setsockopt method before connecting to set both SO_RCVTIMEO and SO_SNDTIMEO, which translate to the OS’s socket options. After connecting it uses the socket read method directly, trusting on Ruby and the OS to handle timeouts, which sounds nice. However, we found that the support for those options is somewhat inconsistent through Ruby MRI’s versions – I didn’t test it on other Ruby implementations – and on different operating systems. An example of a client using this approach:

#client-socket-options.rb
require 'socket'

host = '127.0.0.1'
port = 2000
timeout = 2

tv = [ timeout, 0 ].pack 'l_2'

s = Socket.new Socket::AF_INET, Socket::SOCK_STREAM, 0
s.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, tv
s.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, tv
s.connect Socket.pack_sockaddr_in port, host

begin
  while data = s.read(1000)
    puts data
  end
rescue => e
  puts e
end

s.close

We ran that client on Ruby 1.8.7-p374, 1.9.3-p545 and 2.1.2 at Mac OS X 10.9.4, all of them installed via rvm. The server is the same of the first example. On old Ruby 1.8 the client timed out as expected. On the other Ruby versions it waited the server response instead. Before getting to that conclusion, we also ran some tests using C because we thought that different operating systems could follow or not those socket options. Here is the C client we wrote to test it:

//client.c
#include <stdio.h>
#include <sys/socket.h>
#include <netdb.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    char *host = "127.0.0.1";
    int port = 2000;
    int timeout = 2;

    int sockfd, n;

    char buffer[256];

    struct sockaddr_in serv_addr;
    struct hostent *server;
    struct timeval tv;

    tv.tv_sec = timeout;

    server = gethostbyname(host);
    bcopy((char *)server->h_addr, (char *)&serv_addr.sin_addr.s_addr, server->h_length);
    serv_addr.sin_port = htons(port);
    serv_addr.sin_family = AF_INET;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(struct timeval));
    setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(struct timeval));
    connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

    n = read(sockfd, buffer, 255);

    if (n < 0) {
        perror("error reading from socket");
        return 1;
    }

    printf("%s\n", buffer);
    return 0;
}

We ran that client on Mac OS X 10.9.4 with LLVM 5.1, Ubuntu 14.04 with GCC 4.8.2 and on CentOS 5.8 with GCC 4.1.2 . We used the same server of the first example. On OS X the client timed out as expected, but on Ubuntu and CentOS it didn’t. Don’t forget to test it yourself, specially with newer Ruby versions: one of the posts we found while investigating this described a different behaviour because it was based on Ruby 1.8 five years ago. I couldn’t find the reason behind the difference between Ruby versions – it might be a build option that had a default before, but I can’t pinpoint why without some better knowledge of the Ruby codebase. The same applies for the different operating systems. But the lesson is: setting socket options for sockets on those Ruby builds does not produce the expected behaviour currently.

Advertisements

store numbers compactly in readable strings

Hey. While working on my masters project with a friend, we stumbled upon a minor puzzle. The storage we were going to use was designed to store only string values. But we wanted to store triples of large integers, so just writing them as decimals on strings would use more space than necessary. A number up to (2^32)-1 (i.e., 4294967295) can be stored on mere 32 bits, but when encoded in UTF-8 it takes 80 bits.

Well, reducing the space we needed to store those triples by half could help the project, so I looked around for any tools that could encode numbers as readable strings. Something like Base64 encoding, but that didn’t pad the numbers so much that the resulting string isn’t that smaller than the decimal number. Also, I took the chance to write that in Ruby, as I’m working with it now, and publish my first gem.

Decimal numbers are our way of representing values on base 10, that is, using 10 symbols. Base64, as the name says, uses 64 symbols. The chosen symbols are readable characters – all 26 alphabet letters in upper and lower case, the numbers 0 to 9, plus “+” and “/”. To represent a number what I had to to then was to actually change the base of the number from 10 to 64. This way the number 0 would be “A”, 1 would be “B”, 50 is “y”, 64 is “BA” and so on.

After experimenting a bit, mostly taking care with string building, I noticed that the same code could be used to any set of symbols. And it was a good perspective: there are actually 95 readable characters on the good old ASCII table. So instead of using just the 64 characters of Base64, I also kept a 95 characters set around for even better usage of space.

Code written, I got to the task of setting it up as a gem. It was actually simple, pretty straightforward as in the guide. You keep your code on the lib folder, add gemspec on the Gemfile and create a gemspec file. After that, you create and account on RubyGems, get the API key and then gem build, gem push and that’s it, gem published.

Really sweet. Now anyone who wishes to use it just have to run “gem install num_coder”, and run any of the examples described on the project README. For instance, you could get a list of numbers in the billions and encode it in a single string and back:

> NumCoder.fixed_encode95 [1234567890,1876543290,6758493021], 5
=> “/.y5M7#c69r|iNl”
> NumCoder.fixed_decode95 “/.y5M7#c69r|iNl”, 5
=> [1234567890, 1876543290, 6758493021]

Each number using five characters instead of the expected ten. The representation could be even more compact using an even larger character set for symbols, going for the rest of the UTF-8 range. But then it wouldn’t be that readable, depending on the platform. Halving the space will do for now!