defmodule Mongo.Error do
alias Mongo.Events
defexception [:message, :code, :host, :fail_command, :error_labels, :resumable, :retryable_reads, :retryable_writes, :not_writable_primary_or_recovering, :error_info]
@exceeded_time_limit 262
@failed_to_satisfy_read_preference 133
@host_not_found 7
@host_unreachable 6
@interrupted_at_shutdown 11_600
@interrupted_due_to_repl_state_change 11_602
@network_timeout 89
@not_primary_no_secondary_ok 13_435
@not_primary_or_secondary 13_436
@not_writable_primary 10_107
@primary_stepped_down 189
@retry_change_stream 234
@shutdown_in_progress 91
@socket_exception 9001
@stale_config 13_388
@stale_epoch 150
@stale_shard_version 63
@retryable_writes [
@exceeded_time_limit,
@host_not_found,
@host_unreachable,
@interrupted_at_shutdown,
@interrupted_due_to_repl_state_change,
@network_timeout,
@not_primary_no_secondary_ok,
@not_primary_or_secondary,
@not_writable_primary,
@primary_stepped_down,
@shutdown_in_progress,
@socket_exception
]
@retryable_reads [
@host_not_found,
@host_unreachable,
@interrupted_due_to_repl_state_change,
@network_timeout,
@not_primary_no_secondary_ok,
@not_primary_or_secondary,
@not_writable_primary,
@primary_stepped_down,
@socket_exception,
@interrupted_at_shutdown
]
@resumable [
@exceeded_time_limit,
@interrupted_due_to_repl_state_change,
@stale_epoch,
@failed_to_satisfy_read_preference,
@host_not_found,
@host_unreachable,
@interrupted_at_shutdown,
@interrupted_at_shutdown,
@network_timeout,
@not_primary_no_secondary_ok,
@not_primary_or_secondary,
@not_writable_primary,
@primary_stepped_down,
@retry_change_stream,
@shutdown_in_progress,
@socket_exception,
@stale_config,
@stale_shard_version
]
# https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/server-discovery-and-monitoring.rst#not-writable-primary-and-node-is-recovering
@not_writable_primary_or_recovering [
@interrupted_at_shutdown,
@interrupted_due_to_repl_state_change,
@not_primary_no_secondary_ok,
@not_primary_or_secondary,
@not_writable_primary,
@primary_stepped_down,
@shutdown_in_progress
]
@type t :: %__MODULE__{
message: String.t(),
code: number,
host: String.t(),
error_labels: [String.t()] | nil,
fail_command: boolean,
resumable: boolean,
retryable_reads: boolean,
retryable_writes: boolean,
not_writable_primary_or_recovering: boolean,
error_info: map()
}
def message(e) do
code = if e.code, do: " #{e.code}"
"#{e.message}#{code}"
end
def exception(tag: :tcp, action: action, reason: reason, host: host) do
formatted_reason = :inet.format_error(reason)
%Mongo.Error{message: "#{host} tcp #{action}: #{formatted_reason} - #{inspect(reason)}", host: host, resumable: true}
end
def exception(tag: :ssl, action: action, reason: reason, host: host) do
formatted_reason = :ssl.format_error(reason)
%Mongo.Error{message: "#{host} ssl #{action}: #{formatted_reason} - #{inspect(reason)}", host: host, resumable: false}
end
def exception(%{"code" => code, "errmsg" => msg} = doc) do
error_labels = doc["errorLabels"] || []
resumable = Enum.any?(@resumable, &(&1 == code)) || Enum.any?(error_labels, &(&1 == "ResumableChangeStreamError"))
retryable_reads = Enum.any?(@retryable_reads, &(&1 == code)) || Enum.any?(error_labels, &(&1 == "RetryableReadError"))
retryable_writes = Enum.any?(@retryable_writes, &(&1 == code)) || Enum.any?(error_labels, &(&1 == "RetryableWriteError"))
not_writable_primary_or_recovering = Enum.any?(@not_writable_primary_or_recovering, &(&1 == code))
%Mongo.Error{
message: msg,
code: code,
fail_command: String.contains?(msg, "failCommand") || String.contains?(msg, "failpoint"),
error_labels: error_labels,
resumable: resumable,
retryable_reads: retryable_reads,
retryable_writes: retryable_writes,
not_writable_primary_or_recovering: not_writable_primary_or_recovering
}
|> maybe_add_error_info(doc)
end
def exception(message: message, code: code) do
%Mongo.Error{message: message, code: code, resumable: Enum.any?(@resumable, &(&1 == code))}
end
def exception(message) do
%Mongo.Error{message: message, resumable: false}
end
@doc """
Return true if the error is retryable for read operations.
"""
def should_retry_read(%Mongo.Error{retryable_reads: true}, cmd, opts) do
[{command_name, _} | _] = cmd
result = command_name != :getMore and opts[:read_counter] == 1
if result do
Events.notify(%Mongo.Events.RetryReadEvent{command_name: command_name, command: cmd}, :commands)
end
result
end
def should_retry_read(_error, _cmd, _opts) do
false
end
@doc """
Return true if the error is retryable for writes operations.
"""
def should_retry_write(%Mongo.Error{retryable_writes: true}, cmd, opts) do
[{command_name, _} | _] = cmd
result = opts[:write_counter] == 1
if result do
Events.notify(%Mongo.Events.RetryWriteEvent{command_name: command_name, command: cmd}, :commands)
end
result
end
def should_retry_write(_error, _cmd, _opts) do
false
end
def has_label(%Mongo.Error{error_labels: labels}, label) when is_list(labels) do
Enum.any?(labels, fn l -> l == label end)
end
def has_label(_other, _label) do
false
end
def not_writable_primary?(%Mongo.Error{code: code}) do
code == @not_writable_primary
end
def not_primary_no_secondary_ok?(%Mongo.Error{code: code}) do
code == @not_primary_no_secondary_ok
end
def not_primary_or_secondary?(%Mongo.Error{code: code}) do
code == @not_primary_or_secondary
end
@doc """
Return true if the error == not writable primary or in recovering mode.
"""
def not_writable_primary_or_recovering?(%Mongo.Error{not_writable_primary_or_recovering: result}, opts) do
## no explicit session, no retry counter but not_writable_primary_or_recovering
Keyword.get(opts, :session, nil) == nil && Keyword.get(opts, :retry_counter, nil) == nil && result
end
# catch all function
def not_writable_primary_or_recovering?(_other, _opts) do
false
end
@doc """
Returns true if the error is issued by the failCommand
"""
def fail_command?(%Mongo.Error{fail_command: fail_command}) do
fail_command
end
defp maybe_add_error_info(error, %{"errInfo" => info}) do
error
|> Map.put_new(:error_info, info)
end
defp maybe_add_error_info(error, _), do: error
end
defmodule Mongo.WriteError do
defexception [:n, :ok, :write_errors]
@type t :: %__MODULE__{
n: number,
ok: number,
write_errors: [map]
}
def message(e) do
"n: #{e.n}, ok: #{e.ok}, write_errors: #{inspect(e.write_errors)}"
end
end