Skip to content

Commit a80660a

Browse files
authored
Improve ssl read performance (#981)
https://jira.mongodb.org/browse/RUBY-1364
1 parent aa53a15 commit a80660a

File tree

2 files changed

+69
-3
lines changed

2 files changed

+69
-3
lines changed

lib/mongo/socket.rb

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,11 +169,57 @@ def eof?
169169
private
170170

171171
def read_from_socket(length)
172-
data = String.new
172+
# Just in case
173+
if length == 0
174+
return ''.force_encoding('BINARY')
175+
end
176+
173177
deadline = (Time.now + timeout) if timeout
178+
179+
# We want to have a fixed and reasonably small size buffer for reads
180+
# because, for example, OpenSSL reads in 16 kb chunks max.
181+
# Having a 16 mb buffer means there will be 1000 reads each allocating
182+
# 16 mb of memory and using 16 kb of it.
183+
buf_size = read_buffer_size
184+
data = nil
185+
186+
# If we want to read less than the buffer size, just allocate the
187+
# memory that is necessary
188+
if length < buf_size
189+
buf_size = length
190+
end
191+
192+
# The binary encoding is important, otherwise ruby performs encoding
193+
# conversions of some sort during the write into the buffer which
194+
# kills performance
195+
buf = allocate_string(buf_size)
196+
retrieved = 0
174197
begin
175-
while (data.length < length)
176-
data << @socket.read_nonblock(length - data.length)
198+
while retrieved < length
199+
retrieve = length - retrieved
200+
if retrieve > buf_size
201+
retrieve = buf_size
202+
end
203+
chunk = @socket.read_nonblock(retrieve, buf)
204+
205+
# If we read the entire wanted length in one operation,
206+
# return the data as is which saves one memory allocation and
207+
# one copy per read
208+
if retrieved == 0 && chunk.length == length
209+
return chunk
210+
end
211+
212+
# If we are here, we are reading the wanted length in
213+
# multiple operations. Allocate the total buffer here rather
214+
# than up front so that the special case above won't be
215+
# allocating twice
216+
if data.nil?
217+
data = allocate_string(length)
218+
end
219+
220+
# ... and we need to copy the chunks at this point
221+
data[retrieved, chunk.length] = chunk
222+
retrieved += chunk.length
177223
end
178224
rescue IO::WaitReadable
179225
select_timeout = (deadline - Time.now) if deadline
@@ -186,6 +232,20 @@ def read_from_socket(length)
186232
data
187233
end
188234

235+
def allocate_string(capacity)
236+
if RUBY_VERSION >= '2.4.0'
237+
String.new('', :capacity => capacity, :encoding => 'BINARY')
238+
else
239+
('x'*capacity).force_encoding('BINARY')
240+
end
241+
end
242+
243+
def read_buffer_size
244+
# Buffer size for non-SSL reads
245+
# 64kb
246+
65536
247+
end
248+
189249
def unix_socket?(sock)
190250
defined?(UNIXSocket) && sock.is_a?(UNIXSocket)
191251
end

lib/mongo/socket/ssl.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,12 @@ def verify_certificate!(socket)
168168
end
169169
end
170170
end
171+
172+
def read_buffer_size
173+
# Buffer size for SSL reads.
174+
# Capped at 16k due to https://linux.die.net/man/3/ssl_read
175+
16384
176+
end
171177
end
172178
end
173179
end

0 commit comments

Comments
 (0)