Skip to content

Add open_timeout as an overall timeout option for Socket.tcp #13368

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

Merged
merged 4 commits into from
Jun 14, 2025
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
34 changes: 25 additions & 9 deletions ext/socket/lib/socket.rb
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,7 @@ def accept_nonblock(exception: true)
#
# [:resolv_timeout] Specifies the timeout in seconds from when the hostname resolution starts.
# [:connect_timeout] This method sequentially attempts connecting to all candidate destination addresses.<br>The +connect_timeout+ specifies the timeout in seconds from the start of the connection attempt to the last candidate.<br>By default, all connection attempts continue until the timeout occurs.<br>When +fast_fallback:false+ is explicitly specified,<br>a timeout is set for each connection attempt and any connection attempt that exceeds its timeout will be canceled.
# [:open_timeout] Specifies the timeout in seconds from the start of the method execution.<br>If this timeout is reached while there are still addresses that have not yet been attempted for connection, no further attempts will be made.
# [:fast_fallback] Enables the Happy Eyeballs Version 2 algorithm (enabled by default).
#
# If a block is given, the block is called with the socket.
Expand All @@ -656,11 +657,16 @@ def accept_nonblock(exception: true)
# sock.close_write
# puts sock.read
# }
def self.tcp(host, port, local_host = nil, local_port = nil, connect_timeout: nil, resolv_timeout: nil, fast_fallback: tcp_fast_fallback, &) # :yield: socket
def self.tcp(host, port, local_host = nil, local_port = nil, connect_timeout: nil, resolv_timeout: nil, open_timeout: nil, fast_fallback: tcp_fast_fallback, &) # :yield: socket

if open_timeout && (connect_timeout || resolv_timeout)
raise ArgumentError, "Cannot specify open_timeout along with connect_timeout or resolv_timeout"
end

sock = if fast_fallback && !(host && ip_address?(host))
tcp_with_fast_fallback(host, port, local_host, local_port, connect_timeout:, resolv_timeout:)
tcp_with_fast_fallback(host, port, local_host, local_port, connect_timeout:, resolv_timeout:, open_timeout:)
else
tcp_without_fast_fallback(host, port, local_host, local_port, connect_timeout:, resolv_timeout:)
tcp_without_fast_fallback(host, port, local_host, local_port, connect_timeout:, resolv_timeout:, open_timeout:)
end

if block_given?
Expand All @@ -674,7 +680,7 @@ def self.tcp(host, port, local_host = nil, local_port = nil, connect_timeout: ni
end
end

def self.tcp_with_fast_fallback(host, port, local_host = nil, local_port = nil, connect_timeout: nil, resolv_timeout: nil)
def self.tcp_with_fast_fallback(host, port, local_host = nil, local_port = nil, connect_timeout: nil, resolv_timeout: nil, open_timeout: nil)
if local_host || local_port
local_addrinfos = Addrinfo.getaddrinfo(local_host, local_port, nil, :STREAM, timeout: resolv_timeout)
resolving_family_names = local_addrinfos.map { |lai| ADDRESS_FAMILIES.key(lai.afamily) }.uniq
Expand All @@ -692,6 +698,7 @@ def self.tcp_with_fast_fallback(host, port, local_host = nil, local_port = nil,
resolution_delay_expires_at = nil
connection_attempt_delay_expires_at = nil
user_specified_connect_timeout_at = nil
user_specified_open_timeout_at = open_timeout ? now + open_timeout : nil
last_error = nil
last_error_from_thread = false

Expand Down Expand Up @@ -784,7 +791,10 @@ def self.tcp_with_fast_fallback(host, port, local_host = nil, local_port = nil,

ends_at =
if resolution_store.any_addrinfos?
resolution_delay_expires_at || connection_attempt_delay_expires_at
[(resolution_delay_expires_at || connection_attempt_delay_expires_at),
user_specified_open_timeout_at].compact.min
elsif user_specified_open_timeout_at
user_specified_open_timeout_at
else
[user_specified_resolv_timeout_at, user_specified_connect_timeout_at].compact.max
end
Expand Down Expand Up @@ -885,6 +895,8 @@ def self.tcp_with_fast_fallback(host, port, local_host = nil, local_port = nil,
end
end

raise(Errno::ETIMEDOUT, 'user specified timeout') if expired?(now, user_specified_open_timeout_at)

if resolution_store.empty_addrinfos?
if connecting_sockets.empty? && resolution_store.resolved_all_families?
if last_error_from_thread
Expand Down Expand Up @@ -912,7 +924,7 @@ def self.tcp_with_fast_fallback(host, port, local_host = nil, local_port = nil,
end
end

def self.tcp_without_fast_fallback(host, port, local_host, local_port, connect_timeout:, resolv_timeout:)
def self.tcp_without_fast_fallback(host, port, local_host, local_port, connect_timeout:, resolv_timeout:, open_timeout:)
last_error = nil
ret = nil

Expand All @@ -921,17 +933,21 @@ def self.tcp_without_fast_fallback(host, port, local_host, local_port, connect_t
local_addr_list = Addrinfo.getaddrinfo(local_host, local_port, nil, :STREAM, nil)
end

Addrinfo.foreach(host, port, nil, :STREAM, timeout: resolv_timeout) {|ai|
timeout = open_timeout ? open_timeout : resolv_timeout
starts_at = current_clock_time

Addrinfo.foreach(host, port, nil, :STREAM, timeout:) {|ai|
if local_addr_list
local_addr = local_addr_list.find {|local_ai| local_ai.afamily == ai.afamily }
next unless local_addr
else
local_addr = nil
end
begin
timeout = open_timeout ? open_timeout - (current_clock_time - starts_at) : connect_timeout
sock = local_addr ?
ai.connect_from(local_addr, timeout: connect_timeout) :
ai.connect(timeout: connect_timeout)
ai.connect_from(local_addr, timeout:) :
ai.connect(timeout:)
rescue SystemCallError
last_error = $!
next
Expand Down
26 changes: 26 additions & 0 deletions test/socket/test_socket.rb
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,32 @@ def test_tcp_socket_resolv_timeout_with_connection_failure
RUBY
end

def test_tcp_socket_open_timeout
opts = %w[-rsocket -W1]
assert_separately opts, <<~RUBY
Addrinfo.define_singleton_method(:getaddrinfo) do |_, _, family, *_|
if family == Socket::AF_INET6
sleep
else
[Addrinfo.tcp("127.0.0.1", 12345)]
end
end

assert_raise(Errno::ETIMEDOUT) do
Socket.tcp("localhost", 12345, open_timeout: 0.01)
end
RUBY
end

def test_tcp_socket_open_timeout_with_other_timeouts
opts = %w[-rsocket -W1]
assert_separately opts, <<~RUBY
assert_raise(ArgumentError) do
Socket.tcp("localhost", 12345, open_timeout: 0.01, resolv_timout: 0.01)
end
RUBY
end

def test_tcp_socket_one_hostname_resolution_succeeded_at_least
opts = %w[-rsocket -W1]
assert_separately opts, <<~RUBY
Expand Down
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy