defmodule AgentSea.Gateway.CircuitBreaker do
@moduledoc """
Per-provider circuit breaker on top of the battle-tested `:fuse` library.
Each provider gets a fuse. A failed call `melt/1`s it; once it has melted past
its tolerance within the window the fuse is "blown" and the gateway skips that
provider until it resets. We don't hand-roll the closed/open/half-open state
machine — `:fuse` already has it.
"""
# Tolerate 1 melt per 10s; the 2nd melt within the window blows the fuse.
# Auto-resets 30s after blowing.
@fuse_options {{:standard, 1, 10_000}, {:reset, 30_000}}
@doc "Install the provider's fuse if it isn't already present."
@spec ensure(term()) :: :ok | :reset | {:error, term()}
def ensure(name) do
case :fuse.ask(fuse_name(name), :sync) do
:ok -> :ok
:blown -> :ok
{:error, :not_found} -> :fuse.install(fuse_name(name), @fuse_options)
end
end
@doc "Whether the provider's circuit is currently usable."
@spec ask(term()) :: :ok | :blown
def ask(name) do
case :fuse.ask(fuse_name(name), :sync) do
:ok -> :ok
:blown -> :blown
{:error, :not_found} -> :ok
end
end
@doc "Record a failure against the provider's fuse."
@spec melt(term()) :: :ok
def melt(name), do: :fuse.melt(fuse_name(name))
@doc "Remove the provider's fuse (e.g. on gateway shutdown / test cleanup)."
@spec remove(term()) :: :ok
def remove(name), do: :fuse.remove(fuse_name(name))
defp fuse_name(name), do: :"agentsea_gateway_fuse_#{name}"
end