From 898edd0b0bf17295d844adf57689609602aea678 Mon Sep 17 00:00:00 2001 From: Daniel Barlow Date: Mon, 3 Nov 2014 17:00:05 +0000 Subject: [PATCH 1/2] test for working timeouts in #read --- spec/moped/connection/socket/tcp_spec.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 spec/moped/connection/socket/tcp_spec.rb diff --git a/spec/moped/connection/socket/tcp_spec.rb b/spec/moped/connection/socket/tcp_spec.rb new file mode 100644 index 0000000..46649d3 --- /dev/null +++ b/spec/moped/connection/socket/tcp_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' +require 'moped/connection/socket/tcp' + +describe Moped::Connection::Socket::TCP do + it "raises Moped::Errors::ConnectionFailure if no response within timeout" do + Timeout::timeout(10) do + expect { + # this test relies on the mongo protocol expecting the client + # to speak first + socket = described_class.connect('127.0.0.1', 27017, 2) + socket.read(100) + }.to raise_exception Moped::Errors::ConnectionFailure + end + end +end From c56498f06d745c32fc0bcd8f32250afc75b57213 Mon Sep 17 00:00:00 2001 From: Daniel Barlow Date: Mon, 3 Nov 2014 11:10:45 +0000 Subject: [PATCH 2/2] socket timeouts using select not SO_RCVTIMEO In MRI 2.1.3 - and probably most other versions - setting the SO_RCVTIMEO option on a socket will cause read(2) to return EWOULDBLOCK as expected, but the interpreter internals will retry the operation instead of passing the failure up to interpreted code. Thus, the timeout has no visible effect. What we should do instead is call Kernel.select with the required timeout to check if there is data available, *then* call read(2). If .select fails we raise a Errors::ConnectionFailure on the assumption that something higher up in the stack will catch it and cope. --- lib/moped/connection/socket/connectable.rb | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/moped/connection/socket/connectable.rb b/lib/moped/connection/socket/connectable.rb index 3003a47..2d45d0a 100644 --- a/lib/moped/connection/socket/connectable.rb +++ b/lib/moped/connection/socket/connectable.rb @@ -4,6 +4,7 @@ module Socket module Connectable attr_reader :host, :port + attr_accessor :timeout # Is the socket connection alive? # @@ -44,7 +45,14 @@ def self.included(klass) # @since 1.2.0 def read(length) check_if_alive! - handle_socket_errors { super } + handle_socket_errors { + if Kernel.select([self], nil, [self], @timeout) + super + else + raise Errors::ConnectionFailure, + "timeout #{@timeout} exceeded on read from #{host}:#{port}" + end + } end # Write to the socket. @@ -145,9 +153,7 @@ def connect(host, port, timeout) sock = new(host, port) sock.set_encoding('binary') timeout_val = [ timeout, 0 ].pack("l_2") - sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1) - sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO, timeout_val) - sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDTIMEO, timeout_val) + sock.timeout = timeout sock end rescue Timeout::Error