Skip to content

Commit c740a20

Browse files
author
John Wedoff
committed
Add support for timeouts on read and write operations
1 parent 9241027 commit c740a20

9 files changed

+271
-149
lines changed

Contributors.rdoc

+1
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ Contributions since:
2323
* Cody Cutrer (ccutrer)
2424
* WoodsBagotAndreMarquesLee
2525
* Rufus Post (mynameisrufus)
26+
* Akamai Technologies, Inc. (jwedoff)

lib/net/ber/ber_parser.rb

+103-22
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def parse_ber_object(syntax, id, data)
136136
# invalid BER length case. Because the "lengthlength" value was not used
137137
# inside of #read_ber, we no longer return it.
138138
def read_ber_length
139-
n = getbyte_nonblock
139+
n = ber_timeout_getbyte
140140

141141
if n <= 0x7f
142142
n
@@ -146,7 +146,7 @@ def read_ber_length
146146
raise Net::BER::BerError, "Invalid BER length 0xFF detected."
147147
else
148148
v = 0
149-
read_ber_nonblock(n & 0x7f).each_byte do |b|
149+
ber_timeout_read(n & 0x7f).each_byte do |b|
150150
v = (v << 8) + b
151151
end
152152
v
@@ -177,45 +177,126 @@ def read_ber(syntax = nil)
177177
raise Net::BER::BerError,
178178
"Indeterminite BER content length not implemented."
179179
end
180-
data = read_ber_nonblock(content_length)
180+
data = ber_timeout_read(content_length)
181181

182182
parse_ber_object(syntax, id, data)
183183
end
184184

185185
# Internal: Returns the BER message ID or nil.
186186
def read_ber_id
187-
getbyte_nonblock
187+
ber_timeout_getbyte
188188
end
189189
private :read_ber_id
190190

191+
# Internal: specify the BER socket read timeouts, nil by default (no timeout).
192+
attr_accessor :ber_io_deadline
193+
private :ber_io_deadline
194+
195+
##
196+
# sets a timeout of timeout seconds for read_ber and ber_timeout_write operations in the provided block the proin the future for if there is not already a earlier deadline set
197+
def with_timeout(timeout)
198+
timeout = timeout.to_f
199+
# don't change deadline if run without timeout
200+
return yield if timeout <= 0
201+
# clear deadline if it is not in the future
202+
self.ber_io_deadline = nil unless ber_io_timeout&.send(:>, 0)
203+
new_deadline = Time.now + timeout
204+
# don't add deadline if current deadline is shorter
205+
return yield if ber_io_deadline && ber_io_deadline < new_deadline
206+
old_deadline = ber_io_deadline
207+
begin
208+
self.ber_io_deadline = new_deadline
209+
yield
210+
ensure
211+
self.ber_io_deadline = old_deadline
212+
end
213+
end
214+
215+
# seconds until ber_io_deadline
216+
def ber_io_timeout
217+
ber_io_deadline ? ber_io_deadline - Time.now : nil
218+
end
219+
private :ber_io_timeout
220+
221+
def read_select!
222+
return if IO.select([self], nil, nil, ber_io_timeout)
223+
raise Net::LDAP::LdapError, "Timed out reading from the socket"
224+
end
225+
private :read_select!
226+
227+
def write_select!
228+
return if IO.select(nil, [self], nil, ber_io_timeout)
229+
raise Net::LDAP::LdapError, "Timed out reading from the socket"
230+
end
231+
private :write_select!
232+
191233
# Internal: Replaces `getbyte` with nonblocking implementation.
192-
def getbyte_nonblock
234+
def ber_timeout_getbyte
193235
begin
194236
read_nonblock(1).ord
195237
rescue IO::WaitReadable
196-
if IO.select([self], nil, nil, read_ber_timeout)
197-
read_nonblock(1).ord
198-
else
199-
raise Net::LDAP::LdapError, "Timed out reading from the socket"
200-
end
238+
read_select!
239+
retry
240+
rescue IO::WaitWritable
241+
write_select!
242+
retry
243+
rescue EOFError
244+
# nothing to read on the socket (StringIO)
245+
nil
201246
end
202-
rescue EOFError
203-
# nothing to read on the socket (StringIO)
204-
nil
205247
end
206-
private :getbyte_nonblock
248+
private :ber_timeout_getbyte
207249

