wuarmin
wuarmin

Reputation: 4015

What's the best way to escape from Ruby's TCPSocket::gets after a given timeout?

I have following (simplified) ruby code, which opens ruby's TCP-Socket and reads from it:

socket = TCPSocket.open(server, port)

while line = socket.gets
  line = line.chop
end

Now I want read from the socket just for a given period of time (i.e. 1 minute). So after 1 one minute, the while block should be breaked and the process should exit.
Putting a line like

break if (elapsed_time > 1000)

into the gets-block is not possible, because if nothing is written to the socket, this line of code is not reached.

Upvotes: 2

Views: 228

Answers (1)

Stefan
Stefan

Reputation: 114168

To read from an IO in a non-blocking way, there's read_nonblock. It either reads the available bytes (up to a given maximum) or raise an exception if the IO isn't ready for reading. Upon rescuing the exception, you can call IO.select to wait for the IO to become ready for reading.

IO.select takes up to 3 arrays with IOs to be monitored for 1) reading, 2) writing and 3) exceptions (you can monitor many IOs at once). A basic loop could look like this:

buffer = String.new

loop do
  begin
    buffer << socket.read_nonblock(1024)
  rescue IO::WaitReadable
    IO.select([socket])
  end
end

The above attempts to reads up to 1024 bytes which will be appended to buffer. If the read succeeds, it will read the next 1024 bytes and so on. If a read fails because the socket doesn't have any data, it calls IO.select which monitors the socket and returns as soon as more data is available.

IO.select also takes a 4th argument timeout. If the given value (in seconds) is exceeded, it will return nil which can be used to break the loop conditionally:

loop do
  begin
    buffer << socket.read_nonblock(1024)
  rescue IO::WaitReadable
    break unless IO.select([socket], nil, nil, 10)
  end
end

The above will wait up to 10 seconds for more data to become available or break the loop otherwise.

However, it will wait for 10 seconds per (failed) read attempt. To get a "global" timeout for the whole loop you might need something along these lines:

timeout = 10
buffer = String.new

t = Time.now
remaining = timeout

while remaining > 0
  begin
    buffer << socket.read_nonblock(1024)
  rescue IO::WaitReadable
    break unless IO.select([socket], nil, nil, remaining)
  end

  elapsed = Time.now - t
  remaining = timeout - elapsed
end

The above keeps track of the remaining time (in seconds) and passed that value as a timeout to IO.select.

Finally, you might want to process the lines as soon as they become available. To do so you could check the string buffer for a newline character and extract the line via slice! (possibly in a loop since you might have read multiple lines at once). Each extracted line could then be yielded:

def gets_while(io, timeout)
  buffer = String.new
  
  t = Time.now
  remaining = timeout
  
  while remaining > 0
    begin
      buffer << io.read_nonblock(1024)
      while (newline = buffer.index("\n"))
        yield buffer.slice!(0..newline)
      end
    rescue IO::WaitReadable
      break unless IO.select([io], nil, nil, remaining)
    rescue EOFError
      break # end of stream / connection closed
    end
  
    elapsed = Time.now - t
    remaining = timeout - elapsed
  end
end

Example usage:

socket = TCPSocket.open(server, port)

gets_while(socket, 60) do |line|
  l = line.chomp
  # ...
end

Upvotes: 2

Related Questions