1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
|
# frozen_string_literal: true
# rubocop:todo all
# 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.
# Sample error - mongo 3.4:
# {
# "ok" : 0,
# "errmsg" : "not master",
# "code" : 10107,
# "codeName" : "NotMaster"
# }
#
# Sample response with a write concern error - mongo 3.4:
# {
# "n" : 1,
# "opTime" : {
# "ts" : Timestamp(1527728618, 1),
# "t" : NumberLong(4)
# },
# "electionId" : ObjectId("7fffffff0000000000000004"),
# "writeConcernError" : {
# "code" : 100,
# "codeName" : "CannotSatisfyWriteConcern",
# "errmsg" : "Not enough data-bearing nodes"
# },
# "ok" : 1
# }
module Mongo
class Error
# Class for parsing the various forms that errors can come in from MongoDB
# command responses.
#
# The errors can be reported by the server in a number of ways:
# - {ok:0} response indicates failure. In newer servers, code, codeName
# and errmsg fields should be set. In older servers some may not be set.
# - {ok:1} response with a write concern error (writeConcernError top-level
# field). This indicates that the node responding successfully executed
# the request, but not enough other nodes successfully executed the
# request to satisfy the write concern.
# - {ok:1} response with writeErrors top-level field. This can be obtained
# in a bulk write but also in a non-bulk write. In a non-bulk write
# there should be exactly one error in the writeErrors list.
# The case of multiple errors is handled by BulkWrite::Result.
# - {ok:1} response with writeConcernErrors top-level field. This can
# only be obtained in a bulk write and is handled by BulkWrite::Result,
# not by this class.
#
# Note that writeErrors do not have codeName fields - they just provide
# codes and messages. writeConcernErrors may similarly not provide code
# names.
#
# @since 2.0.0
# @api private
class Parser
include SdamErrorDetection
# @return [ BSON::Document ] The returned document.
attr_reader :document
# @return [ String ] The full error message to be used in the
# raised exception.
attr_reader :message
# @return [ String ] The server-returned error message
# parsed from the response.
attr_reader :server_message
# @return [ Array<Protocol::Message> ] The message replies.
attr_reader :replies
# @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 [ Array<String> ] The set of labels associated with the error.
# @since 2.7.0
attr_reader :labels
# @api private
attr_reader :wtimeout
# Create the new parser with the returned document.
#
# In legacy mode, the code and codeName fields of the document are not
# examined because the status (ok: 1) is not part of the document and
# there is no way to distinguish successful from failed responses using
# the document itself, and a successful response may legitimately have
# { code: 123, codeName: 'foo' } as the contents of a user-inserted
# document. The legacy server versions do not fill out code nor codeName
# thus not reading them does not lose information.
#
# @example Create the new parser.
# Parser.new({ 'errmsg' => 'failed' })
#
# @param [ BSON::Document ] document The returned document.
# @param [ Array<Protocol::Message> ] replies The message replies.
# @param [ Hash ] options The options.
#
# @option options [ true | false ] :legacy Whether document and replies
# are from a legacy (pre-3.2) response
#
# @since 2.0.0
def initialize(document, replies = nil, options = nil)
@document = document || {}
@replies = replies
@options = if options
options.dup
else
{}
end.freeze
parse!
end
# @return [ true | false ] Whether the document 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
# @api experimental
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.
# @api experimental
def write_concern_error_document
document['writeConcernError']
end
# @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
# @api experimental
def write_concern_error_code
write_concern_error_document && write_concern_error_document['code']
end
# @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
# @api experimental
def write_concern_error_code_name
write_concern_error_document && write_concern_error_document['codeName']
end
# @return [ Array<String> | nil ] The error labels associated with this
# write concern error, if there is a write concern error present.
def write_concern_error_labels
write_concern_error_document && write_concern_error_document['errorLabels']
end
class << self
def build_message(code: nil, code_name: nil, message: nil)
if code_name && code
"[#{code}:#{code_name}]: #{message}"
elsif code_name
# This surely should never happen, if there's a code name
# there ought to also be the code provided.
# Handle this case for completeness.
"[#{code_name}]: #{message}"
elsif code
"[#{code}]: #{message}"
else
message
end
end
end
private
def parse!
if document['ok'] != 1 && document['writeErrors']
raise ArgumentError, "writeErrors should only be given in successful responses"
end
@message = +""
parse_single(@message, '$err')
parse_single(@message, 'err')
parse_single(@message, 'errmsg')
parse_multiple(@message, 'writeErrors')
if write_concern_error_document
parse_single(@message, 'errmsg', write_concern_error_document)
end
parse_flag(@message)
parse_code
parse_labels
parse_wtimeout
@server_message = @message
@message = self.class.build_message(
code: code,
code_name: code_name,
message: @message,
)
end
def parse_single(message, key, doc = document)
if error = doc[key]
append(message, error)
end
end
def parse_multiple(message, key)
if errors = document[key]
errors.each do |error|
parse_single(message, 'errmsg', error)
end
end
end
def parse_flag(message)
if replies && replies.first &&
(replies.first.respond_to?(:cursor_not_found?)) && replies.first.cursor_not_found?
append(message, CURSOR_NOT_FOUND)
end
end
def append(message, error)
if message.length > 1
message.concat(", #{error}")
else
message.concat(error)
end
end
def parse_code
if document['ok'] == 1 || @options[:legacy]
@code = @code_name = nil
else
@code = document['code']
@code_name = document['codeName']
end
# Since there is only room for one code, do not replace
# codes of the top level response with write concern error codes.
# In practice this should never be an issue as a write concern
# can only fail after the operation succeeds on the primary.
if @code.nil? && @code_name.nil?
if subdoc = write_concern_error_document
@code = subdoc['code']
@code_name = subdoc['codeName']
end
end
if @code.nil? && @code_name.nil?
# If we have writeErrors, and all of their codes are the same,
# use that code. Otherwise don't set the code
if write_errors = document['writeErrors']
codes = write_errors.map { |e| e['code'] }.compact
if codes.uniq.length == 1
@code = codes.first
# code name may not be returned by the server
@code_name = write_errors.map { |e| e['codeName'] }.compact.first
end
end
end
end
def parse_labels
@labels = document['errorLabels'] || []
end
def parse_wtimeout
@wtimeout = write_concern_error_document &&
write_concern_error_document['errInfo'] &&
write_concern_error_document['errInfo']['wtimeout']
end
end
end
end
|