# frozen_string_literal: true # encoding: utf-8 # 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. require 'mongo/error/read_write_retryable' 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 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 # 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 # 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 # 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 [ 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 # 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)) @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 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'] 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}" end end end end