-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.