diff --git a/lib/mongo/auth/base.rb b/lib/mongo/auth/base.rb index efcae3c151..7c49e96527 100644 --- a/lib/mongo/auth/base.rb +++ b/lib/mongo/auth/base.rb @@ -117,7 +117,7 @@ def dispatch_msg(connection, conversation, msg) else nil end - result = Operation::Result.new(reply, connection.description, connection_global_id) + result = Operation::Result.new(reply, connection.description, connection_global_id, context: context) connection.update_cluster_time(result) reply_document end diff --git a/lib/mongo/collection/helpers.rb b/lib/mongo/collection/helpers.rb index a6feaf9c99..f21dfedc87 100644 --- a/lib/mongo/collection/helpers.rb +++ b/lib/mongo/collection/helpers.rb @@ -30,7 +30,7 @@ module Helpers # @return [ Result ] The result of the execution. def do_drop(operation, session, context) operation.execute(next_primary(nil, session), context: context) - rescue Error::OperationFailure => ex + rescue Error::OperationFailure::Family => ex # NamespaceNotFound if ex.code == 26 || ex.code.nil? && ex.message =~ /ns not found/ false diff --git a/lib/mongo/collection/view/change_stream.rb b/lib/mongo/collection/view/change_stream.rb index ee2d9c23bd..903a24414e 100644 --- a/lib/mongo/collection/view/change_stream.rb +++ b/lib/mongo/collection/view/change_stream.rb @@ -238,7 +238,7 @@ def close unless closed? begin @cursor.close - rescue Error::OperationFailure, Error::SocketError, Error::SocketTimeoutError, Error::MissingConnection + rescue Error::OperationFailure::Family, Error::SocketError, Error::SocketTimeoutError, Error::MissingConnection # ignore end @cursor = nil diff --git a/lib/mongo/collection/view/iterable.rb b/lib/mongo/collection/view/iterable.rb index cd4f6f6694..9f6f713c3b 100644 --- a/lib/mongo/collection/view/iterable.rb +++ b/lib/mongo/collection/view/iterable.rb @@ -100,7 +100,7 @@ def each # # @return [ nil ] Always nil. # - # @raise [ Error::OperationFailure ] If the server cursor close fails. + # @raise [ Error::OperationFailure::Family ] If the server cursor close fails. # # @since 2.1.0 def close_query diff --git a/lib/mongo/collection/view/readable.rb b/lib/mongo/collection/view/readable.rb index 4d4116a75f..d74c7f3108 100644 --- a/lib/mongo/collection/view/readable.rb +++ b/lib/mongo/collection/view/readable.rb @@ -304,7 +304,7 @@ def estimated_document_count(opts = {}) result.n.to_i end end - rescue Error::OperationFailure => exc + rescue Error::OperationFailure::Family => exc if exc.code == 26 # NamespaceNotFound # This should only happen with the aggregation pipeline path diff --git a/lib/mongo/cursor.rb b/lib/mongo/cursor.rb index d24e137b5b..e1e5dd3376 100644 --- a/lib/mongo/cursor.rb +++ b/lib/mongo/cursor.rb @@ -299,7 +299,7 @@ def close end nil - rescue Error::OperationFailure, Error::SocketError, Error::SocketTimeoutError, Error::ServerNotUsable + rescue Error::OperationFailure::Family, Error::SocketError, Error::SocketTimeoutError, Error::ServerNotUsable # Errors are swallowed since there is noting can be done by handling them. ensure end_session diff --git a/lib/mongo/error.rb b/lib/mongo/error.rb index 6c5bbf44e3..8750301076 100644 --- a/lib/mongo/error.rb +++ b/lib/mongo/error.rb @@ -217,6 +217,7 @@ def write_concern_error_labels require 'mongo/error/server_api_conflict' require 'mongo/error/server_api_not_supported' require 'mongo/error/server_not_usable' +require 'mongo/error/server_timeout_error' require 'mongo/error/transactions_not_supported' require 'mongo/error/timeout_error' require 'mongo/error/unknown_payload_type' diff --git a/lib/mongo/error/operation_failure.rb b/lib/mongo/error/operation_failure.rb index 1cc47f520b..236eb7083f 100644 --- a/lib/mongo/error/operation_failure.rb +++ b/lib/mongo/error/operation_failure.rb @@ -18,242 +18,247 @@ module Mongo class Error - # Raised when an operation fails for some reason. - # - # @since 2.0.0 class OperationFailure < Error - extend Forwardable - include SdamErrorDetection - include ReadWriteRetryable + # Implements the behavior for an OperationFailure error. Other errors + # (e.g. ServerTimeoutError) may also implement this, so that they may + # be recognized and treated as OperationFailure errors. + module OperationFailure::Family + extend Forwardable + include SdamErrorDetection + include ReadWriteRetryable - def_delegators :@result, :operation_time + def_delegators :@result, :operation_time - # @!method connection_description - # - # @return [ Server::Description ] Server description of the server that - # the operation that this exception refers to was performed on. - # - # @api private - def_delegator :@result, :connection_description + # @!method connection_description + # + # @return [ Server::Description ] Server description of the server that + # the operation that this exception refers to was performed on. + # + # @api private + def_delegator :@result, :connection_description - # @return [ Integer ] The error code parsed from the document. - # - # @since 2.6.0 - attr_reader :code + # @return [ Integer ] The error code parsed from the document. + # + # @since 2.6.0 + attr_reader :code - # @return [ String ] The error code name parsed from the document. - # - # @since 2.6.0 - attr_reader :code_name + # @return [ String ] The error code name parsed from the document. + # + # @since 2.6.0 + attr_reader :code_name - # @return [ String ] The server-returned error message - # parsed from the response. - # - # @api experimental - attr_reader :server_message + # @return [ String ] The server-returned error message + # parsed from the response. + # + # @api experimental + attr_reader :server_message - # Error codes and code names that should result in a failing getMore - # command on a change stream NOT being resumed. - # - # @api private - CHANGE_STREAM_RESUME_ERRORS = [ - {code_name: 'HostUnreachable', code: 6}, - {code_name: 'HostNotFound', code: 7}, - {code_name: 'NetworkTimeout', code: 89}, - {code_name: 'ShutdownInProgress', code: 91}, - {code_name: 'PrimarySteppedDown', code: 189}, - {code_name: 'ExceededTimeLimit', code: 262}, - {code_name: 'SocketException', code: 9001}, - {code_name: 'NotMaster', code: 10107}, - {code_name: 'InterruptedAtShutdown', code: 11600}, - {code_name: 'InterruptedDueToReplStateChange', code: 11602}, - {code_name: 'NotPrimaryNoSecondaryOk', code: 13435}, - {code_name: 'NotMasterOrSecondary', code: 13436}, + # Error codes and code names that should result in a failing getMore + # command on a change stream NOT being resumed. + # + # @api private + CHANGE_STREAM_RESUME_ERRORS = [ + {code_name: 'HostUnreachable', code: 6}, + {code_name: 'HostNotFound', code: 7}, + {code_name: 'NetworkTimeout', code: 89}, + {code_name: 'ShutdownInProgress', code: 91}, + {code_name: 'PrimarySteppedDown', code: 189}, + {code_name: 'ExceededTimeLimit', code: 262}, + {code_name: 'SocketException', code: 9001}, + {code_name: 'NotMaster', code: 10107}, + {code_name: 'InterruptedAtShutdown', code: 11600}, + {code_name: 'InterruptedDueToReplStateChange', code: 11602}, + {code_name: 'NotPrimaryNoSecondaryOk', code: 13435}, + {code_name: 'NotMasterOrSecondary', code: 13436}, - {code_name: 'StaleShardVersion', code: 63}, - {code_name: 'FailedToSatisfyReadPreference', code: 133}, - {code_name: 'StaleEpoch', code: 150}, - {code_name: 'RetryChangeStream', code: 234}, - {code_name: 'StaleConfig', code: 13388}, - ].freeze + {code_name: 'StaleShardVersion', code: 63}, + {code_name: 'FailedToSatisfyReadPreference', code: 133}, + {code_name: 'StaleEpoch', code: 150}, + {code_name: 'RetryChangeStream', code: 234}, + {code_name: 'StaleConfig', code: 13388}, + ].freeze - # Change stream can be resumed when these error messages are encountered. - # - # @since 2.6.0 - # @api private - CHANGE_STREAM_RESUME_MESSAGES = ReadWriteRetryable::WRITE_RETRY_MESSAGES + # Change stream can be resumed when these error messages are encountered. + # + # @since 2.6.0 + # @api private + CHANGE_STREAM_RESUME_MESSAGES = ReadWriteRetryable::WRITE_RETRY_MESSAGES - # Can the change stream on which this error occurred be resumed, - # provided the operation that triggered this error was a getMore? - # - # @example Is the error resumable for the change stream? - # error.change_stream_resumable? - # - # @return [ true, false ] Whether the error is resumable. - # - # @since 2.6.0 - def change_stream_resumable? - if @result && @result.is_a?(Mongo::Operation::GetMore::Result) - # CursorNotFound exceptions are always resumable because the server - # is not aware of the cursor id, and thus cannot determine if - # the cursor is a change stream and cannot add the - # ResumableChangeStreamError label. - return true if code == 43 + # Can the change stream on which this error occurred be resumed, + # provided the operation that triggered this error was a getMore? + # + # @example Is the error resumable for the change stream? + # error.change_stream_resumable? + # + # @return [ true, false ] Whether the error is resumable. + # + # @since 2.6.0 + def change_stream_resumable? + if @result && @result.is_a?(Mongo::Operation::GetMore::Result) + # CursorNotFound exceptions are always resumable because the server + # is not aware of the cursor id, and thus cannot determine if + # the cursor is a change stream and cannot add the + # ResumableChangeStreamError label. + return true if code == 43 - # Connection description is not populated for unacknowledged writes. - if connection_description.max_wire_version >= 9 - label?('ResumableChangeStreamError') + # Connection description is not populated for unacknowledged writes. + if connection_description.max_wire_version >= 9 + label?('ResumableChangeStreamError') + else + change_stream_resumable_code? + end else - change_stream_resumable_code? + false end - else - false end - end - def change_stream_resumable_code? - CHANGE_STREAM_RESUME_ERRORS.any? { |e| e[:code] == code } - end - private :change_stream_resumable_code? + def change_stream_resumable_code? + CHANGE_STREAM_RESUME_ERRORS.any? { |e| e[:code] == code } + end + private :change_stream_resumable_code? - # @return [ true | false ] Whether the failure includes a write - # concern error. A failure may have a top level error and a write - # concern error or either one of the two. - # - # @since 2.10.0 - def write_concern_error? - !!@write_concern_error_document - end + # @return [ true | false ] Whether the failure includes a write + # concern error. A failure may have a top level error and a write + # concern error or either one of the two. + # + # @since 2.10.0 + def write_concern_error? + !!@write_concern_error_document + end - # Returns the write concern error document as it was reported by the - # server, if any. - # - # @return [ Hash | nil ] Write concern error as reported to the server. - attr_reader :write_concern_error_document + # Returns the write concern error document as it was reported by the + # server, if any. + # + # @return [ Hash | nil ] Write concern error as reported to the server. + attr_reader :write_concern_error_document - # @return [ Integer | nil ] The error code for the write concern error, - # if a write concern error is present and has a code. - # - # @since 2.10.0 - attr_reader :write_concern_error_code + # @return [ Integer | nil ] The error code for the write concern error, + # if a write concern error is present and has a code. + # + # @since 2.10.0 + attr_reader :write_concern_error_code - # @return [ String | nil ] The code name for the write concern error, - # if a write concern error is present and has a code name. - # - # @since 2.10.0 - attr_reader :write_concern_error_code_name + # @return [ String | nil ] The code name for the write concern error, + # if a write concern error is present and has a code name. + # + # @since 2.10.0 + attr_reader :write_concern_error_code_name - # @return [ String | nil ] The details of the error. - # For WriteConcernErrors this is `document['writeConcernError']['errInfo']`. - # For WriteErrors this is `document['writeErrors'][0]['errInfo']`. - # For all other errors this is nil. - attr_reader :details + # @return [ String | nil ] The details of the error. + # For WriteConcernErrors this is `document['writeConcernError']['errInfo']`. + # For WriteErrors this is `document['writeErrors'][0]['errInfo']`. + # For all other errors this is nil. + attr_reader :details - # @return [ BSON::Document | nil ] The server-returned error document. - # - # @api experimental - attr_reader :document + # @return [ BSON::Document | nil ] The server-returned error document. + # + # @api experimental + attr_reader :document - # Create the operation failure. - # - # @example Create the error object - # OperationFailure.new(message, result) - # - # @example Create the error object with a code and a code name - # OperationFailure.new(message, result, :code => code, :code_name => code_name) - # - # @param [ String ] message The error message. - # @param [ Operation::Result ] result The result object. - # @param [ Hash ] options Additional parameters. - # - # @option options [ Integer ] :code Error code. - # @option options [ String ] :code_name Error code name. - # @option options [ BSON::Document ] :document The server-returned - # error document. - # @option options [ String ] server_message The server-returned - # error message parsed from the response. - # @option options [ Hash ] :write_concern_error_document The - # server-supplied write concern error document, if any. - # @option options [ Integer ] :write_concern_error_code Error code for - # write concern error, if any. - # @option options [ String ] :write_concern_error_code_name Error code - # name for write concern error, if any. - # @option options [ Array ] :write_concern_error_labels Error - # labels for the write concern error, if any. - # @option options [ Array ] :labels The set of labels associated - # with the error. - # @option options [ true | false ] :wtimeout Whether the error is a wtimeout. - def initialize(message = nil, result = nil, options = {}) - @details = retrieve_details(options[:document]) - super(append_details(message, @details)) + # @return [ Operation::Result ] the result object for the operation. + # + # @api private + attr_reader :result - @result = result - @code = options[:code] - @code_name = options[:code_name] - @write_concern_error_document = options[:write_concern_error_document] - @write_concern_error_code = options[:write_concern_error_code] - @write_concern_error_code_name = options[:write_concern_error_code_name] - @write_concern_error_labels = options[:write_concern_error_labels] || [] - @labels = options[:labels] || [] - @wtimeout = !!options[:wtimeout] - @document = options[:document] - @server_message = options[:server_message] - end + # Create the operation failure. + # + # @param [ String ] message The error message. + # @param [ Operation::Result ] result The result object. + # @param [ Hash ] options Additional parameters. + # + # @option options [ Integer ] :code Error code. + # @option options [ String ] :code_name Error code name. + # @option options [ BSON::Document ] :document The server-returned + # error document. + # @option options [ String ] server_message The server-returned + # error message parsed from the response. + # @option options [ Hash ] :write_concern_error_document The + # server-supplied write concern error document, if any. + # @option options [ Integer ] :write_concern_error_code Error code for + # write concern error, if any. + # @option options [ String ] :write_concern_error_code_name Error code + # name for write concern error, if any. + # @option options [ Array ] :write_concern_error_labels Error + # labels for the write concern error, if any. + # @option options [ Array ] :labels The set of labels associated + # with the error. + # @option options [ true | false ] :wtimeout Whether the error is a wtimeout. + def initialize(message = nil, result = nil, options = {}) + @details = retrieve_details(options[:document]) + super(append_details(message, @details)) - # Whether the error is a write concern timeout. - # - # @return [ true | false ] Whether the error is a write concern timeout. - # - # @since 2.7.1 - def wtimeout? - @wtimeout - end + @result = result + @code = options[:code] + @code_name = options[:code_name] + @write_concern_error_document = options[:write_concern_error_document] + @write_concern_error_code = options[:write_concern_error_code] + @write_concern_error_code_name = options[:write_concern_error_code_name] + @write_concern_error_labels = options[:write_concern_error_labels] || [] + @labels = options[:labels] || [] + @wtimeout = !!options[:wtimeout] + @document = options[:document] + @server_message = options[:server_message] + end - # Whether the error is MaxTimeMSExpired. - # - # @return [ true | false ] Whether the error is MaxTimeMSExpired. - # - # @since 2.10.0 - def max_time_ms_expired? - code == 50 # MaxTimeMSExpired - end + # Whether the error is a write concern timeout. + # + # @return [ true | false ] Whether the error is a write concern timeout. + # + # @since 2.7.1 + def wtimeout? + @wtimeout + end - # Whether the error is caused by an attempted retryable write - # on a storage engine that does not support retryable writes. - # - # @return [ true | false ] Whether the error is caused by an attempted - # retryable write on a storage engine that does not support retryable writes. - # - # @since 2.10.0 - def unsupported_retryable_write? - # code 20 is IllegalOperation. - # Note that the document is expected to be a BSON::Document, thus - # either having string keys or providing indifferent access. - code == 20 && server_message&.start_with?("Transaction numbers") || false - end + # Whether the error is MaxTimeMSExpired. + # + # @return [ true | false ] Whether the error is MaxTimeMSExpired. + # + # @since 2.10.0 + def max_time_ms_expired? + code == 50 # MaxTimeMSExpired + end + + # Whether the error is caused by an attempted retryable write + # on a storage engine that does not support retryable writes. + # + # @return [ true | false ] Whether the error is caused by an attempted + # retryable write on a storage engine that does not support retryable writes. + # + # @since 2.10.0 + def unsupported_retryable_write? + # code 20 is IllegalOperation. + # Note that the document is expected to be a BSON::Document, thus + # either having string keys or providing indifferent access. + code == 20 && server_message&.start_with?("Transaction numbers") || false + end - private + private - # Retrieve the details from a document - # - # @return [ Hash | nil ] the details extracted from the document - def retrieve_details(document) - return nil unless document - if wce = document['writeConcernError'] - return wce['errInfo'] - elsif we = document['writeErrors']&.first - return we['errInfo'] + # Retrieve the details from a document + # + # @return [ Hash | nil ] the details extracted from the document + def retrieve_details(document) + return nil unless document + if wce = document['writeConcernError'] + return wce['errInfo'] + elsif we = document['writeErrors']&.first + return we['errInfo'] + end end - end - # Append the details to the message - # - # @return [ String ] the message with the details appended to it - def append_details(message, details) - return message unless details && message - message + " -- #{details.to_json}" + # Append the details to the message + # + # @return [ String ] the message with the details appended to it + def append_details(message, details) + return message unless details && message + message + " -- #{details.to_json}" + end end + + # OperationFailure is the canonical implementor of the + # OperationFailure::Family concern. + include OperationFailure::Family end end end diff --git a/lib/mongo/error/server_timeout_error.rb b/lib/mongo/error/server_timeout_error.rb new file mode 100644 index 0000000000..d3e66a0eaf --- /dev/null +++ b/lib/mongo/error/server_timeout_error.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'mongo/error/timeout_error' + +module Mongo + class Error + # Raised when the server returns error code 50. + class ServerTimeoutError < TimeoutError + include OperationFailure::Family + end + end +end diff --git a/lib/mongo/error/socket_timeout_error.rb b/lib/mongo/error/socket_timeout_error.rb index 25b5980fd8..b8332f61f1 100644 --- a/lib/mongo/error/socket_timeout_error.rb +++ b/lib/mongo/error/socket_timeout_error.rb @@ -15,13 +15,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +require 'mongo/error/timeout_error' + module Mongo class Error # Raised when a socket connection times out. # # @since 2.0.0 - class SocketTimeoutError < Error + class SocketTimeoutError < TimeoutError include WriteRetryable include ChangeStreamResumable end diff --git a/lib/mongo/grid/fs_bucket.rb b/lib/mongo/grid/fs_bucket.rb index 8d723cdf55..8651e0545d 100644 --- a/lib/mongo/grid/fs_bucket.rb +++ b/lib/mongo/grid/fs_bucket.rb @@ -528,7 +528,7 @@ def create_index_if_missing!(collection, index_spec, options = {}) if indexes_view.get(index_spec).nil? indexes_view.create_one(index_spec, options) end - rescue Mongo::Error::OperationFailure => e + rescue Mongo::Error::OperationFailure::Family => e # proceed with index creation if a NamespaceNotFound error is thrown if e.code == 26 indexes_view.create_one(index_spec, options) diff --git a/lib/mongo/operation/insert/op_msg.rb b/lib/mongo/operation/insert/op_msg.rb index 7ab863e6af..6b82e2c10b 100644 --- a/lib/mongo/operation/insert/op_msg.rb +++ b/lib/mongo/operation/insert/op_msg.rb @@ -35,7 +35,7 @@ class OpMsg < OpMsgBase def get_result(connection, context, options = {}) # This is a Mongo::Operation::Insert::Result - Result.new(*dispatch_message(connection, context), @ids) + Result.new(*dispatch_message(connection, context), @ids, context: context) end def selector(connection) diff --git a/lib/mongo/operation/insert/result.rb b/lib/mongo/operation/insert/result.rb index 05995de079..cdde68e252 100644 --- a/lib/mongo/operation/insert/result.rb +++ b/lib/mongo/operation/insert/result.rb @@ -47,11 +47,13 @@ class Result < Operation::Result # Global id of the connection on which the operation that # this result is for was performed. # @param [ Array ] ids The ids of the inserted documents. + # @param [ Operation::Context | nil ] context the operation context that + # was active when this result was produced. # # @since 2.0.0 # @api private - def initialize(replies, connection_description, connection_global_id, ids) - super(replies, connection_description, connection_global_id) + def initialize(replies, connection_description, connection_global_id, ids, context: nil) + super(replies, connection_description, connection_global_id, context: context) @inserted_ids = ids end diff --git a/lib/mongo/operation/list_collections/result.rb b/lib/mongo/operation/list_collections/result.rb index 9b45e8b4fb..e964882f04 100644 --- a/lib/mongo/operation/list_collections/result.rb +++ b/lib/mongo/operation/list_collections/result.rb @@ -85,7 +85,7 @@ def validate! if successful? self else - raise Error::OperationFailure.new( + raise operation_failure_class.new( parser.message, self, code: parser.code, diff --git a/lib/mongo/operation/map_reduce/result.rb b/lib/mongo/operation/map_reduce/result.rb index 8959a99d15..6e6660ee93 100644 --- a/lib/mongo/operation/map_reduce/result.rb +++ b/lib/mongo/operation/map_reduce/result.rb @@ -108,7 +108,7 @@ def time # @example Validate the result. # result.validate! # - # @raise [ Error::OperationFailure ] If an error is in the result. + # @raise [ Error::OperationFailure::Family ] If an error is in the result. # # @return [ Result ] The result if verification passed. # diff --git a/lib/mongo/operation/result.rb b/lib/mongo/operation/result.rb index d6989444b8..9508432ccb 100644 --- a/lib/mongo/operation/result.rb +++ b/lib/mongo/operation/result.rb @@ -100,9 +100,13 @@ class Result # @param [ Integer ] connection_global_id # Global id of the connection on which the operation that # this result is for was performed. + # @param [ Operation::Context | nil ] context the context that was active + # when this result was produced. # # @api private - def initialize(replies, connection_description = nil, connection_global_id = nil) + def initialize(replies, connection_description = nil, connection_global_id = nil, context: nil) + @context = context + if replies if replies.is_a?(Array) if replies.length != 1 @@ -138,6 +142,12 @@ def initialize(replies, connection_description = nil, connection_global_id = nil # @api private attr_reader :connection_global_id + # @return [ Operation::Context | nil ] the operation context (if any) + # that was active when this result was produced. + # + # @api private + attr_reader :context + # @api private def_delegators :parser, :not_master?, :node_recovering?, :node_shutting_down? @@ -320,7 +330,7 @@ def ok? # @example Validate the result. # result.validate! # - # @raise [ Error::OperationFailure ] If an error is in the result. + # @raise [ Error::OperationFailure::Family ] If an error is in the result. # # @return [ Result ] The result if verification passed. # @@ -330,16 +340,16 @@ def validate! !successful? ? raise_operation_failure : self end - # The exception instance (of the Error::OperationFailure class) + # The exception instance (of Error::OperationFailure::Family) # that would be raised during processing of this result. # # This method should only be called when result is not successful. # - # @return [ Error::OperationFailure ] The exception. + # @return [ Error::OperationFailure::Family ] The exception. # # @api private def error - @error ||= Error::OperationFailure.new( + @error ||= operation_failure_class.new( parser.message, self, code: parser.code, @@ -453,6 +463,14 @@ def snapshot_timestamp private + def operation_failure_class + if context&.csot? && parser.code == 50 + Error::ServerTimeoutError + else + Error::OperationFailure + end + end + def aggregate_returned_count replies.reduce(0) do |n, reply| n += reply.number_returned diff --git a/lib/mongo/operation/shared/executable.rb b/lib/mongo/operation/shared/executable.rb index 713ea111e8..248a78a3a5 100644 --- a/lib/mongo/operation/shared/executable.rb +++ b/lib/mongo/operation/shared/executable.rb @@ -93,7 +93,7 @@ def result_class end def get_result(connection, context, options = {}) - result_class.new(*dispatch_message(connection, context, options)) + result_class.new(*dispatch_message(connection, context, options), context: context) end # Returns a Protocol::Message or nil as reply. diff --git a/lib/mongo/operation/shared/response_handling.rb b/lib/mongo/operation/shared/response_handling.rb index 9216bd51d1..36f4f8dafd 100644 --- a/lib/mongo/operation/shared/response_handling.rb +++ b/lib/mongo/operation/shared/response_handling.rb @@ -42,7 +42,7 @@ def validate_result(result, connection, context) # Adds error labels to exceptions raised in the yielded to block, # which should perform MongoDB operations and raise Mongo::Errors on # failure. This method handles network errors (Error::SocketError) - # and server-side errors (Error::OperationFailure); it does not + # and server-side errors (Error::OperationFailure::Family); it does not # handle server selection errors (Error::NoServerAvailable), for which # labels are added in the server selection code. # @@ -65,7 +65,7 @@ def add_error_labels(connection, context) rescue Mongo::Error::SocketTimeoutError => e maybe_add_retryable_write_error_label!(e, connection, context) raise e - rescue Mongo::Error::OperationFailure => e + rescue Mongo::Error::OperationFailure::Family => e if context.committing_transaction? if e.write_retryable? || e.wtimeout? || (e.write_concern_error? && !Session::UNLABELED_WRITE_CONCERN_CODES.include?(e.write_concern_error_code) diff --git a/lib/mongo/retryable/read_worker.rb b/lib/mongo/retryable/read_worker.rb index d28532fdb3..2c0118c503 100644 --- a/lib/mongo/retryable/read_worker.rb +++ b/lib/mongo/retryable/read_worker.rb @@ -201,7 +201,7 @@ def modern_read_with_retry(session, server_selector, context, &block) timeout: context&.remaining_timeout_sec ) yield server - rescue *retryable_exceptions, Error::OperationFailure, Auth::Unauthorized, Error::PoolError => e + rescue *retryable_exceptions, Error::OperationFailure::Family, Auth::Unauthorized, Error::PoolError => e e.add_notes('modern retry', 'attempt 1') raise e if session.in_transaction? raise e if !is_retryable_exception?(e) && !e.write_retryable? @@ -225,7 +225,7 @@ def legacy_read_with_retry(session, server_selector, context = nil, &block) context&.check_timeout! attempt = attempt ? attempt + 1 : 1 yield select_server(cluster, server_selector, session) - rescue *retryable_exceptions, Error::OperationFailure, Error::PoolError => e + rescue *retryable_exceptions, Error::OperationFailure::Family, Error::PoolError => e e.add_notes('legacy retry', "attempt #{attempt}") if is_retryable_exception?(e) @@ -256,7 +256,7 @@ def read_without_retry(session, server_selector, &block) begin yield server - rescue *retryable_exceptions, Error::PoolError, Error::OperationFailure => e + rescue *retryable_exceptions, Error::PoolError, Error::OperationFailure::Family => e e.add_note('retries disabled') raise e end @@ -297,7 +297,7 @@ def retry_read(original_error, session, server_selector, context: nil, failed_se else raise e end - rescue Error::OperationFailure, Error::PoolError => e + rescue Error::OperationFailure::Family, Error::PoolError => e e.add_note('modern retry') if e.write_retryable? e.add_note("attempt #{attempt}") diff --git a/lib/mongo/retryable/write_worker.rb b/lib/mongo/retryable/write_worker.rb index 53a33a4137..f40d3ae9ca 100644 --- a/lib/mongo/retryable/write_worker.rb +++ b/lib/mongo/retryable/write_worker.rb @@ -114,7 +114,7 @@ def nro_write_with_retry(write_concern, context:, &block) server.with_connection(connection_global_id: context.connection_global_id) do |connection| yield connection, nil, context end - rescue *retryable_exceptions, Error::PoolError, Error::OperationFailure => e + rescue *retryable_exceptions, Error::PoolError, Error::OperationFailure::Family => e e.add_note('retries disabled') raise e end @@ -195,7 +195,7 @@ def legacy_write_with_retry(server = nil, context:) # Legacy retries do not use txn_num yield connection, nil, context.dup end - rescue Error::OperationFailure => e + rescue Error::OperationFailure::Family => e e.add_note('legacy retry') e.add_note("attempt #{attempt}") server = nil @@ -246,10 +246,10 @@ def modern_write_with_retry(session, server, context, &block) # it later for the retry as well. yield connection, txn_num, context.dup end - rescue *retryable_exceptions, Error::PoolError, Auth::Unauthorized, Error::OperationFailure => e + rescue *retryable_exceptions, Error::PoolError, Auth::Unauthorized, Error::OperationFailure::Family => e e.add_notes('modern retry', 'attempt 1') - if e.is_a?(Error::OperationFailure) + if e.is_a?(Error::OperationFailure::Family) ensure_retryable!(e) else ensure_labeled_retryable!(e, connection_succeeded, session) @@ -308,16 +308,16 @@ def retry_write(original_error, txn_num, context:, failed_server: nil, &block) server.with_connection(connection_global_id: context.connection_global_id) do |connection| yield(connection, txn_num, context) end - rescue Mongo::Error::TimeoutError - raise rescue *retryable_exceptions, Error::PoolError => e maybe_fail_on_retryable(e, original_error, context, attempt) failed_server = server retry - rescue Error::OperationFailure => e + rescue Error::OperationFailure::Family => e maybe_fail_on_operation_failure(e, original_error, context, attempt) failed_server = server retry + rescue Mongo::Error::TimeoutError + raise rescue Error, Error::AuthError => e fail_on_other_error!(e, original_error) rescue Error::RaiseOriginalError diff --git a/lib/mongo/session.rb b/lib/mongo/session.rb index 35ce39e3e1..43087f24c3 100644 --- a/lib/mongo/session.rb +++ b/lib/mongo/session.rb @@ -501,7 +501,7 @@ def with_transaction(options = nil) rescue Mongo::Error => e if e.label?('UnknownTransactionCommitResult') if Utils.monotonic_time >= deadline || - e.is_a?(Error::OperationFailure) && e.max_time_ms_expired? + e.is_a?(Error::OperationFailure::Family) && e.max_time_ms_expired? then transaction_in_progress = false raise @@ -542,7 +542,7 @@ def with_transaction(options = nil) log_warn('with_transaction callback broke out of with_transaction loop, aborting transaction') begin abort_transaction - rescue Error::OperationFailure, Error::InvalidTransactionOperation + rescue Error::OperationFailure::Family, Error::InvalidTransactionOperation end end @with_transaction_deadline = nil diff --git a/spec/integration/client_side_encryption/range_explicit_encryption_prose_spec.rb b/spec/integration/client_side_encryption/range_explicit_encryption_prose_spec.rb index 78266e8d79..0f4cb82c10 100644 --- a/spec/integration/client_side_encryption/range_explicit_encryption_prose_spec.rb +++ b/spec/integration/client_side_encryption/range_explicit_encryption_prose_spec.rb @@ -7,6 +7,10 @@ # rubocop:disable RSpec/ExampleLength describe 'Range Explicit Encryption' do min_server_version '7.0.0-rc0' + + # TODO: RUBY-3423 + max_server_version '7.99.99' + require_libmongocrypt include_context 'define shared FLE helpers' diff --git a/spec/integration/docs_examples_spec.rb b/spec/integration/docs_examples_spec.rb index bf1ae9e1b0..73a9de6ad6 100644 --- a/spec/integration/docs_examples_spec.rb +++ b/spec/integration/docs_examples_spec.rb @@ -9,7 +9,7 @@ # the tests in this file. begin ClientRegistry.instance.global_client('authorized')['_placeholder'].create - rescue Mongo::Error::OperationFailure => e + rescue Mongo::Error::OperationFailure::Family => e # Collection already exists if e.code != 48 raise diff --git a/spec/integration/operation_failure_code_spec.rb b/spec/integration/operation_failure_code_spec.rb index bd124f710d..e777949e6b 100644 --- a/spec/integration/operation_failure_code_spec.rb +++ b/spec/integration/operation_failure_code_spec.rb @@ -17,7 +17,7 @@ collection.insert_one(_id: 1) collection.insert_one(_id: 1) fail('Should have raised') - rescue Mongo::Error::OperationFailure => e + rescue Mongo::Error::OperationFailure::Family => e expect(e.code).to eq(11000) # 4.0 and 4.2 sharded clusters set code name. # 4.0 and 4.2 replica sets and standalones do not, diff --git a/spec/integration/operation_failure_message_spec.rb b/spec/integration/operation_failure_message_spec.rb index d865c2d981..ee2ec0600c 100644 --- a/spec/integration/operation_failure_message_spec.rb +++ b/spec/integration/operation_failure_message_spec.rb @@ -22,7 +22,7 @@ begin client.command(bogus_command: nil) fail('Should have raised') - rescue Mongo::Error::OperationFailure => e + rescue Mongo::Error::OperationFailure::Family => e e.code_name.should == 'CommandNotFound' e.message.should =~ %r,\A\[59:CommandNotFound\]: no such (?:command|cmd): '?bogus_command'?, end @@ -36,7 +36,7 @@ begin client.command(bogus_command: nil) fail('Should have raised') - rescue Mongo::Error::OperationFailure => e + rescue Mongo::Error::OperationFailure::Family => e e.code_name.should be nil e.message.should =~ %r,\A\[59\]: no such (?:command|cmd): '?bogus_command'?, end @@ -53,7 +53,7 @@ collection.insert_one(_id: 1) collection.insert_one(_id: 1) fail('Should have raised') - rescue Mongo::Error::OperationFailure => e + rescue Mongo::Error::OperationFailure::Family => e e.code_name.should be nil e.message.should =~ %r,\A\[11000\]: (?:insertDocument :: caused by :: 11000 )?E11000 duplicate key error (?:collection|index):, end diff --git a/spec/integration/retryable_errors_spec.rb b/spec/integration/retryable_errors_spec.rb index ea1aa960a9..1944701461 100644 --- a/spec/integration/retryable_errors_spec.rb +++ b/spec/integration/retryable_errors_spec.rb @@ -83,7 +83,7 @@ begin collection.find(a: 1).to_a - rescue Mongo::Error::OperationFailure => exception + rescue Mongo::Error::OperationFailure::Family => exception else fail('Expected operation to fail') end @@ -128,7 +128,7 @@ begin collection.insert_one(a: 1) - rescue Mongo::Error::OperationFailure => exception + rescue Mongo::Error::OperationFailure::Family => exception else fail('Expected operation to fail') end diff --git a/spec/mongo/auth/user/view_spec.rb b/spec/mongo/auth/user/view_spec.rb index 4986754855..73e620b0ac 100644 --- a/spec/mongo/auth/user/view_spec.rb +++ b/spec/mongo/auth/user/view_spec.rb @@ -526,7 +526,7 @@ it "raises and reports the write concern error correctly" do begin view.send(method, input) - rescue Mongo::Error::OperationFailure => e + rescue Mongo::Error::OperationFailure::Family => e expect(e.write_concern_error?).to be true expect(e.write_concern_error_document).to eq( "code" => 64, diff --git a/spec/mongo/error/operation_failure_heavy_spec.rb b/spec/mongo/error/operation_failure_heavy_spec.rb index e18373992e..b0b41f9126 100644 --- a/spec/mongo/error/operation_failure_heavy_spec.rb +++ b/spec/mongo/error/operation_failure_heavy_spec.rb @@ -39,7 +39,7 @@ begin authorized_client['foo'].insert_one(test: 1) - rescue Mongo::Error::OperationFailure => exc + rescue Mongo::Error::OperationFailure::Family => exc expect(exc.details).to eq(exc.document['writeConcernError']['errInfo']) expect(exc.server_message).to eq(exc.document['writeConcernError']['errmsg']) expect(exc.code).to eq(exc.document['writeConcernError']['code']) @@ -90,7 +90,7 @@ it 'succeeds and prints the error' do begin collection.insert_one({x: 1}) - rescue Mongo::Error::OperationFailure => e + rescue Mongo::Error::OperationFailure::Family => e insert_events = subscriber.succeeded_events.select { |e| e.command_name == "insert" } expect(insert_events.length).to eq 1 expect(e.message).to match(/\[#{e.code}(:.*)?\].+ -- .+/) diff --git a/spec/runners/change_streams/test.rb b/spec/runners/change_streams/test.rb index e7b2799b6a..756e7e8fda 100644 --- a/spec/runners/change_streams/test.rb +++ b/spec/runners/change_streams/test.rb @@ -111,7 +111,7 @@ def teardown_test def run change_stream = begin @target.watch(@pipeline, ::Utils.snakeize_hash(@options)) - rescue Mongo::Error::OperationFailure => e + rescue Mongo::Error::OperationFailure::Family => e return { result: { error: { @@ -146,7 +146,7 @@ def run begin change = enum.next changes << change - rescue Mongo::Error::OperationFailure => e + rescue Mongo::Error::OperationFailure::Family => e return { result: { error: { diff --git a/spec/runners/crud/operation.rb b/spec/runners/crud/operation.rb index 8e93bccd2d..569a169a01 100644 --- a/spec/runners/crud/operation.rb +++ b/spec/runners/crud/operation.rb @@ -355,7 +355,7 @@ def assert_index_not_exists(client, context) if coll.indexes.map { |doc| doc['name'] }.include?(ixn = arguments.fetch('index')) raise "Index #{ixn} exists in collection #{cn} in database #{dn}, but must not" end - rescue Mongo::Error::OperationFailure => e + rescue Mongo::Error::OperationFailure::Family => e if e.to_s =~ /ns does not exist/ # Success. else diff --git a/spec/runners/transactions/operation.rb b/spec/runners/transactions/operation.rb index 3f74c65305..afd4282ad0 100644 --- a/spec/runners/transactions/operation.rb +++ b/spec/runners/transactions/operation.rb @@ -43,12 +43,10 @@ def execute(target, context) end result - rescue Mongo::Error::OperationFailure => e - result = e.instance_variable_get(:@result) - if result.nil? - raise "OperationFailure had nil result: #{e}" - end - err_doc = result.send(:first_document) + rescue Mongo::Error::OperationFailure::Family => e + raise "OperationFailure had nil result: #{e}" if e.result.nil? + + err_doc = e.result.send(:first_document) error_code_name = err_doc['codeName'] || err_doc['writeConcernError'] && err_doc['writeConcernError']['codeName'] if error_code_name.nil? # Sometimes the server does not return the error code name, diff --git a/spec/runners/unified.rb b/spec/runners/unified.rb index 7ab3b1904c..042b1c3947 100644 --- a/spec/runners/unified.rb +++ b/spec/runners/unified.rb @@ -72,7 +72,7 @@ def define_unified_spec_tests(base_path, paths, expect_failure: false) test.assert_events # HACK: other errors are possible and likely will need to # be added here later as the tests evolve. - rescue Mongo::Error::OperationFailure, Unified::Error::UnsupportedOperation, UsingHash::UsingHashKeyError, Unified::Error::EntityMissing + rescue Mongo::Error::OperationFailure::Family, Unified::Error::UnsupportedOperation, UsingHash::UsingHashKeyError, Unified::Error::EntityMissing rescue => e fail "Expected to raise Mongo::Error::OperationFailure or Unified::Error::UnsupportedOperation or UsingHash::UsingHashKeyError or Unified::Error::EntityMissing, got #{e.class}: #{e}" else diff --git a/spec/runners/unified/ddl_operations.rb b/spec/runners/unified/ddl_operations.rb index 185a251b7a..deae6e1d1c 100644 --- a/spec/runners/unified/ddl_operations.rb +++ b/spec/runners/unified/ddl_operations.rb @@ -188,7 +188,7 @@ def assert_index_not_exists(op) begin index = collection.indexes.get(args.use!('indexName')) raise Error::ResultMismatch, "Index found" - rescue Mongo::Error::OperationFailure => e + rescue Mongo::Error::OperationFailure::Family => e if e.code == 26 # OK else diff --git a/spec/runners/unified/test.rb b/spec/runners/unified/test.rb index 80d9149731..186a8b4385 100644 --- a/spec/runners/unified/test.rb +++ b/spec/runners/unified/test.rb @@ -329,7 +329,7 @@ def set_initial_data begin collection.create(create_options) rescue Mongo::Error => e - if Mongo::Error::OperationFailure === e && ( + if Mongo::Error::OperationFailure::Family === e && ( e.code == 48 || e.message =~ /collection already exists/ ) # Already exists @@ -419,7 +419,7 @@ def execute_operation(op) if expected_error.use('isClientError') # isClientError doesn't actually mean a client error. # It means anything other than OperationFailure. DRIVERS-1799 - if Mongo::Error::OperationFailure === e + if Mongo::Error::OperationFailure::Family === e raise Error::ErrorMismatch, %Q,Expected not OperationFailure ("isClientError") but got #{e}, end end @@ -541,7 +541,7 @@ def kill_sessions root_authorized_client.command( killAllSessions: [], ) - rescue Mongo::Error::OperationFailure => e + rescue Mongo::Error::OperationFailure::Family => e if e.code == 11601 # operation was interrupted, ignore. SERVER-38335 elsif e.code == 13 diff --git a/spec/support/cluster_tools.rb b/spec/support/cluster_tools.rb index 253782da98..3eeeef2beb 100644 --- a/spec/support/cluster_tools.rb +++ b/spec/support/cluster_tools.rb @@ -98,7 +98,7 @@ def reset_priorities def step_down admin_client.database.command( replSetStepDown: 4, secondaryCatchUpPeriodSecs: 2) - rescue Mongo::Error::OperationFailure => e + rescue Mongo::Error::OperationFailure::Family => e # While waiting for secondaries to catch up before stepping down, this node decided to step down for other reasons (189) if e.code == 189 # success @@ -118,7 +118,7 @@ def step_up(address) begin client.database.command(replSetStepUp: 1) break - rescue Mongo::Error::OperationFailure => e + rescue Mongo::Error::OperationFailure::Family => e # Election failed. (125) if e.code == 125 # Possible reason is the node we are trying to elect has deny-listed @@ -261,7 +261,7 @@ def encourage_primary(address) def unfreeze_server(address) begin direct_client(address).use('admin').database.command(replSetFreeze: 0) - rescue Mongo::Error::OperationFailure => e + rescue Mongo::Error::OperationFailure::Family => e # Mongo::Error::OperationFailure: cannot freeze node when primary or running for election. state: Primary (95) if e.code == 95 # The server we want to become primary may have already become the diff --git a/spec/support/common_shortcuts.rb b/spec/support/common_shortcuts.rb index 32d386b36a..8eacdf2c7c 100644 --- a/spec/support/common_shortcuts.rb +++ b/spec/support/common_shortcuts.rb @@ -176,7 +176,7 @@ def kill_all_server_sessions ClientRegistry.instance.global_client('root_authorized').command(killAllSessions: []) # killAllSessions also kills the implicit session which the driver uses # to send this command, as a result it always fails - rescue Mongo::Error::OperationFailure => e + rescue Mongo::Error::OperationFailure::Family => e # "operation was interrupted" unless e.code == 11601 raise @@ -396,7 +396,7 @@ def wait_for_snapshot(db: nil, collection: nil, client: nil) client.start_session(snapshot: true) do |session| client[collection].aggregate([{'$match': {any: true}}], session: session).to_a end - rescue Mongo::Error::OperationFailure => e + rescue Mongo::Error::OperationFailure::Family => e # Retry them as the server demands... if e.code == 246 # SnapshotUnavailable if Mongo::Utils.monotonic_time < start_time + 10 diff --git a/spec/support/shared/session.rb b/spec/support/shared/session.rb index 7188f5f883..acbc3a2ceb 100644 --- a/spec/support/shared/session.rb +++ b/spec/support/shared/session.rb @@ -110,8 +110,8 @@ end it 'raises an error' do - expect([Mongo::Error::OperationFailure, - Mongo::Error::BulkWriteError]).to include(operation_result.class) + expect([Mongo::Error::OperationFailure::Family, + Mongo::Error::BulkWriteError].any? { |e| e === operation_result }).to be true end it 'updates the last use value' do diff --git a/spec/support/spec_setup.rb b/spec/support/spec_setup.rb index dcfa83a533..442a7352cd 100644 --- a/spec/support/spec_setup.rb +++ b/spec/support/spec_setup.rb @@ -28,7 +28,7 @@ def run # more users to any other databases. begin create_user(client, SpecConfig.instance.root_user) - rescue Mongo::Error::OperationFailure => e + rescue Mongo::Error::OperationFailure::Family => e # When testing a cluster that requires auth, root user is already set up # and it is not creatable without auth. # Seems like every mongodb version has its own error message @@ -61,7 +61,7 @@ def create_user(client, user) users = client.use('admin').database.users begin users.create(user) - rescue Mongo::Error::OperationFailure => e + rescue Mongo::Error::OperationFailure::Family => e if e.message =~ /User.*already exists/ users.remove(user.name) users.create(user)