Introduction of Happy Eyeballs Version 2 (RFC8305) in Socket.tcp (#9374)

* Introduction of Happy Eyeballs Version 2 (RFC8305) in Socket.tcp

This is an implementation of Happy Eyeballs version 2 (RFC 8305) in Socket.tcp.

[Background]
Currently, `Socket.tcp` synchronously resolves names and makes connection attempts with `Addrinfo::foreach.`
This implementation has the following two problems.

1. In name resolution, the program stops until the DNS server responds to all DNS queries.
2. In a connection attempt, while an IP address is trying to connect to the destination host and is taking time, the program stops, and other resolved IP addresses cannot try to connect.

[Proposal]
"Happy Eyeballs" ([RFC 8305](https://datatracker.ietf.org/doc/html/rfc8305)) is an algorithm to solve this kind of problem. It avoids delays to the user whenever possible and also uses IPv6 preferentially.

I implemented it into `Socket.tcp` by using `Addrinfo.getaddrinfo` in each thread spawned per address family to resolve the hostname asynchronously, and using `Socket::connect_nonblock` to try to connect with multiple addrinfo in parallel.

[Outcome]

This change eliminates a fatal defect in the following cases.

Case 1. One of the A or AAAA DNS queries does not return

---
require 'socket'

class Addrinfo
  class << self
    # Current Socket.tcp depends on foreach
    def foreach(nodename, service, family=nil, socktype=nil, protocol=nil, flags=nil, timeout: nil, &block)
      getaddrinfo(nodename, service, Socket::AF_INET6, socktype, protocol, flags, timeout: timeout)
        .concat(getaddrinfo(nodename, service, Socket::AF_INET, socktype, protocol, flags, timeout: timeout))
        .each(&block)
    end

    def getaddrinfo(_, _, family, *_)
      case family
      when Socket::AF_INET6 then sleep
      when Socket::AF_INET then [Addrinfo.tcp("127.0.0.1", 4567)]
      end
    end
  end
end

Socket.tcp("localhost", 4567)
---

Because the current `Socket.tcp` cannot resolve IPv6 names, the program stops in this case. It cannot start to connect with IPv4 address.
Though `Socket.tcp` with HEv2 can promptly start a connection attempt with IPv4 address in this case.

 Case 2. Server does not promptly return ack for syn of either IPv4 / IPv6 address family

---
require 'socket'

fork do
  socket = Socket.new(Socket::AF_INET6, :STREAM)
  socket.setsockopt(:SOCKET, :REUSEADDR, true)
  socket.bind(Socket.pack_sockaddr_in(4567, '::1'))
  sleep
  socket.listen(1)
  connection, _ = socket.accept
  connection.close
  socket.close
end

fork do
  socket = Socket.new(Socket::AF_INET, :STREAM)
  socket.setsockopt(:SOCKET, :REUSEADDR, true)
  socket.bind(Socket.pack_sockaddr_in(4567, '127.0.0.1'))
  socket.listen(1)
  connection, _ = socket.accept
  connection.close
  socket.close
end

Socket.tcp("localhost", 4567)
---

The current `Socket.tcp` tries to connect serially, so when its first name resolves an IPv6 address and initiates a connection to an IPv6 server, this server does not return an ACK, and the program stops.
Though `Socket.tcp` with HEv2 starts to connect sequentially and in parallel so a connection can be established promptly at the socket that attempted to connect to the IPv4 server.

In exchange, the performance of `Socket.tcp` with HEv2 will be degraded.

---
100.times { Socket.tcp("www.ruby-lang.org", 80) }
---

This is due to the addition of the creation of IO objects, Thread objects, etc., and calls to `IO::select` in the implementation.

* Avoid NameError of Socket::EAI_ADDRFAMILY in MinGW

* Support Windows with SO_CONNECT_TIME

* Improve performance

I have additionally implemented the following patterns:

- If the host is single-stack, name resolution is performed in the main thread. This reduces the cost of creating threads.
- If an IP address is specified, name resolution is performed in the main thread. This also reduces the cost of creating threads.
- If only one IP address is resolved, connect is executed in blocking mode. This reduces the cost of calling IO::select.

Also, I have added a fast_fallback option for users who wish not to use HE.
Here are the results of each performance test.

```ruby
require 'socket'
require 'benchmark'

HOSTNAME = "www.ruby-lang.org"
PORT = 80

ai = Addrinfo.tcp(HOSTNAME, PORT)

Benchmark.bmbm do |x|
  x.report("Domain name") do
    30.times { Socket.tcp(HOSTNAME, PORT).close }
  end

  x.report("IP Address") do
    30.times { Socket.tcp(ai.ip_address, PORT).close }
  end

  x.report("fast_fallback: false") do
    30.times { Socket.tcp(HOSTNAME, PORT, fast_fallback: false).close }
  end
end
```

```
                           user     system      total        real
Domain name            0.015567   0.032511   0.048078 (  0.325284)
IP Address             0.004458   0.014219   0.018677 (  0.284361)
fast_fallback: false   0.005869   0.021511   0.027380 (  0.321891)
````

And this is the measurement result when executed in a single stack environment.

```
                           user     system      total        real
Domain name            0.007062   0.019276   0.026338 (  1.905775)
IP Address             0.004527   0.012176   0.016703 (  3.051192)
fast_fallback: false   0.005546   0.019426   0.024972 (  1.775798)
```

The following is the result of the run on Ruby 3.3.0.

(on Dual stack environment)

```
                 user     system      total        real
Ruby 3.3.0   0.007271   0.027410   0.034681 (  0.472510)
```

(on Single stack environment)

```
                 user     system      total        real
Ruby 3.3.0  0.005353   0.018898   0.024251 (  1.774535)
```

* Do not cache `Socket.ip_address_list`

As mentioned in the comment at https://github.com/ruby/ruby/pull/9374#discussion_r1482269186, caching Socket.ip_address_list does not follow changes in network configuration.
But if we stop caching, it becomes necessary to check every time `Socket.tcp` is called whether it's a single stack or not, which could further degrade performance in the case of a dual stack.
From this, I've changed the approach so that when a domain name is passed, it doesn't check whether it's a single stack or not and resolves names in parallel each time.

The performance measurement results are as follows.

require 'socket'
require 'benchmark'

HOSTNAME = "www.ruby-lang.org"
PORT = 80

ai = Addrinfo.tcp(HOSTNAME, PORT)

Benchmark.bmbm do |x|
  x.report("Domain name") do
    30.times { Socket.tcp(HOSTNAME, PORT).close }
  end

  x.report("IP Address") do
    30.times { Socket.tcp(ai.ip_address, PORT).close }
  end

  x.report("fast_fallback: false") do
    30.times { Socket.tcp(HOSTNAME, PORT, fast_fallback: false).close }
  end
end

                           user     system      total        real
Domain name            0.004085   0.011873   0.015958 (  0.330097)
IP Address             0.000993   0.004400   0.005393 (  0.257286)
fast_fallback: false   0.001348   0.008266   0.009614 (  0.298626)

* Wait forever if fallback addresses are unresolved, unless resolv_timeout

Changed from waiting only 3 seconds for name resolution when there is no fallback address available, to waiting as long as there is no resolv_timeout.
This is in accordance with the current `Socket.tcp` specification.

* Use exact pattern to match IPv6 address format for specify address family
This commit is contained in:
Misaki Shioi 2024-02-26 12:14:11 +09:00 коммит произвёл GitHub
Родитель 616b414621
Коммит 9ec342e07d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
5 изменённых файлов: 752 добавлений и 2 удалений

Просмотреть файл

@ -599,6 +599,30 @@ class Socket < BasicSocket
__accept_nonblock(exception)
end
RESOLUTION_DELAY = 0.05
private_constant :RESOLUTION_DELAY
CONNECTION_ATTEMPT_DELAY = 0.25
private_constant :CONNECTION_ATTEMPT_DELAY
ADDRESS_FAMILIES = {
ipv6: Socket::AF_INET6,
ipv4: Socket::AF_INET
}.freeze
private_constant :ADDRESS_FAMILIES
HOSTNAME_RESOLUTION_QUEUE_UPDATED = 0
private_constant :HOSTNAME_RESOLUTION_QUEUE_UPDATED
IPV6_ADRESS_FORMAT = /(?i)(?:(?:[0-9A-F]{1,4}:){7}(?:[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){6}(?:[0-9A-F]{1,4}::[0-9A-F]{1,4}|:(?:[0-9A-F]{1,4}:){1,5}[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){5}(?::[0-9A-F]{1,4}::[0-9A-F]{1,4}|:(?:[0-9A-F]{1,4}:){1,4}[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){4}(?::[0-9A-F]{1,4}::[0-9A-F]{1,4}|:(?:[0-9A-F]{1,4}:){1,3}[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){3}(?::[0-9A-F]{1,4}::[0-9A-F]{1,4}|:(?:[0-9A-F]{1,4}:){1,2}[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){2}(?::[0-9A-F]{1,4}::[0-9A-F]{1,4}|:(?:[0-9A-F]{1,4}:)[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){1}(?::[0-9A-F]{1,4}::[0-9A-F]{1,4}|::(?:[0-9A-F]{1,4}:){1,5}[0-9A-F]{1,4}|:)|::(?:[0-9A-F]{1,4}::[0-9A-F]{1,4}|:(?:[0-9A-F]{1,4}:){1,6}[0-9A-F]{1,4}|:))(?:%.+)?/
private_constant :IPV6_ADRESS_FORMAT
@tcp_fast_fallback = true
class << self
attr_accessor :tcp_fast_fallback
end
# :call-seq:
# Socket.tcp(host, port, local_host=nil, local_port=nil, [opts]) {|socket| ... }
# Socket.tcp(host, port, local_host=nil, local_port=nil, [opts])
@ -624,8 +648,491 @@ class Socket < BasicSocket
# sock.close_write
# puts sock.read
# }
#
def self.tcp(host, port, local_host = nil, local_port = nil, connect_timeout: nil, resolv_timeout: nil) # :yield: socket
def self.tcp(host, port, local_host = nil, local_port = nil, connect_timeout: nil, resolv_timeout: nil, fast_fallback: tcp_fast_fallback, &block) # :yield: socket
unless fast_fallback
return tcp_without_fast_fallback(host, port, local_host, local_port, connect_timeout:, resolv_timeout:, &block)
end
# Happy Eyeballs' states
# - :start
# - :v6c
# - :v4w
# - :v4c
# - :v46c
# - :v46w
# - :success
# - :failure
# - :timeout
specified_family_name = nil
hostname_resolution_threads = []
hostname_resolution_queue = nil
hostname_resolution_waiting = nil
hostname_resolution_expires_at = nil
selectable_addrinfos = SelectableAddrinfos.new
connecting_sockets = ConnectingSockets.new
local_addrinfos = []
connection_attempt_delay_expires_at = nil
connection_attempt_started_at = nil
state = :start
connected_socket = nil
last_error = nil
is_windows_environment ||= (RUBY_PLATFORM =~ /mswin|mingw|cygwin/)
ret = loop do
case state
when :start
specified_family_name, next_state = host && specified_family_name_and_next_state(host)
if local_host && local_port
specified_family_name, next_state = specified_family_name_and_next_state(local_host) unless specified_family_name
local_addrinfos = Addrinfo.getaddrinfo(local_host, local_port, ADDRESS_FAMILIES[specified_family_name], :STREAM, timeout: resolv_timeout)
end
if specified_family_name
addrinfos = Addrinfo.getaddrinfo(host, port, ADDRESS_FAMILIES[specified_family_name], :STREAM, timeout: resolv_timeout)
selectable_addrinfos.add(specified_family_name, addrinfos)
hostname_resolution_queue = NoHostnameResolutionQueue.new
state = next_state
next
end
resolving_family_names = ADDRESS_FAMILIES.keys
hostname_resolution_queue = HostnameResolutionQueue.new(resolving_family_names.size)
hostname_resolution_waiting = hostname_resolution_queue.waiting_pipe
hostname_resolution_started_at = current_clocktime if resolv_timeout
hostname_resolution_args = [host, port, hostname_resolution_queue]
hostname_resolution_threads.concat(
resolving_family_names.map { |family|
thread_args = [family, *hostname_resolution_args]
thread = Thread.new(*thread_args) { |*thread_args| hostname_resolution(*thread_args) }
Thread.pass
thread
}
)
hostname_resolution_retry_count = resolving_family_names.size - 1
hostname_resolution_expires_at = hostname_resolution_started_at + resolv_timeout if resolv_timeout
while hostname_resolution_retry_count >= 0
remaining = resolv_timeout ? second_to_timeout(hostname_resolution_started_at + resolv_timeout) : nil
hostname_resolved, _, = IO.select(hostname_resolution_waiting, nil, nil, remaining)
unless hostname_resolved
state = :timeout
break
end
family_name, res = hostname_resolution_queue.get
if res.is_a? Exception
unless (Socket.const_defined?(:EAI_ADDRFAMILY)) &&
(res.is_a?(Socket::ResolutionError)) &&
(res.error_code == Socket::EAI_ADDRFAMILY)
last_error = res
end
if hostname_resolution_retry_count.zero?
state = :failure
break
end
hostname_resolution_retry_count -= 1
else
state = case family_name
when :ipv6 then :v6c
when :ipv4 then hostname_resolution_queue.closed? ? :v4c : :v4w
end
selectable_addrinfos.add(family_name, res)
last_error = nil
break
end
end
next
when :v4w
ipv6_resolved, _, = IO.select(hostname_resolution_waiting, nil, nil, RESOLUTION_DELAY)
if ipv6_resolved
family_name, res = hostname_resolution_queue.get
selectable_addrinfos.add(family_name, res) unless res.is_a? Exception
state = :v46c
else
state = :v4c
end
next
when :v4c, :v6c, :v46c
connection_attempt_started_at = current_clocktime unless connection_attempt_started_at
addrinfo = selectable_addrinfos.get
if local_addrinfos.any?
local_addrinfo = local_addrinfos.find { |lai| lai.afamily == addrinfo.afamily }
if local_addrinfo.nil?
if selectable_addrinfos.empty? && connecting_sockets.empty? && hostname_resolution_queue.closed?
last_error = SocketError.new 'no appropriate local address'
state = :failure
elsif selectable_addrinfos.any?
# Try other Addrinfo in next loop
else
if resolv_timeout && hostname_resolution_queue.opened? &&
(current_clocktime >= hostname_resolution_expires_at)
state = :timeout
else
# Wait for connection to be established or hostname resolution in next loop
connection_attempt_delay_expires_at = nil
state = :v46w
end
end
next
end
end
connection_attempt_delay_expires_at = current_clocktime + CONNECTION_ATTEMPT_DELAY
begin
result = if specified_family_name && selectable_addrinfos.empty? &&
connecting_sockets.empty? && hostname_resolution_queue.closed?
local_addrinfo ?
addrinfo.connect_from(local_addrinfo, timeout: connect_timeout) :
addrinfo.connect(timeout: connect_timeout)
else
socket = Socket.new(addrinfo.pfamily, addrinfo.socktype, addrinfo.protocol)
socket.bind(local_addrinfo) if local_addrinfo
socket.connect_nonblock(addrinfo, exception: false)
end
case result
when 0
connected_socket = socket
state = :success
when Socket
connected_socket = result
state = :success
when :wait_writable
connecting_sockets.add(socket, addrinfo)
state = :v46w
end
rescue SystemCallError => e
last_error = e
socket.close if socket && !socket.closed?
if selectable_addrinfos.empty? && connecting_sockets.empty? && hostname_resolution_queue.closed?
state = :failure
elsif selectable_addrinfos.any?
# Try other Addrinfo in next loop
else
if resolv_timeout && hostname_resolution_queue.opened? &&
(current_clocktime >= hostname_resolution_expires_at)
state = :timeout
else
# Wait for connection to be established or hostname resolution in next loop
connection_attempt_delay_expires_at = nil
state = :v46w
end
end
end
next
when :v46w
if connect_timeout && second_to_timeout(connection_attempt_started_at + connect_timeout).zero?
state = :timeout
next
end
remaining = second_to_timeout(connection_attempt_delay_expires_at)
hostname_resolution_waiting = hostname_resolution_waiting && hostname_resolution_queue.closed? ? nil : hostname_resolution_waiting
hostname_resolved, connectable_sockets, = IO.select(hostname_resolution_waiting, connecting_sockets.all, nil, remaining)
if connectable_sockets&.any?
while (connectable_socket = connectable_sockets.pop)
is_connected =
if is_windows_environment
sockopt = connectable_socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_CONNECT_TIME)
sockopt.unpack('i').first >= 0
else
sockopt = connectable_socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_ERROR)
sockopt.int.zero?
end
if is_connected
connected_socket = connectable_socket
connecting_sockets.delete connectable_socket
connectable_sockets.each do |other_connectable_socket|
other_connectable_socket.close unless other_connectable_socket.closed?
end
state = :success
break
else
failed_ai = connecting_sockets.delete connectable_socket
inspected_ip_address = failed_ai.ipv6? ? "[#{failed_ai.ip_address}]" : failed_ai.ip_address
last_error = SystemCallError.new("connect(2) for #{inspected_ip_address}:#{failed_ai.ip_port}", sockopt.int)
connectable_socket.close unless connectable_socket.closed?
next if connectable_sockets.any?
if selectable_addrinfos.empty? && connecting_sockets.empty? && hostname_resolution_queue.closed?
state = :failure
elsif selectable_addrinfos.any?
# Wait for connection attempt delay timeout in next loop
else
if resolv_timeout && hostname_resolution_queue.opened? &&
(current_clocktime >= hostname_resolution_expires_at)
state = :timeout
else
# Wait for connection to be established or hostname resolution in next loop
connection_attempt_delay_expires_at = nil
end
end
end
end
elsif hostname_resolved&.any?
family_name, res = hostname_resolution_queue.get
selectable_addrinfos.add(family_name, res) unless res.is_a? Exception
else
if selectable_addrinfos.empty? && connecting_sockets.empty? && hostname_resolution_queue.closed?
state = :failure
elsif selectable_addrinfos.any?
# Try other Addrinfo in next loop
state = :v46c
else
if resolv_timeout && hostname_resolution_queue.opened? &&
(current_clocktime >= hostname_resolution_expires_at)
state = :timeout
else
# Wait for connection to be established or hostname resolution in next loop
connection_attempt_delay_expires_at = nil
end
end
end
next
when :success
break connected_socket
when :failure
raise last_error
when :timeout
raise Errno::ETIMEDOUT, 'user specified timeout'
end
end
if block_given?
begin
yield ret
ensure
ret.close
end
else
ret
end
ensure
if fast_fallback
hostname_resolution_threads.each do |thread|
thread&.exit
end
hostname_resolution_queue&.close_all
connecting_sockets.each do |connecting_socket|
connecting_socket.close unless connecting_socket.closed?
end
end
end
def self.specified_family_name_and_next_state(hostname)
if hostname.match?(IPV6_ADRESS_FORMAT) then [:ipv6, :v6c]
elsif hostname.match?(/^([0-9]{1,3}\.){3}[0-9]{1,3}$/) then [:ipv4, :v4c]
end
end
private_class_method :specified_family_name_and_next_state
def self.hostname_resolution(family, host, port, hostname_resolution_queue)
begin
resolved_addrinfos = Addrinfo.getaddrinfo(host, port, ADDRESS_FAMILIES[family], :STREAM)
hostname_resolution_queue.add_resolved(family, resolved_addrinfos)
rescue => e
hostname_resolution_queue.add_error(family, e)
end
end
private_class_method :hostname_resolution
def self.second_to_timeout(ends_at)
return 0 unless ends_at
remaining = (ends_at - current_clocktime)
remaining.negative? ? 0 : remaining
end
private_class_method :second_to_timeout
def self.current_clocktime
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
private_class_method :current_clocktime
class SelectableAddrinfos
PRIORITY_ON_V6 = [:ipv6, :ipv4]
PRIORITY_ON_V4 = [:ipv4, :ipv6]
def initialize
@addrinfo_dict = {}
@last_family = nil
end
def add(family_name, addrinfos)
@addrinfo_dict[family_name] = addrinfos
end
def get
return nil if empty?
if @addrinfo_dict.size == 1
@addrinfo_dict.each { |_, addrinfos| return addrinfos.shift }
end
precedences = case @last_family
when :ipv4, nil then PRIORITY_ON_V6
when :ipv6 then PRIORITY_ON_V4
end
precedences.each do |family_name|
addrinfo = @addrinfo_dict[family_name]&.shift
next unless addrinfo
@last_family = family_name
return addrinfo
end
end
def empty?
@addrinfo_dict.all? { |_, addrinfos| addrinfos.empty? }
end
def any?
!empty?
end
end
private_constant :SelectableAddrinfos
class NoHostnameResolutionQueue
def waiting_pipe
nil
end
def add_resolved(_, _)
raise StandardError, "This #{self.class} cannot respond to:"
end
def add_error(_, _)
raise StandardError, "This #{self.class} cannot respond to:"
end
def get
nil
end
def opened?
false
end
def closed?
true
end
def close_all
# Do nothing
end
end
private_constant :NoHostnameResolutionQueue
class HostnameResolutionQueue
def initialize(size)
@size = size
@taken_count = 0
@rpipe, @wpipe = IO.pipe
@queue = Queue.new
@mutex = Mutex.new
end
def waiting_pipe
[@rpipe]
end
def add_resolved(family, resolved_addrinfos)
@mutex.synchronize do
@queue.push [family, resolved_addrinfos]
@wpipe.putc HOSTNAME_RESOLUTION_QUEUE_UPDATED
end
end
def add_error(family, error)
@mutex.synchronize do
@queue.push [family, error]
@wpipe.putc HOSTNAME_RESOLUTION_QUEUE_UPDATED
end
end
def get
return nil if @queue.empty?
res = nil
@mutex.synchronize do
@rpipe.getbyte
res = @queue.pop
end
@taken_count += 1
close_all if @taken_count == @size
res
end
def closed?
@rpipe.closed?
end
def opened?
!closed?
end
def close_all
@queue.close unless @queue.closed?
@rpipe.close unless @rpipe.closed?
@wpipe.close unless @wpipe.closed?
end
end
private_constant :HostnameResolutionQueue
class ConnectingSockets
def initialize
@socket_dict = {}
end
def all
@socket_dict.keys
end
def add(socket, addrinfo)
@socket_dict[socket] = addrinfo
end
def delete(socket)
@socket_dict.delete socket
end
def empty?
@socket_dict.empty?
end
def each
@socket_dict.keys.each do |socket|
yield socket
end
end
end
private_constant :ConnectingSockets
def self.tcp_without_fast_fallback(host, port, local_host, local_port, connect_timeout:, resolv_timeout:, &block)
last_error = nil
ret = nil
@ -669,6 +1176,7 @@ class Socket < BasicSocket
ret
end
end
private_class_method :tcp_without_fast_fallback
# :stopdoc:
def self.ip_sockets_port0(ai_list, reuseaddr)

Просмотреть файл

@ -669,6 +669,7 @@ SO_SETFIB nil Set the associated routing table for the socket (FreeBSD
SO_RTABLE nil Set the routing table for this socket (OpenBSD)
SO_INCOMING_CPU nil Receive the cpu attached to the socket (Linux 3.19)
SO_INCOMING_NAPI_ID nil Receive the napi ID attached to a RX queue (Linux 4.12)
SO_CONNECT_TIME nil Returns the number of seconds a socket has been connected. This option is only valid for connection-oriented protocols (Windows)
SOPRI_INTERACTIVE nil Interactive socket priority
SOPRI_NORMAL nil Normal socket priority

Просмотреть файл

@ -35,6 +35,7 @@
#ifdef _WIN32
# include <winsock2.h>
# include <ws2tcpip.h>
# include <mswsock.h>
# include <iphlpapi.h>
# if defined(_MSC_VER)
# undef HAVE_TYPE_STRUCT_SOCKADDR_DL

Просмотреть файл

@ -35,6 +35,7 @@ extern "C++" { /* template without extern "C++" */
#endif
#include <winsock2.h>
#include <ws2tcpip.h>
#include <mswsock.h>
#if !defined(_MSC_VER) || _MSC_VER >= 1400
#include <iphlpapi.h>
#endif

Просмотреть файл

@ -778,4 +778,243 @@ class TestSocket < Test::Unit::TestCase
end
end
def test_tcp_socket_v6_hostname_resolved_earlier
opts = %w[-rsocket -W1]
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
begin;
begin
server = TCPServer.new("::1", 0)
rescue Errno::EADDRNOTAVAIL # IPv6 is not supported
exit
end
server_thread = Thread.new { server.accept }
port = server.addr[1]
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?)
server_thread.value.close
server.close
socket.close if socket && !socket.closed?
end;
end
def test_tcp_socket_v4_hostname_resolved_earlier
opts = %w[-rsocket -W1]
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
begin;
server = TCPServer.new("127.0.0.1", 0)
port = server.addr[1]
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
server_thread = Thread.new { server.accept }
socket = Socket.tcp("localhost", port)
assert_true(socket.remote_address.ipv4?)
server_thread.value.close
server.close
socket.close if socket && !socket.closed?
end;
end
def test_tcp_socket_v6_hostname_resolved_in_resolution_delay
opts = %w[-rsocket -W1]
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
begin;
begin
server = TCPServer.new("::1", 0)
rescue Errno::EADDRNOTAVAIL # IPv6 is not supported
exit
end
port = server.addr[1]
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
server_thread = Thread.new { server.accept }
socket = Socket.tcp("localhost", port)
assert_true(socket.remote_address.ipv6?)
server_thread.value.close
server.close
socket.close if socket && !socket.closed?
end;
end
def test_tcp_socket_v6_hostname_resolved_earlier_and_v6_server_is_not_listening
opts = %w[-rsocket -W1]
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
begin;
ipv4_address = "127.0.0.1"
ipv4_server = Socket.new(Socket::AF_INET, :STREAM)
ipv4_server.bind(Socket.pack_sockaddr_in(0, ipv4_address))
port = ipv4_server.connect_address.ip_port
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
ipv4_server_thread = Thread.new { ipv4_server.listen(1); ipv4_server.accept }
socket = Socket.tcp("localhost", port)
assert_equal(ipv4_address, socket.remote_address.ip_address)
accepted, _ = ipv4_server_thread.value
accepted.close
ipv4_server.close
socket.close if socket && !socket.closed?
end;
end
def test_tcp_socket_resolv_timeout
opts = %w[-rsocket -W1]
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
begin;
Addrinfo.define_singleton_method(:getaddrinfo) { |*_| sleep }
port = TCPServer.new("localhost", 0).addr[1]
assert_raise(Errno::ETIMEDOUT) do
Socket.tcp("localhost", port, resolv_timeout: 0.01)
end
end;
end
def test_tcp_socket_resolv_timeout_with_connection_failure
opts = %w[-rsocket -W1]
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
begin;
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
end;
end
def test_tcp_socket_one_hostname_resolution_succeeded_at_least
opts = %w[-rsocket -W1]
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
begin;
begin
server = TCPServer.new("::1", 0)
rescue Errno::EADDRNOTAVAIL # IPv6 is not supported
exit
end
port = server.addr[1]
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
server_thread = Thread.new { server.accept }
socket = nil
assert_nothing_raised do
socket = Socket.tcp("localhost", port)
end
server_thread.value.close
server.close
socket.close if socket && !socket.closed?
end;
end
def test_tcp_socket_all_hostname_resolution_failed
opts = %w[-rsocket -W1]
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
begin;
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
port = TCPServer.new("localhost", 0).addr[1]
assert_raise_with_message(SocketError, "Last hostname resolution error") do
Socket.tcp("localhost", port)
end
end;
end
def test_tcp_socket_v6_address_passed
opts = %w[-rsocket -W1]
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
begin;
begin
server = TCPServer.new("::1", 0)
rescue Errno::EADDRNOTAVAIL # IPv6 is not supported
exit
end
_, port, = server.addr
Addrinfo.define_singleton_method(:getaddrinfo) do |*_|
[Addrinfo.tcp("::1", port)]
end
server_thread = Thread.new { server.accept }
socket = Socket.tcp("::1", port)
assert_true(socket.remote_address.ipv6?)
server_thread.value.close
server.close
socket.close if socket && !socket.closed?
end;
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?)
server_thread.value.close
server.close
socket.close if socket && !socket.closed?
end
end if defined?(Socket)