ruby/test/socket/test_socket.rb
Misaki Shioi c45c600e22
Add open_timeout as an overall timeout option for Socket.tcp (#13368)
* Add `open_timeout` as an overall timeout option for `Socket.tcp`

[Background]
Currently, `TCPSocket.new` and `Socket.tcp` accept two kind of timeout options:
- `resolv_timeout`, which controls the timeout for DNS resolution
- `connect_timeout`, which controls the timeout for the connection attempt

With the introduction of Happy Eyeballs Version 2 (as per [RFC 8305](https://datatracker.ietf.org/doc/html/rfc8305)) in[ Feature #20108](https://bugs.ruby-lang.org/issues/20108) and [Feature #20782](https://bugs.ruby-lang.org/issues/20782), both address resolution and connection attempts are now parallelized.
As a result, the sum of `resolv_timeout` and `connect_timeout` no longer represents the total timeout duration. This is because, in HEv2, name resolution and connection attempts are performed concurrently, causing the two timeouts to overlap.

Example:
When `resolv_timeout: 200ms` and `connect_timeout: 100ms` are set:
1. An IPv6 address  is resolved after the method starts immediately (IPv4 is still being resolved).
2. A connection attempt is initiated to the IPv6 address
3. After 100ms, `connect_timeout` is exceeded. However, since `resolv_timeout` still has 100ms left, the IPv4 resolution continues.
4. After 200ms from the start, the method raises a `resolv_timeout` error.

In this case, the total elapsed time before a timeout is 200ms, not the expected 300ms (100ms + 200ms).

Furthermore, in HEv2, connection attempts are also parallelized.
It starts a new connection attempts every 250ms for resolved addresses. This makes the definition of `connect_timeout` even more ambiguous—specifically, it becomes unclear from which point the timeout is counted.

Additionally, these methods initiate new connection attempts every 250ms (Connection Attempt Delay) for each candidate address, thereby parallelizing connection attempts. However, this behavior makes it unclear from which point in time the connect_timeout is actually measured.
Currently, a `connect_timeout` is raised only after the last connection attempt exceeds the timeout.

Example:
When `connect_timeout: 100ms` is set and 3 address candidates:
1. Start a connection attempt to the address `a`
2. 250ms after step 1, start a new connection attempt to the address `b`
3. 500ms after step 1, start a new connection attempt to the address `c`
4. 1000ms after step 3 (1000ms after starting the connection to `c`, 1250ms after starting the connection to `b,` and 1500ms after starting the connection to `a`) `connect_timeout` is raised

This behavior aims to favor successful connections by allowing more time for each attempt, but it results in a timeout model that is difficult to reason about.

These methods have supported `resolv_timeout` and `connect_timeout` options even before the introduction of HEv2. However, in many use cases, it would be more convenient if a timeout occurred after a specified duration from the start of the method. Similar functions in other languages (such as PHP, Python, and Go) typically allow specifying only an overall timeout.

[Proposal]
I propose adding an `open_timeout` option to `Socket.tcp` in this PR, which triggers a timeout after a specified duration has elapsed from the start of the method.

The name `open_timeout` aligns with the existing accessor used in `Net::HTTP`.
If `open_timeout` is specified together with `resolv_timeout` and `connect_timeout`, I propose that only `open_timeout` be used and the others be ignored. While it is possible to support combinations of `open_timeout`, `resolv_timeout`, and `connect_timeout`, doing so would require defining which timeout takes precedence in which situations. In this case, I believe it is more valuable to keep the behavior simple and easy to understand, rather than supporting more complex use cases.

If this proposal is accepted, I also plan to extend `open_timeout` support to `TCPSocket.new`.

While the long-term future of `resolv_timeout` and `connect_timeout` may warrant further discussion, I believe the immediate priority is to offer a straightforward way to specify an overall timeout.

[Outcome]
If `open_timeout` is also supported by `TCPSocket.new`, users would be able to manage total connection timeouts directly in `Net::HTTP#connect` without relying on `Timeout.timeout`.
aa0f689bf4/lib/net/http.rb (L1657)

---

* Raise an exception if it is specified together with other timeout options

> If open_timeout is specified together with resolv_timeout and connect_timeout, I propose that only open_timeout be used and the others be ignored.

Since this approach may be unclear to users, I’ve decided to explicitly raise an `ArgumentError` if these options are specified together.

* Add doc

* Fix: open_timeout error should be raised even if there are still addresses that have not been tried
2025-06-14 09:54:34 +09:00

1099 lines
31 KiB
Ruby

# frozen_string_literal: true
begin
require "socket"
require "tmpdir"
require "fcntl"
require "etc"
require "test/unit"
rescue LoadError
end
class TestSocket < Test::Unit::TestCase
def test_socket_new
begin
s = Socket.new(:INET, :STREAM)
assert_kind_of(Socket, s)
ensure
s.close
end
end
def test_socket_new_cloexec
return unless defined? Fcntl::FD_CLOEXEC
begin
s = Socket.new(:INET, :STREAM)
assert(s.close_on_exec?)
ensure
s.close
end
end
def test_unpack_sockaddr
sockaddr_in = Socket.sockaddr_in(80, "")
assert_raise(ArgumentError) { Socket.unpack_sockaddr_un(sockaddr_in) }
sockaddr_un = Socket.sockaddr_un("/testdir/s")
assert_raise(ArgumentError) { Socket.unpack_sockaddr_in(sockaddr_un) }
assert_raise(ArgumentError) { Socket.unpack_sockaddr_in("") }
assert_raise(ArgumentError) { Socket.unpack_sockaddr_un("") }
end if Socket.respond_to?(:sockaddr_un)
def test_sysaccept
serv = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
serv.bind(Socket.sockaddr_in(0, "127.0.0.1"))
serv.listen 5
c = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
c.connect(serv.getsockname)
fd, peeraddr = serv.sysaccept
assert_equal(c.getsockname, peeraddr.to_sockaddr)
ensure
serv.close if serv
c.close if c
IO.for_fd(fd).close if fd
end
def test_initialize
Socket.open(Socket::AF_INET, Socket::SOCK_STREAM, 0) {|s|
s.bind(Socket.sockaddr_in(0, "127.0.0.1"))
addr = s.getsockname
assert_nothing_raised { Socket.unpack_sockaddr_in(addr) }
assert_raise(ArgumentError, NoMethodError) { Socket.unpack_sockaddr_un(addr) }
}
Socket.open("AF_INET", "SOCK_STREAM", 0) {|s|
s.bind(Socket.sockaddr_in(0, "127.0.0.1"))
addr = s.getsockname
assert_nothing_raised { Socket.unpack_sockaddr_in(addr) }
assert_raise(ArgumentError, NoMethodError) { Socket.unpack_sockaddr_un(addr) }
}
Socket.open(:AF_INET, :SOCK_STREAM, 0) {|s|
s.bind(Socket.sockaddr_in(0, "127.0.0.1"))
addr = s.getsockname
assert_nothing_raised { Socket.unpack_sockaddr_in(addr) }
assert_raise(ArgumentError, NoMethodError) { Socket.unpack_sockaddr_un(addr) }
}
end
def test_bind
Socket.open(Socket::AF_INET, Socket::SOCK_STREAM, 0) {|bound|
bound.bind(Socket.sockaddr_in(0, "127.0.0.1"))
addr = bound.getsockname
port, = Socket.unpack_sockaddr_in(addr)
Socket.open(Socket::AF_INET, Socket::SOCK_STREAM, 0) {|s|
e = assert_raise(Errno::EADDRINUSE) do
s.bind(Socket.sockaddr_in(port, "127.0.0.1"))
end
assert_match "bind(2) for 127.0.0.1:#{port}", e.message
}
}
end
def test_getaddrinfo
# This should not send a DNS query because AF_UNIX.
assert_raise(Socket::ResolutionError) { Socket.getaddrinfo("www.kame.net", 80, "AF_UNIX") }
end
def test_getaddrinfo_raises_no_errors_on_port_argument_of_0 # [ruby-core:29427]
assert_nothing_raised('[ruby-core:29427]'){ Socket.getaddrinfo('localhost', 0, Socket::AF_INET, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) }
assert_nothing_raised('[ruby-core:29427]'){ Socket.getaddrinfo('localhost', '0', Socket::AF_INET, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) }
assert_nothing_raised('[ruby-core:29427]'){ Socket.getaddrinfo('localhost', '00', Socket::AF_INET, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) }
assert_raise(Socket::ResolutionError, '[ruby-core:29427]'){ Socket.getaddrinfo(nil, nil, Socket::AF_INET, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) }
assert_nothing_raised('[ruby-core:29427]'){ TCPServer.open('localhost', 0) {} }
end
def test_getnameinfo
assert_raise(Socket::ResolutionError) { Socket.getnameinfo(["AF_UNIX", 80, "0.0.0.0"]) }
assert_raise(ArgumentError) {Socket.getnameinfo(["AF_INET", "http\0", "example.net"])}
assert_raise(ArgumentError) {Socket.getnameinfo(["AF_INET", "http", "example.net\0"])}
end
def test_ip_address_list
begin
list = Socket.ip_address_list
rescue NotImplementedError
return
end
list.each {|ai|
assert_instance_of(Addrinfo, ai)
assert(ai.ip?)
}
end
def test_ip_address_list_include_localhost
begin
list = Socket.ip_address_list
rescue NotImplementedError
return
end
assert_includes list.map(&:ip_address), Addrinfo.tcp("localhost", 0).ip_address
end
def test_tcp
TCPServer.open(0) {|serv|
addr = serv.connect_address
addr.connect {|s1|
s2 = serv.accept
begin
assert_equal(s2.remote_address.ip_unpack, s1.local_address.ip_unpack)
ensure
s2.close
end
}
}
end
def test_tcp_cloexec
return unless defined? Fcntl::FD_CLOEXEC
TCPServer.open(0) {|serv|
addr = serv.connect_address
addr.connect {|s1|
s2 = serv.accept
begin
assert(s2.close_on_exec?)
ensure
s2.close
end
}
}
end
def random_port
# IANA suggests dynamic port for 49152 to 65535
# http://www.iana.org/assignments/port-numbers
case RUBY_PLATFORM
when /mingw|mswin/
rand(50000..65535)
else
rand(49152..65535)
end
end
def errors_addrinuse
errs = [Errno::EADDRINUSE]
# MinGW fails with "Errno::EACCES: Permission denied - bind(2) for 0.0.0.0:49721"
errs << Errno::EACCES if /mingw/ =~ RUBY_PLATFORM
errs
end
def test_tcp_server_sockets
port = random_port
begin
sockets = Socket.tcp_server_sockets(port)
rescue *errors_addrinuse
return # not test failure
end
begin
sockets.each {|s|
assert_equal(port, s.local_address.ip_port)
}
ensure
sockets.each {|s|
s.close
}
end
end
def test_tcp_server_sockets_port0
sockets = Socket.tcp_server_sockets(0)
ports = sockets.map {|s| s.local_address.ip_port }
the_port = ports.first
ports.each {|port|
assert_equal(the_port, port)
}
ensure
if sockets
sockets.each {|s|
s.close
}
end
end
if defined? UNIXSocket
def test_unix
Dir.mktmpdir {|tmpdir|
path = "#{tmpdir}/sock"
UNIXServer.open(path) {|serv|
Socket.unix(path) {|s1|
s2 = serv.accept
begin
s2raddr = s2.remote_address
s1laddr = s1.local_address
assert(s2raddr.to_sockaddr.empty? ||
s1laddr.to_sockaddr.empty? ||
s2raddr.unix_path == s1laddr.unix_path)
assert(s2.close_on_exec?)
ensure
s2.close
end
}
}
}
end
def test_unix_server_socket
Dir.mktmpdir {|tmpdir|
path = "#{tmpdir}/sock"
2.times {
serv = Socket.unix_server_socket(path)
begin
assert_kind_of(Socket, serv)
assert(File.socket?(path))
assert_equal(path, serv.local_address.unix_path)
ensure
serv.close
end
}
}
end
def test_accept_loop_with_unix
Dir.mktmpdir {|tmpdir|
tcp_servers = []
clients = []
accepted = []
begin
tcp_servers = Socket.tcp_server_sockets(0)
unix_server = Socket.unix_server_socket("#{tmpdir}/sock")
tcp_servers.each {|s|
addr = s.connect_address
begin
clients << addr.connect
rescue
# allow failure if the address is IPv6
raise unless addr.ipv6?
end
}
addr = unix_server.connect_address
assert_nothing_raised("connect to #{addr.inspect}") {
clients << addr.connect
}
Socket.accept_loop(tcp_servers, unix_server) {|s|
accepted << s
break if clients.length == accepted.length
}
assert_equal(clients.length, accepted.length)
ensure
tcp_servers.each {|s| s.close if !s.closed? }
unix_server.close if unix_server && !unix_server.closed?
clients.each {|s| s.close if !s.closed? }
accepted.each {|s| s.close if !s.closed? }
end
}
end
end
def test_accept_loop
servers = []
begin
servers = Socket.tcp_server_sockets(0)
port = servers[0].local_address.ip_port
Socket.tcp("localhost", port) {|s1|
Socket.accept_loop(servers) {|s2, client_ai|
begin
assert_equal(s1.local_address.ip_unpack, client_ai.ip_unpack)
ensure
s2.close
end
break
}
}
ensure
servers.each {|s| s.close if !s.closed? }
end
end
def test_accept_loop_multi_port
servers = []
begin
servers = Socket.tcp_server_sockets(0)
port = servers[0].local_address.ip_port
servers2 = Socket.tcp_server_sockets(0)
servers.concat servers2
port2 = servers2[0].local_address.ip_port
Socket.tcp("localhost", port) {|s1|
Socket.accept_loop(servers) {|s2, client_ai|
begin
assert_equal(s1.local_address.ip_unpack, client_ai.ip_unpack)
ensure
s2.close
end
break
}
}
Socket.tcp("localhost", port2) {|s1|
Socket.accept_loop(servers) {|s2, client_ai|
begin
assert_equal(s1.local_address.ip_unpack, client_ai.ip_unpack)
ensure
s2.close
end
break
}
}
ensure
servers.each {|s| s.close if !s.closed? }
end
end
def test_udp_server
# http://rubyci.s3.amazonaws.com/rhel_zlinux/ruby-master/log/20230312T023302Z.fail.html.gz
# Errno::EHOSTUNREACH: No route to host - recvmsg(2)
omit if 'rhel_zlinux' == ENV['RUBYCI_NICKNAME']
begin
ifaddrs = Socket.getifaddrs
rescue NotImplementedError
omit "Socket.getifaddrs not implemented"
end
ifconfig = nil
Socket.udp_server_sockets(0) {|sockets|
famlies = {}
sockets.each {|s| famlies[s.local_address.afamily] = s }
nd6 = {}
ifaddrs.reject! {|ifa|
ai = ifa.addr
next true unless ai
s = famlies[ai.afamily]
next true unless s
next true if ai.ipv6_linklocal? # IPv6 link-local address is too troublesome in this test.
case RUBY_PLATFORM
when /linux/
if ai.ip_address.include?('%') and
(Etc.uname[:release][/[0-9.]+/].split('.').map(&:to_i) <=> [2,6,18]) <= 0
# Cent OS 5.6 (2.6.18-238.19.1.el5xen) doesn't correctly work
# sendmsg with pktinfo for link-local ipv6 addresses
next true
end
when /freebsd/
if ifa.addr.ipv6_linklocal?
# FreeBSD 9.0 with default setting (ipv6_activate_all_interfaces
# is not YES) sets IFDISABLED to interfaces which don't have
# global IPv6 address.
# Link-local IPv6 addresses on those interfaces don't work.
ulSIOCGIFINFO_IN6 = 3225971052
ulND6_IFF_IFDISABLED = 8
in6_ondireq = ifa.name
s.ioctl(ulSIOCGIFINFO_IN6, in6_ondireq)
flag = in6_ondireq.unpack('A16L6').last
next true if flag & ulND6_IFF_IFDISABLED != 0
nd6[ai] = flag
end
when /darwin/
if !ai.ipv6?
elsif ai.ipv6_unique_local? && /darwin1[01]\./ =~ RUBY_PLATFORM
next true # iCloud addresses do not work, see Bug #6692
elsif ifr_name = ai.ip_address[/%(.*)/, 1]
# Mac OS X may sets IFDISABLED as FreeBSD does
ulSIOCGIFFLAGS = 3223349521
ulSIOCGIFINFO_IN6 = 3224398156
ulIFF_POINTOPOINT = 0x10
ulND6_IFF_IFDISABLED = 8
in6_ondireq = ifr_name
s.ioctl(ulSIOCGIFINFO_IN6, in6_ondireq)
flag = in6_ondireq.unpack('A16L6').last
next true if (flag & ulND6_IFF_IFDISABLED) != 0
nd6[ai] = flag
in6_ifreq = [ifr_name,ai.to_sockaddr].pack('a16A*')
s.ioctl(ulSIOCGIFFLAGS, in6_ifreq)
next true if in6_ifreq.unpack('A16L1').last & ulIFF_POINTOPOINT != 0
end
ifconfig ||= `/sbin/ifconfig`
next true if ifconfig.scan(/^(\w+):(.*(?:\n\t.*)*)/).find do |_ifname, value|
value.include?(ai.ip_address) && value.include?('POINTOPOINT')
end
end
false
}
skipped = false
begin
port = sockets.first.local_address.ip_port
ping_p = false
th = Thread.new {
Socket.udp_server_loop_on(sockets) {|msg, msg_src|
break if msg == "exit"
rmsg = Marshal.dump([msg, msg_src.remote_address, msg_src.local_address])
ping_p = true
msg_src.reply rmsg
}
}
ifaddrs.each {|ifa|
ai = ifa.addr
Addrinfo.udp(ai.ip_address, port).connect {|s|
ping_p = false
msg1 = "<<<#{ai.inspect}>>>"
s.sendmsg msg1
unless IO.select([s], nil, nil, 10)
nd6options = nd6.key?(ai) ? "nd6=%x " % nd6[ai] : ''
raise "no response from #{ifa.inspect} #{nd6options}ping=#{ping_p}"
end
msg2, addr = s.recvmsg
msg2, _, _ = Marshal.load(msg2)
assert_equal(msg1, msg2)
assert_equal(ai.ip_address, addr.ip_address)
}
}
rescue NotImplementedError, Errno::ENOSYS
skipped = true
omit "need sendmsg and recvmsg: #{$!}"
rescue RuntimeError
skipped = true
omit "UDP server is no response: #{$!}"
ensure
if th
unless skipped
Addrinfo.udp("127.0.0.1", port).connect {|s| s.sendmsg "exit" }
end
unless th.join(10)
th.kill.join(10)
unless skipped
raise "thread killed"
end
end
end
end
}
end
def test_linger
opt = Socket::Option.linger(true, 0)
assert_equal([true, 0], opt.linger)
Addrinfo.tcp("127.0.0.1", 0).listen {|serv|
serv.local_address.connect {|s1|
s2, _ = serv.accept
begin
s1.setsockopt(opt)
s1.close
assert_raise(Errno::ECONNRESET) { s2.read }
ensure
s2.close
end
}
}
end
def timestamp_retry_rw(s1, s2, t1, type)
IO.pipe do |r,w|
# UDP may not be reliable, keep sending until recvmsg returns:
th = Thread.new do
n = 0
begin
s2.send("a", 0, s1.local_address)
n += 1
end while IO.select([r], nil, nil, 0.1).nil?
n
end
timeout = 30
assert_equal([[s1],[],[]], IO.select([s1], nil, nil, timeout))
msg, _, _, stamp = s1.recvmsg
assert_equal("a", msg)
assert(stamp.cmsg_is?(:SOCKET, type))
w.close # stop th
n = th.value
th = nil
n > 1 and
warn "UDP packet loss for #{type} over loopback, #{n} tries needed"
t2 = Time.now.strftime("%Y-%m-%d")
pat = Regexp.union([t1, t2].uniq)
assert_match(pat, stamp.inspect)
t = stamp.timestamp
assert_match(pat, t.strftime("%Y-%m-%d"))
stamp
ensure
if th and !th.join(10)
th.kill.join(10)
end
end
end
def test_timestamp
return if /linux|freebsd|netbsd|openbsd|darwin/ !~ RUBY_PLATFORM
return if !defined?(Socket::AncillaryData) || !defined?(Socket::SO_TIMESTAMP)
t1 = Time.now.strftime("%Y-%m-%d")
stamp = nil
Addrinfo.udp("127.0.0.1", 0).bind {|s1|
Addrinfo.udp("127.0.0.1", 0).bind {|s2|
s1.setsockopt(:SOCKET, :TIMESTAMP, true)
stamp = timestamp_retry_rw(s1, s2, t1, :TIMESTAMP)
}
}
t = stamp.timestamp
pat = /\.#{"%06d" % t.usec}/
assert_match(pat, stamp.inspect)
end
def test_timestampns
return if /linux/ !~ RUBY_PLATFORM || !defined?(Socket::SO_TIMESTAMPNS)
t1 = Time.now.strftime("%Y-%m-%d")
stamp = nil
Addrinfo.udp("127.0.0.1", 0).bind {|s1|
Addrinfo.udp("127.0.0.1", 0).bind {|s2|
begin
s1.setsockopt(:SOCKET, :TIMESTAMPNS, true)
rescue Errno::ENOPROTOOPT
# SO_TIMESTAMPNS is available since Linux 2.6.22
return
end
stamp = timestamp_retry_rw(s1, s2, t1, :TIMESTAMPNS)
}
}
t = stamp.timestamp
pat = /\.#{"%09d" % t.nsec}/
assert_match(pat, stamp.inspect)
end
def test_bintime
return if /freebsd/ !~ RUBY_PLATFORM
t1 = Time.now.strftime("%Y-%m-%d")
stamp = nil
Addrinfo.udp("127.0.0.1", 0).bind {|s1|
Addrinfo.udp("127.0.0.1", 0).bind {|s2|
s1.setsockopt(:SOCKET, :BINTIME, true)
s2.send "a", 0, s1.local_address
msg, _, _, stamp = s1.recvmsg
assert_equal("a", msg)
assert(stamp.cmsg_is?(:SOCKET, :BINTIME))
}
}
t2 = Time.now.strftime("%Y-%m-%d")
pat = Regexp.union([t1, t2].uniq)
assert_match(pat, stamp.inspect)
t = stamp.timestamp
assert_match(pat, t.strftime("%Y-%m-%d"))
assert_equal(stamp.data[-8,8].unpack("Q")[0], t.subsec * 2**64)
end
def test_closed_read
require 'timeout'
require 'socket'
bug4390 = '[ruby-core:35203]'
server = TCPServer.new("localhost", 0)
serv_thread = Thread.new {server.accept}
begin sleep(0.1) end until serv_thread.stop?
sock = TCPSocket.new("localhost", server.addr[1])
client_thread = Thread.new do
assert_raise(IOError, bug4390) {
sock.readline
}
end
begin sleep(0.1) end until client_thread.stop?
Timeout.timeout(1) do
sock.close
sock = nil
client_thread.join
end
ensure
serv_thread.value.close
server.close
end unless RUBY_PLATFORM.include?("freebsd")
def test_connect_timeout
host = "127.0.0.1"
server = TCPServer.new(host, 0)
port = server.addr[1]
serv_thread = Thread.new {server.accept}
sock = Socket.tcp(host, port, :connect_timeout => 30)
accepted = serv_thread.value
assert_kind_of TCPSocket, accepted
assert_equal sock, IO.select(nil, [ sock ])[1][0], "not writable"
sock.close
# some platforms may not timeout when the listener queue overflows,
# but we know Linux does with the default listen backlog of SOMAXCONN for
# TCPServer.
assert_raise(Errno::ETIMEDOUT) do
(Socket::SOMAXCONN*2).times do |i|
sock = Socket.tcp(host, port, :connect_timeout => 0)
assert_equal sock, IO.select(nil, [ sock ])[1][0],
"not writable (#{i})"
sock.close
end
end if RUBY_PLATFORM =~ /linux/
ensure
server.close
accepted.close if accepted
sock.close if sock && ! sock.closed?
end
def test_getifaddrs
begin
list = Socket.getifaddrs
rescue NotImplementedError
return
end
list.each {|ifaddr|
assert_instance_of(Socket::Ifaddr, ifaddr)
}
end
def test_connect_in_rescue
serv = Addrinfo.tcp(nil, 0).listen
addr = serv.connect_address
begin
raise "dummy error"
rescue
s = addr.connect
assert(!s.closed?)
end
ensure
serv.close if serv && !serv.closed?
s.close if s && !s.closed?
end
def test_bind_in_rescue
begin
raise "dummy error"
rescue
s = Addrinfo.tcp(nil, 0).bind
assert(!s.closed?)
end
ensure
s.close if s && !s.closed?
end
def test_listen_in_rescue
begin
raise "dummy error"
rescue
s = Addrinfo.tcp(nil, 0).listen
assert(!s.closed?)
end
ensure
s.close if s && !s.closed?
end
def test_udp_server_sockets_in_rescue
begin
raise "dummy error"
rescue
ss = Socket.udp_server_sockets(0)
ss.each {|s|
assert(!s.closed?)
}
end
ensure
if ss
ss.each {|s|
s.close if !s.closed?
}
end
end
def test_tcp_server_sockets_in_rescue
begin
raise "dummy error"
rescue
ss = Socket.tcp_server_sockets(0)
ss.each {|s|
assert(!s.closed?)
}
end
ensure
if ss
ss.each {|s|
s.close if !s.closed?
}
end
end
def test_recvmsg_udp_no_arg
n = 4097
s1 = Addrinfo.udp("127.0.0.1", 0).bind
s2 = s1.connect_address.connect
s2.send("a" * n, 0)
ret = s1.recvmsg
assert_equal n, ret[0].bytesize, '[ruby-core:71517] [Bug #11701]'
s2.send("a" * n, 0)
IO.select([s1])
ret = s1.recvmsg_nonblock
assert_equal n, ret[0].bytesize, 'non-blocking should also grow'
ensure
s1.close
s2.close
end
def test_udp_read_truncation
s1 = Addrinfo.udp("127.0.0.1", 0).bind
s2 = s1.connect_address.connect
s2.send("a" * 100, 0)
ret = s1.read(10)
assert_equal "a" * 10, ret
s2.send("b" * 100, 0)
ret = s1.read(10)
assert_equal "b" * 10, ret
ensure
s1.close
s2.close
end
def test_udp_recv_truncation
s1 = Addrinfo.udp("127.0.0.1", 0).bind
s2 = s1.connect_address.connect
s2.send("a" * 100, 0)
ret = s1.recv(10, Socket::MSG_PEEK)
assert_equal "a" * 10, ret
ret = s1.recv(10, 0)
assert_equal "a" * 10, ret
s2.send("b" * 100, 0)
ret = s1.recv(10, 0)
assert_equal "b" * 10, ret
ensure
s1.close
s2.close
end
def test_udp_recvmsg_truncation
s1 = Addrinfo.udp("127.0.0.1", 0).bind
s2 = s1.connect_address.connect
s2.send("a" * 100, 0)
ret, addr, rflags = s1.recvmsg(10, Socket::MSG_PEEK)
assert_equal "a" * 10, ret
# AIX does not set MSG_TRUNC for a message partially read with MSG_PEEK.
assert_equal Socket::MSG_TRUNC, rflags & Socket::MSG_TRUNC if !rflags.nil? && /aix/ !~ RUBY_PLATFORM
ret, addr, rflags = s1.recvmsg(10, 0)
assert_equal "a" * 10, ret
assert_equal Socket::MSG_TRUNC, rflags & Socket::MSG_TRUNC if !rflags.nil?
s2.send("b" * 100, 0)
ret, addr, rflags = s1.recvmsg(10, 0)
assert_equal "b" * 10, ret
assert_equal Socket::MSG_TRUNC, rflags & Socket::MSG_TRUNC if !rflags.nil?
addr
ensure
s1.close
s2.close
end
def test_resolurion_error_error_code
begin
Socket.getaddrinfo("example.com", 80, "AF_UNIX")
rescue => e
assert_include([Socket::EAI_FAMILY, Socket::EAI_FAIL], e.error_code)
end
end
def test_tcp_socket_v6_hostname_resolved_earlier
opts = %w[-rsocket -W1]
assert_separately opts, <<~RUBY
begin
begin
# Verify that "localhost" can be resolved to an IPv6 address
Socket.getaddrinfo("localhost", 0, Socket::AF_INET6)
server = TCPServer.new("::1", 0)
rescue Socket::ResolutionError, Errno::EADDRNOTAVAIL # IPv6 is not supported
return
end
_, port, = server.addr
server_thread = Thread.new { server.accept }
Addrinfo.define_singleton_method(:getaddrinfo) do |_, _, family, *_|
case family
when Socket::AF_INET6 then [Addrinfo.tcp("::1", port)]
when Socket::AF_INET then sleep(10); [Addrinfo.tcp("127.0.0.1", port)]
end
end
socket = Socket.tcp("localhost", port)
assert_true(socket.remote_address.ipv6?)
ensure
server_thread&.value&.close
server&.close
socket&.close
end
RUBY
end
def test_tcp_socket_v4_hostname_resolved_earlier
opts = %w[-rsocket -W1]
assert_separately opts, <<~RUBY
begin
server = TCPServer.new("127.0.0.1", 0)
_, port, = server.addr
server_thread = Thread.new { server.accept }
Addrinfo.define_singleton_method(:getaddrinfo) do |_, _, family, *_|
case family
when Socket::AF_INET6 then sleep(10); [Addrinfo.tcp("::1", port)]
when Socket::AF_INET then [Addrinfo.tcp("127.0.0.1", port)]
end
end
socket = Socket.tcp("localhost", port)
assert_true(socket.remote_address.ipv4?)
ensure
server_thread&.value&.close
server&.close
socket&.close
end
RUBY
end
def test_tcp_socket_v6_hostname_resolved_in_resolution_delay
opts = %w[-rsocket -W1]
assert_separately opts, <<~RUBY
begin
begin
# Verify that "localhost" can be resolved to an IPv6 address
Socket.getaddrinfo("localhost", 0, Socket::AF_INET6)
server = TCPServer.new("::1", 0)
rescue Socket::ResolutionError, Errno::EADDRNOTAVAIL # IPv6 is not supported
return
end
_, port, = server.addr
server_thread = Thread.new { server.accept }
delay_time = 0.025 # Socket::RESOLUTION_DELAY (private) is 0.05
Addrinfo.define_singleton_method(:getaddrinfo) do |_, _, family, *_|
case family
when Socket::AF_INET6 then sleep(delay_time); [Addrinfo.tcp("::1", port)]
when Socket::AF_INET then [Addrinfo.tcp("127.0.0.1", port)]
end
end
socket = Socket.tcp("localhost", port)
assert_true(socket.remote_address.ipv6?)
ensure
server_thread&.value&.close
server&.close
socket&.close
end
RUBY
end
def test_tcp_socket_v6_hostname_resolved_earlier_and_v6_server_is_not_listening
opts = %w[-rsocket -W1]
assert_separately opts, <<~RUBY
begin
ipv4_address = "127.0.0.1"
server = Socket.new(Socket::AF_INET, :STREAM)
server.bind(Socket.pack_sockaddr_in(0, ipv4_address))
port = server.connect_address.ip_port
server_thread = Thread.new { server.listen(1); server.accept }
Addrinfo.define_singleton_method(:getaddrinfo) do |_, _, family, *_|
case family
when Socket::AF_INET6 then [Addrinfo.tcp("::1", port)]
when Socket::AF_INET then sleep(0.001); [Addrinfo.tcp(ipv4_address, port)]
end
end
socket = Socket.tcp("localhost", port)
assert_equal(ipv4_address, socket.remote_address.ip_address)
ensure
accepted, _ = server_thread&.value
accepted&.close
server&.close
socket&.close
end
RUBY
end
def test_tcp_socket_resolv_timeout
opts = %w[-rsocket -W1]
assert_separately opts, <<~RUBY
begin
server = TCPServer.new("localhost", 0)
_, port, = server.addr
Addrinfo.define_singleton_method(:getaddrinfo) { |*_| sleep }
assert_raise(Errno::ETIMEDOUT) do
Socket.tcp("localhost", port, resolv_timeout: 0.01)
end
ensure
server&.close
end
RUBY
end
def test_tcp_socket_resolv_timeout_with_connection_failure
opts = %w[-rsocket -W1]
assert_separately opts, <<~RUBY
server = TCPServer.new("127.0.0.1", 12345)
_, port, = server.addr
Addrinfo.define_singleton_method(:getaddrinfo) do |_, _, family, *_|
if family == Socket::AF_INET6
sleep
else
[Addrinfo.tcp("127.0.0.1", port)]
end
end
server.close
assert_raise(Errno::ETIMEDOUT) do
Socket.tcp("localhost", port, resolv_timeout: 0.01)
end
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
begin
begin
# Verify that "localhost" can be resolved to an IPv6 address
Socket.getaddrinfo("localhost", 0, Socket::AF_INET6)
server = TCPServer.new("::1", 0)
rescue Socket::ResolutionError, Errno::EADDRNOTAVAIL # IPv6 is not supported
return
end
_, port, = server.addr
server_thread = Thread.new { server.accept }
Addrinfo.define_singleton_method(:getaddrinfo) do |_, _, family, *_|
case family
when Socket::AF_INET6 then [Addrinfo.tcp("::1", port)]
when Socket::AF_INET then sleep(0.001); raise SocketError
end
end
socket = nil
assert_nothing_raised do
socket = Socket.tcp("localhost", port)
end
ensure
server_thread&.value&.close
server&.close
socket&.close
end
RUBY
end
def test_tcp_socket_all_hostname_resolution_failed
opts = %w[-rsocket -W1]
assert_separately opts, <<~RUBY
begin
server = TCPServer.new("localhost", 0)
_, port, = server.addr
Addrinfo.define_singleton_method(:getaddrinfo) do |_, _, family, *_|
case family
when Socket::AF_INET6 then raise SocketError
when Socket::AF_INET then sleep(0.001); raise SocketError, "Last hostname resolution error"
end
end
assert_raise_with_message(SocketError, "Last hostname resolution error") do
Socket.tcp("localhost", port)
end
ensure
server&.close
end
RUBY
end
def test_tcp_socket_hostname_resolution_failed_after_connection_failure
opts = %w[-rsocket -W1]
assert_separately opts, <<~RUBY
server = TCPServer.new("127.0.0.1", 0)
port = server.connect_address.ip_port
Addrinfo.define_singleton_method(:getaddrinfo) do |_, _, family, *_|
case family
when Socket::AF_INET6 then sleep(0.1); raise Socket::ResolutionError
when Socket::AF_INET then [Addrinfo.tcp("127.0.0.1", port)]
end
end
server.close
# SystemCallError is a workaround for Windows environment
assert_raise(Errno::ECONNREFUSED, SystemCallError) do
Socket.tcp("localhost", port)
end
RUBY
end
def test_tcp_socket_v6_address_passed
opts = %w[-rsocket -W1]
assert_separately opts, <<~RUBY
begin
begin
# Verify that "localhost" can be resolved to an IPv6 address
Socket.getaddrinfo("localhost", 0, Socket::AF_INET6)
server = TCPServer.new("::1", 0)
rescue Socket::ResolutionError, Errno::EADDRNOTAVAIL # IPv6 is not supported
return
end
_, port, = server.addr
server_thread = Thread.new { server.accept }
Addrinfo.define_singleton_method(:getaddrinfo) do |*_|
[Addrinfo.tcp("::1", port)]
end
socket = Socket.tcp("::1", port)
assert_true(socket.remote_address.ipv6?)
ensure
server_thread&.value&.close
server&.close
socket&.close
end
RUBY
end
def test_tcp_socket_fast_fallback_is_false
server = TCPServer.new("127.0.0.1", 0)
_, port, = server.addr
server_thread = Thread.new { server.accept }
socket = Socket.tcp("127.0.0.1", port, fast_fallback: false)
assert_true(socket.remote_address.ipv4?)
ensure
server_thread&.value&.close
server&.close
socket&.close
end
def test_tcp_fast_fallback
opts = %w[-rsocket -W1]
assert_separately opts, <<~RUBY
assert_true(Socket.tcp_fast_fallback)
Socket.tcp_fast_fallback = false
assert_false(Socket.tcp_fast_fallback)
Socket.tcp_fast_fallback = true
assert_true(Socket.tcp_fast_fallback)
RUBY
end
end if defined?(Socket)