defmodule Nebulex.Error do
@moduledoc """
This exception represents command execution errors. For example, the cache
cannot perform a command because it has not started, it does not exist, or
the adapter failed to perform it for any reason.
## Exception fields
See `t:t/0`.
## Error reasons
The `:reason` field can assume the following values:
* `:timeout` - if there is a timeout when executing the cache command.
* `:transaction_aborted` - if a transaction execution fails and aborts.
* `:event_listener_already_exists` - if another cache entry listener with
the same ID already exists.
* `:event_listener_error` - if a cache entry event listener fails when
processing an event.
* `t:Exception.t/0` - if the underlying adapter fails due to an exception.
* `t:any/0` - the command fails with an adapter-specific error.
"""
@typedoc "Error reason type"
@type reason() :: atom() | {atom(), any()} | Exception.t()
@typedoc """
The type for this exception struct.
This exception has the following public fields:
* `:reason` - the error reason. It can be one of the Nebulex-specific
reasons described in the ["Error reasons"](#module-error-reasons)
section in the module documentation.
* `:module` - a custom error formatter module. When it is present, it
invokes `module.format_error(reason, metadata)` to format the error
reason. The argument `metadata` is a keyword with the metadata given
to the exception. See `format_error/2` for more information.
* `:metadata` - the metadata contains the options given to the
exception excluding the `:reason` and `:module` that are part
of the exception fields. For example, when raising an exception
`raise Nebulex.Error, reason: :test, foo: :bar`, the metadata
will be `[foo: :bar]`.
"""
@type t() :: %__MODULE__{reason: reason(), module: module(), metadata: keyword()}
# Exception struct
defexception reason: nil, module: __MODULE__, metadata: []
## Callbacks
@impl true
def exception(opts) do
{reason, opts} = Keyword.pop!(opts, :reason)
{module, opts} = Keyword.pop(opts, :module, __MODULE__)
%__MODULE__{reason: reason, module: module, metadata: opts}
end
@impl true
def message(%__MODULE__{reason: reason, module: module, metadata: metadata}) do
module.format_error(reason, metadata)
end
## Helpers
@doc """
A callback invoked when a custom formatter module is provided.
## Arguments
* `reason` - the error reason.
* `metadata` - a keyword with the metadata given to the exception.
For example, if an adapter returns:
wrap_error Nebulex.Error,
reason: :my_reason,
module: MyAdapter.Formatter,
foo: :bar
the exception invokes:
MyAdapter.Formatter.format_error(:my_reason, foo: :bar)
"""
@spec format_error(any(), keyword()) :: binary()
def format_error(reason, metadata)
def format_error(:timeout, metadata) do
"command execution timed out"
|> maybe_format_metadata(metadata)
end
def format_error(:transaction_aborted, metadata) do
"transaction aborted"
|> maybe_format_metadata(metadata)
end
def format_error(:event_listener_already_exists, metadata) do
"another cache entry listener with the same ID already exists"
|> maybe_format_metadata(metadata)
end
def format_error(:event_listener_error, metadata) do
{original, metadata} = Keyword.pop!(metadata, :original)
{stacktrace, metadata} = Keyword.pop(metadata, :stacktrace, [])
"""
cache entry event listener failed when processing an event.
#{Exception.format(:error, original, stacktrace) |> String.replace("\n", "\n ")}
"""
|> maybe_format_metadata(metadata)
end
def format_error(exception, metadata) when is_exception(exception) do
{stacktrace, metadata} = Keyword.pop(metadata, :stacktrace, [])
"""
the following exception occurred when executing a command.
#{Exception.format(:error, exception, stacktrace) |> String.replace("\n", "\n ")}
"""
|> maybe_format_metadata(metadata)
end
def format_error(reason, metadata) do
{command, metadata} = Keyword.pop(metadata, :command)
prefix = if command, do: "#{command} ", else: ""
"#{prefix}command failed with reason: #{inspect(reason)}"
|> maybe_format_metadata(metadata)
end
@doc """
Formats the error metadata when not empty.
"""
@spec maybe_format_metadata(binary(), keyword()) :: binary()
def maybe_format_metadata(msg, metadata) do
if Enum.empty?(metadata) do
msg
else
"""
#{msg}
Error metadata:
#{inspect(metadata)}
"""
end
end
end
defmodule Nebulex.KeyError do
@moduledoc """
Raised at runtime when a key does not exist in the cache.
This exception denotes the cache executed a command, but there was an issue
with the requested key; for example, it doesn't exist.
## Exception fields
See `t:t/0`.
## Error reasons
The `:reason` field can assume a few Nebulex-specific values:
* `:not_found` - the key doesn't exist in the cache.
* `:expired` - The key doesn't exist in the cache because it is expired.
"""
@typedoc """
The type for this exception struct.
This exception has the following public fields:
* `:reason` - the error reason. The two possible reasons are `:not_found`
or `:expired`. Defaults to `:not_found`.
* `:key` - the requested key.
* `:metadata` - the metadata contains the options given to the
exception excluding the `:reason` and `:key` that are part of
the exception fields. For example, when raising an exception
`raise Nebulex.KeyError, key: :test, foo: :bar`, the metadata
will be `[foo: :bar]`.
"""
@type t() :: %__MODULE__{reason: atom(), key: any(), metadata: keyword()}
# Exception struct
defexception reason: :not_found, key: nil, metadata: []
import Nebulex.Error, only: [maybe_format_metadata: 2]
## Callbacks
@impl true
def exception(opts) do
{key, opts} = Keyword.pop!(opts, :key)
{reason, opts} = Keyword.pop(opts, :reason, :not_found)
%__MODULE__{reason: reason, key: key, metadata: opts}
end
@impl true
def message(%__MODULE__{reason: reason, key: key, metadata: metadata}) do
format_reason(reason, key, metadata)
end
## Helpers
defp format_reason(:not_found, key, metadata) do
"key #{inspect(key)} not found"
|> maybe_format_metadata(metadata)
end
defp format_reason(:expired, key, metadata) do
"key #{inspect(key)} has expired"
|> maybe_format_metadata(metadata)
end
end
defmodule Nebulex.CacheNotFoundError do
@moduledoc """
It is raised when the cache cannot be retrieved from the registry
because it was not started or does not exist.
"""
@typedoc """
The type for this exception struct.
This exception has the following public fields:
* `:message` - the error message.
* `:cache` - the cache name or its PID.
"""
@type t() :: %__MODULE__{message: binary(), cache: atom() | pid()}
# Exception struct
defexception message: nil, cache: nil
@impl true
def exception(opts) do
cache = Keyword.fetch!(opts, :cache)
msg =
"unable to find cache: #{inspect(cache)}. Either the cache name is " <>
"invalid or the cache is not running, possibly because it is not " <>
"started or does not exist"
%__MODULE__{message: msg, cache: cache}
end
end
defmodule Nebulex.QueryError do
@moduledoc """
Raised at runtime when the query is invalid.
"""
@typedoc """
The type for this exception struct.
This exception has the following public fields:
* `:message` - the error message.
* `:query` - the query value.
"""
@type t() :: %__MODULE__{message: binary(), query: any()}
# Exception struct
defexception message: nil, query: nil
## Callbacks
@impl true
def exception(opts) do
query = Keyword.fetch!(opts, :query)
message =
Keyword.get_lazy(opts, :message, fn ->
"""
invalid query:
#{inspect(query)}
"""
end)
%__MODULE__{query: query, message: message}
end
end