@@ -23,6 +23,9 @@ def initialize(*args)
2323 super
2424
2525 @timeout = @time_left = options . fetch ( :global_timeout )
26+ @read_timeout = options [ :read_timeout ]
27+ @write_timeout = options [ :write_timeout ]
28+ @connect_timeout = options [ :connect_timeout ]
2629 end
2730
2831 # Resets the time left counter to initial timeout
@@ -49,7 +52,7 @@ def reset_counter
4952 # @return [void]
5053 def connect ( socket_class , host , port , nodelay : false )
5154 reset_timer
52- ::Timeout . timeout ( @time_left , ConnectTimeoutError ) do
55+ ::Timeout . timeout ( effective_timeout ( @connect_timeout ) , ConnectTimeoutError ) do
5356 @socket = socket_class . open ( host , port )
5457 @socket . setsockopt ( Socket ::IPPROTO_TCP , Socket ::TCP_NODELAY , 1 ) if nodelay
5558 end
@@ -70,10 +73,10 @@ def connect_ssl
7073 begin
7174 @socket . connect_nonblock
7275 rescue IO ::WaitReadable
73- wait_readable_or_timeout
76+ wait_readable_or_timeout ( @connect_timeout )
7477 retry
7578 rescue IO ::WaitWritable
76- wait_writable_or_timeout
79+ wait_writable_or_timeout ( @connect_timeout )
7780 retry
7881 end
7982 end
@@ -88,7 +91,7 @@ def connect_ssl
8891 # @api public
8992 # @return [String, :eof]
9093 def readpartial ( size , buffer = nil )
91- perform_io { read_nonblock ( size , buffer ) }
94+ perform_io ( @read_timeout ) { read_nonblock ( size , buffer ) }
9295 end
9396
9497 # Write to the socket
@@ -100,7 +103,7 @@ def readpartial(size, buffer = nil)
100103 # @api public
101104 # @return [Integer, :eof]
102105 def write ( data )
103- perform_io { write_nonblock ( data ) }
106+ perform_io ( @write_timeout ) { write_nonblock ( data ) }
104107 end
105108
106109 alias << write
@@ -125,18 +128,19 @@ def write_nonblock(data)
125128
126129 # Performs I/O operation with timeout tracking
127130 #
131+ # @param [Numeric, nil] per_op_timeout per-operation timeout limit
128132 # @api private
129133 # @return [Object]
130- def perform_io
134+ def perform_io ( per_op_timeout = nil )
131135 reset_timer
132136
133137 loop do
134138 result = yield
135139 return handle_io_result ( result ) unless WAIT_RESULTS . include? ( result )
136140
137- wait_for_io ( result )
138- rescue IO ::WaitReadable then wait_readable_or_timeout
139- rescue IO ::WaitWritable then wait_writable_or_timeout
141+ wait_for_io ( result , per_op_timeout )
142+ rescue IO ::WaitReadable then wait_readable_or_timeout ( per_op_timeout )
143+ rescue IO ::WaitWritable then wait_writable_or_timeout ( per_op_timeout )
140144 end
141145 rescue EOFError
142146 :eof
@@ -152,32 +156,53 @@ def handle_io_result(result)
152156
153157 # Waits for an I/O readiness based on the result type
154158 #
159+ # @param [Symbol] result the I/O wait type
160+ # @param [Numeric, nil] per_op_timeout per-operation timeout limit
155161 # @api private
156162 # @return [void]
157- def wait_for_io ( result )
163+ def wait_for_io ( result , per_op_timeout = nil )
158164 if result == :wait_readable
159- wait_readable_or_timeout
165+ wait_readable_or_timeout ( per_op_timeout )
160166 else
161- wait_writable_or_timeout
167+ wait_writable_or_timeout ( per_op_timeout )
162168 end
163169 end
164170
165171 # Waits for a socket to become readable
166172 #
173+ # @param [Numeric, nil] per_op per-operation timeout limit
167174 # @api private
168175 # @return [void]
169- def wait_readable_or_timeout
170- @socket . to_io . wait_readable ( @time_left )
176+ def wait_readable_or_timeout ( per_op = nil )
177+ timeout = effective_timeout ( per_op )
178+ result = @socket . to_io . wait_readable ( timeout )
171179 log_time
180+
181+ raise TimeoutError , "Read timed out after #{ per_op } seconds" if per_op && result . nil?
172182 end
173183
174184 # Waits for a socket to become writable
175185 #
186+ # @param [Numeric, nil] per_op per-operation timeout limit
176187 # @api private
177188 # @return [void]
178- def wait_writable_or_timeout
179- @socket . to_io . wait_writable ( @time_left )
189+ def wait_writable_or_timeout ( per_op = nil )
190+ timeout = effective_timeout ( per_op )
191+ result = @socket . to_io . wait_writable ( timeout )
180192 log_time
193+
194+ raise TimeoutError , "Write timed out after #{ per_op } seconds" if per_op && result . nil?
195+ end
196+
197+ # Computes the effective timeout as the minimum of global and per-operation
198+ #
199+ # @param [Numeric, nil] per_op_timeout per-operation timeout limit
200+ # @api private
201+ # @return [Numeric]
202+ def effective_timeout ( per_op_timeout )
203+ return @time_left unless per_op_timeout
204+
205+ [ per_op_timeout , @time_left ] . min
181206 end
182207
183208 # Resets the I/O timer to current time
0 commit comments