Packages

Prevent cascading failures in your Gleam applications. Circuit breaker pattern with sliding window tracking, OTP-supervised state, and support for both Erlang and JavaScript targets.

Current section

Files

Jump to
circuit src circuit.erl
Raw

src/circuit.erl

-module(circuit).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/circuit.gleam").
-export([new/0, failure_threshold/2, window_size/2, reset_timeout/2, transition/4, start/1, state/1, reset/1, record_result/2, call/2]).
-export_type([circuit_state/0, config/0, call_result/0, message/0, actor_state/0, call_error/0, circuit_breaker/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-type circuit_state() :: closed | open | half_open.
-type config() :: {config, integer(), integer(), integer()}.
-type call_result() :: success | {failure, binary()}.
-type message() :: {record_result, call_result()} |
{get_state, gleam@erlang@process:subject(circuit_state())} |
reset.
-type actor_state() :: {actor_state,
circuit_state(),
list(call_result()),
config()}.
-type call_error() :: circuit_open | {call_failed, binary()}.
-opaque circuit_breaker() :: {circuit_breaker,
gleam@erlang@process:subject(message())}.
-file("src/circuit.gleam", 44).
?DOC(
" Returns a `Config` with sensible production defaults:\n"
"\n"
" - `failure_threshold`: 5\n"
" - `window_size`: 10\n"
" - `reset_timeout`: 30_000ms (30 seconds)\n"
"\n"
" These defaults are a reasonable starting point. Tune them for your workload\n"
" using `failure_threshold/2`, `window_size/2`, and `reset_timeout/2`.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let config =\n"
" circuit.new()\n"
" |> circuit.failure_threshold(3)\n"
" |> circuit.window_size(5)\n"
" |> circuit.reset_timeout(10_000)\n"
" ```\n"
).
-spec new() -> config().
new() ->
{config, 5, 10, 30000}.
-file("src/circuit.gleam", 53).
?DOC(
" Sets the number of failures within the sliding window required to trip the\n"
" circuit from `Closed` to `Open`.\n"
"\n"
" A lower value makes the breaker more sensitive. A higher value tolerates\n"
" more failures before tripping.\n"
).
-spec failure_threshold(config(), integer()) -> config().
failure_threshold(Config, Value) ->
{config, Value, erlang:element(3, Config), erlang:element(4, Config)}.
-file("src/circuit.gleam", 62).
?DOC(
" Sets the size of the sliding window — the number of most recent calls that\n"
" are tracked when calculating the failure rate.\n"
"\n"
" For example, a `window_size` of 10 means only the last 10 call results are\n"
" considered. Older results are discarded automatically.\n"
).
-spec window_size(config(), integer()) -> config().
window_size(Config, Value) ->
{config, erlang:element(2, Config), Value, erlang:element(4, Config)}.
-file("src/circuit.gleam", 71).
?DOC(
" Sets how long (in milliseconds) the circuit stays `Open` before\n"
" transitioning to `HalfOpen` to probe for recovery.\n"
"\n"
" A shorter timeout means faster recovery attempts. A longer timeout gives\n"
" the downstream service more time to recover before being probed.\n"
).
-spec reset_timeout(config(), integer()) -> config().
reset_timeout(Config, Value) ->
{config, erlang:element(2, Config), erlang:element(3, Config), Value}.
-file("src/circuit.gleam", 121).
-spec transition(circuit_state(), call_result(), integer(), config()) -> circuit_state().
transition(State, Result, Failures, Config) ->
case {State, Result} of
{closed, {failure, _}} when Failures >= erlang:element(2, Config) ->
open;
{closed, _} ->
closed;
{open, _} ->
open;
{half_open, success} ->
closed;
{half_open, {failure, _}} ->
open
end.
-file("src/circuit.gleam", 136).
-spec handle_message(actor_state(), message()) -> gleam@otp@actor:next(actor_state(), message()).
handle_message(State, Message) ->
case Message of
{record_result, Result} ->
New_window = gleam@list:take(
[Result | erlang:element(3, State)],
erlang:element(3, erlang:element(4, State))
),
Failure_count = gleam@list:count(New_window, fun(R) -> case R of
{failure, _} ->
true;
success ->
false
end end),
New_circuit_state = transition(
erlang:element(2, State),
Result,
Failure_count,
erlang:element(4, State)
),
gleam@otp@actor:continue(
{actor_state,
New_circuit_state,
New_window,
erlang:element(4, State)}
);
{get_state, Subject} ->
gleam@erlang@process:send(Subject, erlang:element(2, State)),
gleam@otp@actor:continue(State);
reset ->
gleam@otp@actor:continue(
{actor_state, closed, [], erlang:element(4, State)}
)
end.
-file("src/circuit.gleam", 183).
?DOC(
" Starts a new circuit breaker process with the given `Config`.\n"
"\n"
" Returns `Ok(CircuitBreaker)` on success, or `Error(actor.StartError)` if\n"
" the underlying OTP actor fails to start.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let assert Ok(breaker) =\n"
" circuit.new()\n"
" |> circuit.failure_threshold(5)\n"
" |> circuit.start()\n"
" ```\n"
).
-spec start(config()) -> {ok, circuit_breaker()} |
{error, gleam@otp@actor:start_error()}.
start(Config) ->
Initial_state = {actor_state, closed, [], Config},
case begin
_pipe = gleam@otp@actor:new(Initial_state),
_pipe@1 = gleam@otp@actor:on_message(_pipe, fun handle_message/2),
gleam@otp@actor:start(_pipe@1)
end of
{ok, Started} ->
{ok, {circuit_breaker, erlang:element(3, Started)}};
{error, E} ->
{error, E}
end.
-file("src/circuit.gleam", 198).
?DOC(
" Returns the current `CircuitState` of the breaker (`Closed`, `Open`, or `HalfOpen`).\n"
"\n"
" This is a synchronous call to the actor — it blocks briefly until the actor\n"
" responds.\n"
).
-spec state(circuit_breaker()) -> circuit_state().
state(Breaker) ->
gleam@erlang@process:call(
erlang:element(2, Breaker),
100,
fun(Field@0) -> {get_state, Field@0} end
).
-file("src/circuit.gleam", 206).
?DOC(
" Manually resets the breaker to `Closed` and clears the sliding window.\n"
"\n"
" Useful in tests or admin tooling. In normal operation the breaker manages\n"
" its own state — you should rarely need to call this directly.\n"
).
-spec reset(circuit_breaker()) -> nil.
reset(Breaker) ->
gleam@erlang@process:send(erlang:element(2, Breaker), reset).
-file("src/circuit.gleam", 215).
?DOC(
" Records a `CallResult` against the breaker without running a function.\n"
"\n"
" Use this when you are managing the call yourself and just want to inform\n"
" the breaker of the outcome. For the common case of wrapping a function,\n"
" prefer `call/2` instead.\n"
).
-spec record_result(circuit_breaker(), call_result()) -> nil.
record_result(Breaker, Result) ->
gleam@erlang@process:send(
erlang:element(2, Breaker),
{record_result, Result}
).
-file("src/circuit.gleam", 241).
?DOC(
" Runs `f` through the circuit breaker and records its result.\n"
"\n"
" - If the circuit is `Open`, `f` is **not called** and `Error(CircuitOpen)` is\n"
" returned immediately.\n"
" - If the circuit is `Closed` or `HalfOpen`, `f` is called. A `Success` result\n"
" returns `Ok(Nil)`. A `Failure(reason)` result returns `Error(CallFailed(reason))`.\n"
" Either way, the result is automatically recorded against the sliding window.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" case circuit.call(breaker, fn() {\n"
" case fetch_user(id) {\n"
" Ok(_) -> circuit.Success\n"
" Error(_) -> circuit.Failure(\"fetch failed\")\n"
" }\n"
" }) {\n"
" Ok(Nil) -> // call succeeded\n"
" Error(circuit.CircuitOpen) -> // blocked — try a fallback\n"
" Error(circuit.CallFailed(reason)) -> // call ran but failed\n"
" }\n"
" ```\n"
).
-spec call(circuit_breaker(), fun(() -> call_result())) -> {ok, nil} |
{error, call_error()}.
call(Breaker, F) ->
case state(Breaker) of
open ->
{error, circuit_open};
_ ->
Result = F(),
record_result(Breaker, Result),
case Result of
success ->
{ok, nil};
{failure, Reason} ->
{error, {call_failed, Reason}}
end
end.