defmodule Tripwire do @moduledoc """ An OTP-native circuit breaker library for Elixir. `Tripwire` wraps target GenServers with a circuit breaker pattern: consecutive failures trip the circuit to `:open`, which fast-fails subsequent calls. After a configurable timeout, a single probe call is allowed through to test recovery. ## Usage # Start the Registry once in your application supervisor: {Registry, keys: :unique, name: Tripwire.Breaker.registry()} # Start a TaskSupervisor for non-blocking forwarding: {Task.Supervisor, name: Tripwire.TaskSupervisor} # Start a breaker for a target: Tripwire.start_link(target_key: :my_service, target_pid: pid) # Call the target through the breaker: Tripwire.call(:my_service, :some_message) ## States * `:closed` - Normal operation. Calls are forwarded to the target. * `:open` - Circuit tripped. Calls are fast-failed. * `:half_open` - Recovery probe. A single call is allowed through. ## Infrastructure Tripwire requires two processes running in the supervision tree: * `Registry` named `:tripwire_breakers` with `keys: :unique`. Maps target keys to breaker PIDs. * `Task.Supervisor` named `Tripwire.TaskSupervisor`. Manages non-blocking task processes that forward calls to targets. Example child specification: children = [ {Registry, keys: :unique, name: Tripwire.Breaker.registry()}, {Task.Supervisor, name: Tripwire.TaskSupervisor}, {MyApp.Dependency, [name: MyApp.Dependency]}, {Tripwire, target_key: :dependency, target_pid: MyApp.Dependency} ] """ alias Tripwire.Breaker @doc """ Call a target GenServer through its circuit breaker. Looks up the breaker registered for `target_key` via `{:via, Registry, ...}` addressing and forwards the call. ## Parameters * `target_key` - Identifies the breaker, matching the `:target_key` option passed to `start_link/1`. * `msg` - Message forwarded to the target GenServer. * `timeout` - Maximum milliseconds to wait for a response (default `5000`). ## Returns * `{:ok, result}` - the target responded successfully. * `{:error, :circuit_open}` - the circuit is open. The call was not forwarded. * `{:error, :breaker_not_registered}` - no breaker registered for this key. * `{:error, :breaker_busy}` - the breaker did not respond in time. * `{:error, {:breaker_crashed, reason}}` - the breaker process crashed. ## Examples Tripwire.call(:weather, :forecast) # => {:ok, :sunny} Tripwire.call(:weather, :forecast, 10_000) # => {:ok, :sunny} Tripwire.call(:unknown, :ping) # => {:error, :breaker_not_registered} """ @spec call(term(), term(), timeout()) :: {:ok, term()} | {:error, term()} def call(target_key, msg, timeout \\ 5000) do via = {:via, Registry, {Breaker.registry(), target_key}} try do GenServer.call(via, {:call, msg, timeout}) catch :exit, {:noproc, _} -> {:error, :breaker_not_registered} :exit, {:timeout, _} -> {:error, :breaker_busy} :exit, {reason, _} -> {:error, {:breaker_crashed, reason}} :exit, reason -> {:error, {:breaker_crashed, reason}} end end @doc """ Start a circuit breaker for a target. The breaker is registered automatically in `Registry` so it can be found by `call/3`. ## Options * `:target_key` - (required) unique key identifying this target. Used in `call/3` to route calls. * `:target_pid` - (required) PID or registered name of the target GenServer. * `:threshold` - consecutive failures before tripping (default `5`). * `:reset_timeout` - milliseconds before `:open` -> `:half_open` (default `60_000`). ## Returns * `{:ok, pid}` - breaker started and registered successfully. * `{:error, {:already_started, pid}}` - a breaker with this key is already running. * `{:error, reason}` - startup failed. """ @spec start_link(keyword()) :: GenServer.on_start() def start_link(opts) do Breaker.start_link(opts) end @doc """ Returns a child specification for including a Tripwire breaker in a supervision tree. Delegates to `Tripwire.Breaker.child_spec/1`. The restart type is `:permanent` so a crashed breaker restarts at the `:closed` state with a failure count of zero. ## Options Same options as `start_link/1`. """ @spec child_spec(keyword()) :: map() def child_spec(opts) do Breaker.child_spec(opts) end end