defmodule Breaker do
@behaviour :gen_statem
def start_link(opts), do: :gen_statem.start_link(__MODULE__, opts, [])
def state(breaker), do: :gen_statem.call(breaker, :state)
def run(breaker, fun) do
case :gen_statem.call(breaker, :ask) do
:go ->
result = fun.()
:gen_statem.cast(breaker, {:report, result})
result
:reject ->
{:error, :circuit_open}
end
end
@impl true
def callback_mode, do: [:handle_event_function, :state_enter]
@impl true
def init(opts) do
data = %{failures: 0, threshold: opts[:threshold], cooldown: opts[:cooldown], trial: false}
{:ok, :closed, data}
end
# Entering a state.
@impl true
def handle_event(:enter, _previous, :open, data) do
{:keep_state_and_data, [{:state_timeout, data.cooldown, :cooldown_over}]}
end
def handle_event(:enter, _previous, _closed_or_half_open, data) do
{:keep_state, %{data | failures: 0, trial: false}}
end
def handle_event({:call, from}, :state, state, _data) do
{:keep_state_and_data, [{:reply, from, state}]}
end
# Asking for permission.
def handle_event({:call, from}, :ask, :closed, _data) do
{:keep_state_and_data, [{:reply, from, :go}]}
end
def handle_event({:call, from}, :ask, :open, _data) do
{:keep_state_and_data, [{:reply, from, :reject}]}
end
def handle_event({:call, from}, :ask, :half_open, %{trial: false} = data) do
{:keep_state, %{data | trial: true}, [{:reply, from, :go}]}
end
def handle_event({:call, from}, :ask, :half_open, %{trial: true}) do
{:keep_state_and_data, [{:reply, from, :reject}]}
end
# Results reported back by callers.
def handle_event(:cast, {:report, {:ok, _}}, :closed, data) do
{:keep_state, %{data | failures: 0}}
end
def handle_event(:cast, {:report, {:error, _}}, :closed, data) do
failures = data.failures + 1
if failures >= data.threshold do
{:next_state, :open, data}
else
{:keep_state, %{data | failures: failures}}
end
end
def handle_event(:cast, {:report, {:ok, _}}, :half_open, data) do
{:next_state, :closed, data}
end
def handle_event(:cast, {:report, {:error, _}}, :half_open, data) do
{:next_state, :open, data}
end
# A call that started before the breaker opened can
# still report afterwards. It changes nothing.
def handle_event(:cast, {:report, _}, :open, _data), do: :keep_state_and_data
# The cooldown is over.
def handle_event(:state_timeout, :cooldown_over, :open, data) do
{:next_state, :half_open, data}
end
end