Skip to content

Commit b941a60

Browse files
author
jwedoff
committed
changes to implement timeouts with non-blocking reads and writes
1 parent 8ef75a0 commit b941a60

12 files changed

+293
-127
lines changed

.rubocop_todo.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ Lint/UselessAssignment:
206206

207207
# Offense count: 48
208208
Metrics/AbcSize:
209-
Max: 116
209+
Max: 118
210210

211211
# Offense count: 4
212212
# Configuration parameters: CountComments, ExcludedMethods.
@@ -221,7 +221,7 @@ Metrics/BlockNesting:
221221
# Offense count: 11
222222
# Configuration parameters: CountComments.
223223
Metrics/ClassLength:
224-
Max: 429
224+
Max: 436
225225

226226
# Offense count: 23
227227
Metrics/CyclomaticComplexity:

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

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# -*- ruby encoding: utf-8 -*-
22
require 'stringio'
3+
require_relative 'ber_parser_nonblock'
34

45
# Implements Basic Encoding Rules parsing to be mixed into types as needed.
56
module Net::BER::BERParser
7+
include Net::BER::BERParserNonblock
68
primitive = {
79
1 => :boolean,
810
2 => :integer,
@@ -133,7 +135,7 @@ def parse_ber_object(syntax, id, data)
133135
# invalid BER length case. Because the "lengthlength" value was not used
134136
# inside of #read_ber, we no longer return it.
135137
def read_ber_length
136-
n = getbyte
138+
n = ber_timeout_getbyte
137139

138140
if n <= 0x7f
139141
n
@@ -143,7 +145,7 @@ def read_ber_length
143145
raise Net::BER::BerError, "Invalid BER length 0xFF detected."
144146
else
145147
v = 0
146-
read(n & 0x7f).each_byte do |b|
148+
ber_timeout_read(n & 0x7f).each_byte do |b|
147149
v = (v << 8) + b
148150
end
149151

@@ -166,7 +168,7 @@ def read_ber(syntax = nil)
166168
# from streams that don't block when we ask for more data (like
167169
# StringIOs). At it is, this can throw TypeErrors and other nasties.
168170

169-
id = getbyte or return nil # don't trash this value, we'll use it later
171+
id = read_ber_id or return nil # don't trash this value, we'll use it later
170172
content_length = read_ber_length
171173

172174
yield id, content_length if block_given?
@@ -175,7 +177,7 @@ def read_ber(syntax = nil)
175177
raise Net::BER::BerError,
176178
"Indeterminite BER content length not implemented."
177179
end
178-
data = read(content_length)
180+
data = ber_timeout_read(content_length)
179181

180182
parse_ber_object(syntax, id, data)
181183
end

lib/net/ber/ber_parser_nonblock.rb

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Implements nonbocking and timeout handling routines for BER parsing.
2+
module Net::BER::BERParserNonblock
3+
# Internal: Returns the BER message ID or nil.
4+
def read_ber_id
5+
ber_timeout_getbyte
6+
end
7+
private :read_ber_id
8+
9+
# Internal: specify the BER socket read timeouts, nil by default (no timeout).
10+
attr_accessor :ber_io_deadline
11+
private :ber_io_deadline
12+
13+
##
14+
# 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
15+
def with_timeout(timeout)
16+
timeout = timeout.to_f
17+
# don't change deadline if run without timeout
18+
return yield if timeout <= 0
19+
# clear deadline if it is not in the future
20+
self.ber_io_deadline = nil unless ber_io_timeout.to_f > 0
21+
new_deadline = Time.now + timeout
22+
# don't add deadline if current deadline is shorter
23+
return yield if ber_io_deadline && ber_io_deadline < new_deadline
24+
old_deadline = ber_io_deadline
25+
begin
26+
self.ber_io_deadline = new_deadline
27+
yield
28+
ensure
29+
self.ber_io_deadline = old_deadline
30+
end
31+
end
32+
33+
# seconds until ber_io_deadline
34+
def ber_io_timeout
35+
ber_io_deadline ? ber_io_deadline - Time.now : nil
36+
end
37+
private :ber_io_timeout
38+
39+
def read_select!
40+
return if IO.select([self], nil, nil, ber_io_timeout)
41+
raise Errno::ETIMEDOUT, "Timed out reading from the socket"
42+
end
43+
private :read_select!
44+
45+
def write_select!
46+
return if IO.select(nil, [self], nil, ber_io_timeout)
47+
raise Errno::ETIMEDOUT, "Timed out reading from the socket"
48+
end
49+
private :write_select!
50+
51+
# Internal: Replaces `getbyte` with nonblocking implementation.
52+
def ber_timeout_getbyte
53+
read_nonblock(1).ord
54+
rescue IO::WaitReadable
55+
read_select!
56+
retry
57+
rescue IO::WaitWritable
58+
write_select!
59+
retry
60+
rescue EOFError
61+
# nothing to read on the socket (StringIO)
62+
nil
63+
end
64+
private :ber_timeout_getbyte
65+
66+
# Internal: Read `len` bytes, respecting timeout.
67+
def ber_timeout_read(len)
68+
buffer ||= ''.force_encoding(Encoding::ASCII_8BIT)
69+
begin
70+
read_nonblock(len, buffer)
71+
return buffer if buffer.bytesize >= len
72+
rescue IO::WaitReadable, IO::WaitWritable
73+
buffer.clear
74+
rescue EOFError
75+
# nothing to read on the socket (StringIO)
76+
nil
77+
end
78+
block ||= ''.force_encoding(Encoding::ASCII_8BIT)
79+
len -= buffer.bytesize
80+
loop do
81+
begin
82+
read_nonblock(len, block)
83+
rescue IO::WaitReadable
84+
read_select!
85+
retry
86+
rescue IO::WaitWritable
87+
write_select!
88+
retry
89+
rescue EOFError
90+
return buffer.empty? ? nil : buffer
91+
end
92+
buffer << block
93+
len -= block.bytesize
94+
return buffer if len <= 0
95+
end
96+
end
97+
private :ber_timeout_read
98+
99+
##
100+
# Writes val as a plain write would, but respecting the dealine set by with_timeout
101+
def ber_timeout_write(val)
102+
total_written = 0
103+
while val.bytesize > 0
104+
begin
105+
written = write_nonblock(val)
106+
rescue IO::WaitReadable
107+
read_select!
108+
retry
109+
rescue IO::WaitWritable
110+
write_select!
111+
retry
112+
end
113+
total_written += written
114+
val = val.byteslice(written..-1)
115+
end
116+
total_written
117+
end
118+
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.key?(:io_timeout) ? [args[:io_timeout]] : []
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)