Skip to content

Commit ee63d9e

Browse files
RUBY-3602 Include server address in OperationFailure and BulkWriteError messages (#3027)
1 parent db52800 commit ee63d9e

13 files changed

Lines changed: 359 additions & 6 deletions

lib/mongo.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ def self.delegate_option(obj, opt)
101101
delegate_option Config, :broken_view_options
102102
delegate_option Config, :validate_update_replace
103103
delegate_option Config, :csfle_convert_to_ruby_types
104+
delegate_option Config, :include_server_address_in_errors
104105
end
105106

106107
# Clears the driver's OCSP response cache.

lib/mongo/bulk_write/result.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ def acknowledged?
2525
@acknowledged
2626
end
2727

28+
# @return [ Array<String> ] Deduplicated list of "host:port" addresses of
29+
# the servers that produced this bulk write's operations.
30+
attr_reader :server_addresses
31+
2832
# Constant for number removed.
2933
#
3034
# @since 2.1.0
@@ -97,13 +101,17 @@ def deleted_count
97101
#
98102
# @param [ BSON::Document, Hash ] results The results document.
99103
# @param [ Boolean ] acknowledged Is the result acknowledged?
104+
# @param [ Array<String> ] server_addresses Deduplicated "host:port"
105+
# addresses of the servers that produced the underlying operation
106+
# results.
100107
#
101108
# @since 2.1.0
102109
#
103110
# @api private
104-
def initialize(results, acknowledged)
111+
def initialize(results, acknowledged, server_addresses = [])
105112
@results = results
106113
@acknowledged = acknowledged
114+
@server_addresses = Array(server_addresses).compact.uniq
107115
end
108116

109117
# Returns the number of documents inserted.
@@ -189,7 +197,7 @@ def upserted_ids
189197
#
190198
# @since 2.1.0
191199
def validate!
192-
raise Error::BulkWriteError.new(@results) if @results['writeErrors'] || @results['writeConcernErrors']
200+
raise Error::BulkWriteError.new(@results, server_addresses: @server_addresses) if @results['writeErrors'] || @results['writeConcernErrors']
193201

194202
self
195203
end

lib/mongo/bulk_write/result_combiner.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ class ResultCombiner
2828
# @return [ Hash ] results The results hash.
2929
attr_reader :results
3030

31+
# @return [ Array<String> ] Deduplicated list of "host:port" addresses of
32+
# the servers that produced the combined operation results.
33+
attr_reader :server_addresses
34+
3135
# Create the new result combiner.
3236
#
3337
# @api private
@@ -39,6 +43,7 @@ class ResultCombiner
3943
def initialize
4044
@results = {}
4145
@count = 0
46+
@server_addresses = []
4247
end
4348

4449
# Adds a result to the overall results.
@@ -68,6 +73,8 @@ def combine!(result, count)
6873
combine_errors!(result)
6974
@count += count
7075
@acknowledged = result.acknowledged?
76+
seed = result.connection_description&.address&.seed
77+
@server_addresses << seed if seed && !@server_addresses.include?(seed)
7178
end
7279

7380
# Get the final result.
@@ -78,7 +85,7 @@ def combine!(result, count)
7885
#
7986
# @since 2.1.0
8087
def result
81-
BulkWrite::Result.new(results, @acknowledged).validate!
88+
BulkWrite::Result.new(results, @acknowledged, @server_addresses).validate!
8289
end
8390

8491
private

lib/mongo/config.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ module Config
2929
# decryption instead of BSON types.
3030
option :csfle_convert_to_ruby_types, default: false
3131

32+
# When this flag is set to true, the (host:port) of the server that produced
33+
# the error is appended to error messages for OperationFailure and
34+
# BulkWriteError. See RUBY-3602.
35+
option :include_server_address_in_errors, default: false
36+
3237
# Set the configuration options.
3338
#
3439
# @example Set the options.

lib/mongo/error/bulk_write_error.rb

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,26 @@ class BulkWriteError < Error
3434
# @return [ BSON::Document ] result The error result.
3535
attr_reader :result
3636

37+
# @return [ Array<String> ] Deduplicated list of "host:port" addresses of
38+
# the servers that produced this bulk write error. Empty when no
39+
# addresses were supplied.
40+
attr_reader :server_addresses
41+
3742
# Instantiate the new exception.
3843
#
3944
# @example Instantiate the exception.
4045
# Mongo::Error::BulkWriteError.new(response)
4146
#
4247
# @param [ Hash ] result A processed response from the server
4348
# reporting results of the operation.
49+
# @param [ Array<String | Mongo::Address | Mongo::Server::Description> ]
50+
# server_addresses Addresses of the servers that produced this error.
51+
# Entries are normalized to "host:port" strings.
4452
#
4553
# @since 2.0.0
46-
def initialize(result)
54+
def initialize(result, server_addresses: nil)
4755
@result = result
56+
@server_addresses = normalize_server_addresses(server_addresses)
4857

4958
# Exception constructor behaves differently for a nil argument and
5059
# for no argument. Avoid passing nil explicitly.
@@ -93,8 +102,27 @@ def build_message
93102

94103
fragment = "Multiple errors: #{fragment}" if errors.length > 1
95104

105+
if Mongo.include_server_address_in_errors && @server_addresses.any?
106+
fragment = "#{fragment} (on #{@server_addresses.join(', ')})"
107+
end
108+
96109
fragment
97110
end
111+
112+
def normalize_server_addresses(value)
113+
return [] if value.nil?
114+
115+
Array(value).filter_map do |entry|
116+
case entry
117+
when String then entry
118+
when Mongo::Address then entry.seed
119+
when Mongo::Server::Description then entry.address&.seed
120+
else
121+
raise ArgumentError,
122+
"server_addresses entries must be String, Mongo::Address, or Mongo::Server::Description; got #{entry.class}"
123+
end
124+
end.uniq
125+
end
98126
end
99127
end
100128
end

lib/mongo/error/operation_failure.rb

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ module OperationFailure::Family
5353
# @api experimental
5454
attr_reader :server_message
5555

56+
# @return [ String | nil ] The address ("host:port") of the server
57+
# that produced this error, if known.
58+
attr_reader :server_address
59+
5660
# Error codes and code names that should result in a failing getMore
5761
# command on a change stream NOT being resumed.
5862
#
@@ -172,6 +176,8 @@ def write_concern_error?
172176
# error document.
173177
# @option options [ String ] server_message The server-returned
174178
# error message parsed from the response.
179+
# @option options [ nil | String | Mongo::Address | Mongo::Server::Description ]
180+
# :server_address The address of the server that produced the error.
175181
# @option options [ Hash ] :write_concern_error_document The
176182
# server-supplied write concern error document, if any.
177183
# @option options [ Integer ] :write_concern_error_code Error code for
@@ -185,7 +191,8 @@ def write_concern_error?
185191
# @option options [ true | false ] :wtimeout Whether the error is a wtimeout.
186192
def initialize(message = nil, result = nil, options = {})
187193
@details = retrieve_details(options[:document])
188-
super(append_details(message, @details))
194+
@server_address = normalize_server_address(options[:server_address])
195+
super(append_server_address(append_details(message, @details)))
189196

190197
@result = result
191198
@code = options[:code]
@@ -241,6 +248,38 @@ def append_details(message, details)
241248

242249
message + " -- #{details.to_json}"
243250
end
251+
252+
# Append the server address suffix to the message when the
253+
# Mongo.include_server_address_in_errors flag is enabled and
254+
# a server address is known.
255+
#
256+
# @return [ String | nil ] the message with the suffix appended,
257+
# or the original message unchanged.
258+
def append_server_address(message)
259+
return message unless Mongo.include_server_address_in_errors
260+
return message if @server_address.nil?
261+
return "(on #{@server_address})" if message.nil? || message.empty?
262+
263+
"#{message} (on #{@server_address})"
264+
end
265+
266+
# Normalize a server_address option into a String "host:port" form.
267+
#
268+
# @param [ nil | String | Mongo::Address | Mongo::Server::Description ] value
269+
#
270+
# @return [ String | nil ] The normalized address, or nil.
271+
def normalize_server_address(value)
272+
case value
273+
when nil then nil
274+
when String then value
275+
when Mongo::Address then value.seed
276+
when Mongo::Server::Description
277+
value.address.is_a?(Mongo::Address) ? value.address.seed : nil
278+
else
279+
raise ArgumentError,
280+
"server_address must be nil, String, Mongo::Address, or Mongo::Server::Description; got #{value.class}"
281+
end
282+
end
244283
end
245284

246285
# OperationFailure is the canonical implementor of the

lib/mongo/operation/result.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,8 @@ def error
355355
wtimeout: parser.wtimeout,
356356
connection_description: connection_description,
357357
document: parser.document,
358-
server_message: parser.server_message
358+
server_message: parser.server_message,
359+
server_address: connection_description
359360
)
360361
end
361362

