Packages

OTP-native circuit breaker library for Elixir. Wraps GenServer calls with a three-state circuit breaker that fast-fails when a dependency is unhealthy and automatically probes for recovery.

Current section

Files

Jump to
tripwire lib tripwire.ex
Raw

lib/tripwire.ex

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