Skip to content

Commit ae7f1cf

Browse files
committed
HELP-74319 Ensure partial reads are retried
1 parent 14e0743 commit ae7f1cf

File tree

2 files changed

+86
-25
lines changed

2 files changed

+86
-25
lines changed

lib/mongo/socket.rb

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -523,32 +523,41 @@ def write_with_timeout(*args, timeout:)
523523

524524
def write_chunk(chunk, timeout)
525525
deadline = Utils.monotonic_time + timeout
526+
526527
written = 0
527-
begin
528-
written += @socket.write_nonblock(chunk[written..-1])
529-
rescue IO::WaitWritable, Errno::EINTR
530-
select_timeout = deadline - Utils.monotonic_time
531-
rv = Kernel.select(nil, [@socket], nil, select_timeout)
532-
if BSON::Environment.jruby?
533-
# Ignore the return value of Kernel.select.
534-
# On JRuby, select appears to return nil prior to timeout expiration
535-
# (apparently due to a EAGAIN) which then causes us to fail the read
536-
# even though we could have retried it.
537-
# Check the deadline ourselves.
538-
if deadline
539-
select_timeout = deadline - Utils.monotonic_time
540-
if select_timeout <= 0
541-
raise_timeout_error!("Took more than #{timeout} seconds to receive data", true)
542-
end
528+
while written < chunk.length
529+
begin
530+
written += @socket.write_nonblock(chunk[written..-1])
531+
rescue IO::WaitWritable, Errno::EINTR
532+
if !wait_for_socket_to_be_writable(deadline)
533+
raise_timeout_error!("Took more than #{timeout} seconds to receive data", true)
543534
end
544-
elsif rv.nil?
545-
raise_timeout_error!("Took more than #{timeout} seconds to receive data (select call timed out)", true)
535+
536+
retry
546537
end
547-
retry
548538
end
539+
549540
written
550541
end
551542

543+
def wait_for_socket_to_be_writable(deadline)
544+
select_timeout = deadline - Utils.monotonic_time
545+
rv = Kernel.select(nil, [@socket], nil, select_timeout)
546+
if BSON::Environment.jruby?
547+
# Ignore the return value of Kernel.select.
548+
# On JRuby, select appears to return nil prior to timeout expiration
549+
# (apparently due to a EAGAIN) which then causes us to fail the read
550+
# even though we could have retried it.
551+
# Check the deadline ourselves.
552+
select_timeout = deadline - Utils.monotonic_time
553+
return select_timeout > 0
554+
elsif rv.nil?
555+
return false
556+
end
557+
558+
true
559+
end
560+
552561
def unix_socket?(sock)
553562
defined?(UNIXSocket) && sock.is_a?(UNIXSocket)
554563
end

spec/mongo/socket_spec.rb

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,6 @@
6868

6969
let(:raw_socket) { socket.instance_variable_get('@socket') }
7070

71-
let(:wait_readable_class) do
72-
Class.new(Exception) do
73-
include IO::WaitReadable
74-
end
75-
end
76-
7771
context 'timeout' do
7872
clean_slate_for_all
7973

@@ -116,4 +110,62 @@
116110
end
117111
end
118112
end
113+
114+
describe '#write' do
115+
let(:target_host) do
116+
host = ClusterConfig.instance.primary_address_host
117+
# Take ipv4 address
118+
Socket.getaddrinfo(host, 0).detect { |ai| ai.first == 'AF_INET' }[3]
119+
end
120+
121+
let(:socket) do
122+
Mongo::Socket::TCP.new(target_host, ClusterConfig.instance.primary_address_port, 1, Socket::PF_INET)
123+
end
124+
125+
let(:raw_socket) { socket.instance_variable_get('@socket') }
126+
127+
context 'with timeout' do
128+
let(:timeout) { 5_000 }
129+
130+
context 'data is less than WRITE_CHUNK_SIZE' do
131+
let(:data) { "a" * 1024 }
132+
133+
context 'when a partial write occurs' do
134+
before do
135+
expect(raw_socket)
136+
.to receive(:write_nonblock)
137+
.twice
138+
.and_return(data.length / 2)
139+
end
140+
141+
it 'eventually writes everything' do
142+
expect(socket.write(data, timeout: timeout)).
143+
to be === data.length
144+
end
145+
end
146+
end
147+
148+
context 'data is greater than WRITE_CHUNK_SIZE' do
149+
let(:data) { "a" * (2 * Mongo::Socket::WRITE_CHUNK_SIZE + 256) }
150+
151+
context 'when a partial write occurs' do
152+
before do
153+
expect(raw_socket)
154+
.to receive(:write_nonblock)
155+
.exactly(4).times
156+
.and_return(Mongo::Socket::WRITE_CHUNK_SIZE,
157+
128,
158+
Mongo::Socket::WRITE_CHUNK_SIZE - 128,
159+
256)
160+
end
161+
162+
it 'eventually writes everything' do
163+
puts "=== spec begins"
164+
expect(socket.write(data, timeout: timeout)).
165+
to be === data.length
166+
end
167+
end
168+
end
169+
end
170+
end
119171
end

0 commit comments

Comments
 (0)