Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Timeout.timeout for TCP connection #195

Merged
merged 1 commit into from
Mar 31, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 13 additions & 32 deletions lib/http/timeout/global.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# rubocop:disable Lint/HandleExceptions
module HTTP
module Timeout
class Global < PerOperation
Expand All @@ -11,24 +10,24 @@ def initialize(*args)
@total_timeout = time_left
end

# Abstracted out from the normal connect for SSL connections
def connect_with_timeout(*args)
def connect(socket_class, host, port)
reset_timer
::Timeout.timeout(time_left, TimeoutError) do
@socket = socket_class.open(host, port)
end

begin
socket.connect_nonblock(*args)
log_time
end

def connect_ssl
reset_timer

begin
socket.connect_nonblock
rescue IO::WaitReadable
IO.select([socket], nil, nil, time_left)
log_time
retry

rescue Errno::EINPROGRESS
IO.select(nil, [socket], nil, time_left)
log_time
retry

rescue Errno::EISCONN
end
end

Expand Down Expand Up @@ -58,26 +57,9 @@ def write(data)
end
end

private

# Create a DNS resolver
def resolve_address(host)
addr = HostResolver.getaddress(host)
return addr if addr

reset_timer

addr = Resolv::DNS.open(:timeout => time_left) do |dns|
dns.getaddress
end
alias_method :<<, :write

log_time

addr

rescue Resolv::ResolvTimeout
raise TimeoutError, "DNS timed out after #{total_timeout} seconds"
end
private

# Due to the run/retry nature of nonblocking I/O, it's easier to keep track of time
# via method calls instead of a block to monitor.
Expand All @@ -96,4 +78,3 @@ def log_time
end
end
end
# rubocop:enable Lint/HandleExceptions
80 changes: 11 additions & 69 deletions lib/http/timeout/per_operation.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# rubocop:disable Lint/HandleExceptions
require "resolv"

module HTTP
module Timeout
class PerOperation < Null
Expand All @@ -20,40 +17,22 @@ def initialize(*args)
@connect_timeout = options.fetch(:connect_timeout, CONNECT_TIMEOUT)
end

def connect(_, host, port)
# https://github.com/celluloid/celluloid-io/blob/master/lib/celluloid/io/tcp_socket.rb
begin
addr = Resolv::IPv4.create(host)
rescue ArgumentError
end

# Guess it's not IPv4! Is it IPv6?
begin
addr ||= Resolv::IPv6.create(host)
rescue ArgumentError
end

unless addr
addr = resolve_address(host)
fail Resolv::ResolvError, "DNS result has no information for #{host}" unless addr
def connect(socket_class, host, port)
::Timeout.timeout(connect_timeout, TimeoutError) do
@socket = socket_class.open(host, port)
end
end

case addr
when Resolv::IPv4
family = Socket::AF_INET
when Resolv::IPv6
family = Socket::AF_INET6
else fail ArgumentError, "unsupported address class: #{addr.class}"
def connect_ssl
socket.connect_nonblock
rescue IO::WaitReadable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I missed this on the last pass, but as the SSL/TLS handshake makes many roundtrips, I believe it's possible for IO::WaitWritable to be raised here too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops I broke Excon then!!

Sent from my iPhone

On Mar 31, 2015, at 08:40, Tony Arcieri notifications@github.com wrote:

In lib/http/timeout/per_operation.rb:

  •    case addr
    
  •    when Resolv::IPv4
    
  •      family = Socket::AF_INET
    
  •    when Resolv::IPv6
    
  •      family = Socket::AF_INET6
    
  •    else fail ArgumentError, "unsupported address class: #{addr.class}"
    
  •  def connect_ssl
    
  •    socket.connect_nonblock
    
  •  rescue IO::WaitReadable
    
    I guess I missed this on the last pass, but as the SSL/TLS handshake makes many roundtrips, I believe it's possible for IO::WaitWritable to be raised here too.


Reply to this email directly or view it on GitHub.

