From: Yui NARUSE Date: 2011-12-22T18:49:17+09:00 Subject: [ruby-dev:45030] [ruby-trunk - Bug #5790][Open] net/http の EOFError と Keep-Alive Issue #5790 has been reported by Yui NARUSE. ---------------------------------------- Bug #5790: net/http の EOFError と Keep-Alive https://bugs.ruby-lang.org/issues/5790 Author: Yui NARUSE Status: Open Priority: Normal Assignee: Category: Target version: 2.0.0 ruby -v: ruby 2.0.0dev (2011-12-21 trunk 34086) [x86_64-freebsd9.0] [ruby-dev:39421] がずっと心に残っていたので、思い立って調べてみたので、 (正確には自分が高頻度で踏むようになったので調べてみた) その調査結果と対策案を提案します。 まず、投げられる原因ですが、根本的な原因は Keep-Alive のタイムアウトです。 HTTP/1.1 ではデフォルトで持続的接続を行うので、複数回のリクエストに渡って 一つの socket が使い回されます。 しかし、リクエスト同士で時間が開いていると、サーバー側でタイムアウトする 可能性があります。この時にクライアント側の read(2) が 0 を返す、 つまり EOFError となることがあります。 HTTP/1.1 は、冪等なメソッドの場合には確認なしにリトライすべきと言っているので、 そのようにするパッチを添付します。 冪等でないメソッドの場合にどうするべきかは悩ましいところです。 http://tools.ietf.org/html/rfc2616#section-8.1.4 http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-17#section-6.1.5 http://www.studyinghttp.net/connections なお、この Keep-Alive における Timeout は、 Apache の場合、FreeBSD ports や pkgsrc では 5 秒、 Debian Packages や RPM では 15 秒でした。 diff --git a/lib/net/http.rb b/lib/net/http.rb index 879cfe0..13bd1a7 100644 --- a/lib/net/http.rb +++ b/lib/net/http.rb @@ -1332,7 +1332,10 @@ module Net #:nodoc: res end + IDEMPOTENT_METHODS_ = %w/GET HEAD PUT DELETE OPTIONS TRACE/ # :nodoc: + def transport_request(req) + count = 0 begin_transport req res = catch(:response) { req.exec @socket, @curr_http_version, edit_path(req.path) @@ -1346,6 +1349,16 @@ module Net #:nodoc: } end_transport req, res res + rescue EOFError, Errno::ECONNRESET => exception + if count == 0 && IDEMPOTENT_METHODS_.include?(req.method) + count += 1 + @socket.close if @socket and not @socket.closed? + D "Conn close because of error #{exception}, and retry" + retry + end + D "Conn close because of error #{exception}" + @socket.close if @socket and not @socket.closed? + raise rescue => exception D "Conn close because of error #{exception}" @socket.close if @socket and not @socket.closed? diff --git a/test/net/http/test_http.rb b/test/net/http/test_http.rb index 1515854..2e7ab4e 100644 --- a/test/net/http/test_http.rb +++ b/test/net/http/test_http.rb @@ -564,3 +564,29 @@ class TestNetHTTPContinue < Test::Unit::TestCase assert_not_match(/HTTP\/1.1 100 continue/, @debug.string) end end + +class TestNetHTTPKeepAlive < Test::Unit::TestCase + CONFIG = { + 'host' => '127.0.0.1', + 'port' => 10081, + 'proxy_host' => nil, + 'proxy_port' => nil, + 'RequestTimeout' => 0.1, + } + + include TestNetHTTPUtils + + def test_keep_alive_get + start {|http| + res = http.get('/') + assert_kind_of Net::HTTPResponse, res + assert_kind_of String, res.body + sleep 1 + assert_nothing_raised { + res = http.get('/') + } + assert_kind_of Net::HTTPResponse, res + assert_kind_of String, res.body + } + end +end diff --git a/test/net/http/utils.rb b/test/net/http/utils.rb index 50f616f..07e0b9f 100644 --- a/test/net/http/utils.rb +++ b/test/net/http/utils.rb @@ -51,6 +51,7 @@ module TestNetHTTPUtils :ServerType => Thread, } server_config[:OutputBufferSize] = 4 if config('chunked') + server_config[:RequestTimeout] = config('RequestTimeout') if config('RequestTimeout') if defined?(OpenSSL) and config('ssl_enable') server_config.update({ :SSLEnable => true, -- http://redmine.ruby-lang.org