# Copyright (C) 2015-2020 MongoDB Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. module Mongo class Error # Raised when an operation fails for some reason. # # @since 2.0.0 class OperationFailure < Error extend Forwardable include SdamErrorDetection # Error codes and code names that should result in a failing write # being retried. # # @since 2.6.0 # @api private WRITE_RETRY_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 => 'NotMasterNoSlaveOk', :code => 13435}, {:code_name => 'NotMasterOrSecondary', :code => 13436}, ].freeze # These are magic error messages that could indicate a master change. # # @since 2.4.2 # @api private WRITE_RETRY_MESSAGES = [ 'not master', 'node is recovering', ].freeze # These are magic error messages that could indicate a cluster # reconfiguration behind a mongos. # # @since 2.1.1 # @api private RETRY_MESSAGES = WRITE_RETRY_MESSAGES + [ 'transport error', 'socket exception', "can't connect", 'connect failed', 'error querying', 'could not get last error', 'connection attempt failed', 'interrupted at shutdown', 'unknown replica set', 'dbclient error communicating with server' ].freeze 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 # @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 server-returned error message # parsed from the response. # # @api experimental attr_reader :server_message # Whether the error is a retryable error according to the legacy # read retry logic. # # @return [ true, false ] # # @since 2.1.1 # @deprecated def retryable? write_retryable? || code.nil? && RETRY_MESSAGES.any?{ |m| message.include?(m) } end # Whether the error is a retryable error according to the modern retryable # reads and retryable writes specifications. # # This method is also used by the legacy retryable write logic to determine # whether an error is a retryable one. # # @return [ true, false ] # # @since 2.4.2 def write_retryable? write_retryable_code? || code.nil? && WRITE_RETRY_MESSAGES.any? { |m| message.include?(m) } end private def write_retryable_code? if code WRITE_RETRY_ERRORS.any? { |e| e[:code] == code } else # return false rather than nil false end end # 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: 'NotMasterNoSlaveOk', 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 # Change stream can be resumed when these error messages are encountered. # # @since 2.6.0 # @api private CHANGE_STREAM_RESUME_MESSAGES = 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 # Connection description is not populated for unacknowledged writes. if connection_description.max_wire_version >= 9 label?('ResumableChangeStreamError') else change_stream_resumable_code? 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? # @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 # @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 [ 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. # # @since 2.5.0, options added in 2.6.0 def initialize(message = nil, result = nil, options = {}) super(message) @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 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 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 end end end