208250
# Internal: Read `len` bytes, respecting timeout.
209-
def read_ber_nonblock(len)
251+
def ber_timeout_read(len)
252+
buffer ||= ''.force_encoding(Encoding::ASCII_8BIT)
210253
begin
211-
read_nonblock(len)
212-
rescue IO::WaitReadable
213-
if IO.select([self], nil, nil, read_ber_timeout)
214-
read_nonblock(len)
215-
else
216-
raise Net::LDAP::LdapError, "Timed out reading from the socket"
254+
read_nonblock(len, buffer)
255+
return buffer if buffer.bytesize >= len
256+
rescue IO::WaitReadable, IO::WaitWritable
257+
buffer.clear
258+
rescue EOFError
259+
# nothing to read on the socket (StringIO)
260+
nil
261+
end
262+
block ||= ''.force_encoding(Encoding::ASCII_8BIT)
263+
len -= buffer.bytesize
264+
loop do
265+
begin
266+
read_nonblock(len, block)
267+
rescue IO::WaitReadable
268+
read_select!
269+
retry
270+
rescue IO::WaitWritable
271+
write_select!
272+
retry
273+
rescue EOFError
274+
return buffer.empty? ? nil : buffer
275+
end
276+
buffer << block
277+
len -= block.bytesize
278+
return buffer if len <= 0
279+
end
280+
end
281+
private :ber_timeout_read
282+
283+
##
284+
# Writes val as a plain write would, but respecting the dealine set by with_timeout
285+
def ber_timeout_write(val)
286+
total_written = 0
287+
while 0 < val.bytesize
288+
begin
289+
written = write_nonblock(val)
290+
rescue IO::WaitReadable
291+
read_select!
292+
retry
293+
rescue IO::WaitWritable
294+
write_select!
295+
retry
217296
end
297+
total_written += written
298+
val = val.byteslice(written..-1)
218299
end
300+
total_written
219301
end
220-
private :read_ber_nonblock
221302
end

lib/net/ldap.rb

+12-5
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,7 @@ def initialize(args = {})
553553
@force_no_page = args[:force_no_page] || DefaultForceNoPage
554554
@encryption = normalize_encryption(args[:encryption]) # may be nil
555555
@connect_timeout = args[:connect_timeout]
556+
@io_timeout = args[:io_timeout]
556557

557558
if pr = @auth[:password] and pr.respond_to?(:call)
558559
@auth[:password] = pr.call
@@ -1293,14 +1294,19 @@ def connection=(connection)
12931294
# result from that, and :use_connection: will not yield at all. If not
12941295
# the return value is whatever is returned from the block.
12951296
def use_connection(args)
1297+
timeout_args = args.slice(:io_timeout).values
12961298
if @open_connection
1297-
yield @open_connection
1299+
@open_connection.with_timeout(*timeout_args) do
1300+
yield(@open_connection)
1301+
end
12981302
else
12991303
begin
13001304
conn = new_connection
1301-
result = conn.bind(args[:auth] || @auth)
1302-
return result unless result.result_code == Net::LDAP::ResultCodeSuccess
1303-
yield conn
1305+
conn.with_timeout(*timeout_args) do
1306+
result = conn.bind(args[:auth] || @auth)
1307+
return result unless result.result_code == Net::LDAP::ResultCodeSuccess
1308+
yield(conn)
1309+
end
13041310
ensure
13051311
conn.close if conn
13061312
end
@@ -1315,7 +1321,8 @@ def new_connection
13151321
:hosts => @hosts,
13161322
:encryption => @encryption,
13171323
:instrumentation_service => @instrumentation_service,
1318-
:connect_timeout => @connect_timeout
1324+
:connect_timeout => @connect_timeout,
1325+
:io_timeout => @io_timeout
13191326

13201327
# Force connect to see if there's a connection error
13211328
connection.socket

0 commit comments

Comments
 (0)