Getting Started with Tripwire

Copy Markdown

In this tutorial we will wrap a GenServer with Tripwire's circuit breaker, trigger the circuit to open by sending failing calls, and watch it recover automatically. Along the way we will encounter a Registry, a TaskSupervisor, a circuit breaker process, the three circuit states (:closed, :open, :half_open), and the return contract of Tripwire.call/3.

This tutorial takes about 20 minutes.

Step 1: Create the project

First, create a new Mix project:

mix new weather_service --sup
cd weather_service

Open mix.exs and add Tripwire to the dependencies:

defp deps do
  [
    {:tripwire, "~> 0.1.0"}
  ]
end

Fetch the dependency:

mix deps.get

You should see Tripwire and its transitive dependencies (if any) being downloaded and compiled.

Notice that Tripwire does not require any special configuration in config/. It ships ready to use once the dependency is added.

Step 2: Define a target GenServer

Next, create a GenServer that our circuit breaker will protect. Tripwire does not require anything special from the target -- any GenServer works.

Open lib/weather_service/weather.ex and add this module:

defmodule WeatherService.Weather do
  use GenServer

  def start_link(opts) do
    GenServer.start_link(__MODULE__, :ok, opts)
  end

  @impl true
  def init(:ok) do
    {:ok, %{}}
  end

  @impl true
  def handle_call(:forecast, _from, state) do
    {:reply, {:ok, :sunny}, state}
  end

  @impl true
  def handle_call(:error, _from, state) do
    {:reply, {:error, :boom}, state}
  end
end

This GenServer responds to :forecast with {:ok, :sunny} and to :error with {:error, :boom}. The error response is how we will simulate a failing dependency without crashing the target process.

Step 3: Wire it all together

Open lib/weather_service/application.ex and replace the contents with:

defmodule WeatherService.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      # Tripwire requires a Registry to look up breakers by target key.
      {Registry, keys: :unique, name: Tripwire.Breaker.registry()},
      # Tripwire uses a TaskSupervisor for non-blocking call forwarding.
      {Task.Supervisor, name: Tripwire.TaskSupervisor},
      # The target GenServer, registered by its module name.
      {WeatherService.Weather, [name: WeatherService.Weather]},
      # The circuit breaker wraps the target.
      {Tripwire, target_key: :weather, target_pid: WeatherService.Weather}
    ]

    opts = [strategy: :one_for_one, name: WeatherService.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Notice the Tripwire child spec takes a target_key (a name we choose) and a target_pid (the registered name of the target GenServer). The default threshold is 5 consecutive failures and the default reset timeout is 60 seconds.

Now compile and verify everything starts:

mix run --no-halt

You should see no startup errors. The application stays running because --no-halt keeps the process alive. Press Ctrl+C twice to stop it.

Step 4: Make a successful call

Let's verify that calls go through the breaker normally. Open a new shell in the project directory and start an IEx session:

iex -S mix

Now make a call through the breaker:

Tripwire.call(:weather, :forecast)

You should see:

{:ok, :sunny}

The breaker is in the :closed state. It forwarded our call to the Weather GenServer and returned the result. Notice that the return value is wrapped in a tuple -- every call through Tripwire returns {:ok, result} on success.

Step 5: Trip the circuit breaker

The circuit breaker exists to protect callers from a failing dependency. Let's simulate a failure by sending the :error message. The Weather GenServer will respond with {:error, :boom}, and Tripwire will count it as a failure.

In the same IEx session, send this sequence:

# Send 6 calls so the 6th will find the circuit open (threshold is 5).
1..6 |> Enum.each(fn _ -> Tripwire.call(:weather, :error) end)

Now try a call that would succeed:

Tripwire.call(:weather, :forecast)

You should see:

{:error, :circuit_open}

The circuit has tripped to :open. The call to the Weather GenServer was fast-failed -- it never reached the target. The breaker returned {:error, :circuit_open} immediately, without blocking on the target's timeout.

Notice the difference. When the circuit is closed, a failing call takes the full timeout duration (5000ms by default). When the circuit is open, the response is instant. This is the protection the circuit breaker provides.

Step 6: Watch automatic recovery

The breaker will automatically transition from :open to :half_open after the reset timeout elapses (default: 60 seconds). In the half-open state, a single call is allowed through as a probe.

If the probe succeeds, the breaker returns to :closed. If it fails, the breaker returns to :open and the timer restarts.

Let's verify this. We will use a shorter reset timeout so we do not need to wait a full minute. Stop the IEx session (press Ctrl+C twice) and update the child spec in lib/weather_service/application.ex:

{Tripwire,
 target_key: :weather,
 target_pid: WeatherService.Weather,
 threshold: 3,
 reset_timeout: 5_000}

We lowered the threshold to 3 failures and the reset timeout to 5 seconds.

Start IEx again:

iex -S mix

First, trip the circuit with 3 failing calls:

1..3 |> Enum.each(fn _ -> Tripwire.call(:weather, :error) end)

Verify the circuit is open:

Tripwire.call(:weather, :forecast)

You should see {:error, :circuit_open}.

Now wait 5 seconds and call again:

# Wait 5 seconds, then:
Tripwire.call(:weather, :forecast)

You should see:

{:ok, :sunny}

The circuit automatically recovered. After 5 seconds, the breaker moved to :half_open, allowed a single probe call through, the probe succeeded, and the breaker returned to :closed. All subsequent calls now flow normally.

Step 7: What we built

You have created a full Tripwire setup. You now have a target GenServer, a Registry for breaker lookups, a TaskSupervisor for non-blocking forwarding, and a circuit breaker protecting the target. You observed the three circuit states in action, closed forwarding calls, open fast-failing them, half-open probing for recovery, all without writing any failure handling logic inside the caller.

From here, you can adjust thresholds and timeouts to match your dependency's behavior, add breakers for each of your GenServer targets, or explore the Tripwire.State module to understand the state machine that governs the transitions.