Project

General

Profile

Actions

Feature #20108

closed

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

Added by shioimm (Misaki Shioi) 5 months ago. Updated 3 months ago.

Status:
Closed
Assignee:
-
Target version:
-
[ruby-core:115985]

Description

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 hostname 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) 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.

See https://github.com/ruby/ruby/pull/9374

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) }
# Socket.tcp (Before) 0.123809
# Socket.tcp (After)  0.224684

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


Related issues 2 (0 open2 closed)

Related to Ruby master - Feature #17525: Implement Happy Eyeballs Version 2 (RFC8305) in Socket.tcpClosedGlass_saga (Masaki Matsushita)Actions
Related to Ruby master - Feature #15628: init_inetsock_internal should fallback to IPv4 if IPv6 is unreachableClosedGlass_saga (Masaki Matsushita)Actions
Actions

Also available in: Atom PDF

Like0
Like0Like0Like1Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0