Current section
Files
Jump to
Current section
Files
lib/ex_break.ex
defmodule ExBreak do
@moduledoc """
Provides circuit breaker functionality around function calls
"""
alias ExBreak.Breaker
@type opt ::
{:timeout_sec, pos_integer}
| {:threshold, pos_integer}
| {:match_exception, (any -> boolean)}
| {:match_return, (any -> boolean)}
| {:on_trip, (Breaker.t() -> any)}
@type opts :: [opt]
@default_opts [timeout_sec: 60 * 15, threshold: 10]
@doc """
Call a function using a circuit breaker.
The first option is the function to call, followed by an optional list of
arguments.
If the function succeeds, and does not return an error tuple (in the form
`{:error, any}`), the value returned by the function will be returned.
If the function fails (meaning it returns an `{:error, any}` tuple), a
counter is incremented in a circuit breaker for that function. If the
counter meets or exceeds the configured circuit breaker threshold, the
breaker for that function is marked as tripped.
If the function fails and the circuit breaker for that function has been
marked as tripped (and has not expired), then `{:error, :circuit_breaker_tripped}`
will be returned.
A list of options can be provided as the final argument to `call`:
- `timeout_sec` (default `900`) The number of seconds after which a tripped
breaker times out and is re-opened
- `threshold` (default `10`) The number of times an error is permitted
before further calls will be ignored and will return an `{:error,
:circuilt_closed}`
- `match_exception` (default `fn _ -> true end`) A function called when an
exception is raised during the function call. If it returns true, the
breaker trip count is incremented.
- `match_return` (defaults to return `true` when matching `{:error, _}`) A
function called on the return value of the function. If it returns `true`,
the breaker trip count is incremented.
- `on_trip` (defaults to `nil`) A function called with the tripped breaker
as an argument when the breaker was tripped
## Example
This simple example increments the breaker when the return value is
`{:error, _}`:
iex> ExBreak.call(fn ret -> ret end, [:ok])
:ok
iex> fun = fn -> {:error, :fail} end
iex> ExBreak.call(fun, [], threshold: 2)
{:error, :fail}
iex> ExBreak.call(fun, [], threshold: 2)
{:error, :fail}
iex> ExBreak.call(fun, [], threshold: 2)
{:error, :circuit_breaker_tripped}
In this example, we only increment when the return value is exactly
`{:error, :bad}`:
iex> fun = fn ret -> ret end
iex> opts = [threshold: 2, match_return: fn
...> {:error, :bad} -> true
...> _ -> false
...> end]
iex> ExBreak.call(fun, [{:error, :not_bad}], opts)
{:error, :not_bad}
iex> ExBreak.call(fun, [{:error, :not_bad}], opts)
{:error, :not_bad}
iex> ExBreak.call(fun, [{:error, :not_bad}], opts)
{:error, :not_bad}
iex> ExBreak.call(fun, [{:error, :bad}], opts)
{:error, :bad}
iex> ExBreak.call(fun, [{:error, :bad}], opts)
{:error, :bad}
iex> ExBreak.call(fun, [{:error, :bad}], opts)
{:error, :circuit_breaker_tripped}
"""
@spec call(function, [any], opts) :: any
def call(fun, args \\ [], opts \\ []) do
opts = Keyword.merge(@default_opts, opts)
lookup(fun, fn pid ->
if Breaker.is_tripped(pid, opts[:timeout_sec]) do
{:error, :circuit_breaker_tripped}
else
Breaker.reset_tripped(pid)
call_func(fun, args, opts)
end
end)
end
# Rewinds a breaker's tripped_at time for testing.
@doc false
@spec rewind_trip(function, integer) :: :ok
def rewind_trip(fun, rewind_sec) do
lookup(fun, fn pid ->
Agent.update(pid, fn breaker ->
tripped_at = DateTime.add(breaker.tripped_at, -rewind_sec, :second)
Map.put(breaker, :tripped_at, tripped_at)
end)
end)
end
defp lookup(fun, callback) do
case ExBreak.Registry.get_breaker(fun) do
{:ok, pid} -> callback.(pid)
error -> error
end
end
defp call_func(fun, args, opts) do
match_exception = Keyword.get(opts, :match_exception, fn _ -> true end)
match_return = Keyword.get(opts, :match_return, fn ret -> match?({:error, _}, ret) end)
try do
apply(fun, args)
rescue
exception ->
if match_exception.(exception) do
increment_breaker(fun, opts)
end
reraise exception, __STACKTRACE__
else
return ->
if match_return.(return) do
increment_breaker(fun, opts)
return
else
reset_breaker(fun)
return
end
end
end
defp increment_breaker(fun, opts) do
lookup(fun, &Breaker.increment(&1, opts[:threshold], opts[:on_trip]))
end
defp reset_breaker(fun) do
lookup(fun, &Breaker.reset_tripped/1)
end
end