spec/integration/bulk_write_error_message_spec.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,26 @@
6363
e.message.scan(' -- ').length.should be <= 1
6464
end
6565
end
66+
67+
context 'when include_server_address_in_errors is enabled' do
68+
around do |example|
69+
original = Mongo.include_server_address_in_errors
70+
Mongo.include_server_address_in_errors = true
71+
example.run
72+
ensure
73+
Mongo.include_server_address_in_errors = original
74+
end
75+
76+
it 'includes the server address suffix in the bulk error message' do
77+
collection.insert_one(_id: 1)
78+
begin
79+
collection.insert_many([ { _id: 1 }, { _id: 2 }, { _id: 2 } ])
80+
rescue Mongo::Error::BulkWriteError => e
81+
expect(e.message).to match(/\(on [^)]+:\d+(?:, [^)]+:\d+)*\)\z/)
82+
expect(e.server_addresses).not_to be_empty
83+
else
84+
raise 'expected BulkWriteError'
85+
end
86+
end
87+
end
6688
end

spec/integration/operation_failure_message_spec.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,22 @@
5252
/User bogus.*is not authorized.*\[18:AuthenticationFailed\]: Authentication failed/)
5353
end
5454
end
55+
56+
context 'when include_server_address_in_errors is enabled' do
57+
around do |example|
58+
original = Mongo.include_server_address_in_errors
59+
Mongo.include_server_address_in_errors = true
60+
example.run
61+
ensure
62+
Mongo.include_server_address_in_errors = original
63+
end
64+
65+
it 'includes the server address in the message' do
66+
client.command(bogus_command: nil)
67+
raise('Should have raised')
68+
rescue Mongo::Error::OperationFailure => e
69+
expect(e.message).to match(/\(on [^)]+:\d+\)\z/)
70+
expect(e.server_address).to be_a(String)
71+
end
72+
end
5573
end
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe Mongo::BulkWrite::ResultCombiner do
6+
describe 'server_addresses accumulation' do
7+
let(:combiner) { described_class.new }
8+
9+
def stub_op_result(seed)
10+
description = instance_double(
11+
Mongo::Server::Description,
12+
address: Mongo::Address.new(seed)
13+
)
14+
result = double('op_result')
15+
allow(result).to receive(:write_concern_error?).and_return(false)
16+
allow(result).to receive(:acknowledged?).and_return(true)
17+
allow(result).to receive(:aggregate_write_errors).and_return(nil)
18+
allow(result).to receive(:aggregate_write_concern_errors).and_return(nil)
19+
allow(result).to receive(:validate!).and_return(true)
20+
allow(result).to receive(:respond_to?).and_return(false)
21+
allow(result).to receive(:connection_description).and_return(description)
22+
result
23+
end
24+
25+
it 'collects unique seeds from combined results' do
26+
combiner.combine!(stub_op_result('h1:27017'), 1)
27+
combiner.combine!(stub_op_result('h2:27017'), 1)
28+
combiner.combine!(stub_op_result('h1:27017'), 1)
29+
30+
final = combiner.result
31+
expect(final.server_addresses).to contain_exactly('h1:27017', 'h2:27017')
32+
end
33+
34+
it 'defaults to empty when no results combined' do
35+
expect(combiner.server_addresses).to eq([])
36+
end
37+
end
38+
end

0 commit comments

Comments
 (0)