diff --git a/lib/mongo/error.rb b/lib/mongo/error.rb index baa07e2334..89585f5694 100644 --- a/lib/mongo/error.rb +++ b/lib/mongo/error.rb @@ -96,7 +96,7 @@ def change_stream_resumable? TRANSIENT_TRANSACTION_ERROR_LABEL = 'TransientTransactionError'.freeze def initialize(msg = nil) - @labels = [] + @labels ||= [] super(msg) end @@ -114,6 +114,18 @@ def label?(label) @labels.include?(label) end + # Gets the set of labels associated with the error. + # + # @example + # error.labels + # + # @return [ Array ] The set of labels. + # + # @since 2.7.0 + def labels + @labels.dup + end + private def add_label(label) diff --git a/lib/mongo/error/operation_failure.rb b/lib/mongo/error/operation_failure.rb index 70bab1a3d6..589dcc51f9 100644 --- a/lib/mongo/error/operation_failure.rb +++ b/lib/mongo/error/operation_failure.rb @@ -181,6 +181,7 @@ def initialize(message = nil, result = nil, options = {}) @result = result @code = options[:code] @code_name = options[:code_name] + @labels = options[:labels] super(message) end end diff --git a/lib/mongo/error/parser.rb b/lib/mongo/error/parser.rb index 90095158c1..f4fe305465 100644 --- a/lib/mongo/error/parser.rb +++ b/lib/mongo/error/parser.rb @@ -62,6 +62,10 @@ class Parser # @since 2.6.0 attr_reader :code_name + # @return [ Array ] labels The set of labels associated with the error. + # @since 2.7.0 + attr_reader :labels + # Create the new parser with the returned document. # # @example Create the new parser. @@ -88,6 +92,7 @@ def parse! document[WRITE_CONCERN_ERROR]) if document[WRITE_CONCERN_ERROR] parse_flag(@message) parse_code + parse_labels end def parse_single(message, key, doc = document) @@ -147,6 +152,10 @@ def parse_code end end end + + def parse_labels + @labels = document['errorLabels'] || [] + end end end end diff --git a/lib/mongo/operation/result.rb b/lib/mongo/operation/result.rb index 1be4058d58..22f2085050 100644 --- a/lib/mongo/operation/result.rb +++ b/lib/mongo/operation/result.rb @@ -267,7 +267,7 @@ def raise_operation_failure raise Error::OperationFailure.new( parser.message, self, - :code => parser.code, :code_name => parser.code_name) + :code => parser.code, :code_name => parser.code_name, :labels => parser.labels) end private :raise_operation_failure @@ -312,6 +312,18 @@ def cluster_time first_document && first_document[CLUSTER_TIME] end + # Gets the set of error labels associated with the result. + # + # @example Get the labels. + # result.labels + # + # @return [ Array ] labels The set of labels. + # + # @since 2.7.0 + def labels + @labels ||= parser.labels + end + private def aggregate_returned_count diff --git a/spec/mongo/error/operation_failure_spec.rb b/spec/mongo/error/operation_failure_spec.rb index 58bbb4dfa2..9e2cb870cb 100644 --- a/spec/mongo/error/operation_failure_spec.rb +++ b/spec/mongo/error/operation_failure_spec.rb @@ -149,4 +149,74 @@ end end end + + describe '#labels' do + + context 'when the result is nil' do + + subject do + described_class.new('not master (10107)', nil, + :code => 10107, :code_name => 'NotMaster') + end + + it 'has no labels' do + expect(subject.labels).to eq([]) + end + end + + context 'when the result is not nil' do + + let(:reply_document) do + { + 'code' => 251, + 'codeName' => 'NoSuchTransaction', + 'errorLabels' => labels, + } + end + + let(:reply) do + Mongo::Protocol::Reply.new.tap do |r| + # Because this was not created by Mongo::Protocol::Reply::deserialize, we need to manually + # initialize the fields. + r.instance_variable_set(:@documents, [reply_document]) + r.instance_variable_set(:@flags, []) + end + end + + let(:result) do + Mongo::Operation::Result.new(reply) + end + + subject do + begin + result.send(:raise_operation_failure) + rescue => e + e + end + end + + context 'when the error has no labels' do + + let(:labels) do + [] + end + + it 'has the correct labels' do + expect(subject.labels).to eq(labels) + end + end + + + context 'when the error has labels' do + + let(:labels) do + [ Mongo::Error::TRANSIENT_TRANSACTION_ERROR_LABEL ] + end + + it 'has the correct labels' do + expect(subject.labels).to eq(labels) + end + end + end + end end diff --git a/spec/mongo/error/parser_spec.rb b/spec/mongo/error/parser_spec.rb index 4c49e5e93f..136e1eff96 100644 --- a/spec/mongo/error/parser_spec.rb +++ b/spec/mongo/error/parser_spec.rb @@ -95,7 +95,7 @@ end end end - + describe '#code' do let(:parser) do described_class.new(document) @@ -105,7 +105,7 @@ let(:document) do { 'ok' => 0, 'errmsg' => 'not master', 'code' => 10107, 'codeName' => 'NotMaster' } end - + it 'returns the code' do expect(parser.code).to eq(10107) end @@ -115,7 +115,7 @@ let(:document) do { 'ok' => 0, 'errmsg' => 'not master' } end - + it 'returns nil' do expect(parser.code).to eq(nil) end @@ -143,7 +143,7 @@ end end end - + describe '#code_name' do let(:parser) do described_class.new(document) @@ -153,7 +153,7 @@ let(:document) do { 'ok' => 0, 'errmsg' => 'not master', 'code' => 10107, 'codeName' => 'NotMaster' } end - + it 'returns the code name' do expect(parser.code_name).to eq('NotMaster') end @@ -163,7 +163,7 @@ let(:document) do { 'ok' => 0, 'errmsg' => 'not master' } end - + it 'returns nil' do expect(parser.code_name).to eq(nil) end @@ -192,7 +192,7 @@ end end end - + describe '#document' do let(:parser) do described_class.new(document) @@ -201,12 +201,12 @@ let(:document) do { 'ok' => 0, 'errmsg' => 'not master', 'code' => 10107, 'codeName' => 'NotMaster' } end - + it 'returns the document' do expect(parser.document).to eq(document) end end - + describe '#replies' do let(:parser) do described_class.new(document) @@ -216,10 +216,44 @@ let(:document) do { 'ok' => 0, 'errmsg' => 'not master', 'code' => 10107, 'codeName' => 'NotMaster' } end - + it 'returns nil' do expect(parser.replies).to eq(nil) end end end + + describe '#labels' do + let(:parser) do + described_class.new(document) + end + + let(:document) do + { + 'code' => 251, + 'codeName' => 'NoSuchTransaction', + 'errorLabels' => labels, + } + end + + context 'when there are no labels' do + let(:labels) do + [] + end + + it 'has the correct labels' do + expect(parser.labels).to eq(labels) + end + end + + context 'when there are labels' do + let(:labels) do + [ Mongo::Error::TRANSIENT_TRANSACTION_ERROR_LABEL ] + end + + it 'has the correct labels' do + expect(parser.labels).to eq(labels) + end + end + end end diff --git a/spec/support/transactions/operation.rb b/spec/support/transactions/operation.rb index 2f82b75903..9101fe452a 100644 --- a/spec/support/transactions/operation.rb +++ b/spec/support/transactions/operation.rb @@ -114,12 +114,12 @@ def execute(collection, session0, session1) { 'errorCodeName' => err_doc['codeName'] || err_doc['writeConcernError']['codeName'], 'errorContains' => e.message, - 'errorLabels' => (e.instance_variable_get(:@labels) || []) + (err_doc['errorLabels'] || []) + 'errorLabels' => e.labels } rescue Mongo::Error => e { 'errorContains' => e.message, - 'errorLabels' => e.instance_variable_get(:@labels) || [] + 'errorLabels' => e.labels } end