if IO.select([socket], nil, nil, connect_timeout)
retry
else
raise TimeoutError, "Connection timed out after #{connect_timeout} seconds"
end

@socket = Socket.new(family, Socket::SOCK_STREAM, 0)

connect_with_timeout(Socket.sockaddr_in(port, addr.to_s))
end

# No changes need to be made for the SSL connection
alias_method :connect_with_timeout, :connect_ssl

# Read data from the socket
def readpartial(size)
socket.read_nonblock(size)
Expand All @@ -75,43 +54,6 @@ def write(data)
raise TimeoutError, "Read timed out after #{write_timeout} seconds"
end
end

private

# Actually do the connect after we're setup
def connect_with_timeout(*args)
socket.connect_nonblock(*args)

rescue IO::WaitReadable
if IO.select([socket], nil, nil, connect_timeout)
retry
else
raise TimeoutError, "Connection timed out after #{connect_timeout} seconds"
end

rescue Errno::EINPROGRESS
if IO.select(nil, [socket], nil, connect_timeout)
retry
else
raise TimeoutError, "Connection timed out after #{connect_timeout} seconds"
end

rescue Errno::EISCONN
end

# Create a DNS resolver
def resolve_address(host)
addr = HostResolver.getaddress(host)
return addr if addr

Resolv::DNS.open(:timeout => connect_timeout) do |dns|
dns.getaddress
end

rescue Resolv::ResolvTimeout
raise TimeoutError, "DNS timed out after #{connect_timeout} seconds"
end
end
end
end
# rubocop:enable Lint/HandleExceptions
2 changes: 1 addition & 1 deletion spec/lib/http/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def simple_response(body, status = 200)
described_class.new options.merge :ssl_context => SSLHelper.client_context
end

include_context "HTTP handling", true do
include_context "HTTP handling" do
let(:server) { dummy_ssl }
end

Expand Down
44 changes: 3 additions & 41 deletions spec/support/http_handling_shared.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
RSpec.shared_context "HTTP handling" do |ssl = false|
RSpec.shared_context "HTTP handling" do
describe "timeouts" do
let(:conn_timeout) { 1 }
let(:read_timeout) { 1 }
Expand Down Expand Up @@ -73,56 +73,18 @@

let(:response) { client.get(server.endpoint).body.to_s }

context "with localhost" do
let(:endpoint) { server.endpoint.sub("127.0.0.1", "localhost") }

it "errors if DNS takes too long" do
# Block the localhost lookup
expect(timeout_class::HostResolver)
.to receive(:getaddress).with("localhost").and_return(nil)

# Request
expect(Resolv::DNS).to receive(:open).with(:timeout => 1) do |_|
sleep 1.25
"127.0.0.1"
end

expect { client.get(server.endpoint.sub("127.0.0.1", "localhost")) }
.to raise_error(HTTP::TimeoutError, /Timed out/)
end
end

it "errors if connecting takes too long" do
socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)

fake_socket_id = double(:to_io => socket)
expect(fake_socket_id).to receive(:connect_nonblock) do |*args|
expect(TCPSocket).to receive(:open) do
sleep 1.25
socket.connect_nonblock(*args)
end

allow_any_instance_of(timeout_class).to receive(:socket).and_return(fake_socket_id)

expect { response }.to raise_error(HTTP::TimeoutError, /Timed out/)
expect { response }.to raise_error(HTTP::TimeoutError, /execution/)
end

it "errors if reading takes too long" do
expect { client.get("#{server.endpoint}/sleep").body.to_s }
.to raise_error(HTTP::TimeoutError, /Timed out/)
end

unless ssl
it "errors if writing takes too long" do
socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
allow_any_instance_of(timeout_class).to receive(:socket).and_return(socket)

expect(socket).to receive(:<<) do |*|
sleep 1.25
end

expect { response }.to raise_error(HTTP::TimeoutError, /Timed out/)
end
end
end
end

Expand Down