ruby-changes:51381
From: naruse <ko1@a...>
Date: Wed, 6 Jun 2018 17:03:52 +0900 (JST)
Subject: [ruby-changes:51381] naruse:r63587 (trunk): Introduce write_timeout to Net::HTTP [Feature #13396]
naruse 2018-06-06 17:03:47 +0900 (Wed, 06 Jun 2018) New Revision: 63587 https://svn.ruby-lang.org/cgi-bin/viewvc.cgi?view=revision&revision=63587 Log: Introduce write_timeout to Net::HTTP [Feature #13396] Modified files: trunk/NEWS trunk/lib/net/http.rb trunk/lib/net/protocol.rb trunk/test/net/http/test_http.rb trunk/test/net/protocol/test_protocol.rb Index: lib/net/http.rb =================================================================== --- lib/net/http.rb (revision 63586) +++ lib/net/http.rb (revision 63587) @@ -575,7 +575,7 @@ module Net #:nodoc: https://github.com/ruby/ruby/blob/trunk/lib/net/http.rb#L575 # # _opt_ sets following values by its accessor. # The keys are ca_file, ca_path, cert, cert_store, ciphers, - # close_on_empty_response, key, open_timeout, read_timeout, ssl_timeout, + # close_on_empty_response, key, open_timeout, read_timeout, write_timeout, ssl_timeout, # ssl_version, use_ssl, verify_callback, verify_depth and verify_mode. # If you set :use_ssl as true, you can use https and default value of # verify_mode is set as OpenSSL::SSL::VERIFY_PEER. @@ -673,6 +673,7 @@ module Net #:nodoc: https://github.com/ruby/ruby/blob/trunk/lib/net/http.rb#L673 @started = false @open_timeout = 60 @read_timeout = 60 + @write_timeout = 60 @continue_timeout = nil @max_retries = 1 @debug_output = nil @@ -741,6 +742,12 @@ module Net #:nodoc: https://github.com/ruby/ruby/blob/trunk/lib/net/http.rb#L742 # it raises a Net::ReadTimeout exception. The default value is 60 seconds. attr_reader :read_timeout + # Number of seconds to wait for one block to be write (via one write(2) + # call). Any number may be used, including Floats for fractional + # seconds. If the HTTP object cannot write data in this many seconds, + # it raises a Net::WriteTimeout exception. The default value is 60 seconds. + attr_reader :write_timeout + # Maximum number of times to retry an idempotent request in case of # Net::ReadTimeout, IOError, EOFError, Errno::ECONNRESET, # Errno::ECONNABORTED, Errno::EPIPE, OpenSSL::SSL::SSLError, @@ -763,6 +770,12 @@ module Net #:nodoc: https://github.com/ruby/ruby/blob/trunk/lib/net/http.rb#L770 @read_timeout = sec end + # Setter for the write_timeout attribute. + def write_timeout=(sec) + @socket.write_timeout = sec if @socket + @write_timeout = sec + end + # Seconds to wait for 100 Continue response. If the HTTP object does not # receive a response in this many seconds it sends the request body. The # default value is +nil+. @@ -944,6 +957,7 @@ module Net #:nodoc: https://github.com/ruby/ruby/blob/trunk/lib/net/http.rb#L957 if use_ssl? if proxy? plain_sock = BufferedIO.new(s, read_timeout: @read_timeout, + write_timeout: @write_timeout, continue_timeout: @continue_timeout, debug_output: @debug_output) buf = "CONNECT #{@address}:#{@port} HTTP/#{HTTPVersion}\r\n" @@ -985,6 +999,7 @@ module Net #:nodoc: https://github.com/ruby/ruby/blob/trunk/lib/net/http.rb#L999 D "SSL established" end @socket = BufferedIO.new(s, read_timeout: @read_timeout, + write_timeout: @write_timeout, continue_timeout: @continue_timeout, debug_output: @debug_output) on_connect Index: lib/net/protocol.rb =================================================================== --- lib/net/protocol.rb (revision 63586) +++ lib/net/protocol.rb (revision 63587) @@ -77,11 +77,18 @@ module Net # :nodoc: https://github.com/ruby/ruby/blob/trunk/lib/net/protocol.rb#L77 class ReadTimeout < Timeout::Error; end + ## + # WriteTimeout, a subclass of Timeout::Error, is raised if a chunk of the + # response cannot be read within the read_timeout. + + class WriteTimeout < Timeout::Error; end + class BufferedIO #:nodoc: internal use only - def initialize(io, read_timeout: 60, continue_timeout: nil, debug_output: nil) + def initialize(io, read_timeout: 60, write_timeout: 60, continue_timeout: nil, debug_output: nil) @io = io @read_timeout = read_timeout + @write_timeout = write_timeout @continue_timeout = continue_timeout @debug_output = debug_output @rbuf = ''.b @@ -89,6 +96,7 @@ module Net # :nodoc: https://github.com/ruby/ruby/blob/trunk/lib/net/protocol.rb#L96 attr_reader :io attr_accessor :read_timeout + attr_accessor :write_timeout attr_accessor :continue_timeout attr_accessor :debug_output @@ -237,9 +245,32 @@ module Net # :nodoc: https://github.com/ruby/ruby/blob/trunk/lib/net/protocol.rb#L245 def write0(*strs) @debug_output << strs.map(&:dump).join if @debug_output - len = @io.write(*strs) - @written_bytes += len - len + case len = @io.write_nonblock(*strs, exception: false) + when Integer + orig_len = len + strs.each_with_index do |str, i| + @written_bytes += str.bytesize + len -= str.bytesize + if len == 0 + if strs.size == i+1 + return orig_len + else + strs = strs[i+1..] # rest + break + end + elsif len < 0 + strs = strs[i..] # str and rest + strs[0] = str[len, -len] + break + else # len > 0 + # next + end + end + # continue looping + when :wait_writable + @io.to_io.wait_writable(@write_timeout) or raise Net::WriteTimeout + # continue looping + end while true end # Index: NEWS =================================================================== --- NEWS (revision 63586) +++ NEWS (revision 63587) @@ -150,6 +150,15 @@ with all sufficient information, see the https://github.com/ruby/ruby/blob/trunk/NEWS#L150 * Matrix#antisymmetric? +* Net + + * New method: + + * Add write_timeout keyword argument to Net::BufferedIO.new. [Feature #13396] + + * Add Net::BufferedIO#write_timeout, Net::BufferedIO#write_timeout=, + Net::HTTP#write_timeout, and Net::HTTP#write_timeout=. [Feature #13396] + * REXML * Improved some XPath implementations: Index: test/net/http/test_http.rb =================================================================== --- test/net/http/test_http.rb (revision 63586) +++ test/net/http/test_http.rb (revision 63587) @@ -529,6 +529,30 @@ module TestNetHTTP_version_1_1_methods https://github.com/ruby/ruby/blob/trunk/test/net/http/test_http.rb#L529 assert_equal data, res.entity end + def test_timeout_during_HTTP_session_write + bug4246 = "expected the HTTP session to have timed out but have not. c.f. [ruby-core:34203]" + + th = nil + # listen for connections... but deliberately do not read + TCPServer.open('localhost', 0) {|server| + port = server.addr[1] + + conn = Net::HTTP.new('localhost', port) + conn.write_timeout = 0.01 + conn.open_timeout = 0.1 + + th = Thread.new do + assert_raise(Net::WriteTimeout) { + conn.post('/', "a"*5_000_000) + } + end + assert th.join(10), bug4246 + } + ensure + th.kill + th.join + end + def test_timeout_during_HTTP_session bug4246 = "expected the HTTP session to have timed out but have not. c.f. [ruby-core:34203]" Index: test/net/protocol/test_protocol.rb =================================================================== --- test/net/protocol/test_protocol.rb (revision 63586) +++ test/net/protocol/test_protocol.rb (revision 63587) @@ -26,4 +26,89 @@ class TestProtocol < Test::Unit::TestCas https://github.com/ruby/ruby/blob/trunk/test/net/protocol/test_protocol.rb#L26 assert_equal("\u3042\r\n.\r\n", sio.string) end end + + def create_mockio + mockio = Object.new + mockio.instance_variable_set(:@str, +'') + mockio.instance_variable_set(:@capacity, 100) + def mockio.string; @str; end + def mockio.to_io; self; end + def mockio.wait_writable(sec); sleep sec; false; end + def mockio.write_nonblock(*strs, exception: true) + if @capacity <= @str.bytesize + if exception + raise Net::WaitWritable + else + return :wait_writable + end + end + len = 0 + strs.each do |str| + len1 = @str.bytesize + break if @capacity <= len1 + @str << str[0, @capacity - @str.bytesize] + len2 = @str.bytesize + len += len2 - len1 + end + len + end + mockio + end + + def test_write0_timeout + mockio = create_mockio + io = Net::BufferedIO.new(mockio) + io.write_timeout = 0.1 + assert_raise(Net::WriteTimeout){ io.write("a"*1000) } + end + + def test_write0_success + mockio = create_mockio + io = Net::BufferedIO.new(mockio) + io.write_timeout = 0.1 + len = io.write("a"*10) + assert_equal "a"*10, mockio.string + assert_equal 10, len + end + + def test_write0_success2 + mockio = create_mockio + io = Net::BufferedIO.new(mockio) + io.write_timeout = 0.1 + len = io.write("a"*100) + assert_equal "a"*100, mockio.string + assert_equal 100, len + end + + def test_write0_success_multi1 + mockio = create_mockio + io = Net::BufferedIO.new(mockio) + io.write_timeout = 0.1 + len = io.write("a"*50, "a"*49) + assert_equal "a"*99, mockio.string + assert_equal 99, len + end + + def test_write0_success_multi2 + mockio = create_mockio + io = Net::BufferedIO.new(mockio) + io.write_timeout = 0.1 + len = io.write("a"*50, "a"*50) + assert_equal "a"*100, mockio.string + assert_equal 100, len + end + + def test_write0_timeout_multi1 + mockio = create_mockio + io = Net::BufferedIO.new(mockio) + io.write_timeout = 0.1 + assert_raise(Net::WriteTimeout){ io.write("a"*50,"a"*51) } + end + + def test_write0_timeout_multi2 + mockio = create_mockio + io = Net::BufferedIO.new(mockio) + io.write_timeout = 0.1 + assert_raise(Net::WriteTimeout){ io.write("a"*50,"a"*50,"a") } + end end -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/