From 00cb12bd9d30738b117dbc1d6ee2ea6307fb2a5a Mon Sep 17 00:00:00 2001 From: Daisuke Aritomo Date: Mon, 14 Jul 2025 23:08:16 +0900 Subject: [PATCH] Replace Timeout.timeout with Socket.tcp(..., open_timeout:) This patch replaces the implementation of #open_timeout from Timeout.timeout from the builtin timeout in Socket.tcp, which was introduced in Ruby 3.5 (https://bugs.ruby-lang.org/issues/21347). The builtin timeout in Socket.tcp is better in several ways. First, it does not rely on a separate Ruby Thread for monitoring Timeout (which is what the timeout library internally does). Also, it is compatible with Ractors, since it does not rely on Mutexes (which is also what the timeout library does). This change allows the following code to work. require 'net/http' Ractor.new { uri = URI('http://example.com/') http = Net::HTTP.new(uri.host, uri.port) http.open_timeout = 1 http.get(uri.path) }.value --- lib/net/http.rb | 20 +++++++++++++------- test/net/http/test_http.rb | 4 ++-- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/net/http.rb b/lib/net/http.rb index f64f7ba..1858404 100644 --- a/lib/net/http.rb +++ b/lib/net/http.rb @@ -1654,14 +1654,20 @@ def connect end debug "opening connection to #{conn_addr}:#{conn_port}..." - s = Timeout.timeout(@open_timeout, Net::OpenTimeout) { - begin - TCPSocket.open(conn_addr, conn_port, @local_host, @local_port) - rescue => e - raise e, "Failed to open TCP connection to " + - "#{conn_addr}:#{conn_port} (#{e.message})" + begin + # Use built-in timeout in Socket.tcp if available + s = if Socket.method(:tcp).parameters.any? {|param| param[0] == :key && param[1] == :open_timeout } + Socket.tcp(conn_addr, conn_port, @local_host, @local_port, open_timeout: @open_timeout) + else + Timeout.timeout(@open_timeout, Net::OpenTimeout) { + TCPSocket.open(conn_addr, conn_port, @local_host, @local_port) + } end - } + rescue => e + e = Net::OpenTimeout.new(e) if e.is_a?(Errno::ETIMEDOUT) # for compatibility with previous versions + raise e, "Failed to open TCP connection to " + + "#{conn_addr}:#{conn_port} (#{e.message})" + end s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) debug "opened" if use_ssl? diff --git a/test/net/http/test_http.rb b/test/net/http/test_http.rb index 366b4cd..af05ff9 100644 --- a/test/net/http/test_http.rb +++ b/test/net/http/test_http.rb @@ -246,8 +246,8 @@ def test_failure_message_includes_failed_domain_and_port # hostname to be included in the error message host = Struct.new(:to_s).new("") port = 2119 - # hack to let TCPSocket.open fail - def host.to_str; raise SocketError, "open failure"; end + # hack to let Socket.tcp fail + def host.match?(_); raise SocketError, "open failure"; end uri = Struct.new(:scheme, :hostname, :port).new("http", host, port) assert_raise_with_message(SocketError, /#{host}:#{port}/) do TestNetHTTPUtils.clean_http_proxy_env{ Net::HTTP.get(uri) }