Ash CILicense: MITHex version badgeHexdocs badge

AshCircuitBreaker

Welcome! This is an extension for the Ash framework which protects your application from cascading failures by adding circuit breaker functionality to actions.

Uses the excellent fuse library to provide robust circuit breaker features that help your application gracefully handle and recover from failures.

Installation

Add ash_circuit_breaker to your list of dependencies in mix.exs:

def deps do
[
{:ash_circuit_breaker, "~> 0.0.1"}
]
end

Quick Start

  1. Add to your resource: Use the circuit DSL section in your Ash resource:
defmodule MyApp.Post do
use Ash.Resource,
domain: MyApp,
extensions: [AshCircuitBreaker]
circuit do
# Protect create action - open circuit after 5 failures in 30 seconds
action :create,
limit: 5,
per: :timer.seconds(30),
reset_after: :timer.minutes(5)
# Protect update action with different thresholds
action :update,
limit: 10,
per: :timer.minutes(1),
reset_after: :timer.minutes(2)
end
# ... rest of your resource definition
end
  1. That's it! Your actions are now protected by circuit breakers. When failures exceed the threshold, subsequent calls will be blocked with an AshCircuitBreaker.CircuitBroken error until the circuit resets.

Basic Usage

Simple Circuit Breaker

circuit do
# Open circuit after 5 failures in 30 seconds, reset after 5 minutes
action :create,
limit: 5,
per: :timer.seconds(30),
reset_after: :timer.minutes(5)
end

Multiple Actions

circuit do
action :create, limit: 5, per: :timer.seconds(30), reset_after: :timer.minutes(5)
action :update, limit: 10, per: :timer.minutes(1), reset_after: :timer.minutes(2)
action :delete, limit: 3, per: :timer.seconds(15), reset_after: :timer.minutes(10)
end

Custom Circuit Names

circuit do
# Use a custom static name
action :create,
limit: 5,
per: :timer.seconds(30),
reset_after: :timer.minutes(5),
name: :my_custom_circuit
# Use a function to generate dynamic names
action :update,
limit: 10,
per: :timer.minutes(1),
reset_after: :timer.minutes(2),
name: fn changeset ->
:"update_circuit_#{changeset.data.id}"
end
end

Advanced Usage

Manual Integration

For more control, you can add circuit breaker protection directly to specific actions:

defmodule MyApp.Post do
use Ash.Resource, domain: MyApp
actions do
create :create do
change {AshCircuitBreaker.Change,
limit: 5,
per: :timer.seconds(30),
reset_after: :timer.minutes(5)}
end
update :update do
change {AshCircuitBreaker.Change,
limit: 10,
per: :timer.minutes(1),
reset_after: :timer.minutes(2)}
end
end
end

Custom Name Functions

The name function determines how circuit breakers are identified and shared:

# Per-user circuit breakers
name: fn changeset, context ->
:"user_#{context.actor.id}_create"
end
# Per-tenant circuit breakers
name: fn changeset ->
:"tenant_#{changeset.data.tenant_id}_action"
end
# Use the built-in name function (default)
name: &AshCircuitBreaker.name_for_breaker/1

Circuit Breaker States

A circuit breaker can be in one of two states:

  1. OK (normal operation): Requests flow through normally. Failures are counted until the limit is reached.
  2. Blown (failing fast): All requests are immediately rejected with CircuitBroken error until the reset period expires.

Error Handling

When a circuit is open, an AshCircuitBreaker.CircuitBroken exception is raised:

case MyApp.create_post(attrs) do
{:ok, post} ->
# Success
{:ok, post}
{:error, %AshCircuitBreaker.CircuitBroken{} = error} ->
# Circuit breaker is open
{:error, "Service temporarily unavailable, please try again later"}
{:error, other_error} ->
# Handle other errors (these may trigger the circuit breaker)
{:error, other_error}
end

In web applications, the exception includes Plug.Exception behaviour for automatic HTTP 503 responses.

Configuration

Parameters

Example Configurations

# High-traffic endpoint with tight failure tolerance
action :api_call, limit: 3, per: :timer.seconds(10), reset_after: :timer.seconds(30)
# Background job with more lenient settings
action :process_data, limit: 20, per: :timer.minutes(5), reset_after: :timer.minutes(10)
# Critical operation with long recovery time
action :payment, limit: 1, per: :timer.seconds(5), reset_after: :timer.minutes(30)

Monitoring

Circuit breaker state can be monitored using fuse's built-in functions:

# Check circuit state
:fuse.ask(:my_circuit_name, :sync)
# Returns: :ok | :blown | {:error, :not_found}
# Reset a blown circuit manually
:fuse.reset(:my_circuit_name)
# Melt (blow) a circuit manually
:fuse.melt(:my_circuit_name)

Testing

In test environments, you may want to disable circuit breakers or use test-friendly configurations:

# Use very high limits in tests
circuit do
action :create, limit: 999, per: :timer.hours(1), reset_after: :timer.seconds(1)
end
# Or conditionally apply circuit breakers
if Mix.env() != :test do
circuit do
action :create, limit: 5, per: :timer.seconds(30), reset_after: :timer.minutes(5)
end
end

Limitations

Reference