🎓 A1Learner
🔍
0 of 46 lessons completed

Elixir Circuit Breakers

A circuit breaker is a guard that stops calls to a failing service. It opens after too many failures and later allows one trial call in its half-open state.

Failures below the limit keep the guard closed

A guard with a limit of 3 stays closed after 2 failures. The failure count is the proof.

defmodule Breaker do
  def new(limit), do: %{state: :closed, failures: 0, limit: limit}
  def fail(state) do
    failures = state.failures + 1
    if failures >= state.limit do
      %{state | state: :open, failures: failures}
    else
      %{state | failures: failures}
    end
  end
end
breaker = Breaker.new(3)
breaker = Breaker.fail(breaker)
breaker = Breaker.fail(breaker)
IO.inspect(breaker.state)
IO.puts(breaker.failures)

Reaching the limit opens the guard

One more failure reaches the limit of 3. The guard then blocks calls instead of passing them on.

defmodule Breaker do
  def new(limit), do: %{state: :closed, failures: 0, limit: limit}
  def fail(state) do
    failures = state.failures + 1
    if failures >= state.limit do
      %{state | state: :open, failures: failures}
    else
      %{state | failures: failures}
    end
  end
  def allowed?(state), do: state.state != :open
end
breaker = Breaker.new(3)
breaker = Breaker.fail(breaker)
breaker = Breaker.fail(breaker)
breaker = Breaker.fail(breaker)
IO.inspect(breaker.state)
IO.inspect(Breaker.allowed?(breaker))

A trial in the half-open state decides the next state

An open guard moves to its half-open state for one trial. A successful trial closes the guard and clears the count.

defmodule Breaker do
  def new(limit), do: %{state: :closed, failures: 0, limit: limit}
  def fail(state) do
    failures = state.failures + 1
    if failures >= state.limit do
      %{state | state: :open, failures: failures}
    else
      %{state | failures: failures}
    end
  end
  def trial(state) do
    if state.state == :open do
      %{state | state: :half_open}
    else
      state
    end
  end
  def succeed(state) do
    if state.state == :half_open do
      %{state | state: :closed, failures: 0}
    else
      %{state | failures: 0}
    end
  end
end
breaker = Breaker.new(2)
breaker = Breaker.fail(breaker)
breaker = Breaker.fail(breaker)
after_trial = Breaker.trial(breaker)
after_success = Breaker.succeed(after_trial)
IO.inspect(after_trial.state)
IO.inspect(after_success.state)
IO.puts(after_success.failures)

Example

defmodule Breaker do
  def new(limit), do: %{state: :closed, failures: 0, limit: limit}
  def fail(state) do
    failures = state.failures + 1
    if failures >= state.limit do
      %{state | state: :open, failures: failures}
    else
      %{state | failures: failures}
    end
  end
  def trial(state) do
    if state.state == :open do
      %{state | state: :half_open}
    else
      state
    end
  end
end
start = Breaker.new(2)
open_guard = Breaker.fail(Breaker.fail(start))
trial_guard = Breaker.trial(open_guard)
open_again = Breaker.fail(trial_guard)
states = [start.state, open_guard.state, trial_guard.state, open_again.state]
IO.inspect(states)

Key points

  • Closed: the guard passes calls through while failures stay below the limit.
  • Open: the guard blocks calls at once after the failure limit is reached.
  • Half-open: the guard allows one trial call to test whether the service recovered.
  • Trial success: a successful trial call closes the guard and clears the failure count.
  • Trial failure: a failed trial call opens the guard again.

In this example, a guard with a limit of 2 starts closed, opens after two failures, moves to its half-open state for a trial, and opens again when the trial fails. The printed list shows each state in order.

Note

the guard counts results, not time, so this lesson shows state changes without any waiting.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

A guard with a limit of 2 has failed twice and is open. What happens on its trial call if that call succeeds?