From: "shioimm (Misaki Shioi) via ruby-core" Date: 2024-01-17T13:29:13+00:00 Subject: [ruby-core:116274] [Ruby master Feature#20108] Introduction of Happy Eyeballs Version 2 (RFC8305) in Socket.tcp Issue #20108 has been updated by shioimm (Misaki Shioi). shugo (Shugo Maeda) wrote in #note-1: > Is there no way to disable Happy Eyeballs? > I'm not sure, but it may have more impact in local networks. There is no way to disable it; HE is intended to avoid fatal delays. Introducing a way to disable it for performance seems to me to add complexity. This is the result of 100 runs on the local network. Before ``` user system total real 0.002695 0.010630 0.013325 ( 0.026457) ``` After ``` user system total real 0.009211 0.024623 0.033834 ( 0.034990) ``` However, in the case of passing an resolved IP address as the first argument of `Socket.tcp`, the overhead could be reduced. I will try to implement this. Thank you. ---------------------------------------- Feature #20108: Introduction of Happy Eyeballs Version 2 (RFC8305) in Socket.tcp https://bugs.ruby-lang.org/issues/20108#change-106296 * Author: shioimm (Misaki Shioi) * Status: Open * Priority: Normal ---------------------------------------- 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](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. 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 ```ruby 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 ```ruby 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. -- https://bugs.ruby-lang.org/ ______________________________________________ ruby-core mailing list -- ruby-core@ml.ruby-lang.org To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org ruby-core info -- https://ml.ruby-lang.org/mailman3/postorius/lists/ruby-core.ml.ruby-lang.org/