Happy Eyeballs 버전 2 알고리즘은 IPv6와 IPv4의 이름 해결을 동시에 시작하고, 먼저 완료된 주소 중 하나로 연결을 시도합니다. 이때, IPv6 연결을 우선시하기 위해 IPv4 이름 해결이 먼저 완료되더라도 50밀리초(Resolution Delay) 동안 IPv6 해결을 기다립니다. 연결 시도 후 250밀리초(Connection Attempt Delay) 내에 연결이 확립되지 않으면 다른 주소로 두 번째 연결을 시도하는 방식으로 진행됩니다. 성공적인 연결이 이루어지면 다른 모든 시도는 취소됩니다.
Socket::TCP
에 이 알고리즘을 구현하는 초기 단계에서는 상태 기반의 코드를 사용했으나, 이는 의도치 않은 중복 처리를 야기할 수 있었습니다. 예를 들어, IPv6와 IPv4 이름 해결이 동시에 완료될 경우 불필요한 IO.select
호출이 발생하는 문제점이 발견되었습니다. 이를 개선하기 위해 상태 관리를 제거하고, 매 루프마다 가능한 모든 처리를 조건부로 실행하는 방식으로 재구현하여 코드의 복잡성을 줄이고 효율성을 높였습니다.
TCPSocket.new
는 C 언어로 구현되어 있어 Happy Eyeballs를 도입하는 데 추가적인 난관이 있었습니다. 특히, 이름 해결 결과를 파이프와 select
시스템 콜을 통해 기다릴 수 있는 내부 함수가 없다는 점이 큰 문제였습니다. Ruby 3.3부터 pthread
라이브러리를 활용하여 getaddrinfo
함수가 실행 중에 중단될 수 있도록 개선되었지만, 기존의 rb_getaddrinfo
함수는 Happy Eyeballs의 병렬 처리 요구사항(주소 패밀리별 스레드, 조건 변수 대신 파이프/select
사용)을 충족하지 못했습니다. 이에 Init_fast_fallback_inet_sock_internal
이라는 새로운 C 함수를 도입하여 주소 패밀리별로 별도의 스레드를 생성하고, select
시스템 콜을 사용하여 이름 해결 및 연결 완료를 기다리도록 설계했습니다.
개발 과정에서는 여러 중요한 이슈가 발생했습니다. 첫째, net/http
테스트에서 ConnectionAttemptDelay
로 인해 쓰기 타임아웃 대신 연결 타임아웃이 발생하는 문제가 있었습니다. 이는 IPv6 연결 시도가 실패한 후 IPv4로 폴백해야 할 때 ConnectionAttemptDelay
(250ms)가 테스트의 연결 타임아웃 (100ms)보다 길어 발생한 것이었습니다. 이 문제는 이전의 모든 연결 시도가 실패했을 경우 connection_attempt_delay_expires_at
변수를 즉시 리셋하여 다음 연결 시도를 즉각적으로 시작할 수 있도록 수정함으로써 해결되었습니다. 둘째, TCPSocket.new
가 FD_SETSIZE
(파일 디스크립터 최대 개수)를 초과하는 파일 디스크립터를 열 때 세그먼트 폴트가 발생하는 문제가 발생했습니다. 이는 select
시스템 콜이 FD_SETSIZE
이상의 FD 번호를 모니터링할 수 없다는 man
페이지의 경고와 일치하는 현상이었습니다. 이 문제를 해결하기 위해 select
대신 Ruby의 내부 함수인 rb_fd_select
를 사용했습니다. rb_fd_select
는 FD 세트의 비트맵 영역을 동적으로 할당하여 FD_SETSIZE
제한을 우회할 수 있도록 설계되어 있어, 현대 운영체제에서 더 큰 FD를 처리할 수 있게 합니다.
최종적으로 Happy Eyeballs 기능은 메서드 인자, 소켓 클래스 접근자, 환경 변수를 통해 활성화/비활성화를 제어할 수 있도록 제공되며, 기본적으로는 활성화된 상태로 배포되었습니다.