From: "jeremyevans0 (Jeremy Evans) via ruby-core" Date: 2024-01-17T17:30:36+00:00 Subject: [ruby-core:116279] [Ruby master Feature#20108] Introduction of Happy Eyeballs Version 2 (RFC8305) in Socket.tcp Issue #20108 has been updated by jeremyevans0 (Jeremy Evans). If you are running a dual-stack network setup, it seems reasonable to have this enabled by default. However, if you are running a pure-IPv4 or pure-IPv6 network, this adds a lot of overhead for no benefit. I think three changes should be made if you want to enable this by default: 1) If only one address family is used, Socket.tcp should use the previous implementation (the pull request uses the Happy Eyeballs implementation even if only one address family is used). 2) Currently in the pull request, if you provide a `local_host` and `local_port`, it can determine which address families are in use, which would allow it to determine if there is only one. However, the pull request currently assumes both IPv4 and IPv6 if `local_host` and `local_port` are not provided. As few calls to `Socket.tcp` set `local_host` and `local_port`, on non-dual-stack hosts, this assumption is going to be wrong most of the time. You should add a way to determine which address families could be used even if `local_host` and `local_port` are not set. Potentially, using `Socket.ip_address_list` and filtering out loopback and link-local addresses could be used to determine if the machine is actually dual-stack. 3) There should be a way to disable this and use the previous implementation, for users who do not want it. Until those changes are made, I think this should be opt-in. ---------------------------------------- Feature #20108: Introduction of Happy Eyeballs Version 2 (RFC8305) in Socket.tcp https://bugs.ruby-lang.org/issues/20108#change-106301 * 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/