Skip to content

Commit e65a573

Browse files
authored
Merge pull request #774 from dimelo/master
RUBY-1117 Ensure the driver returns the response to the current query
2 parents 6d92717 + fa25549 commit e65a573

File tree

8 files changed

+145
-17
lines changed

8 files changed

+145
-17
lines changed

lib/mongo/error.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,5 +99,6 @@ class Error < StandardError
9999
require 'mongo/error/socket_timeout_error'
100100
require 'mongo/error/unchangeable_collection_option'
101101
require 'mongo/error/unexpected_chunk_length'
102+
require 'mongo/error/unexpected_response'
102103
require 'mongo/error/missing_file_chunk'
103104
require 'mongo/error/unsupported_features'
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Copyright (C) 2014-2015 MongoDB, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
module Mongo
16+
class Error
17+
18+
# Raised if the response read from the socket does not match the latest query.
19+
#
20+
# @since 2.2.6
21+
class UnexpectedResponse < Error
22+
23+
# Create the new exception.
24+
#
25+
# @example Create the new exception.
26+
# Mongo::Error::UnexpectedResponse.new(expected_response_to, response_to)
27+
#
28+
# @param [ Integer ] expected_response_to The last request id sent.
29+
# @param [ Integer ] response_to The actual response_to of the reply.
30+
#
31+
# @since 2.2.6
32+
def initialize(expected_response_to, response_to)
33+
super("Unexpected response. Got response for request ID #{response_to} " +
34+
"but expected response for request ID #{expected_response_to}")
35+
end
36+
end
37+
end
38+
end

lib/mongo/protocol/message.rb

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,17 +109,24 @@ def serialize(buffer = BSON::ByteBuffer.new, max_bson_size = nil)
109109
# @param [ IO ] io Stream containing a message
110110
#
111111
# @return [ Message ] Instance of a Message class
112-
def self.deserialize(io, max_message_size = MAX_MESSAGE_SIZE)
113-
length = deserialize_header(BSON::ByteBuffer.new(io.read(16))).first
112+
def self.deserialize(io, max_message_size = MAX_MESSAGE_SIZE, expected_response_to = nil)
113+
length, request_id, response_to, op_code = deserialize_header(BSON::ByteBuffer.new(io.read(16)))
114114

115115
# Protection from potential DOS man-in-the-middle attacks. See
116116
# DRIVERS-276.
117117
if length > (max_message_size || MAX_MESSAGE_SIZE)
118118
raise Error::MaxMessageSize.new(max_message_size)
119119
end
120120

121+
# Protection against returning the response to a previous request. See
122+
# RUBY-1117
123+
if expected_response_to && response_to != expected_response_to
124+
raise Error::UnexpectedResponse.new(expected_response_to, response_to)
125+
end
126+
121127
buffer = BSON::ByteBuffer.new(io.read(length - 16))
122128
message = allocate
129+
123130
fields.each do |field|
124131
if field[:multi]
125132
deserialize_array(message, buffer, field)
@@ -228,7 +235,7 @@ def serialize_header(buffer)
228235
# @param io [IO] Stream containing the header.
229236
# @return [Array<Fixnum>] Deserialized header.
230237
def self.deserialize_header(io)
231-
@length, @request_id, @response_to, @op_code = Header.deserialize(io)
238+
Header.deserialize(io)
232239
end
233240

234241
# A method for declaring a message field

lib/mongo/server/connectable.rb

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,11 @@ def ensure_connected
8888
ensure_same_process!
8989
connect!
9090
begin
91-
yield socket
92-
rescue Exception => e
93-
disconnect!
94-
raise e
91+
result = yield socket
92+
success = true
93+
result
94+
ensure
95+
success or disconnect!
9596
end
9697
end
9798

@@ -102,9 +103,9 @@ def ensure_same_process!
102103
end
103104
end
104105

105-
def read
106+
def read(request_id = nil)
106107
ensure_connected do |socket|
107-
Protocol::Reply.deserialize(socket, max_message_size)
108+
Protocol::Reply.deserialize(socket, max_message_size, request_id)
108109
end
109110
end
110111
end

