Search code examples
rubysocketstimeout

Best way of implementing timeouts on ruby sockets


I am just a mechanical engineer (a newbie to programming) developing a web application for monitoring the power output of customer solar systems. I am currently trying to implement the script my application uses to open a socket connection with and query customer inverters using timeouts and threads. Currently the script (which is set via a cron to run every 15 minutes) is very trivial and looks something like this:

System.all.each do |system|
  @sock = TCPSocket.open(system.ip,51101)
  @query = "\x0A\x05\x03\x00\xB5\x00\x01\x94\x68\x0D"
  @sock.write(@query)      
  @s = @sock.read(9)
  # do stuff with @s
  @sock.close
end

One of the big problems I want to solve before anything else: after sunset every day when all customer inverters shut down for the day and this script attempts its first socket.write/read, I believe it "hangs" because in order for it to start connecting and querying again, I have to restart my computer in the morning. So I'd like to implement timeouts properly both on the socket itself and also on socket.read so that this doesn't happen. From the stack overflow research I've done so far, I thought maybe I should do something like the following to do the same thing as above, but with a socket that times out after 2 seconds:

@port = 51101
System.all.each do |system|
  @sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
  @sock_address = Socket.sockaddr_in(@port, system.ip)

  begin
    @sock.connect_nonblock(@sock_address)
    @query = "\x0A\x05\x03\x00\xB5\x00\x01\x94\x68\x0D"
    @sock.write(@query)      
    @s = @sock.read(9)
    # do stuff with @s
    @sock.close
  rescue Errno::EINPROGRESS
    if IO.select(nil, [@sock], nil, 2)
      begin
        @sock.connect_nonblock(@sock_address)
        @query = "\x0A\x05\x03\x00\xB5\x00\x01\x94\x68\x0D"
        @sock.write(@query)      
        @s = @sock.read(9)
        # do stuff with @s
        @sock.close
      rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
        # do something
      end
    end
  end
end

But there are a number of things I don't understand about this code. Firstly, when is Errno::EINPROGRESS raised? Does the second @sock.connect_nonblock(@sock_address) appear in line 6, because this code would retry the connection if the system had already been trying for "too long" but not over 2 seconds? Also from what I read Errno::EINPROGRESS seems to be a Windows thing. What would be the Mac OS equivalent? Or these questions could be totally beside the point. Please let me know either way if I'm going in the right direction. Or if you could provide me with a commented snippet demonstrating the right way to do this.

Secondly, if the socket connects just fine, how could I implement a timeout of the socket.read so that it doesn't make everything hang (which is what I think is actually happening when the inverters shut down)? I keep reading that the ruby timeout.rb should not be used. How could I make it only try reading for a few seconds and then close the socket if it hangs? Thank you so much in advance.


Solution

  • From what I understand now, it is easiest (not necessarily best--depends on your needs) just to avoid this problem of trying to have my socket connection attempt time out as well as my socket.read time out. Since my script is simple and I am using Ruby 1.8.7, I am using the system_timer gem (ruby's timeout library is not reliable for Ruby 1.8.x and system_timer gets around timeout's issues for Ruby 1.8.x).

    http://systemtimer.rubyforge.org/

    The documentation is good.