Packages

Aerospike driver for Elixir with an OTP-native cluster runtime

Current section

Files

Jump to
aerospike_driver lib aerospike error.ex
Raw

lib/aerospike/error.ex

defmodule Aerospike.Error do
@moduledoc """
Exception raised for Aerospike client and server errors.
Result codes from the wire protocol are represented as atoms (e.g. `:key_not_found`,
`:timeout`). Use `from_result_code/2` to build an error from a result code atom.
"""
alias Aerospike.Protocol.ResultCode
@enforce_keys [:code, :message]
defexception [:code, :message, node: nil, in_doubt: false]
@typedoc """
Aerospike client or server error.
`:code` is the normalized result-code atom used by the public API.
`:node` is set when the failing operation can attribute the error to one
cluster node. `:in_doubt` is `true` when a write-style operation may have
reached the server before the client observed the failure.
"""
@type t :: %__MODULE__{
code: atom(),
message: String.t(),
node: String.t() | nil,
in_doubt: boolean()
}
@typedoc "Option accepted when constructing an `Aerospike.Error`."
@type option ::
{:code, atom()}
| {:message, String.t()}
| {:node, String.t() | nil}
| {:in_doubt, boolean()}
@typedoc "Keyword options accepted when constructing an `Aerospike.Error`."
@type opts :: [option()]
@typedoc "Input accepted by `exception/1`."
@type exception_input :: opts() | map() | String.t() | atom()
@doc """
Builds an exception struct from keyword, map, string, or atom input.
Keyword and map input may include `:code`, `:message`, `:node`, and
`:in_doubt`. String input becomes a client error message, and atom input is
treated as an Aerospike result code.
"""
@spec exception(exception_input()) :: t()
def exception(message) when is_binary(message) do
%__MODULE__{code: :client_error, message: message}
end
def exception(code) when is_atom(code) do
from_result_code(code)
end
def exception(opts) when is_map(opts) do
opts
|> Map.to_list()
|> exception()
end
def exception(opts) when is_list(opts) do
code = Keyword.get(opts, :code, :client_error)
%__MODULE__{
code: code,
message: Keyword.get(opts, :message, ResultCode.message(code)),
node: Keyword.get(opts, :node, nil),
in_doubt: Keyword.get(opts, :in_doubt, false)
}
end
@doc """
Builds an error struct from a result code atom.
The default human-readable message is derived from the result code.
Options:
* `:message` — override the message string
* `:node` — optional node name (for cluster-aware errors)
* `:in_doubt` — whether the outcome is uncertain (default `false`)
## Examples
iex> e = Aerospike.Error.from_result_code(:key_not_found)
iex> e.code
:key_not_found
iex> e.in_doubt
false
iex> e = Aerospike.Error.from_result_code(:timeout, node: "BB9", in_doubt: true)
iex> e.node
"BB9"
iex> e.in_doubt
true
"""
@spec from_result_code(atom(), opts()) :: t()
def from_result_code(code, opts \\ []) when is_atom(code) do
default_msg = ResultCode.message(code)
%__MODULE__{
code: code,
message: Keyword.get(opts, :message, default_msg),
node: Keyword.get(opts, :node, nil),
in_doubt: Keyword.get(opts, :in_doubt, false)
}
end
# Server result codes that indicate the partition map the client addressed
# is out of sync with the cluster's current ownership. These are a routing
# signal — retry against a different replica (and trigger a tend) — not a
# node-health signal.
#
# Today only `:partition_unavailable` (wire code 11) qualifies. If the
# Aerospike server grows another "not-mine" result code in the future,
# widen this list after validating the retry semantics end to end.
@rebalance_codes [:partition_unavailable]
@doc """
Returns `true` when the error represents a cluster rebalance / partition-
ownership signal that should trigger a re-route rather than a same-replica
retry.
Rebalance-class errors mean "the server you addressed does not own this
partition right now" — the retry layer consumes this as a cue to pick the
next replica (and usually to ask the Tender for a fresh partition map).
Transport errors (`:network_error`, `:timeout`, pool errors) and server
errors unrelated to ownership (`:key_not_found`, `:generation_error`,
etc.) are **not** rebalance-class.
Accepts `t/0` or any value; non-`Error` values (including bare atoms like
`:cluster_not_ready`) return `false` so callers can pattern-match uniformly
on whatever the command path returned.
## Examples
iex> err = Aerospike.Error.from_result_code(:partition_unavailable)
iex> Aerospike.Error.rebalance?(err)
true
iex> err = Aerospike.Error.from_result_code(:key_not_found)
iex> Aerospike.Error.rebalance?(err)
false
iex> Aerospike.Error.rebalance?(:cluster_not_ready)
false
"""
@spec rebalance?(t() | term()) :: boolean()
def rebalance?(%__MODULE__{code: code}), do: code in @rebalance_codes
def rebalance?(_other), do: false
@impl true
@doc """
Formats the error for exception messages.
"""
@spec message(t()) :: String.t()
def message(%__MODULE__{code: code, message: msg}) do
"Aerospike error #{code}: #{msg}"
end
end