Skip to content

Commit 67eb761

Browse files
p-mongop
authored andcommitted
RUBY-1905 Indicate which server operations were attempted on and attempt number in driver-surfaced exceptions (#1508)
1 parent 6922894 commit 67eb761

File tree

12 files changed

+242
-42
lines changed

12 files changed

+242
-42
lines changed

lib/mongo/error.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
require 'mongo/error/notable'
16+
1517
module Mongo
1618
# Base error class for all Mongo related errors.
1719
#
1820
# @since 2.0.0
1921
class Error < StandardError
22+
include Notable
2023

2124
# The error code field.
2225
#
@@ -141,6 +144,7 @@ def add_label(label)
141144
end
142145
end
143146

147+
require 'mongo/error/auth_error'
144148
require 'mongo/error/sdam_error_detection'
145149
require 'mongo/error/parser'
146150
require 'mongo/error/write_retryable'

lib/mongo/error/auth_error.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copyright (C) 2018-2019 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 when authentication fails.
19+
#
20+
# Note: This class is derived from RuntimeError for
21+
# backwards compatibility reasons. It is subject to
22+
# change in future major versions of the driver.
23+
#
24+
# @since 2.10.1
25+
class AuthError < RuntimeError
26+
include Notable
27+
end
28+
end
29+
end

lib/mongo/error/bulk_write_error.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def to_s
5050
else
5151
''
5252
end
53-
self.class.name + messages
53+
"#{self.class}: #{messages}" + notes_tail
5454
end
5555
end
5656
end

lib/mongo/error/notable.rb

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Copyright (C) 2019 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 < StandardError
17+
18+
# A module encapsulating note tracking functionality, since currently
19+
# the driver does not have a single exception hierarchy root.
20+
#
21+
# @since 2.10.1
22+
# @api private
23+
module Notable
24+
25+
# Returns an array of strings with additional information about the
26+
# exception.
27+
#
28+
# @return [ Array<String> ] Additional information strings.
29+
#
30+
# @since 2.10.1
31+
# @api public
32+
def notes
33+
if @notes
34+
@notes.dup
35+
else
36+
[]
37+
end
38+
end
39+
40+
# @api private
41+
def add_note(note)
42+
unless @notes
43+
@notes = []
44+
end
45+
@notes << note
46+
end
47+
48+
# @api public
49+
def message
50+
super + notes_tail
51+
end
52+
53+
# @api public
54+
def to_s
55+
super + notes_tail
56+
end
57+
58+
# @api public
59+
def inspect
60+
msg = super
61+
if msg.end_with?('>')
62+
msg[0...msg.length-1] + notes_tail + '>'
63+
else
64+
msg + notes_tail
65+
end
66+
end
67+
68+
private
69+
70+
# @api private
71+
def notes_tail
72+
msg = ''
73+
unless notes.empty?
74+
msg += " (#{notes.join(', ')})"
75+
end
76+
msg
77+
end
78+
end
79+
end
80+
end

lib/mongo/operation/shared/executable.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,18 @@ module Executable
2525
def do_execute(server)
2626
unpin_maybe(session) do
2727
add_error_labels do
28-
get_result(server).tap do |result|
29-
process_result(result, server)
28+
add_server_diagnostics(server) do
29+
get_result(server).tap do |result|
30+
process_result(result, server)
31+
end
3032
end
3133
end
3234
end
3335
end
3436

3537
def execute(server)
3638
do_execute(server).tap do |result|
37-
validate_result(result)
39+
validate_result(result, server)
3840
end
3941
end
4042

lib/mongo/operation/shared/response_handling.rb

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ module ResponseHandling
2222

2323
private
2424

25-
def validate_result(result)
25+
def validate_result(result, server)
2626
unpin_maybe(session) do
2727
add_error_labels do
28-
result.validate!
28+
add_server_diagnostics(server) do
29+
result.validate!
30+
end
2931
end
3032
end
3133
end
@@ -75,6 +77,18 @@ def unpin_maybe(session)
7577
end
7678
raise
7779
end
80+
81+
# Yields to the block and, if the block raises an exception, adds a note
82+
# to the exception with the address of the specified server.
83+
#
84+
# This method is intended to add server address information to exceptions
85+
# raised during execution of operations on servers.
86+
def add_server_diagnostics(server)
87+
yield
88+
rescue Mongo::Error, Mongo::Error::AuthError => e
89+
e.add_note("on #{server.address.seed}")
90+
raise e
91+
end
7892
end
7993
end
8094
end

lib/mongo/operation/shared/write.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def execute(server)
4141
else
4242
self.class::Command.new(spec).execute(server)
4343
end
44-
validate_result(result)
44+
validate_result(result, server)
4545
end
4646

4747
# Execute the bulk write operation.

lib/mongo/retryable.rb

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -206,19 +206,25 @@ def write_with_retry(session, write_concern, ending_transaction = false, &block)
206206
return legacy_write_with_retry(server, session, &block)
207207
end
208208

209+
txn_num = if session.in_transaction?
210+
session.txn_num
211+
else
212+
session.next_txn_num
213+
end
209214
begin
210-
txn_num = session.in_transaction? ? session.txn_num : session.next_txn_num
211215
yield(server, txn_num, false)
212216
rescue Error::SocketError, Error::SocketTimeoutError => e
217+
e.add_note("attempt 1")
213218
if session.in_transaction? && !ending_transaction
214-
raise
219+
raise e
215220
end
216221
retry_write(e, session, txn_num, &block)
217222
rescue Error::OperationFailure => e
223+
e.add_note("attempt 1")
218224
if e.unsupported_retryable_write?
219225
raise_unsupported_error(e)
220226
elsif (session.in_transaction? && !ending_transaction) || !e.write_retryable?
221-
raise
227+
raise e
222228
end
223229

224230
retry_write(e, session, txn_num, &block)
@@ -274,16 +280,17 @@ def legacy_write_with_retry(server = nil, session = nil)
274280
server ||= select_server(cluster, ServerSelector.primary, session)
275281
yield server
276282
rescue Error::OperationFailure => e
283+
e.add_note("attempt #{attempt + 1}")
277284
server = nil
278285
if attempt > client.max_write_retries
279-
raise
286+
raise e
280287
end
281288
if e.write_retryable? && !(session && session.in_transaction?)
282289
log_retry(e, message: 'Legacy write retry')
283290
cluster.scan!(false)
284291
retry
285292
else
286-
raise
293+
raise e
287294
end
288295
end
289296
end
@@ -296,13 +303,15 @@ def modern_read_with_retry(session, server_selector, &block)
296303
begin
297304
yield server
298305
rescue Error::SocketError, Error::SocketTimeoutError => e
306+
e.add_note("attempt #{attempt + 1}")
299307
if session.in_transaction?
300-
raise
308+
raise e
301309
end
302310
retry_read(e, server_selector, session, &block)
303311
rescue Error::OperationFailure => e
312+
e.add_note("attempt #{attempt + 1}")
304313
if session.in_transaction? || !e.write_retryable?
305-
raise
314+
raise e
306315
end
307316
retry_read(e, server_selector, session, &block)
308317
end
@@ -315,23 +324,25 @@ def legacy_read_with_retry(session, server_selector)
315324
attempt += 1
316325
yield server
317326
rescue Error::SocketError, Error::SocketTimeoutError => e
327+
e.add_note("attempt #{attempt + 1}")
318328
if attempt > client.max_read_retries || (session && session.in_transaction?)
319-
raise
329+
raise e
320330
end
321331
log_retry(e, message: 'Legacy read retry')
322332
server = select_server(cluster, server_selector, session)
323333
retry
324334
rescue Error::OperationFailure => e
335+
e.add_note("attempt #{attempt + 1}")
325336
if cluster.sharded? && e.retryable? && !(session && session.in_transaction?)
326337
if attempt > client.max_read_retries
327-
raise
338+
raise e
328339
end
329340
log_retry(e, message: 'Legacy read retry')
330341
sleep(client.read_retry_interval)
331342
server = select_server(cluster, server_selector, session)
332343
retry
333344
else
334-
raise
345+
raise e
335346
end
336347
end
337348
end
@@ -354,7 +365,8 @@ def retry_write_allowed?(session, write_concern)
354365
def retry_read(original_error, server_selector, session, &block)
355366
begin
356367
server = select_server(cluster, server_selector, session)
357-
rescue
368+
rescue => e
369+
original_error.add_note("later retry failed: #{e.class}: #{e}")
358370
raise original_error
359371
end
360372

@@ -363,11 +375,17 @@ def retry_read(original_error, server_selector, session, &block)
363375
begin
364376
yield server, true
365377
rescue Error::SocketError, Error::SocketTimeoutError => e
378+
e.add_note("attempt 2")
366379
raise e
367380
rescue Error::OperationFailure => e
368-
raise original_error unless e.write_retryable?
381+
unless e.write_retryable?
382+
original_error.add_note("later retry failed: #{e.class}: #{e}")
383+
raise original_error
384+
end
385+
e.add_note("attempt 2")
369386
raise e
370-
rescue
387+
rescue => e
388+
original_error.add_note("later retry failed: #{e.class}: #{e}")
371389
raise original_error
372390
end
373391
end
@@ -379,15 +397,25 @@ def retry_write(original_error, session, txn_num, &block)
379397
# a socket error or a not master error should have marked the respective
380398
# server unknown). Here we just need to wait for server selection.
381399
server = select_server(cluster, ServerSelector.primary, session)
382-
raise original_error unless (server.retry_writes? && txn_num)
400+
unless server.retry_writes?
401+
original_error.add_note('did not retry because server selected for retry does not supoprt retryable writes')
402+
raise original_error
403+
end
383404
log_retry(original_error, message: 'Write retry')
384405
yield(server, txn_num, true)
385406
rescue Error::SocketError, Error::SocketTimeoutError => e
407+
e.add_note('attempt 2')
386408
raise e
387409
rescue Error::OperationFailure => e
388-
raise original_error unless e.write_retryable?
389-
raise e
390-
rescue
410+
if e.write_retryable?
411+
e.add_note('attempt 2')
412+
raise e
413+
else
414+
original_error.add_note("later retry failed: #{e.class}: #{e}")
415+
raise original_error
416+
end
417+
rescue => e
418+
original_error.add_note("later retry failed: #{e.class}: #{e}")
391419
raise original_error
392420
end
393421

spec/integration/change_stream_spec.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def clear_fail_point_before
9494
it 'watch raises error' do
9595
expect do
9696
authorized_collection.watch
97-
end.to raise_error(Mongo::Error::OperationFailure, "Failing command due to 'failCommand' failpoint (100)")
97+
end.to raise_error(Mongo::Error::OperationFailure, /Failing command due to 'failCommand' failpoint \(100\)/)
9898
end
9999
end
100100

@@ -243,7 +243,7 @@ def clear_fail_point_before
243243

244244
expect do
245245
enum.next
246-
end.to raise_error(Mongo::Error::OperationFailure, "Failing command due to 'failCommand' failpoint (101)")
246+
end.to raise_error(Mongo::Error::OperationFailure, /Failing command due to 'failCommand' failpoint \(101\)/)
247247
end
248248
end
249249
end
@@ -354,7 +354,7 @@ def clear_fail_point_before
354354

355355
expect do
356356
enum.try_next
357-
end.to raise_error(Mongo::Error::OperationFailure, "Failing command due to 'failCommand' failpoint (101)")
357+
end.to raise_error(Mongo::Error::OperationFailure, /Failing command due to 'failCommand' failpoint \(101\)/)
358358
end
359359
end
360360

@@ -377,7 +377,7 @@ def clear_fail_point_before
377377

378378
expect do
379379
enum.try_next
380-
end.to raise_error(Mongo::Error::OperationFailure, "Failing command due to 'failCommand' failpoint (101)")
380+
end.to raise_error(Mongo::Error::OperationFailure, /Failing command due to 'failCommand' failpoint \(101\)/)
381381
end
382382
end
383383
end

0 commit comments

Comments
 (0)