mirror of
https://github.com/ruby/ruby.git
synced 2025-08-15 13:39:04 +02:00

* 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
1099 lines
31 KiB
Ruby
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)
|