Current section

Files

Jump to
retort lib retort resources timeout.ex
Raw

lib/retort/resources/timeout.ex

defmodule Retort.Resources.Timeout do
@moduledoc """
Timeouts for a module that `use Retort.Resources` can be configured for all `Retort.Client.Generic` calls.
## Module Timeout
The default timeout is `5_000` milliseconds as is the default throughout OTP. A default timeout for all actions in
your module can be set.
Either as a config
config :retort, MyRPC.Authors
timeout: 10_000 # milliseconds
or at runtime in the `Application` environment
Retort.Resources.Timeout.put(MyRPC.Authors, 10_000)
## Call Timeout
If only some of your `Retort.Client.Generic` calls are timing out (such as `:index`), you can set the timeout for just
that call.
Either as a config
config :retort, MyRPC.Authors,
timeout: [
index: 10_000 # milliseconds
]
or at runtime
Retort.Resources.Timeout.put(MyRPC.Authors, :index, 10_000)
The `Calcinator.Resources` callbacks implemented by `use Retort.Resources` call the following `Retort.Client.Generic`
calls.
| `Calcinator.Resources` callback | `Retort.Client.Generic` call | :timeout key |
|---------------------------------|-----------------------------------|--------------|
| `Calcinator.Resources.delete/2` | `Retort.Client.Generic.destroy/3` | `:destroy` |
| `Calcinator.Resources.get/2` | `Retort.Client.Generic.show/4` | `:show` |
| `Calcinator.Resources.insert/2` | `Retort.Client.Generic.create/4` | `:create` |
| `Calcinator.Resources.list/1` | `Retort.Client.Generic.index/3` | `:index` |
| `Calcinator.Resources.update/2` | `Retort.Client.Generic.update/5` | `:update` |
NOTE: `Calcinator.update/2` calls `Calcinator.Resources.get/2` to get the pre-existing data to generate the changeset
for `Calcinator.Resources.update/2`, so either of those may timeout, so you may need to increase the `:show` timeout
if the `Calcinator.Resources.get/2` part of the `Calcinator.update/2` is what is timing out and not the update itself.
"""
# Constants
@function_names ~w(create destroy index show update)a
# Types
@typedoc """
The name of a `Retort.Client.Generic` function call that can take a timeout.
"""
@type function_name :: :create | :destroy | :index | :show | :update
# Macros
defmacrop is_function_name(data) do
quote do
unquote(data) in @function_names
end
end
defmacrop is_timeout(data) do
# See Record.is_record/2 for how to define guards
case Macro.Env.in_guard?(__CALLER__) do
true ->
quote do
is_integer(unquote(data)) or unquote(data) == :infinity
end
false ->
quote do
data = unquote(data)
is_integer(data) || data == :infinity
end
end
end
# Functions
@doc """
Default timeout (5 seconds / 5,000 milliseconds) for `Retort.Client.Generic` calls by `use Retort.Resources` modules.
"""
@spec default :: timeout
def default, do: 5_000 # milliseconds
@doc """
Deletes `timeout` configuration for `module`. The `default/0` timeout will be used for all `Retort.Client.Generic`
calls.
"""
@spec delete(module) :: :ok
def delete(module) when is_atom(module) do
case Application.get_env(:retort, module) do
nil ->
:ok
keyword when is_list(keyword) ->
case Keyword.delete(keyword, :timeout) do
[] ->
Application.delete_env(:retort, module)
updated_keyword ->
Application.put_env(:retort, module, updated_keyword)
end
end
end
@doc """
Deletes `timeout` configuration for `function_name` calls to `Retort.Client.Generic` made by `module`. The
`default/0` timeout will be used instead.
If there is no timeout for the `module`, then delete still succeeds
iex> module = Retort.TestAuthors
iex> Retort.Resources.Timeout.get(module)
nil
iex> Retort.Resources.Timeout.delete(module, :index)
:ok
If there is no timeout for the `function_name`, then delete still succeeds
iex> module = Retort.TestAuthors
iex> Retort.Resources.Timeout.get(module, :index)
nil
iex> Retort.Resources.Timeout.delete(module, :index)
:ok
If there was previously a `module`-wide timeout, it is converted to per function name timeouts with no entry for
`function_name`. All other function names have the original `module`-wide timeout.
iex> module = Retort.TestAuthors
iex> Retort.Resources.Timeout.put(module, 10_000)
iex> Retort.Resources.Timeout.delete(module, :update)
iex> Retort.Resources.Timeout.get(module)
[create: 10_000, destroy: 10_000, index: 10_000, show: 10_000]
If there was previously only a function name timeout for `function_name` then the entire timeout configuration is
removed for `module`
iex> module = Retort.TestAuthors
iex> Retort.Resources.Timeout.put(module, :index, 15_000)
iex> Retort.Resources.Timeout.get(module)
[index: 15_000]
iex> Retort.Resources.Timeout.delete(module, :index)
iex> Retort.Resources.Timeout.get(module)
nil
If there was previously function name timeouts for other function names besides `function_name` then those valeus will
remain
iex> module = Retort.TestAuthors
iex> Retort.Resources.Timeout.put(module, :destroy, 7_500)
iex> Retort.Resources.Timeout.put(module, :index, 15_000)
iex> Retort.Resources.Timeout.get(module)
[index: 15_000, destroy: 7_500]
iex> Retort.Resources.Timeout.delete(module, :destroy)
iex> Retort.Resources.Timeout.get(module)
[index: 15_000]
"""
@spec delete(module, function_name) :: :ok
def delete(module, function_name) when is_atom(module) and is_function_name(function_name) do
case Application.get_env(:retort, module) do
nil ->
:ok
environment when is_list(environment) ->
delete_from_environment(module, environment, function_name)
end
end
@doc """
The configured timeout(s) for the `module` that called `use Retort.Resources`.
When there is no timeout configuration, `nil` will be returned
iex> module = Retort.TestAuthors
iex> Retort.Resources.Timeout.delete(module)
iex> Retort.Resources.Timeout.get(module)
nil
When there is module-wide timeout, that timeout will be returned
iex> module = Retort.TestAuthors
iex> Retort.Resources.Timeout.put(module, 10_000)
iex> Retort.Resources.Timeout.get(module)
10_000
When there are function_name-specific timeouts, the list of those timeouts will be returned
iex> module = Retort.TestAuthors
iex> Retort.Resources.Timeout.put(module, :destroy, 10_000)
iex> Retort.Resources.Timeout.put(module, :index, 10_000)
iex> Retort.Resources.Timeout.get(module)
[index: 10_000, destroy: 10_000]
## Returns
* `Keyword.t` - Maps `Retort.Client.Generic` function name to its timeout. If a function name is not in the
`Keyword.t`, then the `Retort.Resources.Timeout.default/0` should be used.
* `timeout` - The timeout for all `Retort.Client.Generic` calls.
* `nil` - No timeouts are configured for `module`. The `Retort.Resources.Timeout.default/0` should be used.
"""
@spec get(module) :: Keyword.t | timeout | nil
def get(module) do
:retort
|> Application.get_env(module, [])
|> Keyword.get(:timeout)
end
@doc """
The configured timeout for `Retort.Client.Generic` calls of `function_name` by `module` that called
`use Retort.Resources`.
When there is no timeout configuration for `module`, returns `nil`
iex> Retort.Resources.Timeout.get(Retort.TestAuthors, :index)
nil
When there is a `module`-wide timeout, but no `function_name` timeout, the `module`-wide timeout will be returned.
iex> module = Retort.TestAuthors
iex> Retort.Resources.Timeout.put(module, 10_000)
iex> Retort.Resources.Timeout.get(module, :index)
10_000
When there are function name timeouts, but none for `function_name`, `nil` is returned.
iex> module = Retort.TestAuthors
iex> Retort.Resources.Timeout.put(module, :index, 10_000)
iex> Retort.Resources.Timeout.get(module, :show)
nil
If there is a function name timeout for `function_name`, it is returned.
iex> module = Retort.TestAuthors
iex> Retort.Resources.Timeout.put(module, :index, 10_000)
iex> Retort.Resources.Timeout.get(module, :index)
10_000
## Returns
* `timeout` - if timeout is configured for `function_name`
* `nil` - if timeout is not configured for `function_name` or `module`
"""
@spec get(module, function_name) :: timeout | nil
def get(module, function_name) when is_function_name(function_name) do
case get(module) do
keyword when is_list(keyword) ->
Keyword.get(keyword, function_name)
timeout when is_timeout(timeout) ->
timeout
nil ->
nil
end
end
@doc """
Like `get/2`, but ensures the timeout is not `nil`
If there is no timeout configuration for `module`, `default/0` is returned.
iex> module = Retort.TestAuthors
iex> Retort.Resources.Timeout.get(module)
nil
iex> Retort.Resources.Timeout.get_or_default(module, :index) == Retort.Resources.Timeout.default()
true
If there is a `module`-wide timeout, it is returned.
iex> module = Retort.TestAuthors
iex> Retort.Resources.Timeout.put(module, 10_000)
iex> Retort.Resources.Timeout.get_or_default(module, :index)
10_000
If there are function name timeouts, but not for `function_name`, `default/0` is returned.
iex> module = Retort.TestAuthors
iex> Retort.Resources.Timeout.put(module, :index, 10_000)
iex> Retort.Resources.Timeout.get_or_default(module, :show) == Retort.Resources.Timeout.default()
true
If there is a function name timeout for `function_name`, then it is returned.
iex> module = Retort.TestAuthors
iex> timeout = 10_000
iex> Retort.Resources.Timeout.put(module, :index, timeout)
iex> Retort.Resources.Timeout.get_or_default(module, :index) == timeout
true
"""
@spec get_or_default(module, function_name) :: timeout
def get_or_default(module, function_name) when is_atom(module) and is_function_name(function_name) do
module
|> get(function_name)
|> Kernel.||(default())
end
@doc """
Sets the `timeout` for all `Retort.Client.Generic` calls made by `module` that called `use Retort.Resources`
If no previous timeout was set for `module`, it will now have one
iex> module = Retort.TestAuthors
iex> Retort.Resources.Timeout.delete(module)
iex> Retort.Resources.Timeout.get(module)
nil
iex> Retort.Resources.Timeout.put(module, 10_000)
iex> Retort.Resources.Timeout.get(module)
10_000
If a previous, single timeout was set for `module`, it will be overridden
iex> module = Retort.TestAuthors
iex> Retort.Resources.Timeout.put(module, 7_500)
iex> Retort.Resources.Timeout.get(module)
7_500
iex> Retort.Resources.Timeout.put(module, 10_000)
iex> Retort.Resources.Timeout.get(module)
10_000
If a previous, per `function_name` timeout was set, then all those disappear and they will all share the module
timeout
iex> module = Retort.TestAuthors
iex> Retort.Resources.Timeout.put(module, :index, 15_000)
iex> Retort.Resources.Timeout.put(module, :show, 7_500)
iex> Retort.Resources.Timeout.get(module)
[show: 7_500, index: 15_000]
iex> Retort.Resources.Timeout.put(module, 10_000)
iex> Retort.Resources.Timeout.get(module)
10_000
"""
@spec put(module, timeout) :: :ok
def put(module, timeout) when is_atom(module) and is_timeout(timeout) do
updated_environment = :retort
|> Application.get_env(module, [])
|> Keyword.put(:timeout, timeout)
Application.put_env(:retort, module, updated_environment)
end
@doc """
Sets the `timeout` for `Retort.Client.Generic` calls of `function_name` by `module` that called
`use Retort.Resources`.
If there was previously no timeout configuration for `module`, then configuration for only `function_name` is added.
iex> module = Retort.TestAuthors
iex> Retort.Resources.Timeout.get(module)
nil
iex> Retort.Resources.Timeout.put(module, :index, 10_000)
iex> Retort.Resources.Timeout.get(module)
[index: 10_000]
If there was previously a `module`-wide timeout, that timeout is applied to all other function names while the given
`timeout` is used for `function_name`.
iex> module = Retort.TestAuthors
iex> Retort.Resources.Timeout.put(module, 10_000)
iex> Retort.Resources.Timeout.put(module, :index, 15_000)
iex> Retort.Resources.Timeout.get(module)
[create: 10_000, destroy: 10_000, index: 15_000, show: 10_000, update: 10_000]
If there was previously only a function name timeout for `function_name`, it is replaced.
iex> module = Retort.TestAuthors
iex> Retort.Resources.Timeout.put(module, :index, 10_000)
iex> Retort.Resources.Timeout.get(module)
[index: 10_000]
iex> Retort.Resources.Timeout.put(module, :index, 15_000)
iex> Retort.Resources.Timeout.get(module)
[index: 15_000]
If there was previous function name timeouts for other function names, a new entry for `function_name` is added with
`timeout`.
iex> module = Retort.TestAuthors
iex> Retort.Resources.Timeout.put(module, :index, 15_000)
iex> Retort.Resources.Timeout.get(module)
[index: 15_000]
iex> Retort.Resources.Timeout.put(module, :show, 10_000)
iex> Retort.Resources.Timeout.get(module)
[show: 10_000, index: 15_000]
"""
@spec put(module, function_name, timeout) :: :ok
def put(module, function_name, timeout)
when is_atom(module) and is_function_name(function_name) and is_timeout(timeout) do
previous_environment = Application.get_env(:retort, module, [])
updated_timeout = previous_environment
|> Keyword.get(:timeout)
|> case do
nil ->
[{function_name, timeout}]
module_timeout when is_timeout(module_timeout) ->
Enum.map @function_names, fn
^function_name ->
{function_name, timeout}
other_function_name ->
{other_function_name, module_timeout}
end
timeout_by_function_name when is_list(timeout_by_function_name) ->
Keyword.put(timeout_by_function_name, function_name, timeout)
end
updated_environment = Keyword.put(previous_environment, :timeout, updated_timeout)
Application.put_env(:retort, module, updated_environment)
end
@doc """
Sets the `timeout` for `Retort.Client.Generic` calls of `function_name` by `module` that called
`use Retort.Resources` during the duration of this function call. `timeout` is restored to its original value after
this function call returns.
The return value `func` is returned.
iex> Retort.Resources.Timeout.put Retort.TestAuthors, :index, 1, fn ->
...> {:error, :timeout}
...> end
{:error, :timeout}
## Returns
* `term` - Returns value returned from the passed function.
"""
@spec put(module, function_name, timeout, (() -> term)) :: term
def put(module, function_name, timeout, func) do
temporary_environment module, fn ->
put(module, function_name, timeout)
func.()
end
end
## Private Functions
defp delete_from_environment(module, environment, function_name) do
timeout = Keyword.get(environment, :timeout)
delete_from_timeout(module, environment, timeout, function_name)
end
defp delete_from_timeout(_module, _environment, nil, _function_name), do: :ok
defp delete_from_timeout(module, environment, module_timeout, function_name) when is_timeout(module_timeout) do
timeout_by_function_name = @function_names
|> List.delete(function_name)
|> Enum.map(
fn other_function_name ->
{other_function_name, module_timeout}
end
)
updated_environment = Keyword.put(environment, :timeout, timeout_by_function_name)
Application.put_env(:retort, module, updated_environment)
end
defp delete_from_timeout(module, environment, timeout_by_function_name, function_name)
when is_list(timeout_by_function_name) do
case Keyword.delete(timeout_by_function_name, function_name) do
[] ->
case Keyword.delete(environment, :timeout) do
[] ->
Application.delete_env(:retort, module)
updated_environment ->
Application.put_env(:retort, module, updated_environment)
end
updated_timeout_by_function_name ->
updated_environment = Keyword.put(environment, :timeout, updated_timeout_by_function_name)
Application.put_env(:retort, module, updated_environment)
end
end
defp temporary_environment(module, func) do
previous_environment = Application.get_env(:retort, module)
try do
func.()
after
case previous_environment do
nil -> Application.delete_env(:retort, module)
_ -> Application.put_env(:retort, module, previous_environment)
end
end
end
end