lib/mongo/server/connection.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ def ping
159159

160160
def deliver(messages)
161161
write(messages)
162-
messages.last.replyable? ? read : nil
162+
messages.last.replyable? ? read(messages.last.request_id) : nil
163163
end
164164

165165
def authenticate!

lib/mongo/server/connection_pool.rb

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,10 @@ def inspect
103103
#
104104
# @since 2.0.0
105105
def with_connection
106-
begin
107-
connection = checkout
108-
yield(connection)
109-
ensure
110-
checkin(connection) if connection
111-
end
106+
connection = checkout
107+
yield(connection)
108+
ensure
109+
checkin(connection) if connection
112110
end
113111

114112
private

spec/mongo/server/connection_spec.rb

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,89 @@
250250
end
251251
end
252252

253+
context 'when the response_to does not match the request_id' do
254+
255+
let(:documents) do
256+
[{ 'name' => 'bob' }, { 'name' => 'alice' }]
257+
end
258+
259+
let(:insert) do
260+
Mongo::Protocol::Insert.new(TEST_DB, TEST_COLL, documents)
261+
end
262+
263+
let(:query_bob) do
264+
Mongo::Protocol::Query.new(TEST_DB, TEST_COLL, { 'name' => 'bob' })
265+
end
266+
267+
let(:query_alice) do
268+
Mongo::Protocol::Query.new(TEST_DB, TEST_COLL, { 'name' => 'alice' })
269+
end
270+
271+
after do
272+
authorized_collection.delete_many
273+
end
274+
275+
before do
276+
# Fake a query for which we did not read the response. See RUBY-1117
277+
allow(query_bob).to receive(:replyable?) { false }
278+
connection.dispatch([ insert, query_bob ])
279+
end
280+
281+
it 'raises an UnexpectedResponse' do
282+
expect {
283+
connection.dispatch([ query_alice ])
284+
}.to raise_error(Mongo::Error::UnexpectedResponse,
285+
/Got response for request ID \d+ but expected response for request ID \d+/)
286+
end
287+
288+
it "doesn't break subsequent requests" do
289+
expect {
290+
connection.dispatch([ query_alice ])
291+
}.to raise_error(Mongo::Error::UnexpectedResponse)
292+
293+
expect(connection.dispatch([ query_alice ]).documents.first['name']).to eq('alice')
294+
end
295+
end
296+
297+
context 'when a request is brutaly interrupted (Thread.kill)' do
298+
299+
let(:documents) do
300+
[{ 'name' => 'bob' }, { 'name' => 'alice' }]
301+
end
302+
303+
let(:insert) do
304+
Mongo::Protocol::Insert.new(TEST_DB, TEST_COLL, documents)
305+
end
306+
307+
let(:query_bob) do
308+
Mongo::Protocol::Query.new(TEST_DB, TEST_COLL, { 'name' => 'bob' })
309+
end
310+
311+
let(:query_alice) do
312+
Mongo::Protocol::Query.new(TEST_DB, TEST_COLL, { 'name' => 'alice' })
313+
end
314+
315+
before do
316+
connection.dispatch([ insert ])
317+
end
318+
319+
after do
320+
authorized_collection.delete_many
321+
end
322+
323+
it "closes the socket and does not use it for subsequent requests" do
324+
t = Thread.new {
325+
# Kill the thread just before the reply is read
326+
allow(Mongo::Protocol::Reply).to receive(:deserialize_header) { t.kill }
327+
connection.dispatch([ query_bob ])
328+
}
329+
t.join
330+
allow(Mongo::Protocol::Reply).to receive(:deserialize_header).and_call_original
331+
expect(connection.dispatch([ query_alice ]).documents.first['name']).to eq('alice')
332+
end
333+
end
334+
335+
253336
context 'when the message exceeds the max size' do
254337

255338
context 'when the message is an insert' do

spec/mongo/server/monitor_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@
150150
described_class.new(address, listeners)
151151
end
152152

153-
it 'defaults to 5' do
153+
it 'defaults to 10' do
154154
expect(monitor.heartbeat_frequency).to eq(10)
155155
end
156156
end

0 commit comments

Comments
 (0)