[前][次][番号順一覧][スレッド一覧]

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/

[前][次][番号順一覧][スレッド一覧]